:root {
  --bg: #0e0f12;
  --bg-elev: #14161a;
  --fg: #e9ecef;
  --muted: #8d96a0;
  --accent: #7ad0a3;
  --workload: #6ca0dc;
  --infra: #6b7280;
  --rule: #1f232a;
  --rule-strong: #2c323b;
  --ok: #7ad0a3;
  --bad: #f08080;
  --skipped: #d9b380;
  --clr-proc: #6ca0dc;
  --clr-sc: #d9a06a;
  --clr-tls: #5eb8b8;
  --clr-dns: #a78bda;
  /* Sticky-scroll geometry. Pinning order top-to-bottom:
   *   1. .top-header                  (position: fixed)            height = --header-h
   *   2. .steps-list > summary        (sticky, top = --header-h)   height = --step-summary-h
   *   3. .proc-tree details > summary (sticky, top = previous +
   *                                   depth * --proc-row-h)        height = --proc-row-h
   * The header is forced to exactly --header-h via height + overflow:hidden
   * AND body padding-top = --header-h, so sticky elements pin flush against
   * the header with no visible gap. Update --header-h if header padding/font
   * changes meaningfully.
   *
   * INVARIANT: each pinned layer's ACTUAL rendered height MUST equal the
   * CSS variable below. The downstream layer's `top:` is computed from the
   * variable, not the rendered height. If a layer wraps to a 2nd line
   * (e.g., long step label flexes the chips to a new row), it becomes
   * taller than the variable says — nested layers then pin INSIDE its
   * visual footprint and are hidden by its higher z-index.
   * Both .steps-list summary and .proc-tree details summary therefore
   * use flex-wrap: nowrap + overflow: hidden to stay single-line. */
  /* Whole-pixel sticky heights. Sub-pixel values (e.g. 1.45rem =
   * 23.2px) cause iOS Safari to render visible gaps between
   * stacked sticky proc-tree summaries — depth-N's `top:` offset
   * is calc'd from `depth * --proc-row-h`, and any fractional
   * mismatch between assumed and rendered row height accumulates
   * with depth. Whole px keeps the math exact. */
  --header-h: 40px;
  /* 34, deliberately 0-1px UNDER the rendered step summary height
   * (~34.92 at the current padding). Setting it equal-or-above
   * leaves a sub-pixel gap above the first proc-tree sticky on
   * high-DPR displays once both are pinned; setting it under
   * forces the proc sticky's `top: calc(--header-h +
   * --step-summary-h - 1px)` to land at-or-above the step
   * summary's actual bottom, hiding the seam behind the step
   * summary's higher z-index. */
  --step-summary-h: 34px;
  --proc-row-h: 23px;
  color-scheme: dark;
}
/* ── Theme: carbon — neon / cyberpunk ───────────────────────── */
body.theme-carbon {
  --bg: #131519;
  --bg-elev: #1a1c22;
  --fg: #dde0e5;
  --muted: #868e9c;
  --accent: #40e0d0;
  --workload: #ff6b9d;
  --infra: #6b7280;
  --rule: #232830;
  --rule-strong: #313845;
  --ok: #b8e040;
  --bad: #ff6b6b;
  --skipped: #ff8c42;
  --clr-proc: #ff6b9d;
  --clr-sc: #b8e040;
  --clr-tls: #40e0d0;
  --clr-dns: #ff8c42;
  color-scheme: dark;
}

/* ── Theme: slate — soft pastels on gray ───────────────────── */
body.theme-slate {
  --bg: #1a1d24;
  --bg-elev: #22262e;
  --fg: #d4d8de;
  --muted: #8891a0;
  --accent: #d4a840;
  --workload: #e07878;
  --infra: #6b7280;
  --rule: #2a2f3a;
  --rule-strong: #383f4d;
  --ok: #7ad0a3;
  --bad: #f08080;
  --skipped: #d9b380;
  --clr-proc: #e07878;
  --clr-sc: #d4a840;
  --clr-tls: #60c8a0;
  --clr-dns: #8888d8;
  color-scheme: dark;
}

/* ── Theme: nord — aurora palette ──────────────────────────── */
body.theme-nord {
  --bg: #2e3440;
  --bg-elev: #3b4252;
  --fg: #eceff4;
  --muted: #8892a4;
  --accent: #a3be8c;
  --workload: #a3be8c;
  --infra: #6b7280;
  --rule: #434c5e;
  --rule-strong: #4c566a;
  --ok: #a3be8c;
  --bad: #bf616a;
  --skipped: #ebcb8b;
  --clr-proc: #a3be8c;
  --clr-sc: #bf616a;
  --clr-tls: #ebcb8b;
  --clr-dns: #88c0d0;
  color-scheme: dark;
}

/* ── Theme: solarized — canonical solarized accents ────────── */
body.theme-solarized {
  --bg: #002b36;
  --bg-elev: #073642;
  --fg: #eee8d5;
  --muted: #839496;
  --accent: #859900;
  --workload: #d33682;
  --infra: #657b83;
  --rule: #0a4050;
  --rule-strong: #11505f;
  --ok: #859900;
  --bad: #dc322f;
  --skipped: #b58900;
  --clr-proc: #d33682;
  --clr-sc: #859900;
  --clr-tls: #cb4b16;
  --clr-dns: #2aa198;
  color-scheme: dark;
}

/* ── Theme: dracula — the classic dev palette ──────────────── */
body.theme-dracula {
  --bg: #282a36;
  --bg-elev: #343746;
  --fg: #f8f8f2;
  --muted: #6272a4;
  --accent: #bd93f9;
  --workload: #ff79c6;
  --infra: #6272a4;
  --rule: #3a3d4c;
  --rule-strong: #44475a;
  --ok: #50fa7b;
  --bad: #ff5555;
  --skipped: #f1fa8c;
  --clr-proc: #8be9fd;
  --clr-sc: #f1fa8c;
  --clr-tls: #ff79c6;
  --clr-dns: #bd93f9;
  color-scheme: dark;
}

/* ── Theme: vaporwave — neon-on-twilight, hot-pink ish ─────── */
body.theme-vaporwave {
  --bg: #1a0b2e;
  --bg-elev: #251047;
  --fg: #f4e8ff;
  --muted: #b88fd9;
  --accent: #ff71ce;
  --workload: #01cdfe;
  --infra: #7a5ca8;
  --rule: #34185a;
  --rule-strong: #4a2480;
  --ok: #05ffa1;
  --bad: #ff3399;
  --skipped: #fffb96;
  --clr-proc: #01cdfe;
  --clr-sc: #fffb96;
  --clr-tls: #ff71ce;
  --clr-dns: #b967ff;
  color-scheme: dark;
}

/* ── Theme: synthwave — sunset purple + hot neon ───────────── */
body.theme-synthwave {
  --bg: #2b1055;
  --bg-elev: #3b1a6e;
  --fg: #fdf6e3;
  --muted: #a787c5;
  --accent: #ff2e88;
  --workload: #02d8ff;
  --infra: #785a9e;
  --rule: #4a2e7a;
  --rule-strong: #5e3b94;
  --ok: #00ffb0;
  --bad: #ff2e88;
  --skipped: #ffd400;
  --clr-proc: #02d8ff;
  --clr-sc: #ffd400;
  --clr-tls: #ff2e88;
  --clr-dns: #b967ff;
  color-scheme: dark;
}

/* ── Theme: synth84 — Robb Owen's Synthwave '84 palette ────── *
 * Drawn from robb0wen/synthwave-vscode (MIT, 2019). Signature
 * mauve-grey bg with hot-pink variable colour and mustard-yellow
 * strings/storage. */
body.theme-synth84 {
  --bg: #262335;
  --bg-elev: #241b2f;
  --fg: #f4eee4;
  --muted: #848bbd;
  --accent: #ff7edb;
  --workload: #36f9f5;
  --infra: #848bbd;
  --rule: #34294f;
  --rule-strong: #463565;
  --ok: #72f1b8;
  --bad: #fe4450;
  --skipped: #fede5d;
  --clr-proc: #36f9f5;
  --clr-sc: #fede5d;
  --clr-tls: #ff8b39;
  --clr-dns: #ff7edb;
  color-scheme: dark;
}

/* ── Theme: kabukicho — Tokyo neon, plum + hot magenta ─────── *
 * Drawn from victoriadrake/kabukicho-vscode. Deep plum field,
 * hot magenta keywords, sky-cyan variables — closest to the
 * "neon vaporwave" aesthetic in the wild. */
body.theme-kabukicho {
  --bg: #1f1529;
  --bg-elev: #130e11;
  --fg: #d4cdde;
  --muted: #6071cc;
  --accent: #f92aad;
  --workload: #58c7e0;
  --infra: #495495;
  --rule: #2a2139;
  --rule-strong: #3a2d4d;
  --ok: #72f1b8;
  --bad: #fe4450;
  --skipped: #fede5d;
  --clr-proc: #58c7e0;
  --clr-sc: #fede5d;
  --clr-tls: #f92aad;
  --clr-dns: #b141f1;
  color-scheme: dark;
}

/* ── Theme: galax — near-black plum + rose ─────────────────── *
 * Drawn from DanielLvovsky/GalaxTheme. The darkest of the
 * vapor-family palettes; rose cursor + deep purple keywords on
 * an almost-black plum field. */
body.theme-galax {
  --bg: #09040e;
  --bg-elev: #180f1d;
  --fg: #f0eff4;
  --muted: #546e7a;
  --accent: #f72585;
  --workload: #f72585;
  --infra: #546e7a;
  --rule: #1a1224;
  --rule-strong: #2a1b3a;
  --ok: #aaffaa;
  --bad: #d7335c;
  --skipped: #ffcc66;
  --clr-proc: #f72585;
  --clr-sc: #ffcc66;
  --clr-tls: #850ad6;
  --clr-dns: #4895ef;
  color-scheme: dark;
}

/* ── Theme: sand — warm earth tones ────────────────────────── */
body.theme-sand {
  --bg: #f7f3ee;
  --bg-elev: #ffffff;
  --fg: #2c2416;
  --muted: #7a6e5e;
  --accent: #c06040;
  --workload: #c06040;
  --infra: #8a7e70;
  --rule: #e0d8cc;
  --rule-strong: #c8bfb0;
  --ok: #6a8c3c;
  --bad: #c44040;
  --skipped: #b08028;
  --clr-proc: #c06040;
  --clr-sc: #808020;
  --clr-tls: #a04068;
  --clr-dns: #406898;
  color-scheme: light;
}

/* ── Theme: light — jewel tones ────────────────────────────── */
body.theme-light {
  --bg: #f5f6f8;
  --bg-elev: #ffffff;
  --fg: #1a1d24;
  --muted: #5c6370;
  --accent: #2850a0;
  --workload: #c03050;
  --infra: #7c8490;
  --rule: #d8dce2;
  --rule-strong: #bcc2cc;
  --ok: #1a9a5c;
  --bad: #d04040;
  --skipped: #b8860b;
  --clr-proc: #c03050;
  --clr-sc: #207840;
  --clr-tls: #2850a0;
  --clr-dns: #c07020;
  color-scheme: light;
}

* { box-sizing: border-box; }
html, body { background: var(--bg); }
body {
  margin: 0;
  font: 15px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  color: var(--fg);
  min-height: 100vh;
  /* Top padding == --header-h so body content starts exactly where
   * the fixed header ends. Sticky elements then pin flush. */
  padding: var(--header-h) 2rem 2rem;
}
main {
  margin: 0 auto;
}

/* ── Fixed top header ──────────────────────────────────────── */
.top-header {
  position: fixed;
  inset: 0 0 auto 0;
  background: var(--bg-elev);
  padding: .4rem 1rem;
  z-index: 100;
  font-size: 12px;
  display: flex;
  flex-direction: column;
  gap: .2rem;
  /* Pin height so it always matches body padding-top; if either row's
   * content is too wide, it clips at the right edge rather than wrapping
   * to a third line and overlapping pinned sticky elements. */
  height: var(--header-h);
  overflow: visible;
}
.top-header .brand-row {
  /* 1fr | auto | 1fr — the centre cell holds the run picker
   * and stays geometrically centred in the header regardless
   * of how wide the left brand cluster or the right theme
   * button gets. Each `1fr` side absorbs equal slack, so
   * cycling themes can't drift the picker. */
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: baseline;
  gap: .35rem;
  white-space: nowrap;
  min-width: 0;
}
.top-header .brand-row .brand-left {
  display: flex;
  align-items: baseline;
  gap: .35rem;
  justify-self: start;
  min-width: 0;
}
.top-header .brand-row .brand-right {
  display: flex;
  align-items: baseline;
  justify-self: end;
}
.top-header .run-row {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: .35rem;
  flex-wrap: nowrap;
  white-space: nowrap;
  min-width: 0;
}
.top-header .brand-row { color: var(--muted); }
.top-header .brand { color: var(--fg); font-weight: 600; }
/* `a.sha` (links to the source commit on GitHub) */
.top-header .sha { color: var(--accent); text-decoration: none; }
.top-header .sep { color: var(--rule-strong); }
.top-header .run-num { color: var(--muted); }

/* ── Fixture picker (custom dropdown) ────────────────────── */
.fixture-picker {
  position: relative;
  display: inline-block;
}
.fixture-toggle {
  background: var(--bg-elev);
  color: var(--fg);
  border: 1px solid var(--rule-strong);
  border-radius: 3px;
  padding: 0 6px;
  font-size: 12px;
  cursor: pointer;
  /* `height` + `line-height: 1` + `align-items: center` puts the
   * label glyphs in the geometric middle of the button. Falling
   * back to `align-items: baseline` (the old value) made the text
   * sit closer to the bottom of the box because the mono font's
   * cap-height is below the line-box midpoint. */
  height: 24px;
  box-sizing: border-box;
  line-height: 1;
  user-select: none;
  /* Constrain the toggle so a long live-source label (e.g. a
   * full repo path) can't push the theme cycle button off the
   * right edge of the header on mobile. The internal label
   * truncates with ellipsis; the chevron stays visible. */
  max-width: 40vw;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  overflow: hidden;
  white-space: nowrap;
}
.fixture-toggle-label {
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
.fixture-toggle:hover { border-color: var(--fg); }
.fixture-overlay {
  position: fixed;
  inset: 0;
  z-index: 199;
}
.fixture-menu {
  position: absolute;
  top: 100%;
  /* Centre the dropdown horizontally under the picker. The
   * picker itself sits in the header's centre grid cell, so the
   * menu reads as belonging to the same vertical axis instead
   * of hanging off to one side. */
  left: 50%;
  right: auto;
  transform: translateX(-50%);
  margin: 2px 0 0;
  padding: 2px 0;
  list-style: none;
  background: var(--bg-elev);
  border: 1px solid var(--rule-strong);
  border-radius: 3px;
  z-index: 200;
  min-width: 100%;
  white-space: nowrap;
  /* Long histories overflow vertically — cap at 70vh and let
   * the dropdown scroll so the settings entry at the bottom
   * stays reachable. */
  max-height: 70vh;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.fixture-option {
  padding: 2px 8px;
  font-size: 11px;
  cursor: pointer;
  color: var(--muted);
}
.fixture-option:hover { background: var(--rule); color: var(--fg); }
.fixture-option.active { color: var(--fg); font-weight: 600; }
/* Bundled fixtures: run-# label on the left, "(demo)" tag on
 * the right. Same layout shape as `.fixture-option.live`. */
.fixture-option.bundled {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
}
.fixture-option.bundled .fixture-label {
  flex: 0 0 auto;
  white-space: nowrap;
}
.fixture-option.bundled .fixture-tag {
  flex: 0 0 auto;
  font-size: 9px;
  letter-spacing: .02em;
  white-space: nowrap;
  padding-left: .75rem;
}
/* Section labels in the dropdown ("live" / "github"). Not
 * selectable; just visual grouping above the rows that follow. */
.fixture-divider {
  padding: 4px 8px 0;
  font-size: 9px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted);
  border-top: 1px solid var(--rule);
  margin-top: 2px;
  pointer-events: none;
  /* Stay on one line (no wrapping mid-slug). If the divider text
   * is the menu's widest row, it forces the menu wider via
   * max-content — but capped at `max-width: 92vw` on the menu,
   * past which the divider ellipsizes with `…` instead of
   * triggering an in-menu horizontal scrollbar. Pathological case:
   * a ~50-char `org/repo` on a 375px viewport. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.fixture-divider:first-child { border-top: 0; margin-top: 0; }
/* The `live · {repo}` divider doubles as a refresh affordance.
 * Override the base divider's `pointer-events: none` so the inline
 * button is clickable, and use flex so the button sits at the row's
 * right edge with the label ellipsizing if the repo name overflows. */
.fixture-divider.live-divider {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  pointer-events: auto;
  overflow: visible; /* let the button render fully; label keeps its own clipping */
}
.fixture-divider.live-divider .fixture-divider-label {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Icon-only action buttons that live inside the live divider —
 * settings (cog) on the left, refresh (↻) on the right. Identical
 * outer shape; only the spin animation is refresh-specific. */
.fixture-refresh-btn,
.fixture-settings-btn {
  flex: 0 0 auto;
  background: var(--bg);
  border: 1px solid var(--rule-strong);
  border-radius: 3px;
  color: var(--muted);
  cursor: pointer;
  padding: 3px 6px;
  display: inline-flex;
  align-items: center;
  line-height: 0;
}
@media (hover: hover) {
  .fixture-refresh-btn:hover,
  .fixture-settings-btn:hover { color: var(--fg); border-color: var(--fg); }
}
.fixture-refresh-btn:active,
.fixture-settings-btn:active { color: var(--accent); border-color: var(--accent); }
.fixture-refresh-btn svg,
.fixture-settings-btn svg { width: 12px; height: 12px; display: block; }
.fixture-refresh-btn.spinning svg {
  animation: fixture-refresh-spin .7s linear infinite;
}
@keyframes fixture-refresh-spin { to { transform: rotate(360deg); } }
.fixture-option.live {
  display: flex;
  /* Use margin on the date (below) for label↔date spacing instead
   * of `gap`. Firefox doesn't always include flex `gap` in the
   * container's intrinsic / max-content size, so the menu was
   * sized to `label + date` without the gap and the date's last
   * couple of chars hung off the right edge of the menu, causing
   * a horizontal scrollbar on desktop. Margins on flex items DO
   * contribute to intrinsic size in every browser. */
  align-items: baseline;
  justify-content: space-between;
  min-width: 0;
  color: var(--fg);
}
.fixture-option.live .fixture-label {
  /* Don't allow the label to shrink — the menu container then
   * has to be wide enough for the row's intrinsic width. Relying
   * on `width: max-content` on .fixture-menu alone didn't work
   * cross-browser (Waterfox/Firefox computed the abs-positioned
   * flex container's shrink-to-fit using the shrunk children
   * rather than max-content, causing truncation on desktop).
   * With flex-shrink:0 here, every browser sizes the row to its
   * unwrapped content and the menu grows to fit. */
  flex: 0 0 auto;
  white-space: nowrap;
}
.fixture-option.live .fixture-date {
  flex: 0 0 auto;
  font-size: 9px;
  letter-spacing: .02em;
  white-space: nowrap;
  /* Padding rather than margin / gap so the spacing is INSIDE the
   * date's box, which guarantees it's counted in every browser's
   * max-content calculation for the flex container. Firefox at
   * minimum has known quirks with `gap` (and possibly margin) on
   * absolutely-positioned flex containers; padding on the item
   * itself sidesteps the entire question. */
  padding-left: .75rem;
}
/* Row "just appeared in this refresh" flash. Warm yellow bg
 * tint that peaks at insertion and fades to transparent over
 * 1.2s. `both` keeps the end state stable so the row settles
 * cleanly to its baseline. */
.fixture-option.live.is-new {
  animation: fixture-new-flash 1.2s ease-out both;
}
/* Two-stop linear fade, matching the step-new-flash shape — the
 * `ease-out` curve does the perceptual easing, extra inner
 * keyframes would just introduce visible kinks in the rate of
 * change. */
@keyframes fixture-new-flash {
  from { background-color: rgb(255 222 122 / 0.30); }
  to   { background-color: transparent; }
}
/* Size the dropdown to its widest non-wrapping row, capped at 92vw
 * so it never pushes past the right edge on mobile. Without
 * `width: max-content`, the menu was inheriting `min-width: 100%`
 * of the (tiny) toggle on desktop, which forced the live rows'
 * `.fixture-label` to ellipsize even when the viewport had plenty
 * of room. On mobile, the 92vw cap still kicks in and the existing
 * flex-shrink + ellipsis rules handle overflow. */
.fixture-menu {
  width: max-content;
  max-width: 92vw;
  /* Reserve a vertical-scrollbar gutter unconditionally. Without
   * this, `width: max-content` sized the menu to exactly its row
   * content; once the menu hit `max-height: 70vh` and a vertical
   * scrollbar appeared, the scrollbar took its width out of the
   * content area and pushed the last chars of the date off the
   * right edge → horizontal scrollbar. With `stable`, the gutter
   * is reserved up-front and included in max-content, so adding
   * the scrollbar later doesn't change the content area. */
  scrollbar-gutter: stable;
}
.fixture-option.disabled {
  cursor: default;
  opacity: .6;
}
.fixture-option.disabled:hover { background: transparent; color: var(--muted); }
/* Clickable hint that replaces a verbose error row in the picker.
 * Stays in the bad-colour family so the failure is recognisable at
 * a glance, but uses hover affordances so the user knows it leads
 * somewhere (settings modal). */
.fixture-option.gh-err-hint {
  color: var(--bad);
  font-style: italic;
  cursor: pointer;
}
.fixture-option.gh-err-hint:hover {
  background: var(--rule);
  color: var(--bad);
}

/* ── Loading overlay (live-run fetch in flight) ──────────── *
 * Renders centered on the viewport with a dimmed backdrop;
 * sits above the main content (z 50) but below the fixed
 * header (z 100) so the picker stays accessible — user can
 * pick a different run while a slow fetch is in flight. The
 * main element gets `.loading-dim` for a subtle desaturate so
 * it reads as "background context" instead of "interactive UI".
 */
main.loading-dim {
  filter: blur(1.5px) saturate(.5);
  pointer-events: none;
  transition: filter .15s ease-out;
}
.loading-overlay {
  position: fixed;
  inset: var(--header-h, 2.2rem) 0 0 0;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: .9rem;
  background: color-mix(in srgb, var(--bg) 65%, transparent);
  backdrop-filter: blur(2px);
  z-index: 50;
  animation: overlay-fade-in .15s ease-out;
}
@keyframes overlay-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
/* The loading overlay always renders a single card. In error
 * mode the card's border turns red and the error-message slot
 * (always present, reserved as empty during a clean load) is
 * filled with the failure text. A backdrop tap dismisses;
 * dismiss only applies in error mode. */
.loading-overlay.error-mode { cursor: pointer; }
.loading-card {
  cursor: default;
  background: var(--bg-elev);
  /* Border + glow reflect the load state: blue while running
   * (with a subtle breathing pulse to reinforce "active"),
   * green during the success linger, red on error. Color and
   * glow transition smoothly so a state change reads as one
   * visual beat rather than a hard snap. The pulse runs on
   * box-shadow only — border-color stays steady so the card's
   * outline doesn't visibly waver. */
  border: 1px solid var(--workload);
  border-radius: 6px;
  padding: 1rem 1.1rem;
  /* Lock the card to a single size for BOTH loading and error
   * states. width pins the horizontal dimension; min-height
   * gives the card a floor that's tall enough for the step
   * list. The error-message slot reserves its own line via
   * `min-height: 1.4em` so the two states land flush. */
  width: min(420px, 90vw);
  min-height: 250px;
  display: flex;
  flex-direction: column;
  gap: .6rem;
  box-sizing: border-box;
  box-shadow: 0 0 22px color-mix(in srgb, var(--workload) 14%, transparent);
  /* Outward glow that breathes — blur radius grows + intensity
   * lifts together, so the halo visibly expands and contracts
   * rather than just flickering at constant size. 1.5s cycle
   * gives ~2 breaths during a typical 2-3s load. */
  animation: loading-card-pulse 1.5s ease-in-out infinite;
  transition: border-color .25s ease-out;
}
@keyframes loading-card-pulse {
  0%, 100% { box-shadow: 0 0 16px color-mix(in srgb, var(--workload)  8%, transparent); }
  50%      { box-shadow: 0 0 32px color-mix(in srgb, var(--workload) 22%, transparent); }
}
.loading-overlay.success-mode .loading-card {
  border-color: var(--ok);
  box-shadow: 0 0 22px color-mix(in srgb, var(--ok) 22%, transparent);
  /* Settle to a steady glow — the pulse only runs while a load
   * is in flight, so success/error states freeze it. */
  animation: none;
}
.loading-overlay.error-mode .loading-card {
  border-color: var(--bad);
  box-shadow: 0 0 22px color-mix(in srgb, var(--bad) 22%, transparent);
  animation: none;
}
.loading-card-head {
  display: flex;
  align-items: center;
  gap: .6rem;
  /* Lock the head to the spinner's diameter so removing the
   * spinner in error mode doesn't shrink the head and slide
   * everything below it. The title vertical-centers in the
   * same 18px box the loading text + spinner occupied. */
  min-height: 18px;
}
.load-error-title {
  /* Match `.loading-text` font properties so the head reads as
   * the same element across loading/error states — only the
   * color and the text content swap. Combined with the spinner
   * sitting on the right (so the head's left edge stays flush
   * in both states), the title stays put when a load fails. */
  color: var(--bad);
  font: 500 13px/1 ui-monospace, monospace;
  letter-spacing: .02em;
}
.load-error-message {
  color: var(--fg);
  word-break: break-word;
  white-space: pre-wrap;
  /* Reserve one line of vertical space even when empty so the
   * loading card lands at the same height in both states — on
   * success the slot is rendered empty (aria-hidden) and on
   * failure it carries the error text. Assumes the typical
   * error fits one line at mobile width; longer errors will
   * wrap and grow the card, which we accept as a rare case.
   * The line-height matches the min-height exactly so a filled
   * slot lands at the same height as the empty placeholder
   * (body's inherited 1.55 would have made the filled state
   * ~2px taller, slipping every element below it down). */
  min-height: 1.4em;
  line-height: 1.4;
}
.loading-spinner {
  width: 18px;
  height: 18px;
  border: 2px solid var(--rule);
  border-top-color: var(--accent);
  border-right-color: var(--accent);
  border-radius: 50%;
  animation: loading-spin .9s linear infinite;
  flex: 0 0 auto;
  /* Sits at the right edge of the head while the loading text
   * stays flush left. On error the head only contains the title
   * (no spinner), and the title remains in the same left position
   * — so the failure transition doesn't visually jump the text. */
  margin-left: auto;
}
@keyframes loading-spin {
  to { transform: rotate(360deg); }
}
.loading-text {
  color: var(--fg);
  font: 500 13px/1 ui-monospace, monospace;
  letter-spacing: .02em;
}
/* Per-step progress list under the spinner. Each row shows a
 * status glyph (·/✓/✗), the step label, and the elapsed time.
 * The time column is monospaced + right-aligned + stable-width
 * so it doesn't jitter as digits grow.
 */
.load-steps {
  list-style: none;
  margin: 0;
  padding: .35rem 0 0;
  min-width: 220px;
}
.load-step {
  display: grid;
  grid-template-columns: 1rem 1fr auto;
  gap: .55rem;
  align-items: baseline;
  font-size: 11px;
  padding: 2px 0;
  color: var(--muted);
  transition: color .15s ease-out;
}
.load-step-glyph {
  display: inline-block;
  width: 1rem;
  text-align: center;
  font-weight: 600;
}
.load-step.pending .load-step-glyph { color: var(--rule-strong); }
.load-step.running .load-step-glyph {
  /* Soft pulse while the step is in flight. */
  color: var(--accent);
  animation: load-step-pulse 1.1s ease-in-out infinite;
}
.load-step.done {
  color: var(--fg);
}
.load-step.done .load-step-glyph {
  color: var(--ok);
}
.load-step.failed {
  color: var(--bad);
}
.load-step.failed .load-step-glyph {
  color: var(--bad);
}
.load-step-time {
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  /* Time text sits at 13px (.mono) while the row's other cells
   * are 11px. Letting line-height inherit means the time span's
   * larger font expands the row's line box the moment a step
   * gains a duration string, growing each row by ~3px on its
   * Pending → Running transition. Lock the time's line-height
   * to match the 11px font so the row stays the same height
   * whether the cell is empty or filled. */
  line-height: 13px;
}
.load-step.done .load-step-time,
.load-step.failed .load-step-time {
  color: var(--fg);
}
@keyframes load-step-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: .35; }
}

/* ── GitHub-load modal ───────────────────────────────────── */
.gh-modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,.55);
  z-index: 300;
}
.gh-modal {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(90vw, 640px);
  max-height: 88vh;
  overflow-y: auto;
  background: var(--bg-elev);
  border: 1px solid var(--rule-strong);
  border-radius: 6px;
  z-index: 301;
  box-shadow: 0 6px 24px rgba(0,0,0,.5);
}
.gh-modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: .7rem 1rem .5rem;
  border-bottom: 1px solid var(--rule);
}
.gh-modal-head h3 { margin: 0; font-size: 14px; font-weight: 600; }
.gh-modal-close {
  background: transparent;
  color: var(--muted);
  border: 0;
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  padding: 0 .25rem;
}
.gh-modal-close:hover { color: var(--fg); }
.gh-modal-body { padding: .6rem 1rem 1rem; display: flex; flex-direction: column; gap: 1rem; }
.gh-section { display: flex; flex-direction: column; gap: .35rem; }
.gh-label {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--muted);
}
.gh-token-row { display: flex; flex-direction: column; gap: .35rem; }
.gh-token-buttons { display: flex; gap: .25rem; flex-wrap: wrap; align-items: stretch; }
.gh-token {
  /* Full-width inside the column-stacked token row — buttons sit
   * on their own row below via `.gh-token-buttons`. */
  width: 100%;
  min-width: 0;
  background: var(--bg);
  border: 1px solid var(--rule-strong);
  border-radius: 3px;
  padding: 3px 6px;
  color: var(--fg);
  font-size: 11px;
}
.gh-token:focus { outline: none; border-color: var(--accent); }
.gh-btn {
  background: transparent;
  border: 1px solid var(--rule-strong);
  color: var(--muted);
  padding: 3px 9px;
  border-radius: 3px;
  font-size: 11px;
  cursor: pointer;
  font-family: ui-monospace, monospace;
}
.gh-btn.primary { color: var(--fg); border-color: var(--accent); }
.gh-btn.copy { min-width: 52px; }
.gh-btn.copy .copy-stack {
  display: inline-grid;
  grid-template-areas: "stack";
  align-items: center;
  justify-items: center;
}
.gh-btn.copy .copy-label,
.gh-btn.copy .copy-tick {
  grid-area: stack;
  transition: opacity 320ms ease, transform 320ms ease, filter 320ms ease;
}
.gh-btn.copy .copy-tick {
  display: inline-flex;
  align-items: center;
  line-height: 0;
  opacity: 0;
  transform: scale(1.4);
  filter: blur(3px);
}
.gh-btn.copy.is-copied .copy-label,
.gh-btn.copy.is-copied .copy-tick {
  transition: opacity 260ms ease, transform 260ms ease, filter 260ms ease;
}
.gh-btn.copy.is-copied .copy-label {
  opacity: 0;
  transform: scale(0.6);
  filter: blur(3px);
}
.gh-btn.copy.is-copied .copy-tick {
  opacity: 1;
  transform: scale(1);
  filter: blur(0);
}
.gh-btn:disabled { opacity: .5; cursor: default; }
/* Gate :hover behind `@media (hover: hover)` so it only applies on
 * pointing-device-equipped clients. Mobile Safari/Chrome treat tap
 * as setting :hover until the user taps somewhere else, which made
 * the brighter border (`var(--fg)`) linger after the .just-refreshed
 * flash → the visible "white border" the user reported. With the
 * media query, taps on mobile never enter :hover; mouse/trackpad
 * users still get the hover affordance. */
@media (hover: hover) {
  .gh-btn:hover { border-color: var(--fg); color: var(--fg); }
  .gh-btn:disabled:hover { border-color: var(--rule-strong); color: var(--muted); }
}
/* Kill the focus ring on tap (mobile leaves :focus on tapped
 * buttons), keep it for keyboard via :focus-visible. */
.gh-btn { -webkit-tap-highlight-color: transparent; }
.gh-btn:focus { outline: none; }
.gh-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.gh-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid var(--rule);
  border-radius: 3px;
  max-height: 220px;
  overflow-y: auto;
}
/* `<ul class="gh-list">` is always in the DOM so its keyed <For>
 * children survive across repos changes; this class hides it while
 * the placeholder / loading / err state is rendered above. */
.gh-list.hidden { display: none; }
.gh-row {
  padding: 4px 8px;
  display: flex;
  gap: .5rem;
  align-items: baseline;
  cursor: pointer;
  border-bottom: 1px solid var(--rule);
  min-width: 0;
}
.gh-row:last-child { border-bottom: 0; }
.gh-row:hover { background: var(--rule); }
.gh-row.active { background: var(--rule); color: var(--fg); }
/* Fixed-width columns flank the variable-width message in the
 * commit-list rows. sha + date keep their natural width; the
 * message gets the leftover space and truncates with ellipsis
 * so a long subject doesn't push the date off the row. */
.gh-row-sha { flex: 0 0 auto; }
.gh-row-date { flex: 0 0 auto; white-space: nowrap; }
.gh-row-msg {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.gh-err { color: var(--bad); }

/* Error banner inside the modal's repository section. The picker
 * dropdown only shows a short hint pointing here; this is where
 * the actual GitHub error message lives. */
.gh-runs-err {
  border: 1px solid var(--bad);
  border-radius: 4px;
  padding: .5rem .65rem;
  display: flex;
  flex-direction: column;
  gap: .25rem;
  background: color-mix(in srgb, var(--bad) 8%, transparent);
}
.gh-runs-err .gh-err-head { color: var(--bad); font-weight: 600; font-size: 13px; }
.gh-runs-err .gh-err-body {
  color: var(--fg);
  word-break: break-word;
  white-space: pre-wrap;
}
.gh-runs-err .gh-err-hint-line { color: var(--fg); }

/* In-flight indicator while a runs fetch is pending. Sits in the
 * same slot as the error banner; a small left-side spinner gives
 * the user a "GitHub is being contacted" signal so the modal
 * doesn't feel frozen between repo-click and result. */
.gh-runs-loading {
  display: flex;
  align-items: center;
  gap: .5rem;
  color: var(--muted);
}
.gh-runs-spinner {
  width: .85rem;
  height: .85rem;
  border: 2px solid var(--rule);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: gh-runs-spin .8s linear infinite;
  flex: 0 0 auto;
}
@keyframes gh-runs-spin { to { transform: rotate(360deg); } }

/* ── Run info row (inside main, below header) ────────────── */
.run-info-row {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: .35rem;
  margin-bottom: .4rem;
  font-size: 13px;
}
.run-info-row .repo { color: var(--accent); font-weight: 600; }
.run-info-row .sep { color: var(--rule-strong); }
.run-info-row .run-num { color: var(--muted); }

/* ── Theme cycle button (in header) ────────────────────────── */
/* Prefetch progress chip inside the GitHub-source modal.
 * "N/M · 5.4s" while running, "N/M cached" once complete.
 */
.prefetch-chip {
  flex-shrink: 0;
  padding: 0 6px;
  border-radius: 3px;
  font-size: 10px;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  line-height: 1.6;
  font-variant-numeric: tabular-nums;
  letter-spacing: .02em;
}
.prefetch-chip.done {
  color: var(--ok);
  background: color-mix(in srgb, var(--ok) 12%, transparent);
  border-color: color-mix(in srgb, var(--ok) 35%, transparent);
}

/* Status + clear-cache row under the prefetch checkbox. */
.gh-prefetch-status {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
  margin-top: .25rem;
}

/* "Prefetch all runs" checkbox in the GitHub-source modal.
 * `align-items: center` (not baseline) — checkboxes don't have a
 * meaningful text baseline, so baseline alignment renders the box
 * floating high relative to the label. Center-aligns the box's
 * geometric centre with the label's line-box centre. */
.gh-toggle {
  display: flex;
  align-items: center;
  gap: .45rem;
  font-size: 12px;
  cursor: pointer;
  user-select: none;
}
.gh-toggle input[type="checkbox"] {
  accent-color: var(--accent);
  cursor: pointer;
  flex-shrink: 0;
}

.theme-cycle {
  flex-shrink: 0;
  background: transparent;
  border: 1px solid var(--rule-strong);
  color: var(--muted);
  padding: 0 6px;
  border-radius: 3px;
  font-family: ui-monospace, monospace;
  font-size: 12px;
  cursor: pointer;
  letter-spacing: .03em;
  /* Match .fixture-toggle so the two header buttons sit at the
   * same height and the inner label is centred (rather than
   * baseline-aligned, which left visible extra space at the top). */
  height: 24px;
  box-sizing: border-box;
  line-height: 1;
  display: inline-flex;
  align-items: center;
}
.theme-cycle:hover {
  border-color: var(--fg);
  color: var(--fg);
}
.theme-icon {
  margin-right: 3px;
  font-size: 11px;
}

/* ── Verify badge ──────────────────────────────────────────── */
.verify-badge {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .3rem .6rem;
  border-radius: 4px;
  font-size: 12px;
  /* Hard-lock the badge to a whole-pixel single-line height so
   * surrounding content can't shift by sub-pixel amounts when the
   * signed/unsigned/failed variants swap. min-height (not height)
   * keeps the mobile wrap behaviour: when content overflows the
   * row, flex-wrap moves it to a new line and the badge grows
   * past the minimum. */
  line-height: 18px;
  min-height: 30px;
  box-sizing: border-box;
  border: 1px solid;
  flex-wrap: wrap;
}
@media (max-width: 560px) {
  .verify-badge {
    flex-direction: column;
    align-items: flex-start;
    gap: .2rem;
    max-width: 100%;
    min-width: 0;
  }
  .verify-badge > * {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
  }
}
.verify-badge.verified {
  border-color: #3b8;
  color: #3b8;
  background: color-mix(in srgb, #3b8 8%, transparent);
}
.verify-badge.failed {
  border-color: #e55;
  color: #e55;
  background: color-mix(in srgb, #e55 8%, transparent);
}
.verify-badge.unsigned {
  border-color: var(--rule-strong);
  color: var(--muted);
}
.verify-icon {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
}
.verify-icon::before {
  content: "";
  display: inline-block;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  flex-shrink: 0;
}
.verified .verify-icon::before {
  background: #3b8;
  /* CSS checkmark: two white lines forming an "L" rotated 45° */
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.5 8.5 7 11l4.5-5' fill='none' stroke='%23fff' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.failed .verify-icon::before {
  background: #e55;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M5 5l6 6M11 5l-6 6' fill='none' stroke='%23fff' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");
}
.unsigned .verify-icon::before {
  border: 1.5px solid var(--muted);
  background: transparent;
}
.verify-badge { cursor: pointer; user-select: none; }
/* Expand affordance — only on the .verified variant, since
 * .unsigned has no detail to reveal and .failed currently
 * isn't a click-to-expand. Same CSS-border chevron pattern as
 * `details > summary::before` so the click affordance reads
 * the same way across the viewer. Rotates from -45° (pointing
 * right) to 45° (pointing down) when the badge is expanded.
 */
/* Chevron only at <560px, where `.verify-detail` is hidden
 * by default (see the @media rule above) and the click is
 * the only way to reveal the detail spans. At ≥560px the
 * badge already shows everything inline so the chevron would
 * be noise. Pinned absolutely to the first row so it doesn't
 * end up at the bottom of the badge's column-flex when
 * expanded. */
@media (max-width: 560px) {
  .verify-badge.verified {
    position: relative;
  }
  .verify-badge.verified::after {
    content: "";
    position: absolute;
    top: 13px;
    right: .6rem;
    width: 3.5px;
    height: 3.5px;
    border-right: 1.5px solid currentColor;
    border-bottom: 1.5px solid currentColor;
    transform: rotate(-45deg);
    transition: transform 150ms ease;
    opacity: .7;
  }
  .verify-badge.verified.expanded::after {
    transform: rotate(45deg);
  }
  /* Hide the version chip while expanded — the badge grows
   * tall on the column-flex narrow layout and the chip would
   * otherwise wrap to a new line. ≥560px the chip stays
   * always-visible pinned to the right of the row. */
  .verify-badge.expanded ~ .version-chip {
    display: none;
  }
}
.verify-detail {
  opacity: .7;
}

/* Verify badge + version chip sit on the same row above the
 * overview grid. Badge fills the row; chip stays at its
 * intrinsic width pinned to the right but stretches
 * vertically so its box matches the badge's height exactly
 * (the verify-badge's `min-height: 30px` floor drives the
 * row height). No flex-wrap on the row — chip always rides
 * the same line as the badge. flex-basis: 0 on the badge so
 * it grows from zero rather than intrinsic content width;
 * that's what keeps the chip from being pushed to a second
 * line at mid-width viewports (e.g. 700px landscape) where
 * the badge's intrinsic content was wider than the row could
 * accommodate. */
.verify-row {
  display: flex;
  gap: .5rem;
  align-items: stretch;
}
.verify-row > .verify-badge {
  flex: 1 1 0;
  min-width: 0;
}
.version-chip {
  flex: 0 0 auto;
  padding: 0 8px;
  border-radius: 3px;
  font-size: 10.5px;
  color: var(--muted);
  background: var(--bg-elev);
  border: 1px solid var(--rule-strong);
  font-variant-numeric: tabular-nums;
  letter-spacing: .02em;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
@media (max-width: 560px) {
  .verify-detail { display: none; }
  .verify-badge.expanded .verify-detail { display: inline; }
}
.verify-key {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
  max-width: 100%;
}

/* ── Overview section ───────────────────────────────────────── */
.overview-section {
  margin-top: .6rem;
  display: flex;
  flex-direction: column;
  gap: .4rem;
  min-width: 0;
  overflow: hidden;
}
.overview-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
  gap: .2rem .8rem;
  padding: .6rem .8rem;
  background: var(--bg-elev);
  border-radius: 6px;
  border: 1px solid var(--rule);
}
.overview-item {
  display: flex;
  gap: .5rem;
  align-items: baseline;
  font-size: 12.5px;
}
.overview-label {
  color: var(--muted);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .06em;
  flex-shrink: 0;
  min-width: 5ch;
}
.overview-value {
  color: var(--fg);
  min-width: 0;
}
.overview-counts {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem .8rem;
  align-items: center;
  padding: .5rem .8rem;
  background: var(--bg-elev);
  border-radius: 6px;
  border: 1px solid var(--rule);
  font-size: 12px;
}
.overview-count {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
}

@keyframes flash-a {
  0%   { box-shadow: 0 0 3px 0px currentColor; transform: scale(1.03); }
  100% { box-shadow: 0 0 0 0 transparent; transform: scale(1); }
}
@keyframes flash-b {
  0%   { box-shadow: 0 0 3px 0px currentColor; transform: scale(1.03); }
  100% { box-shadow: 0 0 0 0 transparent; transform: scale(1); }
}
.flash-a .step-badge.changed {
  animation: flash-a 1s ease-out;
}
.flash-b .step-badge.changed {
  animation: flash-b 1s ease-out;
}

/* ── Typography ─────────────────────────────────────────────── */
h1 { font-size: 1.55rem; font-weight: 600; margin: 0 0 .4rem; letter-spacing: -0.01em; }
h1 span { color: var(--accent); }
h2 {
  font-size: .85rem;
  font-weight: 600;
  margin: 1.6rem 0 .4rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
h2 .muted, h2 .small {
  text-transform: none;
  letter-spacing: 0;
  font-weight: 400;
}
h3.orphan-h3 {
  font-size: 11.5px;
  font-weight: 600;
  margin: .6rem 0 .2rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
p { margin: .6rem 0; }
.small { font-size: 12.5px; }
.muted { color: var(--muted); }
.accent { color: var(--accent); }

code, .mono {
  font-family: ui-monospace, "SF Mono", "JetBrains Mono", monospace;
  font-size: 13px;
}
code {
  background: var(--rule);
  padding: 1px 6px;
  border-radius: 4px;
}
.break {
  overflow-wrap: anywhere;
  word-break: break-word;
  min-width: 0;
}

section { margin-top: 1.4rem; }
section h2 { margin-top: 0; }

/* ── Collapsible details (default styling) ────────────────── */
details {
  margin: .2rem 0;
  border-left: 2px solid var(--rule);
  padding: 0 0 0 .6rem;
}
details > summary {
  cursor: pointer;
  font-size: .8rem;
  font-weight: 600;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  user-select: none;
  list-style: none;
  display: flex;
  align-items: center;
  gap: .3rem;
  padding: .35rem 0;
  flex-wrap: wrap;
}
details > summary::-webkit-details-marker { display: none; }
details > summary::before {
  content: "";
  display: inline-block;
  width: 3.5px;
  height: 3.5px;
  border-right: 1.5px solid var(--muted);
  border-bottom: 1.5px solid var(--muted);
  transform: rotate(-45deg);
  transition: transform 150ms ease;
  flex-shrink: 0;
  margin-right: 4px;
}
details[open] > summary::before {
  content: "";
  transform: rotate(45deg);
}
details > summary .muted {
  text-transform: none;
  letter-spacing: 0;
  font-weight: 400;
  font-size: 11.5px;
}

/* ── Metric grid (dl) ──────────────────────────────────────── */
.metric {
  display: grid;
  grid-template-columns: minmax(13ch, 22ch) 1fr;
  gap: .3rem .8rem;
  font-size: 13px;
  margin: .4rem 0;
  align-items: baseline;
}
.metric.small { font-size: 12px; }
.metric dt { color: var(--muted); }
.metric dd { margin: 0; font-variant-numeric: tabular-nums; min-width: 0; }

ul { padding-left: 1.2rem; margin: .4rem 0; }
li { margin: .2rem 0; }
.empty { padding: .4rem 0; }

/* ── Steps list (primary view) ─────────────────────────────── */
.steps-list {
  list-style: none;
  padding: 0;
  margin: .4rem 0;
}
.steps-list .step {
  margin: 0;
  border-bottom: 1px solid var(--rule);
  /* Override the global `.empty { padding: .4rem 0 }` rule (used
   * for no-data placeholder messages) so empty AND non-empty
   * steps share a baseline of zero li-level padding — keeps
   * every row's outer height equal, governed by the summary's
   * padding alone. */
  padding: 0;
}
.steps-list .step:last-child { border-bottom: 0; }
/* Empty rows: muted via opacity. The base `border-bottom: 1px
 * solid var(--rule)` stays in place, dimmed alongside the row's
 * content by the .55 opacity. Keeping the 1px border width
 * constant (no transparent-or-removed override) preserves layout
 * across the empty→active transition — nothing shifts when the
 * row's opacity flips to 1. */
.steps-list .step.empty { opacity: .55; }
/* "Just gained activity" flash. Has to target the row's <summary>
 * (not the <li>) because the summary is sticky and carries an
 * opaque `background: var(--bg-elev)` to occlude scrolling
 * content; a keyframe on the parent would be hidden behind it.
 * End state is `var(--bg-elev)` rather than `transparent` so the
 * sticky's opacity invariant survives the animation. */
.steps-list .step.is-new > details > summary {
  animation: step-new-flash 1.2s ease-out both;
}
/* Two-stop linear fade — the `ease-out` curve shapes the decay so
 * extra inner keyframes would just introduce visible kinks in the
 * rate of change (color-mix interpolation amplifies that). */
@keyframes step-new-flash {
  from { background-color: color-mix(in srgb, rgb(255 222 122) 50%, var(--bg-elev)); }
  to   { background-color: var(--bg-elev); }
}
.steps-list details {
  border-left: 0;
  padding: 0;
  margin: 0;
}
.steps-list summary {
  text-transform: none;
  letter-spacing: 0;
  font-weight: 400;
  color: var(--fg);
  font-size: 13px;
  /* Lock the outer height to --step-summary-h exactly so the
   * sticky math (first proc-tree sticky pins at `top: --header-h
   * + --step-summary-h - 1px`) matches the rendered summary's
   * bottom regardless of the system font's default line-height.
   * Without this, summaries rendered ~34.92px in Chromium /
   * ~33.4px in Firefox at the same padding, both off from a
   * single declared 34/35, leaving a gap above the first proc
   * sticky in whichever browser the declared value didn't match.
   * Explicit `line-height: 1.5` keeps the 13px font from
   * overflowing the 20.2px content area regardless of font. */
  height: var(--step-summary-h);
  padding: .4rem .35rem;
  line-height: 1.5;
  align-items: baseline;
  gap: .5rem;
  /* Single-line: same trick as the fixed header. Wrapping would make
   * the summary taller than --step-summary-h and nested proc stickies
   * (which use --step-summary-h to compute their `top:` offset) would
   * end up pinned UNDER the step summary's visual bottom, hidden by
   * the step's higher z-index. Force one line; clip long labels. */
  flex-wrap: nowrap;
  overflow: hidden;
  white-space: nowrap;
  min-width: 0;
  /* Pin the open step's summary just under the fixed top-header so
   * deep scrolling inside a step's process tree always shows which
   * step you're in. When the step closes, the summary detaches and
   * scrolls normally. The `- 1px` extends the sticky 1px up into
   * the header's zone, hidden behind the header's higher z-index;
   * absorbs any subpixel rounding gap on high-DPR displays. */
  position: sticky;
  top: calc(var(--header-h, 3.4rem) - 1px);
  /* Use the same bg as the fixed header so the pinned region reads as
   * one continuous surface, no tonal "edge" at the boundary. */
  background: var(--bg-elev);
  z-index: 5;
  border-bottom: 1px solid var(--rule);
}
.steps-list summary .step-label {
  /* Label gets the leftover space and truncates with ellipsis. */
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.steps-list .step > details[open] > summary {
  /* Slight visual cue when the summary is the active sticky element. */
  border-bottom-color: var(--rule-strong);
}
.steps-list summary::before {
  content: "";
  display: inline-block;
  width: 3.5px;
  height: 3.5px;
  border-right: 1.5px solid var(--muted);
  border-bottom: 1.5px solid var(--muted);
  transform: rotate(-45deg) translateY(-1px);
  transition: transform 150ms ease;
  flex-shrink: 0;
  margin-right: 2px;
}
.steps-list details[open] > summary::before {
  content: "";
  transform: rotate(45deg) translateY(-1px);
}
.steps-list summary .step-idx {
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}
.steps-list summary .step-label {
  flex: 1;
  min-width: 0;
  font-weight: 500;
}
.steps-list summary .step-counts {
  flex-shrink: 0;
  font-size: 10px;
  letter-spacing: .04em;
  display: inline-flex;
  gap: .25rem;
}
.step-badge {
  padding: 0 4px;
  border-radius: 3px;
  font-weight: 600;
  letter-spacing: .04em;
}
.step-badge.proc-badge {
  background: rgba(108,160,220,.12);
  color: var(--clr-proc);
  border: 1px solid rgba(108,160,220,.25);
}
.step-badge.sc-badge {
  background: rgba(217,179,128,.12);
  color: var(--clr-sc);
  border: 1px solid rgba(217,179,128,.25);
}
.step-badge.tls-badge {
  background: rgba(94,184,184,.12);
  color: var(--clr-tls);
  border: 1px solid rgba(94,184,184,.25);
}
.step-badge.dns-badge {
  background: rgba(167,139,218,.12);
  color: var(--clr-dns);
  border: 1px solid rgba(167,139,218,.25);
}
.step-badge.zero {
  opacity: .3;
}
.steps-list summary .step-dur {
  color: var(--muted);
  flex-shrink: 0;
  font-variant-numeric: tabular-nums;
}
.steps-list summary .step-result {
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: .04em;
  padding: 1px 6px;
  border-radius: 3px;
  flex-shrink: 0;
}
.steps-list summary .step-result.ok { background: rgba(122,208,163,.15); color: var(--ok); border: 1px solid rgba(122,208,163,.35); }
.steps-list summary .step-result.bad { background: rgba(240,128,128,.15); color: var(--bad); border: 1px solid rgba(240,128,128,.35); }
.steps-list summary .step-result.skipped { background: rgba(217,179,128,.15); color: var(--skipped); border: 1px solid rgba(217,179,128,.35); }

.step-body {
  padding: .1rem 0 .8rem 1rem;
  border-left: 2px solid var(--rule);
  margin-left: .35rem;
}
.step-body .metric { margin: .2rem 0 .6rem; }
.step-orphans { margin-top: .8rem; }

/* ── Action-type chip (composite / DockerHub / node20 / run / …) ─
 *
 * Rendered next to the step label so a reader can tell an outer
 * composite-action step apart from its inner composite_substep
 * entries at a glance — and equally distinguish a `uses:` node
 * action from a docker container action from a plain `run:` step.
 * Sourced from the runner's `Publish step telemetry` block.
 */
.action-chip {
  flex-shrink: 0;
  padding: 0 5px;
  border-radius: 3px;
  font-weight: 600;
  font-size: 10px;
  letter-spacing: .04em;
  text-transform: lowercase;
  border: 1px solid var(--rule-strong);
  background: var(--bg-elev);
  color: var(--muted);
}
.action-chip.at-composite {
  color: var(--clr-dns);
  border-color: rgba(167,139,218,.4);
  background: rgba(167,139,218,.10);
}
.action-chip.at-docker {
  color: var(--clr-tls);
  border-color: rgba(94,184,184,.4);
  background: rgba(94,184,184,.10);
}
.action-chip.at-node {
  color: var(--ok);
  border-color: rgba(122,208,163,.4);
  background: rgba(122,208,163,.08);
}
.action-chip.at-run {
  color: var(--clr-proc);
  border-color: rgba(108,160,220,.4);
  background: rgba(108,160,220,.08);
}
.action-chip.at-pre,
.action-chip.at-runner,
.action-chip.at-other {
  opacity: .7;
}

/* "12 sub" chip beside the action-type chip on a composite parent. */
.steps-list summary .substep-count {
  flex-shrink: 0;
  padding: 0 5px;
  border-radius: 3px;
  font-size: 10px;
  letter-spacing: .04em;
  color: var(--clr-dns);
  background: rgba(167,139,218,.08);
  border: 1px solid rgba(167,139,218,.3);
}

/* ── Composite sub-steps (folded under their parent) ──────────── */
.composite-parent > details > summary {
  /* Subtle accent so the parent reads as a "container" row even
   * before opening it. Matches the at-composite chip's hue. */
  border-left: 2px solid rgba(167,139,218,.45);
}
.substeps {
  margin-top: .8rem;
  padding-top: .5rem;
  border-top: 1px dashed var(--rule);
}
.substeps-h {
  margin: 0 0 .35rem;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .06em;
}
.substeps-list {
  margin: 0;
  border-left: 2px dashed rgba(167,139,218,.35);
  padding-left: .6rem;
}
.substeps-list .step {
  border-bottom: 1px solid var(--rule);
}
.substeps-list .step:last-child {
  border-bottom: 0;
}
/* Nested substep summaries also pin — stacked directly below
 * their parent's pinned summary so the reader always sees both
 * "which composite parent am I in" + "which sub-step am I in"
 * when deep-scrolling inside a sub-step's process tree.
 *
 * Top offset = header height + the parent step summary's height
 * (one --step-summary-h, minus 1px to absorb subpixel rounding
 * the same way the parent does against the header). The same
 * --step-summary-h drives nested proc-stickies further down, so
 * keep this in sync with that math.
 */
.substeps-list summary {
  top: calc(var(--header-h) + var(--step-summary-h) - 2px);
  /* z-index lower than the parent summary's (5) so the parent
   * stays visually on top when both are pinned simultaneously. */
  z-index: 4;
}

/* ── Clickable toggle for hide-empty steps ───────────────────── */
h2.toggle-heading {
  cursor: pointer;
  width: fit-content;
}
.toggle-heading-btn {
  border: 1px solid var(--rule-strong);
  border-radius: 4px;
  padding: 0 6px;
  margin-left: 2px;
}
@media (hover: hover) {
  h2.toggle-heading:hover .toggle-heading-btn {
    border-color: var(--fg);
    color: var(--fg);
  }
}

.search-box {
  display: flex;
  align-items: center;
  background: var(--bg-elev);
  border: 1px solid var(--rule-strong);
  border-radius: 4px;
  padding: 2px 8px 2px 4px;
  width: fit-content;
}
.search-box:focus-within {
  border-color: var(--accent);
}
.steps-toolbar {
  display: flex;
  align-items: center;
  gap: .4rem;
  margin-bottom: .3rem;
}
.expand-toggle {
  background: var(--bg-elev);
  border: 1px solid var(--rule-strong);
  border-radius: 4px;
  cursor: pointer;
  padding: 0;
  flex-shrink: 0;
  /* Explicit width AND height so every toolbar button is exactly
   * 26×26 regardless of what else sits in the steps-toolbar row.
   * The previous `align-self: stretch` made the height track the
   * search-box's rendered height, which varied a couple of pixels
   * across browsers and platforms (Firefox vs Chromium font
   * defaults) and broke the pixel-perfect dimensions assertion.
   * The row's `align-items: center` (set on .steps-toolbar)
   * vertically centres the buttons against any taller siblings. */
  width: 26px;
  height: 26px;
  box-sizing: border-box;
  position: relative;
}
.expand-h, .expand-v {
  position: absolute;
  top: 50%;
  left: 50%;
  background: var(--muted);
  border-radius: 1px;
  transition: transform 380ms ease;
}
.expand-h {
  width: 10px;
  height: 1.5px;
  transform: translate(-50%, -50%);
}
.expand-v {
  width: 1.5px;
  height: 10px;
  transform: translate(-50%, -50%);
}
.expand-toggle.expanded .expand-v {
  transform: translate(-50%, -50%) scaleY(0);
}
@media (hover: hover) {
  .expand-toggle:hover .expand-h,
  .expand-toggle:hover .expand-v { background: var(--fg); }
  .expand-toggle:hover { border-color: var(--fg); }
}
.argv-toggle,
.infra-toggle {
  background: var(--bg-elev);
  border: 1px solid var(--rule-strong);
  border-radius: 4px;
  cursor: pointer;
  padding: 0 6px;
  flex-shrink: 0;
  font-size: 11px;
  color: var(--muted);
  box-sizing: border-box;
  /* Explicit dimensions to match `.expand-toggle` — see the
   * comment there for why `align-self: stretch` was dropped. */
  width: 26px;
  height: 26px;
  transition: color 150ms, border-color 150ms, background 150ms;
}
@media (hover: hover) {
  .argv-toggle:hover,
  .infra-toggle:hover { color: var(--fg); border-color: var(--fg); }
}
/* Kill the default focus ring on tap — mobile browsers leave
 * :focus on a tapped button until the user taps elsewhere, which
 * reads as the toggle's "box outline" sticking around. Keyboard
 * users still get a visible focus indicator via :focus-visible. */
.argv-toggle:focus,
.infra-toggle:focus { outline: none; }
.argv-toggle:focus-visible,
.infra-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.argv-toggle.active,
.infra-toggle.active { color: var(--accent); border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, transparent); }
.infra-toggle {
  /* Make the cloud-cog svg inherit the button's color (set via
   * currentColor in the SVG) and sit centered. Width/height pin
   * the icon to the same visual weight as the `>_` text in the
   * neighbouring argv-toggle so the two buttons share a height. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.infra-toggle svg {
  /* 18px so the actions ring + the cog's 8 teeth each stay
   * pixel-resolvable. Slightly taller than the argv-toggle's `>_`
   * text but the buttons still align on baseline because they're
   * the same outer height (driven by .argv-toggle's font-size +
   * padding). */
  width: 18px;
  height: 18px;
  display: block;
}
/* Trim the horizontal padding on the infra toggle so its outer
 * width matches the argv toggle's — the 18px svg is wider than
 * the `>_` text glyphs, and at the base 0 6px padding the infra
 * button ended up visibly wider. */
.infra-toggle {
  padding: 0 3px;
}
.search-icon {
  color: var(--muted);
  font-size: 13px;
  margin-right: 3px;
  flex-shrink: 0;
}
.search-input {
  background: transparent;
  border: none;
  color: var(--fg);
  font-size: 11px;
  width: 16ch;
  outline: none;
}
.search-input::placeholder {
  color: var(--muted);
  opacity: .7;
}
/* Hide WebKit's native search-clear; we render our own sibling
 * `.search-clear` outside the input so it can be styled as a
 * peer button in the steps toolbar. */
.search-input::-webkit-search-cancel-button { display: none; }
.search-clear {
  background: var(--bg-elev);
  border: 1px solid var(--rule-strong);
  border-radius: 4px;
  cursor: pointer;
  padding: 0 8px;
  flex-shrink: 0;
  font-size: 14px;
  line-height: 1;
  color: var(--muted);
  box-sizing: border-box;
  align-self: stretch;
}
@media (hover: hover) {
  .search-clear:hover { color: var(--fg); border-color: var(--fg); }
}

/* ── Tree style chip toggle ──────────────────────────────── */
.tree-style-toggle {
  display: inline-flex;
  align-items: center;
  gap: .25rem;
  font-size: 11.5px;
}
.tree-style-toggle .chip {
  background: transparent;
  border: 1px solid var(--rule-strong);
  color: var(--muted);
  padding: 1px 7px;
  border-radius: 4px;
  font-family: ui-monospace, monospace;
  font-size: 11px;
  cursor: pointer;
  letter-spacing: .03em;
}
.tree-style-toggle .chip:hover {
  border-color: var(--muted);
  color: var(--fg);
}
.tree-style-toggle .chip.active {
  border-color: var(--accent);
  color: var(--accent);
  background: rgba(122, 208, 163, .08);
}

/* ── Process tree (shared base; variants below) ────────────── */
.proc-tree {
  margin: .4rem 0;
  padding-top: .2rem;
}
.proc-tree details,
.proc-tree .proc-leaf {
  border-left: 2px solid transparent;
  padding: 1px 0;
  margin: 0;
}
/* Sticky-scroll: every open <details> in the proc tree pins its
 * summary just under whatever's already pinned above. Depth comes
 * from `style:--depth=N` set by the Leptos render fn. The result
 * is a VS-Code-style breadcrumb of open process ancestors that
 * stays visible while scrolling deep into one parent's subtree.
 * bg is --bg-elev so the whole pinned stack reads as one surface.
 *
 * Z-index has two axes so sticky transitions look right:
 *   1. Depth (shallower = higher) — when a deeper summary enters
 *      its "leaving phase" (pushed up early by its small <details>
 *      box's bottom), it slides BEHIND still-correctly-pinned
 *      shallower summaries instead of rendering above them.
 *   2. Sibling-idx (earlier = higher) — when peer subtrees hand
 *      off the pin slot during scroll, the leaving sibling stays
 *      visually on top while the incoming one slides up
 *      underneath it. Without this, document-order wins and the
 *      incoming sibling pops out in front of the outgoing one,
 *      which reads as a jump-cut instead of a smooth handoff.
 *
 * `isolation: isolate` on .proc-tree contains the large z-index
 * values to a local stacking context, so a depth-0 sibling-0
 * summary at z=1000 doesn't render over the fixed page header
 * (which sits at z=100 in the page stacking context). */
.proc-tree { isolation: isolate; }
.proc-tree details > summary {
  position: sticky;
  /* -1px to overlap the previous sticky by one device pixel and
   * absorb subpixel rounding (same trick as the step summary). */
  top: calc(var(--header-h) + var(--step-summary-h)
            + var(--depth, 0) * var(--proc-row-h) - 1px);
  background: var(--bg-elev);
  /* Paint a 1px slice of bg-elev just above the sticky's top
   * edge. When two sibling stickies hand off the pinned slot
   * during scroll, the previous one's box scrolls past by a
   * sub-pixel amount before the next snaps in — Android Firefox
   * paints one frame in that sub-pixel slot where the row of
   * text behind shows through. Chromium and iOS Safari handle
   * the handoff atomically; only Gecko-on-mobile needs this.
   * Invisible at rest (overlaps the previous sticky in the same
   * colour) so it's a free fix on the browsers that don't need
   * it. See: pids 2949 → 2968, 2969 → 2970 in run #175.
   */
  box-shadow: 0 -1px 0 var(--bg-elev);
  z-index: calc(1000 - var(--depth, 0) * 100 - var(--sibling-idx, 0));
}
.unattributed-section .proc-tree details > summary {
  /* No step summary above the unattributed proc tree. */
  top: calc(var(--header-h) + var(--depth, 0) * var(--proc-row-h) - 1px);
}
/* Proc nodes inside a composite sub-step have TWO sticky summaries
 * above them (the composite parent's step summary AND the sub-step's
 * own step summary), so the proc-head sticky offset has to clear
 * both. Without this rule, deep proc-tree summaries pin under the
 * top-level step summary and overlap the still-pinned sub-step
 * summary below it.
 */
.substeps-list .proc-tree details > summary {
  top: calc(var(--header-h) + 2 * var(--step-summary-h)
            + var(--depth, 0) * var(--proc-row-h) - 2px);
}
/* Tree-char variants are flat (always-expanded prefix) so the
 * nested-details sticky doesn't apply there. */
.proc-tree.style-treechars details > summary,
.proc-tree.style-tree-dots details > summary {
  position: static;
}
.proc-tree details > summary,
.proc-tree .proc-leaf > .proc-head {
  text-transform: none;
  letter-spacing: 0;
  font-weight: 400;
  color: var(--fg);
  font-size: 12.5px;
  display: flex;
  align-items: baseline;
  gap: .5rem;
  padding: 2px 0 2px .5rem;
  /* Fix the row's box to --proc-row-h. The sticky stacking math
   * uses `depth * --proc-row-h` to compute each summary's `top:`
   * offset; without an explicit height the intrinsic line-height
   * + padding rendered slightly under the assumed value and the
   * drift accumulated into sub-pixel gaps on iOS Safari at depth
   * (e.g. step 12 of the run-175 fixture). box-sizing: border-box
   * keeps the existing padding intact inside the fixed-height box. */
  height: var(--proc-row-h);
  box-sizing: border-box;
  flex-wrap: nowrap;
  overflow: hidden;
  white-space: nowrap;
  min-width: 0;
}
.proc-tree .proc-head .comm,
.proc-tree .proc-head .pid {
  /* Inside the now-clipping summary, let comm and pid shrink and
   * truncate rather than push siblings (kids, badges) off the right. */
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
.proc-tree .proc-head .comm {
  flex-shrink: 1;
}
.proc-tree .proc-head .pid {
  flex-shrink: 0;
}
.proc-tree .proc-leaf > .proc-head { padding-left: 1.05em; }
.proc-tree .proc-head .comm { font-weight: 600; color: var(--fg); }
.proc-tree .proc-head .comm.exec { color: var(--workload); }
.proc-tree .proc-head .pid { font-size: 11.5px; }
.proc-tree .proc-head .kids {
  color: var(--rule-strong);
  margin-left: auto;
}
.proc-tree .argv {
  font-size: 11px;
  color: var(--muted);
  padding: 0 .3rem .15rem 1.55em;
  line-height: 1.45;
  cursor: pointer;
  overflow-wrap: anywhere;
  word-break: break-word;
}
.proc-tree .argv.argv-collapsed {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.proc-children {
  margin-left: .55em;
  padding-left: .25rem;
}

/* ── Variant: lines (colored left borders) ──────────────── */
.proc-tree.style-lines details.workload,
.proc-tree.style-lines .proc-leaf.workload {
  border-left-color: var(--workload);
}
.proc-tree.style-lines details.infra,
.proc-tree.style-lines .proc-leaf.infra {
  border-left-color: var(--infra);
  color: var(--infra);
  opacity: .65;
}
.proc-tree.style-lines .proc-children {
  border-left: 1px solid var(--rule);
}

/* ── Variant: dots (filled/outline dot, no lines) ──────── */
.proc-tree.style-dots details.infra,
.proc-tree.style-dots .proc-leaf.infra {
  color: var(--infra);
  opacity: .65;
}
.proc-tree.style-dots .proc-head .comm::before {
  content: "●";
  display: inline-block;
  margin-right: .35em;
  font-size: 10px;
  vertical-align: 1.5px;
  color: var(--workload);
}
.proc-tree.style-dots .infra .proc-head .comm::before {
  content: "○";
  color: var(--infra);
}
.proc-tree.style-dots .proc-children {
  margin-left: 1.1em;
  padding-left: 0;
}

/* ── Variant: plain (just indent + comm color) ─────────── */
.proc-tree.style-plain details.infra,
.proc-tree.style-plain .proc-leaf.infra {
  color: var(--infra);
  opacity: .65;
}
.proc-tree.style-plain .proc-children {
  margin-left: 1.1em;
  padding-left: 0;
}

/* ── Variant: tree-chars (always-expanded prefix-driven) ── */
.proc-tree.style-treechars .treechars-list {
  font-size: 12.5px;
  line-height: 1.5;
}
.proc-tree.style-treechars .tc-row {
  display: flex;
  align-items: baseline;
  gap: .3rem;
  padding: 1px 0;
}
.proc-tree.style-treechars .tc-row.event {
  font-size: 11.5px;
}
.proc-tree.style-treechars .tc-row.argv {
  font-size: 11px;
  cursor: pointer;
}
.proc-tree.style-treechars .tc-row.argv.argv-collapsed .argv-text {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.proc-tree.style-treechars .tc-row.argv.argv-collapsed {
  max-width: 100%;
}
.proc-tree.style-treechars .tc-prefix {
  color: var(--rule-strong);
  white-space: pre;
  flex-shrink: 0;
}
.proc-tree.style-treechars .tc-row.workload .comm { color: var(--fg); font-weight: 600; }
.proc-tree.style-treechars .tc-row.infra .comm {
  color: var(--infra);
  font-weight: 600;
}
.proc-tree.style-treechars .tc-row.infra { opacity: .7; }
.proc-tree.style-treechars .comm.exec { color: var(--workload); }
.proc-tree.style-treechars .pid { font-size: 11px; }
.proc-tree.style-treechars .badge {
  display: inline-block;
  padding: 0 4px;
  font-size: 10px;
  border-radius: 3px;
  border: 1px solid var(--rule-strong);
  margin-right: .3rem;
  text-transform: uppercase;
  font-weight: 600;
  letter-spacing: .04em;
  flex-shrink: 0;
}
.proc-tree.style-treechars .syscall-badge { color: #d9b380; }
.proc-tree.style-treechars .tls-badge { color: #5eb8b8; }
.proc-tree.style-treechars .dns-badge { color: #a78bda; }
.proc-tree.style-treechars .argv-text { color: var(--muted); }

/* ── Variant: tree + dots (box-drawing prefix AND ● after) ─ */
.proc-tree.style-tree-dots .treechars-list {
  font-size: 12.5px;
  line-height: 1.5;
}
.proc-tree.style-tree-dots .tc-row {
  display: flex;
  align-items: baseline;
  gap: .3rem;
  padding: 1px 0;
}
.proc-tree.style-tree-dots .tc-row.event { font-size: 11.5px; }
.proc-tree.style-tree-dots .tc-row.argv { font-size: 11px; cursor: pointer; }
.proc-tree.style-tree-dots .tc-row.argv.argv-collapsed .argv-text {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.proc-tree.style-tree-dots .tc-row.argv.argv-collapsed {
  max-width: 100%;
}
.proc-tree.style-tree-dots .tc-prefix {
  color: var(--rule-strong);
  white-space: pre;
  flex-shrink: 0;
}
.proc-tree.style-tree-dots .tc-dot {
  font-size: 10px;
  flex-shrink: 0;
  vertical-align: 1px;
}
.proc-tree.style-tree-dots .dot-workload { color: var(--workload); }
.proc-tree.style-tree-dots .dot-infra { color: var(--infra); }
.proc-tree.style-tree-dots .tc-row.workload .comm { color: var(--fg); font-weight: 600; }
.proc-tree.style-tree-dots .tc-row.infra .comm {
  color: var(--infra);
  font-weight: 600;
}
.proc-tree.style-tree-dots .tc-row.infra { opacity: .7; }
.proc-tree.style-tree-dots .comm.exec { color: var(--workload); }
.proc-tree.style-tree-dots .pid { font-size: 11px; }
.proc-tree.style-tree-dots .badge {
  display: inline-block;
  padding: 0 4px;
  font-size: 10px;
  border-radius: 3px;
  border: 1px solid var(--rule-strong);
  margin-right: .3rem;
  text-transform: uppercase;
  font-weight: 600;
  letter-spacing: .04em;
  flex-shrink: 0;
}
.proc-tree.style-tree-dots .syscall-badge { color: #d9b380; }
.proc-tree.style-tree-dots .tls-badge { color: #5eb8b8; }
.proc-tree.style-tree-dots .dns-badge { color: #a78bda; }
.proc-tree.style-tree-dots .argv-text { color: var(--muted); }

/* ── Event badges in process head ──────────────────────────── */
.event-badges {
  display: inline-flex;
  gap: .25rem;
}
.event-badges .badge {
  font-size: 10px;
  font-weight: 600;
  padding: 0 4px;
  border-radius: 3px;
  letter-spacing: .04em;
}
.event-badges .syscall-badge {
  background: rgba(217,179,128,.12);
  color: var(--clr-sc);
  border: 1px solid rgba(217,179,128,.3);
}
.event-badges .tls-badge {
  background: rgba(94,184,184,.12);
  color: var(--clr-tls);
  border: 1px solid rgba(94,184,184,.3);
}
.event-badges .dns-badge {
  background: rgba(167,139,218,.12);
  color: var(--clr-dns);
  border: 1px solid rgba(167,139,218,.3);
}

/* ── Event list (used inline under processes + for orphans) ── */
.event-list {
  list-style: none;
  padding: 0;
  margin: .3rem 0 .3rem 1.55em;
}
.event-list.inline {
  border-left: 1px dashed var(--rule-strong);
  padding-left: .5rem;
  margin-left: 1.55em;
}
.event {
  padding: .35rem .25rem;
  border-bottom: 1px solid var(--rule);
  font-size: 12px;
}
.event:last-child { border-bottom: 0; }
.event-head {
  display: flex;
  gap: .5rem;
  align-items: baseline;
  flex-wrap: wrap;
}
.event-head .method,
.event-head .syscall,
.event-head .dns-qtype {
  font-weight: 600;
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: .04em;
  padding: 1px 5px;
  border-radius: 3px;
  flex-shrink: 0;
}
.event-head .syscall {
  color: var(--clr-sc);
  border: 1px solid rgba(217,179,128,.35);
}
.event-head .method {
  color: var(--clr-tls);
  border: 1px solid rgba(94,184,184,.35);
}
.event-head .dns-qtype {
  color: var(--clr-dns);
  border: 1px solid rgba(167,139,218,.35);
}
.event-head .dest,
.event-head .url,
.event-head .dns-qname {
  color: var(--fg);
  flex: 1;
  min-width: 0;
}
.event-meta {
  font-size: 11px;
  margin-top: .15rem;
}
.event-meta .comm { color: var(--fg); }

/* ── Mobile ────────────────────────────────────────────────── */
@media (max-width: 560px) {
  body { padding: var(--header-h) 1rem 1.5rem; }
  .metric { grid-template-columns: 1fr; gap: 0; }
  .metric dt { margin-top: .5rem; font-size: 11.5px; }
  .metric dd { margin-bottom: .35rem; }
  .proc-tree .proc-head .comm { font-size: 12px; }
  .proc-tree .proc-head .pid { font-size: 11px; }
  .proc-tree .argv { font-size: 11px; }
  details { padding-left: .5rem; }
  details details { margin-left: .15rem; }
  .top-header { font-size: 11.5px; }
  .steps-list summary { gap: .4rem; }
  .steps-list summary .step-label { font-size: 12px; }
  .event-list { margin-left: 1.2em; }
}

@media (max-width: 380px) {
  body { padding: var(--header-h) .75rem 1rem; }
}
