/* Cyclopes design system: the app's look, on the web.
   No build step: plain CSS custom properties + component classes.

   This is a port of lib/theme.dart. A terminal in the logo's own colours:
   champagne on near-black by default, since the audience is developers, who mostly
   work dark, with the original ink-on-paper light mode kept intact for anyone
   who wants it. One mono type family everywhere, hairline rules, no shadows.
   Green is reserved for "healthy", amber for warnings, red for errors. No
   decorative colour anywhere else. */

/* ---------- tokens ----------
   Dark is the default, and it is the mark's own palette: --bg-0 and --ink
   below are lifted straight off the logo, its ground and its line, and
   every other neutral is a mix of those two. The eye in the header is drawn
   in --ink, so on this ground the glyph *is* the logo.

   The two dimmed neutrals are mixes of the same pair, and every one of them
   clears 4.5:1 against --bg-0, --bg-1 and --bg-2 alike, in both themes. They
   used not to: --ink-faint sat at 3.4:1 dark and 2.4:1 light while carrying
   most of the explanatory prose on the site, which made "read the small grey
   text" a thing this product asked of people at 3am. Dimmer than the ink is a
   hierarchy; too dim to read is a bug. */
:root, :root[data-theme="dark"] {
  color-scheme: dark;

  --bg-0: #07080e;          /* the logo's ground */
  --bg-1: #10111a;          /* panels, badges */
  --bg-2: #191a25;
  --border: #282730;        /* hairline */
  --border-bright: #383743;

  --ink: #eedbc1;           /* the logo's line */
  --ink-muted: #b4a694;     /* ink mixed 25% into the ground */
  --ink-faint: #928779;     /* ink mixed 40% */

  /* Severity keeps its green → amber → red ramp, warmed to sit beside the
     champagne rather than fight it. These are the only non-brand hues, and
     they are never decorative. Info is not a hue at all: it is the ink. */
  --accent: #82c494;
  --accent-dim: #9ed2ac;
  --accent-glow: rgb(130 196 148 / 0.13);

  --level-error: #e8695c;
  --level-warning: #e2a94a;
  --level-info: var(--ink-muted);
  --level-fatal: #ff8a7d;
  --level-debug: var(--ink-faint);
  --ok: var(--accent);
  --on-accent: var(--bg-0);
  --error-glow: rgb(232 105 92 / 0.1);
  --error-line: rgb(232 105 92 / 0.45);

  --appbar-bg: rgb(7 8 14 / 0.82);
}

/* The original ink-on-paper mode, kept intact. */
:root[data-theme="light"] {
  color-scheme: light;

  --bg-0: #fbfbf8;
  --bg-1: #f2f2ec;
  --bg-2: #e9e9e1;
  --border: #e5e6de;
  --border-bright: #d6d7cc;

  --ink: #20241f;
  --ink-muted: #575a55;     /* ink mixed 25% into the paper */
  --ink-faint: #666964;     /* ink mixed 32% */

  --accent: #3e7a52;
  --accent-dim: #2f6041;
  --accent-glow: rgb(62 122 82 / 0.12);

  --level-error: #b23a31;
  --level-warning: #a26e0b;
  --level-info: var(--ink-muted);
  --level-fatal: #7f1d1d;
  --level-debug: var(--ink-faint);
  --on-accent: #fbfbf8;
  --error-glow: rgb(178 58 49 / 0.07);
  --error-line: rgb(178 58 49 / 0.4);

  --appbar-bg: rgb(251 251 248 / 0.85);
}

:root {
  /* Width of the reading column the ambient gutters measure against.
     Pages with a different column override it (see .auth-card). */
  --cy-col: 760px;

  --font-mono: ui-monospace, Menlo, "SF Mono", Monaco, "JetBrains Mono",
    "Cascadia Code", Consolas, "Liberation Mono", "Courier New", monospace;
  /* All-monospace: UI font === code font. */
  --font-ui: var(--font-mono);

  /* ---------- type scale ----------
     Eight sizes, and nothing between them. The site used to carry twenty-seven
     distinct ones, picked a selector at a time, and the bottom of that range
     (0.66rem, and 0.72rem for whole paragraphs) was too small to read before
     colour was even involved. A monospace face also runs optically smaller than
     a proportional one at the same size, which is why the body is 15px here and
     would be 14 in something like Inter.

     --fs-micro is for uppercase tracked labels and nothing else: caps and
     letter-spacing carry it, and running prose at that size does not. The floor
     for anything made of sentences is --fs-0. */
  --fs-micro: 0.76rem;   /* 11.4px, uppercase only */
  --fs-0: 0.82rem;       /* 12.3px, the smallest readable */
  --fs-1: 0.88rem;       /* 13.2px, secondary */
  --fs-2: 1rem;          /* 15px, body */
  --fs-3: 1.15rem;       /* 17.3px */
  --fs-4: 1.4rem;        /* 21px */
  --fs-5: 1.6rem;        /* 24px */
  --fs-6: 2rem;          /* 30px */

  /* Hairlines, not corners. The app rounds everything to 3px and stops. */
  --radius-s: 2px;
  --radius-m: 3px;

  /* Nothing floats: surfaces are separated by a rule, never by a shadow. */
  --shadow-1: none;

  --space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px;
  --space-5: 24px; --space-6: 32px; --space-7: 48px;
}

/* ---------- reset-ish ---------- */
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  font-family: var(--font-ui);
  font-size: 15px;
  line-height: 1.6;
  /* The default is the real ink. It used to be --ink-muted, which meant every
     unclassed paragraph, table cell and list item on the site was secondary
     text by default and the only way to be primary was to be a heading. The
     hierarchy is carried by weight, size and the dimmed neutrals above, applied
     where something genuinely is an aside. */
  color: var(--ink);
  background: var(--bg-0);
  -webkit-font-smoothing: antialiased;
  /* Native controls (checkboxes, radios, the caret) take the accent too. */
  accent-color: var(--accent);
  caret-color: var(--ink);
}
a { color: var(--accent); text-decoration: none; }
/* One focus ring, everywhere, and only for the keyboard. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-s);
}
.field input:focus-visible, .field select:focus-visible, .field textarea:focus-visible {
  outline: none;
}
a:hover { text-decoration: underline; }
h1, h2, h3, h4 { line-height: 1.25; letter-spacing: -0.01em; color: var(--ink); font-weight: 600; }
code, pre, .mono { font-family: var(--font-mono); }
/* Three tiers and no more. The body is --ink; .muted is for a line that
   supports the one above it, and .faint is for metadata that is only read when
   somebody goes looking (a timezone, a row count, a keyboard hint). Prose that
   tells somebody what a control does is never .faint: if it is worth writing it
   is worth reading, and the way to make it recede is to say less of it. */
.muted { color: var(--ink-muted); }
.faint { color: var(--ink-faint); }
::selection { background: var(--accent-glow); }
::placeholder { color: var(--ink-faint); }

/* ---------- background ----------
   Flat, like the app. The ground is the logo's ground and nothing is layered
   on top of it. */
.bg-mesh { position: fixed; inset: 0; z-index: -1; background: var(--bg-0); }

/* ---------- the mark ----------
   Drawn by static/js/eye.js in the theme's own colours; --cy-stroke is the
   optical compensation the painter applies, set per instance. */
.cy-eye { display: block; overflow: visible; }
.cy-eye .cy-line {
  fill: none;
  stroke: var(--ink);
  stroke-width: var(--cy-stroke, 0.045);
  stroke-linecap: round;
}
.cy-eye .cy-dot { fill: var(--ink); }
.cy-eye .cy-iris-fill { fill: var(--ok); transition: fill 0.4s ease; }
.cy-eye[data-severity="warn"] .cy-iris-fill,
.cy-eye[data-severity="warning"] .cy-iris-fill { fill: var(--level-warning); }
.cy-eye[data-severity="error"] .cy-iris-fill,
.cy-eye[data-severity="fatal"] .cy-iris-fill { fill: var(--level-error); }

/* ---------- the handover ----------
   The launch frame: the server paints the bare mark on the logo's ground, the
   eye opens, glances left and right the way a person checks a room, settles,
   and the page is already behind it. */
#cy-boot { display: none; }
html.cy-booting { overflow: hidden; }
html.cy-booting #cy-boot {
  position: fixed; inset: 0; z-index: 999;
  display: flex; align-items: center; justify-content: center;
  background: var(--bg-0);
  /* If the script never arrives, the frame still gets out of the way. */
  animation: cy-boot-out 0.45s ease 3.4s forwards;
}
#cy-boot.cy-boot-done { opacity: 0; transition: opacity 0.42s ease; }
@keyframes cy-boot-out { to { opacity: 0; visibility: hidden; } }
@media (prefers-reduced-motion: reduce) {
  html.cy-booting #cy-boot { display: none; }
  html.cy-booting { overflow: auto; }
}

/* ---------- layout ---------- */
.container { max-width: 1200px; margin: 0 auto; padding: var(--space-5); }

.appbar {
  position: sticky; top: 0; z-index: 50;
  display: flex; align-items: center; gap: var(--space-5);
  padding: var(--space-3) var(--space-5);
  background: var(--appbar-bg);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--border);
}
.appbar-nav { display: flex; gap: var(--space-4); flex: 1; }
.appbar-nav a { color: var(--ink-muted); }
.appbar-nav a::before { content: "/"; color: var(--ink-faint); margin-right: 2px; }
.appbar-nav a.active, .appbar-nav a:hover { color: var(--accent); text-decoration: none; }
.appbar-right { display: flex; align-items: center; gap: var(--space-3); margin-left: auto; }
.appbar-email { font-size: var(--fs-0); }
.inline-form { display: inline; }

.brand {
  display: inline-flex; align-items: center; gap: var(--space-2);
  color: var(--ink); font-weight: 700; letter-spacing: -0.02em;
}
.brand:hover { text-decoration: none; }
.brand-eye { display: inline-flex; width: 22px; height: 22px; flex: none; }
.brand-cursor { color: var(--accent); animation: blink 1.1s steps(1) infinite; margin-left: -2px; }

.theme-toggle {
  border: 0; background: none; cursor: pointer; padding: 0 2px;
  color: var(--ink-faint); font: inherit; font-size: 14px; line-height: 1;
}
.theme-toggle:hover { color: var(--ink); }

/* Plan and balance in the app bar. Reads as a label, not a button: no fill,
   no border, the same muted ink as the email next to it. The only thing that
   ever raises its voice is a balance low enough to change what happens on the
   next alert, and then only by one step of colour. */
.purse {
  display: inline-flex; align-items: baseline; gap: 5px;
  font-size: var(--fs-0); color: var(--ink-muted);
  white-space: nowrap;
}
.purse:hover { color: var(--ink); text-decoration: none; }
.purse-plan { color: var(--ink-faint); }
.purse-plan::after { content: " ·"; }
.purse-count { color: inherit; }
.purse-unit { color: var(--ink-faint); }
.purse-low { color: var(--warn, #d08a2c); }
.purse-low .purse-unit, .purse-low .purse-plan { color: inherit; opacity: 0.75; }

@media (max-width: 720px) {
  .appbar { gap: var(--space-3); padding: var(--space-3); }
  .appbar-email { display: none; }
  /* The tier is the part you already know; the number is the part that
     changes. On a narrow bar only the number survives. */
  .purse-plan { display: none; }
}

/* ---------- surfaces ----------
   A panel is a hairline and a slightly lifted ground. Nothing floats. */
.glass, .card {
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  box-shadow: var(--shadow-1);
}
.card { padding: var(--space-5); }
/* Prose in a card ran the whole width of the panel, which on a wide window is
   about a hundred and fifty characters a line: roughly double what anybody
   reads comfortably, and the reason the billing copy looked like a wall. 68ch
   is the measure `.page-head` already uses; this is the same number, applied
   where the sentences actually are. Opt-in per card, because plenty of cards
   hold tables and metrics rather than paragraphs. */
.card-prose > p { max-width: 68ch; }
.appbar.glass { border-radius: 0; box-shadow: none; background: var(--appbar-bg); }
.card h3 { margin-top: 0; }
.card h3::before { content: "# "; color: var(--ink-faint); }

/* ---------- buttons ([ label ] bordered mono) ---------- */
.btn {
  display: inline-flex; align-items: center; gap: var(--space-2);
  font: inherit; font-weight: 600; font-size: var(--fs-1);
  padding: 7px 14px; border-radius: var(--radius-s);
  border: 1px solid var(--border-bright); cursor: pointer;
  color: var(--ink); background: var(--bg-1);
  transition: border-color 0.12s, background 0.12s, color 0.12s;
}
.btn:hover { border-color: var(--ink-faint); background: var(--bg-2); text-decoration: none; }
.btn:active { transform: translateY(1px); }
.btn-primary { border-color: var(--accent); color: var(--accent); background: transparent; }
.btn-primary:hover { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
.btn-ghost { background: transparent; }
.btn-danger { background: transparent; border-color: var(--error-line); color: var(--level-error); }
.btn-danger:hover { background: var(--error-glow); border-color: var(--level-error); }
.btn-lg { padding: 11px 22px; font-size: var(--fs-2); }
.btn-sm { padding: 3px 9px; font-size: var(--fs-0); }

/* ---------- forms ---------- */
.field { display: block; margin-bottom: var(--space-4); }
.field label {
  display: block; font-size: var(--fs-0); font-weight: 600; color: var(--ink-muted);
  margin-bottom: var(--space-1); text-transform: lowercase; letter-spacing: 0.02em;
}
.field label::before { content: "› "; color: var(--ink-faint); }
.field input, .field select, .field textarea {
  width: 100%; font: inherit; color: var(--ink);
  background: var(--bg-0);
  border: 1px solid var(--border-bright);
  border-radius: var(--radius-s);
  padding: 9px 11px;
  transition: border-color 0.12s, box-shadow 0.12s;
}
.field input:focus, .field select:focus, .field textarea:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-glow);
}
.field .helptext, .field .errorlist { font-size: var(--fs-0); color: var(--ink-muted); margin: var(--space-1) 0 0; }
.errorlist { color: var(--level-error); list-style: none; padding: 0; }

/* Django's password validators hand back a real `<ul>` of rules, and nothing
   was resetting it: browser disc bullets and browser indentation, sitting
   inside a card that has neither anywhere else. The rules are worth showing
   (they are the difference between guessing at a password and being told the
   shape of one), so they are marked the way every other list on this site is,
   with a faint interpunct rather than a filled circle. */
.field .helptext ul { list-style: none; margin: var(--space-1) 0 0; padding: 0; }
.field .helptext li { position: relative; padding-left: var(--space-4); line-height: 1.55; }
.field .helptext li::before {
  content: "·";
  position: absolute; left: var(--space-2);
  color: var(--ink-faint);
}

/* ---------- the balance, and the numbers under it ---------- */
.balance-label {
  margin: 0; font-size: var(--fs-micro); text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--ink-muted); font-weight: 700;
}
.balance {
  margin: var(--space-2) 0 var(--space-1);
  display: flex; align-items: baseline; gap: var(--space-3);
  font-size: var(--fs-6); line-height: 1;
  font-variant-numeric: tabular-nums;
}
.balance-unit { font-size: var(--fs-1); color: var(--ink-muted); letter-spacing: 0.04em; }
/* The arithmetic behind the number, not a sentence: smaller than the prose
   below it so the eye takes it as a breakdown rather than as the next line. */
.balance-parts { margin: 0 0 var(--space-4); font-size: var(--fs-0); color: var(--ink-faint); }

/* An item that leaves the org context, held back from the ones that do not. */
.snav-aside { margin-top: var(--space-4); }

/* A caveat inside a docs section, for the case where following the
   instructions above it would fail. Amber rather than red: nothing is broken,
   something is not built yet, and the difference matters to a reader deciding
   whether to trust the rest of the page. */
.docs-warn {
  margin-top: var(--space-4);
  padding: var(--space-4);
  border: 1px solid var(--level-warning);
  border-radius: var(--radius-m);
  background: var(--bg-1);
}
.docs-warn > p:first-child { margin-top: 0; }
.docs-warn > :last-child { margin-bottom: 0; }

/* ---------- the docs index: one card per SDK ----------
   A door, not the documentation. Sized so a group of four sits on one row on a
   laptop and stacks on a phone, and so the package name is readable, since
   that is the string somebody is about to paste into a manifest. */
.docs-sdk-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: var(--space-3);
  margin-top: var(--space-4);
}
.docs-sdk-card {
  display: flex; flex-direction: column; gap: 2px;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  background: var(--bg-1);
}
.docs-sdk-card:hover { border-color: var(--accent); text-decoration: none; }
.docs-sdk-name { color: var(--ink); font-weight: 600; }
.docs-sdk-card:hover .docs-sdk-name { color: var(--accent); }
.docs-sdk-pkg { font-size: var(--fs-0); color: var(--ink-muted); overflow-wrap: anywhere; }
.docs-sdk-ver { font-size: var(--fs-0); }
.docs-crumb { margin: 0 0 var(--space-1); font-size: var(--fs-0); color: var(--ink-muted); }
.docs-table td, .docs-table th { vertical-align: top; }

/* ---------- chips / badges ---------- */
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--fs-0);
  padding: 1px 8px; border-radius: var(--radius-s);
  background: var(--bg-2); border: 1px solid var(--border);
  color: var(--ink-muted);
}
.badge-level { font-weight: 700; text-transform: uppercase; font-size: var(--fs-micro); letter-spacing: 0.06em; }
.badge-level::before { content: "●"; margin-right: 3px; font-size: 0.7em; vertical-align: middle; }
.badge-level-error { color: var(--level-error); }
.badge-level-warning { color: var(--level-warning); }
.badge-level-info { color: var(--level-info); }
.badge-ok { color: var(--ok); }

/* ---------- flash messages ---------- */
.flash {
  padding: var(--space-3) var(--space-4); border-radius: var(--radius-s);
  border: 1px solid var(--border-bright); background: var(--bg-1);
  margin-bottom: var(--space-3);
}
.flash::before { content: "▸ "; color: var(--accent); }
.flash-success { border-color: var(--accent); background: var(--accent-glow); }
.flash-error { border-color: var(--error-line); background: var(--error-glow); }

/* ---------- code ---------- */
.code-block {
  background: var(--bg-0);
  border: 1px solid var(--border);
  border-radius: var(--radius-s);
  padding: var(--space-4);
  overflow-x: auto;
  font-size: var(--fs-1);
  line-height: 1.65;
  color: var(--ink);
}
.tok-kw { color: var(--accent-dim); }
.tok-str { color: var(--level-warning); }
.tok-num { color: var(--level-info); }

/* ---------- hero (landing) ---------- */
.hero { display: flex; justify-content: center; padding-top: var(--space-7); }
.hero-card { max-width: 660px; padding: var(--space-7); }
.hero h1 { font-size: var(--fs-6); margin: 0 0 var(--space-3); }
.hero h1::before { content: "❯ "; color: var(--accent); }
.hero .lead { color: var(--ink-muted); font-size: 1rem; margin-bottom: var(--space-5); }
.cursor { color: var(--accent); animation: blink 1.1s steps(1) infinite; }
@keyframes blink { 50% { opacity: 0; } }

/* ---------- tables ---------- */
.table { width: 100%; border-collapse: collapse; }
.table th {
  text-align: left; font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: 0.07em;
  color: var(--ink-muted); font-weight: 700; padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--border-bright);
}
.table td { padding: var(--space-3); border-bottom: 1px solid var(--border); }
.table tr:hover td { background: var(--bg-2); }

/* ---------- tabs ---------- */
.tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border-bright); margin-bottom: var(--space-4); }
.tabs a {
  padding: var(--space-2) var(--space-4); color: var(--ink-muted); font-weight: 600; font-size: var(--fs-1);
  border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.tabs a:hover { color: var(--ink); text-decoration: none; }
.tabs a.active { color: var(--accent); border-bottom-color: var(--accent); }

/* ---------- page scaffolding ---------- */
.page-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); margin-bottom: var(--space-5); }
.page-head h2 { margin: 0; font-size: var(--fs-3); }
.page-head h2::before { content: "❯ "; color: var(--accent); }
/* The sentence under a page title says what the page is for, and on most
   screens it is the only place that is said. It is secondary to the title and
   to nothing else, so it beats a bare .faint wherever a template used one, and
   it stops at a measure rather than running the width of a 1200px shell. */
.page-head > div > p { margin: 4px 0 0; max-width: 68ch; color: var(--ink-muted); }

.card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: var(--space-4); }
.project-card { display: block; color: var(--ink); transition: border-color 0.12s, box-shadow 0.12s; }
.project-card:hover { text-decoration: none; border-color: var(--accent); background: var(--bg-2); }
.project-card h3 { margin: 0 0 var(--space-1); }
.project-card h3::before { content: "▹ "; color: var(--accent); }

.empty-state { text-align: center; padding: var(--space-7); }

.dsn-row { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-3); }
.dsn { flex: 1; overflow-x: auto; white-space: nowrap; background: var(--bg-0);
       border: 1px solid var(--border-bright); border-radius: var(--radius-s); padding: 8px 12px; color: var(--ink); }
.dsn-inactive .dsn { opacity: 0.5; text-decoration: line-through; }

/* ---------- issue list ---------- */
.filter-bar { display: flex; gap: var(--space-2); }
/* appearance: none because the native control paints itself, and it does not
   paint the same everywhere: over the translucent app bar Chrome rendered the
   identical select light while the one in a filter bar came out dark. Drawing
   the caret ourselves from two gradients in currentColor keeps it one control
   in both themes, with no image to fetch and nothing to keep in sync. */
.filter-select {
  font: inherit; font-size: var(--fs-0); color: var(--ink);
  background-color: var(--bg-0);
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 13px center, right 9px center;
  background-size: 4px 4px, 4px 4px;
  background-repeat: no-repeat;
  appearance: none;
  border: 1px solid var(--border-bright);
  border-radius: var(--radius-s); padding: 5px 26px 5px 10px;
}
.filter-select:hover { border-color: var(--accent-dim); }

/* An editable cell. Reads like .filter-select, which is what the rest of the
   console's controls look like, minus the chevron it paints for a <select>:
   an arrow on a text box says a menu is coming and none is, and the 26px it
   reserves for one clips the value instead. */
.cell-input {
  font: inherit; font-size: var(--fs-0); color: var(--ink);
  background-color: var(--bg-0);
  border: 1px solid var(--border-bright);
  border-radius: var(--radius-s); padding: 5px 8px;
  width: 100%; max-width: 22ch;
}
.cell-input:hover { border-color: var(--accent-dim); }
.cell-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); }
.cell-num { max-width: none; text-align: right; font-family: var(--mono, monospace); }

/* A had-or-not-had cell in a comparison table. The tick carries the accent
   because it is the thing being shopped for; the cross stays faint, because a
   feature a tier lacks is not an error and a column of red would read as one. */
.tick { font-size: 1.05em; line-height: 1; }
.tick-yes { color: var(--ok); }
.tick-no { color: var(--ink-faint); }
.table-card { padding: 0; overflow: hidden; }

/* A comparison table has one column per thing being compared, so it is the one
   table here that legitimately outgrows the column. It scrolls inside itself
   rather than pushing the page sideways. */
.table-scroll { overflow-x: auto; }
.table-scroll .table { min-width: 620px; }

/* The plan column stays put while the comparison scrolls under it.
   Nine columns of prose outgrow every panel this table is put in, and the
   thing being scrolled away from is the one thing you need in view: which
   plan a row is, what it costs, and the button that buys it. Sticky needs an
   opaque background of its own, or the cells sliding beneath show through. */
.plan-cell { position: sticky; left: 0; z-index: 1; background: var(--bg-1); }
.table tr:hover .plan-cell { background: var(--bg-2); }
.plan-name { display: flex; align-items: baseline; gap: var(--space-2); white-space: nowrap; }

/* Ten columns squeezed into a panel gave every cell about sixty pixels, so
   "email, webhook, push and in-app only" came out as five stacked fragments
   and read as a list of five things rather than one sentence. The table
   already scrolls, so width is free: a floor of thirteen characters buys two
   lines instead of five and costs nothing but scroll nobody minds. */
.plan-table td, .plan-table th { min-width: 13ch; }
.plan-table .plan-cell { min-width: 20ch; }

/* ---------- the buy buttons, wired ----------
   A pair of buttons in a table cell is easy to read past, and this is the one
   control on the page that takes money. So the pair is enclosed as one
   component with a trace around it, and a dot runs that trace the way the
   pulses run the flow map: the same visual language the product already uses
   for "something is moving through here", pointed at the thing you can press.

   `offset-path: border-box` means the dot follows the rounded rectangle
   without anybody hard-coding its corners, so it works whatever the buttons
   are labelled and whatever width they come out at. */
.plan-buy {
  position: relative;
  display: inline-flex;
  gap: var(--space-2);
  margin-top: var(--space-2);
  padding: var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
}
.plan-buy:hover { border-color: var(--border-bright); }

@supports (offset-path: border-box) {
  .plan-buy::after {
    content: "";
    position: absolute;
    width: 5px; height: 5px;
    border-radius: 50%;
    background: var(--accent);
    box-shadow: 0 0 6px var(--accent);
    offset-path: border-box;
    offset-rotate: 0deg;
    animation: planCurrent 6s linear infinite;
  }
  /* Rows are staggered so five of these do not travel in lockstep, which
     reads as decoration rather than as current. Same trick, same reason, as
     the stagger on the landing page's walks. */
  .table tr:nth-child(2n) .plan-buy::after { animation-delay: -1.5s; }
  .table tr:nth-child(3n) .plan-buy::after { animation-delay: -3s; }
  .table tr:nth-child(5n) .plan-buy::after { animation-delay: -4.5s; }
  .plan-buy:hover::after { animation-duration: 2.4s; }
}
@keyframes planCurrent {
  from { offset-distance: 0%; }
  to { offset-distance: 100%; }
}
@media (prefers-reduced-motion: reduce) {
  .plan-buy::after { animation: none; display: none; }
}
.issue-table { table-layout: fixed; }
.issue-table th:first-child { width: 55%; }
.issue-cell { overflow: hidden; }
.issue-title { display: flex; gap: var(--space-2); align-items: baseline; color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.issue-title:hover { text-decoration: none; }
.issue-title:hover .issue-type { color: var(--accent); }
.issue-type { font-weight: 700; }
.issue-value { color: var(--ink-muted); overflow: hidden; text-overflow: ellipsis; }
.issue-culprit { font-size: var(--fs-0); margin-top: 2px; }
.row-actions { text-align: right; white-space: nowrap; }
.nowrap { white-space: nowrap; }
.issue-table th:nth-child(2) { width: 14%; }
.issue-table th:nth-child(3) { width: 8%; }
.issue-table th:nth-child(4) { width: 8%; }
.issue-table th:nth-child(5) { width: 15%; }
.pager { display: flex; align-items: center; justify-content: center; gap: var(--space-4); margin-top: var(--space-4); }

/* ---------- issue detail ---------- */
.issue-header { display: flex; justify-content: space-between; gap: var(--space-5); margin-bottom: var(--space-4); }
.issue-h { margin: 0 0 var(--space-2); display: flex; align-items: center; gap: var(--space-3); }
.issue-value-lg { margin: 0 0 var(--space-2); color: var(--ink-muted); font-size: 1rem; }
.issue-meta { display: flex; gap: var(--space-2); flex-wrap: wrap; margin-top: var(--space-3); }
.status-actions { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; justify-content: flex-end; }
.status-chip { text-transform: uppercase; font-weight: 700; letter-spacing: 0.05em; }
.status-unresolved { color: var(--level-error); border-color: var(--error-line); }
.status-resolved { color: var(--ok); border-color: var(--accent); }
.status-ignored { color: var(--ink-faint); }

.event-card { margin-top: var(--space-4); }
.event-nav { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-3); }
.event-nav-btns { display: flex; gap: var(--space-2); }
.event-meta { display: flex; gap: var(--space-2); flex-wrap: wrap; margin-bottom: var(--space-4); }

/* ---------- stack trace ---------- */
.trace-heading { margin: var(--space-4) 0 var(--space-2); font-size: var(--fs-2); }
.section-heading {
  margin: var(--space-5) 0 var(--space-2);
  font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: 0.08em;
  font-weight: 700; color: var(--ink-muted);
}
.section-heading::before { content: "## "; color: var(--border-bright); }
.trace { display: flex; flex-direction: column; gap: 2px; }
.frame { border: 1px solid var(--border); border-radius: var(--radius-s); background: var(--bg-0); }
.frame summary {
  display: flex; align-items: baseline; gap: var(--space-2);
  padding: 7px 11px; cursor: pointer; list-style: none; font-size: var(--fs-1);
}
.frame summary::-webkit-details-marker { display: none; }
.frame summary::before { content: "▸"; color: var(--ink-faint); font-size: var(--fs-0); }
.frame[open] summary::before { content: "▾"; color: var(--accent); }
.frame-app { border-left: 2px solid var(--accent); }
.frame-lib { opacity: 0.6; }
.frame-loc { color: var(--ink); font-weight: 600; }
.frame-fn { color: var(--accent-dim); }
.frame-context { padding: 0 11px 8px; }
.frame-code { margin: 0; padding: var(--space-2) var(--space-3); font-size: var(--fs-0); background: var(--bg-1); border: 1px solid var(--border); }
.ctx-line { display: block; white-space: pre; color: var(--ink-muted); }
.ctx-crash { background: var(--error-glow); border-left: 2px solid var(--level-error); margin: 0 -12px; padding: 0 10px; color: var(--ink); }
.frame-vars { display: grid; grid-template-columns: minmax(120px, max-content) 1fr; gap: 2px var(--space-4); padding: var(--space-2) var(--space-3) var(--space-3); margin: 0; font-size: var(--fs-0); border-top: 1px solid var(--border); }
.frame-vars dt { color: var(--accent-dim); }
.frame-vars dd { margin: 0; overflow-wrap: anywhere; color: var(--ink); }
.tag-list { display: flex; gap: var(--space-2); flex-wrap: wrap; }

/* breadcrumbs + contexts */
.breadcrumb-table td { padding: 4px var(--space-3); font-size: var(--fs-0); vertical-align: top; }
.breadcrumb-table .badge-level { font-size: var(--fs-0); }
.breadcrumb-msg { color: var(--ink); overflow-wrap: anywhere; width: 100%; }
.context-group { display: flex; gap: var(--space-3); align-items: baseline; margin-bottom: var(--space-2); }
.context-name { color: var(--accent-dim); min-width: 90px; }

/* ---------- auth pages ---------- */
.auth-card {
  max-width: 440px;
  margin: var(--space-7) auto;
  position: relative;
  z-index: 1;
}
/* A sign-in card is narrow, so its gutters would be enormous. Pretend the
   column is wider than it is and the streams stay off the card. */
body:has(.auth-card) { --cy-col: 900px; }
.auth-mark { display: flex; justify-content: center; margin-bottom: var(--space-5); }
.auth-card h1 { font-size: var(--fs-4); margin-top: 0; }
.auth-card h1::before { content: "❯ "; color: var(--accent); }
.auth-alt { margin-top: var(--space-4); font-size: var(--fs-1); color: var(--ink-muted); }
.auth-divider {
  display: flex; align-items: center; gap: var(--space-3);
  margin: var(--space-4) 0 var(--space-3);
  color: var(--ink-faint); font-size: var(--fs-micro);
  text-transform: uppercase; letter-spacing: 0.1em;
}
.auth-divider::before, .auth-divider::after {
  content: ""; flex: 1; height: 1px; background: var(--border-bright);
}
.auth-sso { width: 100%; justify-content: center; }
/* The social buttons POST rather than link, so each is wrapped in a form. The
   wrapper must not introduce a gap the anchors never had. */
.auth-sso-form { display: block; margin-bottom: var(--space-2); }
.auth-sso-form .auth-sso { width: 100%; }

.rule-form { margin-top: var(--space-4); border-top: 1px dashed var(--border-bright); padding-top: var(--space-4); }

/* ---------- log stream + session timeline ---------- */
.badge-level-debug { color: var(--ink-faint); }
.badge-level-fatal { color: var(--level-error); }
.nowrap { white-space: nowrap; }
.filter-notice { display: flex; gap: var(--space-2); align-items: center; margin-bottom: var(--space-3); font-size: var(--fs-1); }
.log-table td { vertical-align: top; }
.log-message-cell { width: 100%; overflow-wrap: anywhere; }
.log-attrs summary { cursor: pointer; font-size: var(--fs-0); }
.log-attrs summary::marker { color: var(--ink-faint); }
.kv-table td { padding: 2px var(--space-3) 2px 0; font-size: var(--fs-0); vertical-align: top; }
.chip-accent { color: var(--accent-dim); border-color: var(--accent-dim); }

.trail-card { margin-top: var(--space-4); }
.trail-title { margin-top: 0; display: flex; gap: var(--space-2); align-items: baseline; }
.timeline { list-style: none; margin: 0; padding: 0; }
.timeline-item {
  display: flex; gap: var(--space-3); align-items: baseline;
  padding: 3px 0 3px var(--space-3);
  border-left: 2px solid var(--border);
  overflow-wrap: anywhere;
}
.timeline-error { border-left-color: var(--level-error); background: var(--error-glow); }
.timeline-activity { border-left-color: var(--accent); }
.timeline-time { min-width: 60px; }

/* ---------- journeys ---------- */
.journey-lookup { display: flex; gap: var(--space-5); flex-wrap: wrap; margin-bottom: var(--space-4); }
.agg-grid { display: grid; gap: var(--space-4); }
.bar-cell { width: 30%; min-width: 120px; }
.bar { height: 8px; border-radius: var(--radius-s); background: var(--accent); opacity: 0.75; min-width: 2px; display: inline-block; vertical-align: middle; }
.bar-warn { background: var(--level-warning); }
.bar-cell .mono { margin-left: var(--space-2); }
.trail-more { margin-left: auto; font-size: var(--fs-0); font-weight: 400; }

/* ---------- monitors ---------- */
.status-dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; background: var(--ink-faint); vertical-align: baseline; }
.status-up { background: var(--ok); box-shadow: 0 0 6px var(--accent-glow); }
.status-down { background: var(--level-error); }
.sparkline { display: flex; align-items: flex-end; gap: 2px; height: 48px; margin-bottom: var(--space-4); }
.spark { flex: 1; max-width: 10px; min-height: 3px; background: var(--accent); opacity: 0.7; border-radius: 1px 1px 0 0; }
.spark-fail { background: var(--level-error); opacity: 0.9; min-height: 60%; }

/* ---------- global terminal (dashboard embed + full-screen page) ----------
   A real terminal pane, in the mark's colours, following the site theme like
   everything else. Its colours are the --t-* palette below.

   Rows come from logs/_term_line.html; #term-out / #term-poll are the swap
   targets for the logs:stream cursor poll (logs/_term_append.html). */
.term {
  border: 1px solid var(--border-bright);
  border-radius: var(--radius-m);
  overflow: hidden;
  box-shadow: var(--shadow-1);
  margin-bottom: var(--space-5);
}

/* The terminal palette, and the two places that share it.

   Every --t-* below is derived from the theme tokens, so the pane follows
   light and dark automatically and cannot end up pale on a dark page.

   It used to carry a second, hand-written palette plus its own persisted
   light/dark toggle, a leftover from when this was the one deliberately dark
   surface in a light UI. Now that the whole product has a theme, that toggle
   only ever fought it: a browser that had once set it to light kept a white
   terminal for good.

   The message and value tints are mixes rather than fixed hues, so a warning
   line reads softer than its level label in either theme without anybody
   maintaining a second set of numbers.

   `.lp-term` is on the selector because the landing pages draw real .term-line
   rows in their demonstration panes rather than lookalike markup. These tokens
   are not global: they only mean anything inside a stream, so they hang off the
   two things that are one rather than off :root. */
.term, .lp-term {
  --t-bg: var(--bg-0);
  --t-fg: var(--ink);
  --t-dim: var(--ink-faint);
  --t-dimmer: var(--border-bright);
  --t-hover: var(--bg-2);
  --t-proj: var(--accent);
  --t-logger: var(--ink-muted);
  --t-val: color-mix(in oklab, var(--ink) 70%, var(--bg-0));
  --t-debug: var(--level-debug);
  --t-debug-msg: color-mix(in oklab, var(--ink) 55%, var(--bg-0));
  --t-info: var(--ink-muted);
  --t-warn: var(--level-warning);
  --t-warn-msg: color-mix(in oklab, var(--level-warning) 65%, var(--ink));
  --t-warn-val: color-mix(in oklab, var(--level-warning) 45%, var(--ink));
  --t-err: var(--level-error);
  --t-err-msg: color-mix(in oklab, var(--level-error) 65%, var(--ink));
  --t-err-val: color-mix(in oklab, var(--level-error) 45%, var(--ink));
  --t-fatal: var(--level-fatal);
  --t-fatal-msg: color-mix(in oklab, var(--level-fatal) 65%, var(--ink));
  --t-fatal-val: color-mix(in oklab, var(--level-fatal) 45%, var(--ink));
}

.term-head {
  display: flex;
  gap: var(--space-3);
  align-items: baseline;
  padding: var(--space-2) var(--space-3);
  background: var(--bg-1);
  border-bottom: 1px solid var(--border);
}
.term-title { color: var(--ink); font-weight: 600; }
.term-dot { color: var(--accent); }
.term-sub { color: var(--ink-faint); font-size: 12px; }
.term-link { margin-left: auto; font-size: 12px; }
#term-out {
  height: 320px;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--t-bg);
  color: var(--t-fg);
  padding: var(--space-2) var(--space-3);
  font-size: 12.5px;
  line-height: 1.6;
}
/* Full-screen terminal page: break out of the centered container and let the
   pane own the whole viewport below the app bar. */
main.container:has(.term-page) { max-width: none; padding: var(--space-3) var(--space-4); }
.term-page {
  display: flex;
  flex-direction: column;
  height: calc(100dvh - 78px); /* app bar + main padding */
  min-height: 0;
}
.term-page .page-head { margin-bottom: var(--space-3); }
.term-page .term {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  margin-bottom: 0;
}
.term-page #term-out { flex: 1; height: auto; }
/* Browser-fullscreen (the ⛶ button): just the pane, edge to edge. */
.term:fullscreen { display: flex; flex-direction: column; border: 0; border-radius: 0; background: var(--t-bg); }
.term:fullscreen #term-out { flex: 1; height: auto; }
.term-fs {
  margin-left: var(--space-3); border: 0; background: none; cursor: pointer;
  color: var(--ink-faint); font-size: 14px; line-height: 1;
}
.term-fs:hover { color: var(--accent); }
.term:fullscreen .term-fs { color: var(--t-dim); }

/* One row = aligned columns: time · project · level · message. Grid keeps the
   gutters straight and gives wrapped messages a hanging indent for free. */
.term-line {
  display: grid;
  grid-template-columns: 14ch 11ch 5ch 1fr;
  column-gap: var(--space-3);
  align-items: baseline;
  padding: 1px 2px;
  border-radius: 2px;
}
.term-line:hover { background: var(--t-hover); }
.term-ts { color: var(--t-dim); }
.term-proj { color: var(--t-proj); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.term-lvl { font-weight: 700; font-size: 11px; letter-spacing: 0.5px; color: var(--t-dim); }
.term-logger { color: var(--t-logger); }
.term-msg { white-space: pre-wrap; word-break: break-word; min-width: 0; }

/* Structured key=value attributes: dim keys, soft-cyan values. The message
   text stays the brightest thing on the line. */
.term-k { color: var(--t-dim); }
.term-eq { color: var(--t-dimmer); }
.term-v { color: var(--t-val); }

/* Level palette (classic terminal scheme): debug green-grey, info blue,
   warn amber, error/fatal red. Warn/error lines also tint the message and
   values so problems pop out of the stream at a glance. */
.term-lvl-debug .term-lvl { color: var(--t-debug); }
.term-lvl-debug .term-msg-text { color: var(--t-debug-msg); }
.term-lvl-info .term-lvl { color: var(--t-info); }
.term-lvl-warning .term-lvl { color: var(--t-warn); }
.term-lvl-warning .term-msg-text { color: var(--t-warn-msg); }
.term-lvl-warning .term-v { color: var(--t-warn-val); }
.term-lvl-error .term-lvl { color: var(--t-err); }
.term-lvl-error .term-msg-text { color: var(--t-err-msg); }
.term-lvl-error .term-v { color: var(--t-err-val); }
.term-lvl-fatal .term-lvl { color: var(--t-fatal); font-weight: 800; }
.term-lvl-fatal .term-msg-text { color: var(--t-fatal-msg); font-weight: 600; }
.term-lvl-fatal .term-v { color: var(--t-fatal-val); }
.term-empty { color: var(--t-dim); padding: var(--space-4); text-align: center; }
.term-live { color: var(--accent); font-weight: 700; font-size: 0.8em; }
@media (max-width: 720px) {
  .term-line { grid-template-columns: 14ch 5ch 1fr; }
  .term-proj { display: none; }
}

/* ---------- shell: top bar + collapsible left sidebar (console-style) ---------- */
.shell { display: flex; align-items: stretch; }
.sidenav {
  position: sticky; top: 50px;
  height: calc(100dvh - 50px);
  width: 200px; flex: none;
  display: flex; flex-direction: column;
  background: var(--bg-1);
  border-right: 1px solid var(--border);
  padding-top: var(--space-3);
  transition: width 0.15s ease;
}
.snav { display: flex; flex-direction: column; gap: 2px; padding: 0 var(--space-2); flex: 1; overflow-y: auto; }
.snav-item {
  display: flex; gap: var(--space-3); align-items: center;
  padding: 7px 10px; border-radius: var(--radius-m);
  color: var(--ink-muted); font-size: var(--fs-2); white-space: nowrap;
}
.snav-item:hover { background: var(--bg-2); color: var(--ink); }
.snav-item.active { background: var(--accent-glow); color: var(--accent-dim); font-weight: 600; }
.snav-ico { width: 1.3em; text-align: center; flex: none; }
.snav-collapse {
  display: flex; gap: var(--space-3); align-items: center;
  border: 0; border-top: 1px solid var(--border); background: none;
  color: var(--ink-faint); cursor: pointer; padding: 9px 18px;
  font: inherit; font-size: var(--fs-0); white-space: nowrap;
}
.snav-collapse:hover { color: var(--ink); }
html.snav-min .sidenav { width: 52px; }
html.snav-min .snav-label { display: none; }
html.snav-min .snav-item { justify-content: center; padding: 7px 0; }
html.snav-min .snav-collapse { justify-content: center; padding: 9px 0; }
html.snav-min .snav-collapse-ico { transform: rotate(180deg); }
.sidenav:not(:has(.snav-item)) { display: none; }  /* auth pages: no org nav */
.shell-main { flex: 1; min-width: 0; }
@media (max-width: 720px) {
  .sidenav { width: 52px; }
  .sidenav .snav-label { display: none; }
  .snav-item { justify-content: center; padding: 7px 0; }
  .snav-collapse { display: none; }
}

/* ---------- logs explorer ---------- */
main.container:has(.explorer) { max-width: none; }
.explorer-bar { display: flex; gap: var(--space-2); margin-bottom: var(--space-3); }
.explorer-query { flex: 1; font-family: var(--font-mono); }
.explorer-timeline { padding: var(--space-3) var(--space-4); margin-bottom: var(--space-4); }
.timeline-head { display: flex; gap: var(--space-4); align-items: baseline; margin-bottom: var(--space-2); font-size: var(--fs-1); }
.timeline-head .btn { margin-left: auto; }
.histogram { display: flex; align-items: flex-end; gap: 1px; height: 84px; border-bottom: 1px solid var(--border); }
.hist-col { flex: 1; min-width: 2px; height: 100%; display: flex; flex-direction: column-reverse; }
.hist-col:hover { background: var(--accent-glow); }
.hist-seg { width: 100%; }
.hist-debug { background: var(--level-debug); }
.hist-info { background: var(--level-info); }
.hist-warning { background: var(--level-warning); }
.hist-error { background: var(--level-error); }
.hist-fatal { background: var(--level-fatal); }
.explorer-grid { display: grid; grid-template-columns: 220px 1fr; gap: var(--space-4); align-items: start; }
.explorer-facets { padding: var(--space-3); position: sticky; top: 62px; }
.facet-title { margin: 0 0 var(--space-2); font-size: var(--fs-micro); font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-muted); }
.facet-group { margin-bottom: var(--space-3); }
.facet-name { font-size: var(--fs-0); color: var(--ink-muted); margin-bottom: 4px; }
.facet-row { display: flex; gap: var(--space-2); align-items: baseline; padding: 3px 6px; border-radius: var(--radius-s); color: var(--ink-muted); font-size: var(--fs-1); }
.facet-row:hover { background: var(--bg-2); }
.facet-row.active { background: var(--accent-glow); color: var(--accent-dim); }
.facet-value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
.facet-count { color: var(--ink-faint); }
.sev-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; flex: none; background: var(--ink-faint); vertical-align: baseline; }
.sev-debug { background: var(--level-debug); }
.sev-info { background: var(--level-info); }
.sev-warning { background: var(--level-warning); }
.sev-error { background: var(--level-error); }
.sev-fatal { background: var(--level-fatal); }
.sev-cell { width: 20px; }
.log-expand summary { cursor: pointer; list-style: none; }
.log-expand summary::-webkit-details-marker { display: none; }
.attr-preview { color: var(--ink-faint); font-size: var(--fs-0); }
.log-detail {
  margin: var(--space-2) 0; padding: var(--space-2) var(--space-3);
  background: var(--bg-0); border: 1px solid var(--border); border-radius: var(--radius-s);
}
.log-row-error .sev-dot, .log-row-fatal .sev-dot { box-shadow: 0 0 5px var(--error-line); }
@media (max-width: 900px) {
  .explorer-grid { grid-template-columns: 1fr; }
  .explorer-facets { position: static; }
}

/* ---------- ops console ----------
   The operator's view. Deliberately the same surface as everything else, because a
   separate skin for staff would be a second design to maintain and a second
   thing to get wrong. Only two shapes are new: the platform tiles and the
   attention rows. */
.ops-tiles {
  display: grid; gap: var(--space-3);
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  margin-bottom: var(--space-6);
}
.ops-tile { display: flex; flex-direction: column; gap: 2px; padding: var(--space-4); }
.ops-tile-value { font-size: var(--fs-4); font-weight: 700; color: var(--ink); letter-spacing: -0.03em; }
.ops-tile-label { font-size: var(--fs-0); color: var(--ink-muted); }
.ops-tile-note { font-size: var(--fs-0); color: var(--ink-muted); }

/* A queue at zero stays on the page, dimmed. "Nothing is broken" is the answer
   the operator came for; hiding the row makes it look like we stopped checking. */
.ops-queue-off { opacity: 0.45; }
.ops-queue-count { width: 5ch; text-align: right; font-weight: 700; }
.ops-count-on { color: var(--level-warning); }
.ops-count-urgent { color: var(--level-error); }
.ops-queue-on td { background: var(--bg-2); }

.ops-workers { margin-bottom: var(--space-4); }
.ops-worker { display: flex; align-items: center; gap: var(--space-3); padding: 3px 0; }

/* The impersonation banner. The only element in the product allowed to be
   loud: an operator who forgets they are wearing somebody else's face will
   make a change in that person's name and believe it was their own. */
.ops-impersonating {
  position: sticky; top: 0; z-index: 60;
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-2) var(--space-5);
  background: var(--level-error); color: var(--bg-0);
  font-size: var(--fs-0); font-weight: 600;
}
.ops-impersonating strong { font-weight: 700; }
.ops-impersonating .btn {
  margin-left: auto; flex: none;
  background: transparent; border-color: var(--bg-0); color: var(--bg-0);
}
.ops-impersonating .btn:hover { background: var(--bg-0); color: var(--level-error); }
.ops-impersonating-mark { flex: none; }
.ops-quota-grid { display: grid; gap: 0 var(--space-4); grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.ops-actions { display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: center; }
/* A chip that is also a button, used for revoking a project key in place. */
.ops-chip-button { font: inherit; cursor: pointer; }
.ops-chip-button:hover { border-color: var(--level-error); color: var(--level-error); }

/* ---------- journeys: flow map, defined journeys, replay ---------- */
.journey-tabs { margin-top: calc(-1 * var(--space-2)); }
.chip-warn { color: var(--level-warning); border-color: var(--level-warning); }
.field-error { color: var(--level-error); font-size: var(--fs-0); margin-top: 2px; }
.form-actions { display: flex; gap: var(--space-3); margin-top: var(--space-4); }
/* The routing ladder in alerts/policy_edit.html, which is still a table: its
   rows are rungs of one rule and read across, not blocks of their own. */
.step-table td { vertical-align: top; padding: var(--space-2) var(--space-2); }
.step-table input, .step-table select { width: 100%; }

/* snippet tabs: one code block visible at a time */
.snippet-picker { display: flex; gap: var(--space-2); margin-bottom: var(--space-2); flex-wrap: wrap; }
.snippet-picker .btn.active { color: var(--accent); border-color: var(--accent); }
.getting-started { text-align: left; }
.getting-started .code-block { text-align: left; }

/* flow map. Geometry comes from the server; colour lives only here. */
.flow-card { padding-bottom: var(--space-3); }
/* Wraps, because the head now carries a title, a count, a follow box, a step
   picker and five buttons, and on a laptop beside a 380px inspector that is
   more than one line's worth. Without this the title is what gives, breaking
   "Where sessions go" across three lines to make room for a button. */
.flow-head {
  display: flex; flex-wrap: wrap; gap: var(--space-3);
  align-items: baseline; margin-bottom: var(--space-3);
}
.flow-head h3 { flex: 0 0 auto; white-space: nowrap; }
.flow-head h3 { margin: 0; }
.flow-head .btn { margin-left: auto; }
.flow-toggle { display: inline-flex; gap: var(--space-2); align-items: center; font-size: var(--fs-1); color: var(--ink-muted); }
.flow-tools { display: flex; gap: var(--space-1); align-items: center; margin-left: auto; }
.flow-zoom {
  min-width: 5ch; text-align: center;
  font-size: var(--fs-0); color: var(--ink-faint); font-variant-numeric: tabular-nums;
}

/* The canvas. A window of its own size onto a drawing that keeps its real
   dimensions. See the note in _flow_panel.html about the missing viewBox.
   It has to be given a height, because there is no longer an aspect ratio to
   derive one from. */
.flow-stage {
  position: relative; /* the nav pad and minimap sit on it */
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  background: var(--bg-0);
  cursor: grab;
  min-height: 420px;
  /* A dotted ground, so panning an empty region still reads as movement. */
  background-image: radial-gradient(var(--border) 1px, transparent 1px);
  background-size: 22px 22px;
}
.flow-stage:focus-visible { outline: 1px solid var(--accent); outline-offset: -1px; }
.flow-stage.is-panning { cursor: grabbing; }
.flow { display: block; width: 100%; height: 100%; touch-action: none; }

.flow-edge { fill: none; stroke: var(--border-bright); opacity: 0.85; transition: opacity 120ms linear; }
/* context-stroke: the head takes the colour of whatever edge drew it, so the
   lit state needs no second marker and no colour in JS. */
.flow-arrow path { fill: context-stroke; }
.flow-edge-back { stroke-dasharray: 3 3; opacity: 0.5; }
.flow-edge-exit { stroke: var(--ink-faint); opacity: 0.45; }
/* Dim the field while a node is focused, then relight what touches it. */
.flow[data-focus] .flow-edge { opacity: 0.12; }
.flow[data-focus] .flow-edge.is-lit { opacity: 1; stroke: var(--accent); }
.flow[data-focus] .flow-node:not(:hover):not(:focus) rect { opacity: 0.45; }

.flow-node rect {
  fill: var(--bg-1);
  stroke: var(--border-bright);
  stroke-width: 1;
}
.flow-node text {
  fill: var(--ink);
  font-family: var(--font-mono);
  font-size: 11.5px;
  pointer-events: none;
}
.flow-node:hover rect, .flow-node:focus rect { stroke: var(--accent); }
.flow-node:focus { outline: none; }
.flow-node-entry rect { stroke: var(--accent); }
.flow-node-exit rect { fill: transparent; stroke-dasharray: 3 3; }
.flow-node-exit text { fill: var(--ink-faint); }
.flow-node-errors rect { stroke: var(--error-line); }
.flow-error-dot { fill: var(--level-error); }
.flow-badge { fill: var(--ink-faint); font-size: 11px; text-anchor: end; }

.flow-legend { display: flex; gap: var(--space-2); align-items: center; flex-wrap: wrap; margin: var(--space-3) 0 0; font-size: var(--fs-0); }
.flow-key { display: inline-block; width: 10px; height: 10px; border: 1px solid var(--border-bright); border-radius: 2px; }
.flow-key-entry { border-color: var(--accent); }
.flow-key-exit { border-style: dashed; }
.flow-key-back { height: 0; width: 14px; border: 0; border-top: 1px dashed var(--border-bright); }

/* journey analytics */
.journey-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--space-3); margin-bottom: var(--space-4); }
.stat-tile { display: flex; flex-direction: column; gap: 2px; }
.stat-label { font-size: var(--fs-0); text-transform: lowercase; }
.stat-value { font-size: var(--fs-4); line-height: 1.2; }
.stat-warn { color: var(--level-warning); }

/* replay */
.replay-meta { display: flex; gap: var(--space-3); align-items: center; flex-wrap: wrap; margin-bottom: var(--space-3); }
.replay-meta .btn { margin-left: auto; }
.replay-journeys { display: flex; gap: var(--space-2); flex-wrap: wrap; margin-bottom: var(--space-3); }
.replay:focus { outline: 1px solid var(--accent); outline-offset: 2px; }
.replay-controls {
  display: flex; gap: var(--space-2); align-items: center; flex-wrap: wrap;
  padding-bottom: var(--space-3); margin-bottom: var(--space-3);
  border-bottom: 1px solid var(--border);
}
.replay-scrub { flex: 1; min-width: 140px; accent-color: var(--accent); }
.replay-clock { min-width: 5ch; }
.replay-steps { list-style: none; margin: 0; padding: 0; }
.replay-step {
  border-left: 2px solid var(--border);
  padding: var(--space-2) var(--space-3);
  opacity: 1;
  cursor: pointer;
  transition: opacity 120ms linear, background 120ms linear;
}
.replay-step.is-past { opacity: 0.4; }
.replay-step.is-current { border-left-color: var(--accent); background: var(--accent-glow); }
.replay-step[data-error="1"] { border-left-color: var(--level-error); background: var(--error-glow); }
.replay-step-pre { border-left-style: dotted; }
.replay-head { display: flex; gap: var(--space-2); align-items: baseline; flex-wrap: wrap; }
.replay-offset { min-width: 7ch; }
.replay-name { overflow-wrap: anywhere; }
.replay-time { margin-left: auto; }
.replay-gap {
  font-size: var(--fs-0);
  padding: 2px 0 var(--space-2);
  border-top: 1px dotted var(--border);
  margin-top: var(--space-2);
}
.replay-props {
  display: grid; grid-template-columns: max-content 1fr; gap: 0 var(--space-3);
  margin: var(--space-2) 0 0; font-size: var(--fs-1);
}
.replay-props dt { color: var(--ink-faint); }
.replay-props dd { margin: 0; overflow-wrap: anywhere; }
.replay-attached { list-style: none; margin: var(--space-2) 0 0; padding: 0 0 0 var(--space-3); border-left: 1px solid var(--border); }
.replay-attached li { display: flex; gap: var(--space-2); align-items: baseline; font-size: var(--fs-1); padding: 1px 0; overflow-wrap: anywhere; }
.replay-attached-errors { border-left-color: var(--error-line); }

@media (prefers-reduced-motion: reduce) {
  .replay-step, .flow-edge { transition: none; }
}

/* A full-width bar and its label must share a row, not stack. */
.bar-row { display: flex; align-items: center; gap: var(--space-2); }
.bar-track { flex: 1; min-width: 40px; display: block; }
.bar-track .bar { display: block; }
.bar-pct { min-width: 4ch; text-align: right; }
/* A step name is the one stat that is a word, not a number. */
.stat-value-name { font-size: 1rem; overflow-wrap: anywhere; line-height: 1.35; }
/* Few buckets shouldn't stretch into slabs. */
.histogram-timing { height: 72px; gap: 3px; justify-content: flex-start; }
.histogram-timing .hist-col { flex: 0 0 auto; width: 28px; }
.hist-timing { background: var(--accent); opacity: 0.65; }

/* ---------- journeys: the full-screen flow workspace ---------- */
/* Break out of the centred container and let the map own the viewport, the
   same trick the terminal page uses. */
main.container:has(.flow-page) { max-width: none; padding: var(--space-3) var(--space-4); }
.flow-page {
  display: flex;
  flex-direction: column;
  height: calc(100dvh - 78px); /* app bar + main padding */
  min-height: 0;
}
.flow-page .page-head { margin-bottom: var(--space-3); }
.flow-page .page-head .filter-bar { margin-left: auto; }
.flow-page .journey-tabs { margin-bottom: var(--space-3); }

.flow-workspace {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: 1fr 380px;
  gap: var(--space-4);
}
.flow-canvas {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  background: var(--bg-1);
  padding: var(--space-3) var(--space-4);
  overflow: hidden auto;
}
/* The map takes what is left, but never less than this. The panel above it
   grew a traffic strip and the one below it holds the scorecard and the
   legend; on a short window those three together were squeezing the map, the
   only part anybody came for, down to a couple of centimetres. Below the floor
   the panel scrolls instead. */
.flow-canvas .flow-stage { flex: 1 1 auto; min-height: 260px; }
.flow-canvas .flow { max-height: none; height: 100%; }
.flow-canvas .empty-state { margin: auto; border: 0; background: none; }

.flow-inspector {
  display: flex;
  flex-direction: column;
  min-height: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  background: var(--bg-1);
  overflow: hidden;
}
.inspector-head {
  display: flex; gap: var(--space-3); align-items: baseline;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
}
.inspector-head h3 { margin: 0; overflow-wrap: anywhere; }
.inspector-head h3::before { content: "# "; color: var(--ink-faint); }
.inspector-head .btn, .inspector-head .faint { margin-left: auto; }
.inspector-body { flex: 1; min-height: 0; overflow-y: auto; padding: var(--space-3) var(--space-4); }
.inspector-body h4 {
  font-size: var(--fs-0); text-transform: lowercase; color: var(--ink-muted);
  margin: var(--space-4) 0 var(--space-2);
}
.inspector-body h4::before { content: "› "; color: var(--ink-faint); }
.inspector-hints { list-style: none; padding: 0; margin: var(--space-4) 0 0; font-size: var(--fs-0); }
.inspector-hints li { padding: 1px 0; }
.inspector-hints li::before { content: "· "; }

.inspector-stats { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-2); margin: 0; }
.inspector-stats dt { color: var(--ink-muted); font-size: var(--fs-0); }
.inspector-stats dd { margin: 0; font-size: var(--fs-3); }
.inspector-note { margin: var(--space-3) 0 0; font-size: var(--fs-1); }

.inspector-edges { list-style: none; margin: 0; padding: 0; }
.inspector-edges li { display: flex; gap: var(--space-2); align-items: center; padding: 2px 0; font-size: var(--fs-1); }
.edge-name { flex: 0 0 45%; overflow-wrap: anywhere; }
.edge-n { min-width: 4ch; text-align: right; }

.inspector-sessions { list-style: none; margin: 0; padding: 0; }
.session-row {
  display: flex; gap: var(--space-2); justify-content: space-between; align-items: baseline;
  width: 100%; padding: var(--space-2) var(--space-2);
  background: none; border: 0; border-bottom: 1px solid var(--border);
  color: var(--ink); font: inherit; font-size: var(--fs-1); text-align: left; cursor: pointer;
}
.session-row:hover { background: var(--accent-glow); color: var(--accent); }

/* A step selected on the map stays lit while its panel is open. */
.flow-node.is-selected rect { stroke: var(--accent); stroke-width: 2; }

/* The replay embedded in the 380px inspector, rather than a full page. */
.replay-compact { padding: 0; border: 0; background: none; margin-top: var(--space-3); }
.replay-compact .replay-controls { padding-bottom: var(--space-2); margin-bottom: var(--space-2); }
.replay-compact .replay-scrub { width: 100%; margin-bottom: var(--space-2); }
.replay-compact .replay-step { padding: var(--space-2) var(--space-2); font-size: var(--fs-0); }
.replay-compact .replay-time { display: none; }
.replay-compact .replay-props { grid-template-columns: 1fr; gap: 0; }
.replay-compact .replay-props dt { margin-top: 2px; }

@media (max-width: 1100px) {
  .flow-workspace { grid-template-columns: 1fr; grid-template-rows: 1fr auto; }
  .flow-page { height: auto; }
  .flow-inspector { max-height: 60vh; }
}
/* Browser-fullscreen (the ⛶ button): the workspace, edge to edge.

   Fullscreen is not the same page made bigger, it is the page with everything
   that is not the graph taken out of the way. The scorecard and the legend are
   furniture for the normal view; in here they cost 110px of height that a
   forty-step map has better uses for. */
.flow-workspace:fullscreen {
  position: relative;
  grid-template-columns: 1fr;
  background: var(--bg-0);
  padding: var(--space-4);
  gap: var(--space-4);
}
.flow-workspace:fullscreen .flow-scorecard,
.flow-workspace:fullscreen .flow-legend { display: none; }

/* The inspector stops being a column and becomes a panel over the map, so the
   canvas has the whole width rather than three quarters of it. */
.flow-workspace:fullscreen .flow-inspector {
  position: absolute;
  top: var(--space-4);
  right: var(--space-4);
  bottom: var(--space-4);
  width: 360px;
}
/* Nothing selected, so there is nothing to read: the hints list is what the
   resting panel is made of, and covering a fifth of the map with instructions
   you followed to get here is the opposite of diving in. Clicking a step swaps
   real content in and the panel appears; closing it swaps the hints back and
   the panel goes away again, with no JavaScript either way. */
.flow-workspace:fullscreen .flow-inspector:has(.inspector-hints) { display: none; }
/* The scorecard yields before the map does. It is the footnote; the map is the
   page. Sized as a share of the panel, it grew with the panel and shrank with
   it, which meant that on a short window the map and the scorecard starved
   together rather than the scorecard giving way first. */
.flow-scorecard {
  flex: 0 1 auto; max-height: 22%; min-height: 0;
  overflow-y: auto; margin-top: var(--space-3);
}
.flow-scorecard h4 {
  font-size: var(--fs-0); text-transform: lowercase; color: var(--ink-muted);
  margin: 0 0 var(--space-2);
}
.flow-scorecard h4::before { content: "› "; color: var(--ink-faint); }
.scorecard-list { list-style: none; margin: 0; padding: 0; }
.scorecard-list li {
  display: flex; gap: var(--space-3); align-items: center;
  padding: var(--space-2) 0; border-top: 1px solid var(--border); font-size: var(--fs-1);
}
.scorecard-name { flex: 0 0 140px; }
.scorecard-list .bar-track { flex: 0 0 160px; }
.scorecard-pct { min-width: 4ch; text-align: right; }
.scorecard-note { color: var(--ink-faint); overflow-wrap: anywhere; }

/* Traffic as a pulse travelling the trace: a circuit board, not a diagram.

   The dash pattern is in real user units and comes from the element's own
   style attribute, because the gap has to be this edge's length minus the dot
   and a stylesheet cannot know that. --pulse-len is the same length, and it is
   what the keyframes count down, so the dot crosses the whole path exactly
   once per cycle. --pulse-dur is length / speed, computed in flowmap.py, which
   is what makes every dot on the map travel at the same rate. */
.flow-pulse {
  fill: none;
  stroke: var(--pulse-tint, var(--accent));
  stroke-linecap: round;
  stroke-dashoffset: var(--pulse-len, 100);
  opacity: 0.95;
  /* The one place a glow earns its keep: it reads as current in a trace.
     Cheap here, because only the busiest edges carry a pulse. */
  filter: drop-shadow(0 0 2px var(--pulse-tint, var(--accent)));
  pointer-events: none;
  animation: flow-pulse var(--pulse-dur, 3s) linear var(--pulse-delay, 0s) infinite;
}
@keyframes flow-pulse {
  from { stroke-dashoffset: var(--pulse-len, 100); }
  to   { stroke-dashoffset: 0; }
}

/* The pulse palette.

   Colour is reserved for severity everywhere else in this system, and these
   twelve are the deliberate exception. A map where every dot is the same green
   is a map where you cannot answer "where did the traffic leaving Cart
   actually go?", because the four dots that left together are indistinguishable
   the moment they separate. Hues are spread evenly and kept at one lightness so
   no trace reads as more important than another: they are labels, not levels.
   Deliberately not the severity ramp's red or amber, which would say "error"
   on a picture about traffic. */
.flow-pulse-h0  { --pulse-tint: #82c494; }
.flow-pulse-h1  { --pulse-tint: #6fc8d8; }
.flow-pulse-h2  { --pulse-tint: #7fa8e8; }
.flow-pulse-h3  { --pulse-tint: #a394e6; }
.flow-pulse-h4  { --pulse-tint: #d18fd6; }
.flow-pulse-h5  { --pulse-tint: #e58fb0; }
.flow-pulse-h6  { --pulse-tint: #e0a878; }
.flow-pulse-h7  { --pulse-tint: #d4c274; }
.flow-pulse-h8  { --pulse-tint: #a8cc74; }
.flow-pulse-h9  { --pulse-tint: #74cca9; }
.flow-pulse-h10 { --pulse-tint: #74c9c0; }
.flow-pulse-h11 { --pulse-tint: #9ab6d8; }
:root[data-theme="light"] .flow-pulse-h0  { --pulse-tint: #2f6041; }
:root[data-theme="light"] .flow-pulse-h1  { --pulse-tint: #146b7d; }
:root[data-theme="light"] .flow-pulse-h2  { --pulse-tint: #2f5fa8; }
:root[data-theme="light"] .flow-pulse-h3  { --pulse-tint: #5a45a8; }
:root[data-theme="light"] .flow-pulse-h4  { --pulse-tint: #8c3a92; }
:root[data-theme="light"] .flow-pulse-h5  { --pulse-tint: #a33a63; }
:root[data-theme="light"] .flow-pulse-h6  { --pulse-tint: #96551c; }
:root[data-theme="light"] .flow-pulse-h7  { --pulse-tint: #7d6a10; }
:root[data-theme="light"] .flow-pulse-h8  { --pulse-tint: #4f7a17; }
:root[data-theme="light"] .flow-pulse-h9  { --pulse-tint: #1c7a55; }
:root[data-theme="light"] .flow-pulse-h10 { --pulse-tint: #157069; }
:root[data-theme="light"] .flow-pulse-h11 { --pulse-tint: #3d5f80; }

/* The walks: one dot per measured session route.

   Almost nothing here, because the animation is SMIL on the elements
   themselves: position, size and timing all come from flowmap.py, which is the
   only thing that knows how far apart two steps ended up. This file says what
   colour they are, which is the same contract flow.js works to. */
.flow-walks { pointer-events: none; }
.flow-walk-dot {
  fill: var(--pulse-tint, var(--accent));
  /* The one place a glow earns its keep: it reads as a live thing moving
     across a static diagram. Cheap, because there are at most fourteen. */
  filter: drop-shadow(0 0 3px var(--pulse-tint, var(--accent)));
}
.flow-walk-x path {
  fill: none;
  stroke: var(--pulse-tint, var(--accent));
  stroke-width: 2.2;
  stroke-linecap: round;
  filter: drop-shadow(0 0 3px var(--pulse-tint, var(--accent)));
}
.flow-key-death { border: 0; color: var(--ink-muted); font-size: 0.9em; }

/* ---------- the flow map, live ---------- */
/* The bar that says where the followed person is standing, and the ring on the
   box they are standing in. Nothing else on the map moves while this updates:
   see _flow_here.html for why that constraint shapes the whole feature. */
.flow-here {
  flex: 0 0 auto;
  display: flex; gap: var(--space-2); align-items: center;
  margin-bottom: var(--space-2); padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border); border-radius: 3px;
  font-size: var(--fs-0);
}
.flow-here-step { color: var(--accent); }
/* A pressed toggle reads as on. The same treatment the snippet picker's
   selected tab gets, because it is the same statement: this one is live. */
.flow-tools .btn[aria-pressed="true"] { color: var(--accent); border-color: var(--accent); }
.flow-here.is-waiting .live-dot { opacity: 0.3; animation: none; }
.flow-here.is-offmap::after {
  content: "not on the map yet · redraw to add it";
  color: var(--ink-faint); margin-left: auto;
}

/* The step they are on. A ring rather than a fill, so the label underneath
   stays readable: this marks a box, it does not replace one. */
.flow-node.is-here rect {
  stroke: var(--accent); stroke-width: 2;
  animation: flow-here 1.6s ease-in-out infinite;
}
.flow-node.is-here text { fill: var(--accent); }
@keyframes flow-here {
  0%, 100% { stroke-opacity: 1; }
  50%      { stroke-opacity: 0.35; }
}
@media (prefers-reduced-motion: reduce) {
  .flow-node.is-here rect { animation: none; }
}

/* While a step is focused, only its own traces stay live. A walk crosses
   several steps, so it has no one step to belong to: it dims with everything
   else rather than pretending to an answer it does not have. */
.flow[data-focus] .flow-pulse { opacity: 0.06; }
.flow[data-focus] .flow-pulse.is-lit { opacity: 1; }
.flow[data-focus] .flow-walk { opacity: 0.08; }

@media (prefers-reduced-motion: reduce) {
  .flow-pulse, .flow-walks { display: none; }
}

/* ---------- journeys: shared page furniture ---------- */
/* Every journeys surface gets the width the map already had. The tabs sit
   inside one container, and a tab strip whose pages are alternately full-bleed
   and centred reads as the page jumping sideways when you click it. */
main.container:has(.journeys-page) { max-width: none; padding: var(--space-3) var(--space-4); }
.page-head-note { margin-left: var(--space-3); }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}
.chip-error { color: var(--level-error); border-color: var(--error-line); }
.table td.num, .table th.num { text-align: right; font-variant-numeric: tabular-nums; }

/* ---------- journeys: traffic over time ---------- */
/* The strip that puts the clock back into the map. Bars are anchors, so this
   works with keyboard and middle-click like any other link. */
.flow-series { flex: 0 0 auto; margin-bottom: var(--space-3); }
.series-head {
  display: flex; gap: var(--space-3); align-items: baseline;
  margin-bottom: var(--space-2); font-size: var(--fs-0);
}
.series-head .btn { margin-left: auto; }
.series-hint { margin-left: auto; }
@container (max-width: 620px) { .series-hint { display: none; } }
@media (max-width: 1000px) { .series-hint { display: none; } }

.series-bars {
  display: flex; align-items: flex-end; gap: 1px;
  height: 46px; padding: 0 1px;
  border-bottom: 1px solid var(--border);
  background:
    /* A rule at the halfway mark, so a spike has something to be tall against. */
    linear-gradient(var(--border), var(--border)) 0 50% / 100% 1px no-repeat;
}
.series-bar {
  position: relative;
  flex: 1 1 0; min-width: 0; height: 100%;
  display: flex; align-items: flex-end;
  text-decoration: none;
}
.series-fill {
  width: 100%;
  /* A bucket with no traffic still draws a hairline. An empty column that
     vanished entirely would let its neighbours close ranks, and "nothing
     happened at 4am" would read as "4am did not happen". */
  height: max(var(--h, 0%), 1px);
  background: var(--ink-faint);
  transition: background 90ms linear, height 120ms ease-out;
}
.series-bar:hover .series-fill,
.series-bar:focus-visible .series-fill { background: var(--accent); }
/* Errors are a tick at the foot of the bar, not the colour of the whole bar.
   Recolouring it made the strip read as "everything is on fire" on any app
   that throws steadily, and buried the thing the strip is actually for, which
   is the shape of the traffic. The height still means volume; the red only
   ever means "and some of it went wrong here". */
.series-bar.has-errors .series-fill {
  background:
    linear-gradient(var(--level-error), var(--level-error)) 0 100% / 100% 3px no-repeat,
    var(--ink-faint);
}
.series-bar.has-errors:hover .series-fill {
  background:
    linear-gradient(var(--level-error), var(--level-error)) 0 100% / 100% 3px no-repeat,
    var(--accent);
}
.series-bar.is-current .series-fill { background: var(--accent); }
.series-bar.is-current::after {
  content: ""; position: absolute; inset: 0;
  border: 1px solid var(--accent); background: var(--accent-glow);
}
.series-bar:focus-visible { outline: 1px solid var(--accent); outline-offset: 1px; }

/* One tooltip, drawn by the bar that is hovered. Not a title attribute: the
   browser's own delay is about a second, which is far too slow for something
   you are sweeping a pointer across. */
.series-tip {
  position: absolute; bottom: calc(100% + 6px); left: 50%;
  transform: translateX(-50%);
  display: none; z-index: 3;
  padding: 3px 8px; white-space: nowrap; font-size: var(--fs-0);
  color: var(--ink); background: var(--bg-2);
  border: 1px solid var(--border-bright); border-radius: var(--radius-s);
}
.series-bar:hover .series-tip, .series-bar:focus-visible .series-tip { display: block; }
/* The first and last few bars would push their tooltip off the panel. */
.series-bar:nth-child(-n+6) .series-tip { left: 0; transform: none; }
.series-bar:nth-last-child(-n+6) .series-tip { left: auto; right: 0; transform: none; }

.series-axis {
  display: flex; justify-content: space-between;
  font-size: var(--fs-0); padding-top: 2px;
}

/* ---------- journeys: following one person ---------- */
.flow-focus {
  display: flex; gap: var(--space-2); align-items: center; flex-wrap: wrap;
  margin: 0 0 var(--space-3); font-size: var(--fs-0);
}
.flow-focus-who { color: var(--ink); overflow-wrap: anywhere; }
.flow-focus .btn { margin-left: auto; }
/* The follow box takes the free space so it sits hard against the tools,
   which keep their own margin. Two competing `margin-left: auto` would split
   the gap between them and leave the box floating in the middle of the head,
   which reads as an accident. */
.flow-follow-form { margin-left: auto; }
.flow-head .flow-follow-form ~ .flow-tools { margin-left: var(--space-2); }
.flow-follow { min-width: 15ch; font-size: var(--fs-0); }
.flow-jump { max-width: 18ch; font-size: var(--fs-0); }

/* The trail, laid over the whole map rather than replacing it: their route is
   lit, everything else stays visible but recedes, because the comparison
   between the two is the entire point of drawing it this way. */
.flow:has(.is-trail) .flow-node:not(.is-trail) { opacity: 0.28; }
.flow:has(.is-trail) .flow-edge:not(.is-trail) { opacity: 0.12; }
.flow:has(.is-trail) .flow-pulse:not(.is-trail) { display: none; }
.flow-node.is-trail rect { stroke: var(--accent); stroke-width: 2; }
.flow-edge.is-trail { stroke: var(--accent); opacity: 1; }

/* ── what they did while they were standing here ──────────────────────────
   An action is not a place on this map. `show actions` makes each one a box of
   its own, which answers "where do actions happen" across everybody; these
   answer "what did *this* person do *here*", and they hang under the step
   because that is the relationship.

   Hidden until the dot arrives. Four labels under every lit step at once is a
   map nobody can read, and the question is only ever asked about one step. */
.flow-act {
  fill: var(--ink-faint);
  font-family: var(--font-mono);
  font-size: 10px;
  opacity: 0;
  transition: opacity 0.18s ease;
}
.flow-act::before { content: "· "; }
.flow-node.is-riding .flow-act { opacity: 1; fill: var(--ink-muted); }
/* The step the dot is on. Steady rather than pulsing: `is-here` pulses because
   it is a live claim that keeps being re-asserted, and this one is simply
   where the dot is this frame. */
.flow-node.is-riding rect { stroke: var(--accent); stroke-width: 2.5; }
.flow-node.is-riding text { fill: var(--accent); }
/* The map is being driven, so the hand cursor would be a lie about who is
   steering it. */
.flow-stage.is-riding { cursor: default; }

/* ---------- journeys: navigation on the canvas ---------- */
.flow-nav {
  position: absolute; right: var(--space-3); bottom: var(--space-3);
  display: flex; align-items: flex-end; gap: var(--space-2);
  pointer-events: none; /* the gap between the two clusters stays draggable */
}
.flow-nav > * { pointer-events: auto; }

.flow-pad {
  display: grid;
  grid-template-columns: repeat(3, 26px);
  grid-template-rows: repeat(3, 26px);
  gap: 2px;
}
/* up · left home right · down, in document order. */
.flow-pad-btn:nth-child(1) { grid-area: 1 / 2; }
.flow-pad-btn:nth-child(2) { grid-area: 2 / 1; }
.flow-pad-btn:nth-child(3) { grid-area: 2 / 2; }
.flow-pad-btn:nth-child(4) { grid-area: 2 / 3; }
.flow-pad-btn:nth-child(5) { grid-area: 3 / 2; }
.flow-pad-btn {
  display: flex; align-items: center; justify-content: center;
  font: inherit; font-size: var(--fs-0); line-height: 1;
  color: var(--ink-muted);
  background: var(--appbar-bg); backdrop-filter: blur(6px);
  border: 1px solid var(--border); border-radius: var(--radius-s);
  cursor: pointer;
}
.flow-pad-btn:hover { color: var(--accent); border-color: var(--accent); }
.flow-pad-home { color: var(--ink-faint); }

.flow-minimap {
  width: 168px; height: 108px;
  padding: 4px;
  background: var(--appbar-bg); backdrop-filter: blur(6px);
  border: 1px solid var(--border); border-radius: var(--radius-s);
  cursor: crosshair; touch-action: none;
}
.flow-minimap svg { display: block; width: 100%; height: 100%; }
.mini-edge { fill: none; stroke: var(--border-bright); stroke-width: 6; opacity: 0.5; }
.mini-node { fill: var(--ink-faint); opacity: 0.7; }
.mini-node-entry { fill: var(--accent); }
.mini-node-exit { fill: var(--border-bright); }
.mini-node-errors { fill: var(--level-error); }
.mini-node.is-trail { fill: var(--accent); opacity: 1; }
/* The window, drawn as an outline so the map underneath still reads. */
.mini-view { fill: var(--accent-glow); stroke: var(--accent); stroke-width: 6; }

@media (max-width: 700px) {
  /* Two clusters of chrome over a small map is more furniture than map. */
  .flow-minimap { display: none; }
}

/* ---------- journeys: live sessions ---------- */
.live-list { display: flex; flex-direction: column; min-height: 0; }
.live-head {
  display: flex; gap: var(--space-3); align-items: baseline;
  padding: var(--space-3) 0; font-size: var(--fs-0);
}
/* The one thing on the page that is allowed to blink: it is the only honest
   way to say "this list is live" without a spinner that means "waiting". */
.live-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--accent); align-self: center;
  animation: live-pulse 2s ease-in-out infinite;
}
@keyframes live-pulse { 50% { opacity: 0.25; } }
@media (prefers-reduced-motion: reduce) { .live-dot { animation: none; } }

.live-table { table-layout: auto; }
.live-table td { vertical-align: middle; }
.live-step { color: var(--ink); }
.live-table tr.has-errors .live-step { color: var(--level-error); }
.live-table tr.is-watching { background: var(--accent-glow); }
.live-actions { text-align: right; white-space: nowrap; }
.live-actions .btn + .btn { margin-left: var(--space-2); }


/* ---------- ambient log streams ----------
   Shared chrome, not landing-page decoration: the front door, the sign-in page
   and the sign-up page all have wide empty margins, and a product whose pitch
   is that it shows you things should not answer that with blank space.

   Any page can opt in by including partials/ambient.html. --cy-col is the
   width of that page's reading column, which is what the gutters are measured
   against.
   Two log streams standing in the empty space either side of the column. The
   product's own .term-line markup, dimmed to atmosphere: masked to nothing at
   the top and bottom so lines arrive and leave without a hard edge, faded
   toward the text so they never compete with it, and inert to the pointer.

   They appear only when there is genuinely room. Below the breakpoint the
   gutters do not exist, and a stream squeezed into no space would be noise. */
.cy-ambient {
  position: fixed;
  top: 0;
  bottom: 0;
  z-index: 0;
  display: none;
  overflow: hidden;
  pointer-events: none;
  user-select: none;
  /* The overlay: fully transparent through the middle third, gone by both
     ends. Doubled as -webkit- for Safari, which still wants the prefix. */
  --lp-fade: linear-gradient(
    to bottom,
    transparent 0%,
    rgb(0 0 0 / 0.55) 22%,
    rgb(0 0 0 / 0.55) 72%,
    transparent 100%
  );
  -webkit-mask-image: var(--lp-fade);
  mask-image: var(--lp-fade);
}
.cy-ambient-left { left: 0; }
.cy-ambient-right { right: 0; }

.cy-ambient-feed {
  position: absolute;
  bottom: var(--space-6);
  width: 100%;
  padding: 0 var(--space-4);
  font-size: 10.5px;
  line-height: 1.75;
  opacity: 0.38;
  color: var(--ink-muted);
}
/* Sitting flush against the reading column would make the eye try to parse
   them. Each stream hugs the outer edge of its own gutter and fades out well
   before the text starts. */
.cy-ambient-left .cy-ambient-feed {
  -webkit-mask-image: linear-gradient(to right, rgb(0 0 0 / 0.8), transparent 78%);
  mask-image: linear-gradient(to right, rgb(0 0 0 / 0.8), transparent 78%);
}
.cy-ambient-right .cy-ambient-feed {
  -webkit-mask-image: linear-gradient(to left, rgb(0 0 0 / 0.8), transparent 78%);
  mask-image: linear-gradient(to left, rgb(0 0 0 / 0.8), transparent 78%);
}
/* Mirrored, so the right-hand stream sits against the right-hand edge. Without
   this its text starts next to the reading column, and the brightest text landing
   in the most faded part of the mask, which left it far dimmer than its twin.

   Flex rather than the grid the left column keeps: a 1fr message column would
   stretch back across the gutter and drag the text into the fade again. Here
   both cells shrink to their content and the row is pushed to the edge. */
.cy-ambient-right .term-line {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  text-align: right;
}
.cy-ambient-right .term-ts { order: 2; flex: none; }
.cy-ambient-right .term-msg { order: 1; min-width: 0; }

.cy-ambient .term-line {
  grid-template-columns: 7ch 1fr;
  column-gap: var(--space-2);
  padding: 0;
}
.cy-ambient .term-proj,
.cy-ambient .term-lvl { display: none; }
.cy-ambient .term-msg { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* A new line rises into place rather than appearing. */
.cy-ambient .term-line.is-new { animation: lp-line-in 0.5s ease both; }
@keyframes lp-line-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}

/* Only where a gutter actually exists: 760px of column plus enough either
   side for a stream to be a stream. */
@media (min-width: 1180px) {
  .cy-ambient { display: block; width: calc((100vw - var(--cy-col)) / 2); }
}
@media (prefers-reduced-motion: reduce) {
  .cy-ambient .term-line.is-new { animation: none; }
}

/* Blocks are pushed up as they come into view, the way a line arrives.

   The hidden state is gated on `[data-reveal]`, which only a page that also
   loads landing.js puts on its <main>. It used to hang off `html.js` alone,
   on the belief that a page without the script would still be readable. It
   would not: html.js is set by the inline script in base.html, on every page
   in the product, so any page using .lp-block without landing.js rendered
   every one of them at opacity 0 and nothing ever set .is-in. /pricing did
   exactly that, and read as a page of blank space with a price grid floating
   in the middle of it. The marker has to be the thing that ships together
   with the script, or the guard is not a guard. */
@media (prefers-reduced-motion: no-preference) {
  html.js [data-reveal] .lp-block { opacity: 0; transform: translateY(14px); }
  html.js [data-reveal] .lp-block.is-in {
    opacity: 1;
    transform: none;
    transition: opacity 0.5s ease, transform 0.55s cubic-bezier(0.2, 0.9, 0.2, 1);
  }
}

/* ── the visual escalation ladder editor ─────────────────────────────────── */
/* A ladder is a sequence with timing. The left rail carries the cumulative
   clock so "when does the third person get woken" is readable rather than
   arithmetic. */
.ladder { display: flex; flex-direction: column; gap: .5rem; margin: 1rem 0; }

.rung {
  display: grid;
  grid-template-columns: 7.5rem 1fr;
  gap: .75rem 1rem;
  padding: .75rem 1rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
  cursor: grab;
}
.rung:active { cursor: grabbing; }
.rung-dragging { opacity: .45; }
.rung-over { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }

.rung-head { display: flex; flex-direction: column; gap: .15rem; }
.rung-num {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.6rem; height: 1.6rem; border-radius: 50%;
  background: var(--accent); color: var(--bg);
  font-size: .8rem; font-weight: 600;
}
.rung-clock { font-size: .78rem; color: var(--muted); font-variant-numeric: tabular-nums; }

.rung-body { display: flex; flex-wrap: wrap; gap: .75rem 1.25rem; align-items: flex-start; }
.rung-field { display: flex; flex-direction: column; gap: .2rem; min-width: 8rem; }
.rung-label { font-size: .72rem; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
.rung-hint { font-size: .72rem; color: var(--muted); }
.rung-select, .rung-number { padding: .3rem .4rem; }
.rung-number { width: 5rem; }

.rung-people { display: flex; flex-wrap: wrap; gap: .35rem; max-width: 32rem; }
.rung-person {
  display: inline-flex; align-items: center; gap: .2rem;
  padding: .15rem .5rem; border: 1px solid var(--line); border-radius: 999px;
  font-size: .82rem; cursor: pointer;
}
.rung-person-on { border-color: var(--accent); }
/* Not decorative: an SMS rung naming somebody with no verified number spends
   its delay and wakes nobody, and looks perfectly fine in a dropdown. */
.rung-person-warn { color: var(--warn, #d08a2c); }

.rung-warn {
  grid-column: 1 / -1;
  margin: 0; padding: .4rem .6rem;
  border-left: 2px solid var(--warn, #d08a2c);
  font-size: .82rem; color: var(--muted);
}

.rung-actions { grid-column: 1 / -1; display: flex; gap: .35rem; }

.ladder-bar { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.ladder-status { font-size: .85rem; color: var(--muted); }
.ladder-status-ok { color: var(--ok, #2e9e5b); }
.ladder-status-warn { color: var(--warn, #d08a2c); }


/* ── ladder templates ─────────────────────────────────────────────────────── */
/* Each card has to carry three things: the shape, the reasoning, and what it
   still needs before it would page anybody. The third is the one that saves
   somebody discovering at 3am that their ladder resolves to nobody. */
.preset-grid {
  display: grid; gap: .75rem; margin-top: .75rem;
  grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
}
.preset {
  display: flex; flex-direction: column; gap: .5rem;
  padding: .9rem 1rem;
  border: 1px solid var(--line); border-radius: 8px;
}
.preset-blocked { opacity: .82; }
.preset-name { margin: 0; font-size: .95rem; }
.preset-who { margin: 0; font-size: .82rem; color: var(--muted); }

.preset-rungs { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: .25rem; }
.preset-rungs li { display: flex; align-items: center; gap: .4rem; font-size: .8rem; }
.preset-when { min-width: 3rem; color: var(--muted); font-variant-numeric: tabular-nums; }

.preset-why { margin: 0; font-size: .8rem; color: var(--muted); line-height: 1.55; }
.preset-cost { margin: 0; font-size: .78rem; color: var(--muted); }

.preset-needs {
  padding: .5rem .7rem;
  border-left: 2px solid var(--warn, #d08a2c);
  font-size: .78rem; color: var(--muted);
}
.preset-needs-head { margin: 0 0 .3rem; }
.preset-needs ul { margin: 0; padding-left: 1.1rem; display: flex; flex-direction: column; gap: .25rem; }

/* ---------- first-run introductions ---------- */
/* A native <dialog>, so the browser owns modality and focus. Styled as a panel
   like any other card: an explanation, not an advertisement. */
.intro {
  max-width: 560px;
  width: calc(100% - var(--space-6));
  padding: 0;
  border: 1px solid var(--border-bright);
  border-radius: var(--radius-m);
  background: var(--bg-1);
  color: var(--ink);
}
.intro::backdrop { background: rgb(0 0 0 / 0.55); }
.intro-inner { padding: var(--space-5); margin: 0; }
.intro-head { margin-bottom: var(--space-3); }
.intro-eyebrow { display: block; font-size: var(--fs-0); margin-bottom: var(--space-1); }
.intro h2 { margin: 0; font-size: var(--fs-3); }
.intro h2::before { content: "❯ "; color: var(--accent); }
.intro-lead { color: var(--ink); margin: 0 0 var(--space-3); }
.intro-points { list-style: none; margin: 0 0 var(--space-3); padding: 0; }
.intro-points li {
  position: relative;
  padding: var(--space-1) 0 var(--space-1) var(--space-4);
  color: var(--ink-muted);
  font-size: var(--fs-2);
}
.intro-points li::before { content: "·"; position: absolute; left: var(--space-2); color: var(--accent); }
.intro-tip {
  border-left: 2px solid var(--border-bright);
  padding-left: var(--space-3);
  margin: 0 0 var(--space-3);
  color: var(--ink-muted);
  font-size: var(--fs-2);
}
.intro-actions {
  display: flex; gap: var(--space-3); align-items: center; flex-wrap: wrap;
  margin-top: var(--space-4); padding-top: var(--space-3);
  border-top: 1px solid var(--border);
}
.intro-actions .btn-ghost { margin-left: auto; color: var(--ink-faint); }

/* ---------- people, with their visits under them ----------
   The live list nests the other way round, sessions with a user column, which
   is right for "who is here now" and wrong for "what has this person done".
   Both exist because they are two questions, not one question at two zooms. */
.people { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-3); }
.person {
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  background: var(--bg-1);
}
/* Somebody who has been hitting errors is the row worth opening first, so the
   rail says so before the chip inside it is read. */
.person-erring { box-shadow: inset 2px 0 0 var(--level-error); }
.person-head {
  display: flex; align-items: baseline; gap: var(--space-4);
  padding: var(--space-3) var(--space-4);
  cursor: pointer;
  list-style: none;
}
.person-head::-webkit-details-marker { display: none; }
.person-head::before {
  content: "▾";
  color: var(--ink-faint); font-size: var(--fs-0);
  transition: transform 0.12s;
}
.person > details:not([open]) .person-head::before { transform: rotate(-90deg); }
.person-head:hover { background: var(--bg-2); }
.person-who { color: var(--ink); font-weight: 600; overflow-wrap: anywhere; }
.person-facts { display: flex; gap: var(--space-3); color: var(--ink-muted); font-size: var(--fs-1); }
.person-seen { margin-left: auto; color: var(--ink-faint); font-size: var(--fs-0); white-space: nowrap; }
.person-now { color: var(--accent); }
.person-visits { margin: 0; border-top: 1px solid var(--border); }
.person-visits td, .person-visits th { padding: 5px var(--space-4); }
.person-go { text-align: right; white-space: nowrap; }
.person-go .btn { margin-left: var(--space-2); }
.person-more {
  margin: 0;
  padding: var(--space-2) var(--space-4) var(--space-3);
  color: var(--ink-faint);
  font-size: var(--fs-0);
}

/* ---------- the store buttons ----------
   The phone is the last node on the same wire as everything else in this
   product, so the buttons sit on a trace rather than beside one: the rail runs
   the width of the block, crosses the face of each button under its words, and
   a pulse travels the whole run and passes through them both.

   The buttons are transparent on purpose. An opaque face would hide the trace
   and the circuit would be two stubs either side of a rectangle, which is a
   decoration; a face the line crosses is a component wired into something. */
.store {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-5);
  /* Room under the buttons for the bus, and a stub either side of it. */
  padding: 0 18px 26px;
  margin: var(--space-5) 0 0;
}
/* The bus. Under the buttons rather than across their faces: a trace that
   crossed the words would be a line through a label, and the thing being drawn
   is a component wired into a board, not a strikethrough. */
.store-rail {
  position: absolute;
  left: 0; right: 0; bottom: 8px;
  height: 1px;
  background: var(--border-bright);
}
/* Terminals. Solid rather than knocked out, so they read the same on the
   landing page's ground and inside a card, which are two different colours. */
.store-rail::before, .store-rail::after {
  content: "";
  position: absolute; top: -2px;
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--border-bright);
}
.store-rail::before { left: 0; }
.store-rail::after { right: 0; }

.store-pulse {
  position: absolute; top: -2px; left: 0;
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-glow);
  animation: storeRun 6s linear infinite;
}
/* A second one, half a run behind, so the wire is never empty for long and the
   two buttons are not always lit in the same order. */
.store-pulse-late { animation-delay: 3s; }
@keyframes storeRun {
  0% { left: 0; opacity: 0; }
  4% { opacity: 1; }
  94% { opacity: 1; }
  100% { left: 100%; opacity: 0; }
}

.store-btn {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 1 1 210px;
  padding: 11px var(--space-4);
  border: 1px solid var(--border-bright);
  border-radius: var(--radius-m);
  color: var(--ink);
  background: transparent;
  transition: border-color 0.15s, background 0.15s;
}
.store-btn:hover { border-color: var(--accent); background: var(--accent-glow); text-decoration: none; }
/* The drop line: each button hangs off the bus by a stub with a junction on
   the end of it, which is where the pulse below arrives. */
.store-btn::after {
  content: "";
  position: absolute;
  left: 50%; top: 100%;
  width: 1px; height: 18px;
  margin-left: -0.5px;
  background: var(--border-bright);
}
.store-btn::before {
  content: "";
  position: absolute;
  left: 50%; top: calc(100% + 16px);
  width: 5px; height: 5px;
  margin-left: -2.5px;
  border-radius: 50%;
  background: var(--border-bright);
  transition: background 0.15s;
}
.store-btn:hover::before, .store-btn:hover::after { background: var(--accent); }
/* Each button lights while the pulse is inside it. The percentages are where
   that button sits along the rail, so the two are the same event seen twice:
   the light is the signal arriving, not a second animation about it. */
.store-btn-a { animation: storeLitA 6s linear infinite; }
@keyframes storeLitA {
  0%, 21% { border-color: var(--border-bright); }
  27%, 33% { border-color: var(--accent); }
  40%, 100% { border-color: var(--border-bright); }
}
.store-btn-b { animation: storeLitB 6s linear infinite; }
@keyframes storeLitB {
  0%, 68% { border-color: var(--border-bright); }
  74%, 80% { border-color: var(--accent); }
  87%, 100% { border-color: var(--border-bright); }
}
.store-btn:hover { animation: none; }

.store-mark { width: 21px; height: 24px; flex: none; fill: var(--ink); }
.store-words { display: grid; line-height: 1.25; }
.store-kicker { color: var(--ink-faint); font-size: var(--fs-0); }
.store-name { font-size: var(--fs-3); font-weight: 600; letter-spacing: -0.01em; }

/* Nothing runs for a reader who asked for stillness, and what is left is a
   wired pair of buttons rather than an empty rail. */
@media (prefers-reduced-motion: reduce) {
  .store-pulse { display: none; }
  .store-btn { animation: none; }
}
/* Junctions read as junctions only while they are the width apart the bus was
   drawn for; wrapped, they stack and the geometry is a lie. */
.store-btn { min-width: 0; }
/* One per row on a phone, where two of them side by side leave no room for the
   words each store requires. */
/* One per row on a phone, where two of them side by side leave no room for the
   words each store requires. The bus goes with the row: a horizontal run under
   a vertical stack is a wire connected to nothing. */
@media (max-width: 620px) {
  .store { gap: var(--space-4); padding-bottom: 0; }
  .store-btn { flex: 1 1 100%; }
  .store-btn::before, .store-btn::after { content: none; }
  .store-rail { display: none; }
}

/* ---------- the introduction figures ----------
   One per page, performing what that page does. Everything here is CSS: there
   is no script, nothing observes anything, and a figure only animates while its
   <dialog> is open, because a closed dialog is display:none and a rule that
   does not paint does not run.

   Three rules hold across all of them. They are aria-hidden, so they may say
   nothing the prose does not. They are legible at rest, because the reduced
   motion block below stops every one of them dead and what is left has to still
   be a picture. And they use the product's own colours for the product's own
   meanings: red is a failure, amber costs money or is degraded, green is
   healthy or free, and everything structural is ink. */
.ifig {
  display: grid;
  align-content: center;
  gap: var(--space-2);
  min-height: 108px;
  margin: 0 0 var(--space-4);
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  background: var(--bg-0);
  font-size: var(--fs-0);
  color: var(--ink-muted);
  overflow: hidden;
}
.ifig b { font-weight: 600; }
.ifig .is-err { color: var(--level-error); }
.ifig .is-warn { color: var(--level-warning); }
.ifig .is-ok { color: var(--accent); }

/* A reader who asked for stillness gets the diagram and none of the motion.
   Anything that animates in from nothing has to be given its arrived state
   here, or stillness would mean an empty box. */
@media (prefers-reduced-motion: reduce) {
  .ifig *, .ifig *::before, .ifig *::after { animation: none !important; }
  .ifig .ifig-crash, .ifig .ifig-walker, .ifig .ifig-packet { opacity: 0; }
  .ifig .ifig-lines li, .ifig .ifig-members li, .ifig .ifig-stages li,
  .ifig .ifig-rungs li, .ifig .ifig-steps li { opacity: 1; }
  .ifig .ifig-count b:first-child, .ifig .ifig-verdict b:first-child,
  .ifig .ifig-swap b:first-child { opacity: 0; }
  /* Chips that fade in on a loop have to be simply present when nothing loops. */
  .ifig .ifig-filter, .ifig .ifig-team, .ifig .ifig-walker,
  .ifig .ifig-beat.is-missed { opacity: 1; }
  .ifig .ifig-beat.is-missed { border-color: var(--error-line); }
}

/* ── issues: a crash loop collapsing into one line ───────────────────────── */
.ifig-stack { position: relative; display: grid; }
.ifig-crash {
  position: absolute; left: 0; right: 0; top: 0;
  height: 2px;
  border-radius: 1px;
  background: var(--error-line);
  animation: ifigFall 6s ease-in-out infinite;
  animation-delay: calc(var(--i) * 0.22s);
}
@keyframes ifigFall {
  0% { opacity: 0; transform: translateY(calc(var(--i) * -9px)); }
  12% { opacity: 1; transform: translateY(calc(var(--i) * -9px)); }
  46%, 100% { opacity: 0; transform: translateY(0); }
}
.ifig-issue {
  display: flex; align-items: baseline; gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-left: 2px solid var(--level-error);
  border-radius: var(--radius-s);
  background: var(--bg-1);
}
.ifig-err { color: var(--ink); font-weight: 600; }
.ifig-where { color: var(--ink-faint); }
.ifig-count { position: relative; margin-left: auto; color: var(--ink); }
.ifig-count b { display: block; }
.ifig-count b:first-child { position: absolute; right: 0; animation: ifigWas 6s ease-in-out infinite; }
.ifig-count b:last-child { animation: ifigBecomes 6s ease-in-out infinite; }
@keyframes ifigWas { 0%, 40% { opacity: 1; } 50%, 100% { opacity: 0; } }
@keyframes ifigBecomes { 0%, 40% { opacity: 0; } 50%, 100% { opacity: 1; } }

/* ── logs: everything, then only what was asked for ──────────────────────── */
.ifig-lines { list-style: none; margin: 0; padding: 0; display: grid; gap: 3px; }
.ifig-lines li { display: flex; align-items: center; gap: var(--space-2); }
.ifig-lvl {
  width: 5px; height: 5px; flex: none;
  border-radius: 50%;
  background: var(--ink-faint);
}
.ifig-lvl.is-err { background: var(--level-error); }
.ifig-lvl.is-warn { background: var(--level-warning); }
.ifig-filter {
  justify-self: start;
  padding: 1px 7px;
  border: 1px solid var(--accent);
  border-radius: var(--radius-s);
  color: var(--accent-dim);
  animation: ifigFilter 7s ease-in-out infinite;
}
@keyframes ifigFilter { 0%, 34% { opacity: 0; } 44%, 88% { opacity: 1; } 96%, 100% { opacity: 0; } }
.ifig-logs .ifig-lines li:not(.is-kept) { animation: ifigSift 7s ease-in-out infinite; }
@keyframes ifigSift { 0%, 34% { opacity: 1; } 46%, 88% { opacity: 0.14; } 96%, 100% { opacity: 1; } }
.ifig-logs .is-kept { color: var(--ink); }

/* ── terminal: lines arriving, and a caret that never finishes ───────────── */
.ifig-lines-live li { opacity: 0; animation: ifigArrive 5s ease-out infinite; animation-delay: calc(var(--i) * 0.5s); }
@keyframes ifigArrive {
  0% { opacity: 0; transform: translateY(6px); }
  8%, 82% { opacity: 1; transform: none; }
  94%, 100% { opacity: 0; transform: none; }
}
.ifig-caret { color: var(--accent); animation: blink 1.1s steps(1) infinite; }

/* ── journeys: one session walking, and where it stopped ─────────────────── */
.ifig-path { display: flex; align-items: center; gap: var(--space-2); }
.ifig-node {
  flex: 1;
  padding: 3px 0;
  border: 1px solid var(--border-bright);
  border-radius: var(--radius-s);
  background: var(--bg-1);
  color: var(--ink-muted);
  text-align: center;
}
.ifig-node-exit { border-style: dashed; }
.ifig-journey .ifig-node { animation: ifigStepOn 7s ease-in-out infinite; animation-delay: calc(var(--i) * 1.1s); }
@keyframes ifigStepOn {
  0%, 6% { color: var(--ink-muted); border-color: var(--border-bright); }
  12%, 26% { color: var(--ink); border-color: var(--ink-faint); }
  34%, 100% { color: var(--ink-muted); border-color: var(--border-bright); }
}
/* The last stop is where the session ended, so it lands in the failure colour
   rather than lighting up like a screen somebody went on to use. */
.ifig-journey .ifig-node-exit { animation-name: ifigStepExit; }
@keyframes ifigStepExit {
  0%, 6% { color: var(--ink-faint); border-color: var(--border-bright); }
  12%, 26% { color: var(--level-error); border-color: var(--error-line); }
  34%, 100% { color: var(--ink-faint); border-color: var(--border-bright); }
}
/* The walker waits under a screen for as long as the session was on it, then
   crosses to the next. Plateaus rather than a constant glide, because moving
   between screens is the event and sitting on one is the time. */
.ifig-trail { position: relative; height: 7px; }
.ifig-walker {
  position: absolute; top: 0; left: 11%;
  height: 7px; width: 7px;
  border-radius: 50%;
  background: var(--accent);
  animation: ifigWalk 7s ease-in-out infinite;
}
@keyframes ifigWalk {
  0% { left: 11%; opacity: 0; }
  6%, 22% { left: 11%; opacity: 1; }
  30%, 46% { left: 36%; }
  54%, 70% { left: 61%; }
  78%, 92% { left: 86%; opacity: 1; }
  100% { left: 86%; opacity: 0; }
}

/* ── the flow map: the same screens, weighed by traffic ──────────────────── */
.ifig-map .ifig-node { flex: 0 1 auto; padding: 3px var(--space-2); }
.ifig-edge {
  flex: 1;
  min-width: 14px;
  height: var(--w, 2px);
  border-radius: 3px;
  background: var(--border-bright);
  animation: ifigWeigh 6s ease-in-out infinite;
  animation-delay: calc(var(--i) * 0.9s);
}
@keyframes ifigWeigh {
  0%, 8% { background: var(--border-bright); }
  16%, 50% { background: var(--ink-faint); }
  60%, 100% { background: var(--border-bright); }
}
/* The heaviest line is the one into `exit`, which is the whole reading. */
.ifig-edge-exit { animation-name: ifigWeighExit; }
@keyframes ifigWeighExit {
  0%, 8% { background: var(--border-bright); }
  16%, 50% { background: var(--level-error); }
  60%, 100% { background: var(--border-bright); }
}
.ifig-loop {
  height: 14px;
  margin: 0 22%;
  border: 1px dashed var(--border-bright);
  border-top: 0;
  border-radius: 0 0 14px 14px;
}

/* ── journeys you define: two met, one blocking ──────────────────────────── */
.ifig-steps { list-style: none; margin: 0; padding: 0; display: flex; gap: var(--space-3); flex-wrap: wrap; }
.ifig-steps li { display: flex; align-items: center; gap: 6px; }
.ifig-steps i {
  width: 9px; height: 9px;
  border: 1px solid var(--border-bright);
  border-radius: 2px;
}
.ifig-steps li:not(.is-blocker) i { animation: ifigMet 6s ease-in-out infinite; animation-delay: calc(var(--i) * 0.7s); }
@keyframes ifigMet {
  0%, 10% { background: transparent; border-color: var(--border-bright); }
  18%, 78% { background: var(--accent); border-color: var(--accent); }
  88%, 100% { background: transparent; border-color: var(--border-bright); }
}
.ifig-steps .is-blocker { color: var(--level-warning); }
.ifig-steps .is-blocker i { animation: ifigBlocked 6s ease-in-out infinite; }
@keyframes ifigBlocked {
  0%, 30% { border-color: var(--border-bright); }
  40%, 78% { border-color: var(--level-warning); }
  88%, 100% { border-color: var(--border-bright); }
}
.ifig-meter {
  height: 5px;
  margin-top: var(--space-2);
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: 3px;
  overflow: hidden;
}
.ifig-meter span {
  display: block; height: 100%; width: 62%;
  background: var(--accent);
  transform-origin: left;
  animation: ifigFill 6s ease-in-out infinite;
}
@keyframes ifigFill { 0%, 10% { transform: scaleX(0); } 40%, 78% { transform: scaleX(1); } 88%, 100% { transform: scaleX(0); } }
.ifig-meter-note { color: var(--ink-faint); }
.ifig-meter-note b { color: var(--level-warning); }

/* ── replay: a playhead over steps that already happened ─────────────────── */
.ifig-track { position: relative; display: flex; align-items: center; gap: var(--space-3); height: 18px; }
.ifig-track::before {
  content: ""; position: absolute; left: 0; right: 0; top: 50%;
  border-top: 1px solid var(--border);
}
.ifig-tick {
  position: relative;
  flex: 1;
  height: 9px;
  border-left: 1px solid var(--border-bright);
  animation: ifigPassed 7s linear infinite;
  animation-delay: calc(var(--i) * 1.05s);
}
@keyframes ifigPassed {
  0%, 4% { border-color: var(--border-bright); }
  10%, 88% { border-color: var(--ink-faint); }
  96%, 100% { border-color: var(--border-bright); }
}
.ifig-tick.is-err { border-color: var(--level-error); animation: none; }
.ifig-head {
  position: absolute; top: 0; left: 0;
  width: 2px; height: 18px;
  background: var(--accent);
  animation: ifigScrub 7s linear infinite;
}
@keyframes ifigScrub { 0% { left: 0; } 88%, 100% { left: 100%; } }
.ifig-stamp { color: var(--ink-faint); }

/* ── monitors: a beat, and the one that never came ───────────────────────── */
.ifig-beats { display: flex; align-items: flex-end; gap: var(--space-2); height: 26px; }
.ifig-beat {
  flex: 1;
  height: 100%;
  border-radius: 2px;
  background: var(--bg-2);
  animation: ifigBeat 7s ease-out infinite;
  animation-delay: calc(var(--i) * 0.7s);
}
@keyframes ifigBeat {
  0%, 4% { background: var(--bg-2); }
  10%, 86% { background: var(--accent); }
  94%, 100% { background: var(--bg-2); }
}
/* The missed one is the point: it stays empty, and it is outlined so the gap
   reads as a check that was due rather than as the end of the row. */
.ifig-beat.is-missed { background: transparent; border: 1px dashed transparent; animation-name: ifigMissed; }
@keyframes ifigMissed {
  0%, 4% { border-color: transparent; }
  10%, 86% { border-color: var(--error-line); }
  94%, 100% { border-color: transparent; }
}
.ifig-verdict { position: relative; height: 1.5em; color: var(--accent); }
.ifig-verdict b { position: absolute; left: 0; }
.ifig-verdict b:first-child { animation: ifigWas 7s ease-in-out infinite; }
.ifig-verdict b:last-child { animation: ifigBecomes 7s ease-in-out infinite; }

/* ── incidents: opened, paged, acknowledged, closed ──────────────────────── */
.ifig-stages { list-style: none; margin: 0; padding: 0; display: flex; gap: var(--space-2); }
.ifig-stages li {
  flex: 1;
  padding: var(--space-2) 0;
  border-top: 2px solid var(--border-bright);
  color: var(--ink-faint);
  text-align: center;
  animation: ifigStage 8s ease-in-out infinite;
  animation-delay: calc(var(--i) * 1.3s);
}
@keyframes ifigStage {
  0%, 6% { border-top-color: var(--border-bright); color: var(--ink-faint); }
  14%, 62% { border-top-color: currentColor; color: inherit; }
  74%, 100% { border-top-color: var(--border-bright); color: var(--ink-faint); }
}
.ifig-stages li.is-err { color: var(--ink-faint); animation-name: ifigStageErr; }
@keyframes ifigStageErr {
  0%, 6% { border-top-color: var(--border-bright); color: var(--ink-faint); }
  14%, 62% { border-top-color: var(--level-error); color: var(--level-error); }
  74%, 100% { border-top-color: var(--border-bright); color: var(--ink-faint); }
}
.ifig-stages li.is-warn { animation-name: ifigStageWarn; color: var(--ink-faint); }
@keyframes ifigStageWarn {
  0%, 6% { border-top-color: var(--border-bright); color: var(--ink-faint); }
  14%, 62% { border-top-color: var(--level-warning); color: var(--level-warning); }
  74%, 100% { border-top-color: var(--border-bright); color: var(--ink-faint); }
}
.ifig-stages li.is-ok { animation-name: ifigStageOk; color: var(--ink-faint); }
@keyframes ifigStageOk {
  0%, 6% { border-top-color: var(--border-bright); color: var(--ink-faint); }
  14%, 62% { border-top-color: var(--accent); color: var(--accent); }
  74%, 100% { border-top-color: var(--border-bright); color: var(--ink-faint); }
}
.ifig-stages li:not(.is-err):not(.is-warn):not(.is-ok) { animation-name: ifigStagePlain; }
@keyframes ifigStagePlain {
  0%, 6% { border-top-color: var(--border-bright); color: var(--ink-faint); }
  14%, 62% { border-top-color: var(--ink-faint); color: var(--ink); }
  74%, 100% { border-top-color: var(--border-bright); color: var(--ink-faint); }
}

/* ── routing: the circuit that screen draws, in miniature ────────────────── */
.ifig-rungs { list-style: none; margin: 0; padding: 0; display: grid; gap: 5px; }
.ifig-rungs li {
  display: grid;
  grid-template-columns: 2.2rem 9px 1fr;
  align-items: center;
  gap: var(--space-2);
  opacity: 0.4;
  animation: ifigRung 8s ease-in-out infinite;
  animation-delay: calc(var(--i) * 1.2s);
}
@keyframes ifigRung { 0%, 4% { opacity: 0.4; } 10%, 62% { opacity: 1; } 72%, 100% { opacity: 0.4; } }
.ifig-clock { color: var(--ink-faint); text-align: right; }
.ifig-rungs i {
  width: 7px; height: 7px;
  border: 1px solid var(--ink-muted);
  border-radius: 50%;
  background: var(--ink-muted);
}
.ifig-ack i { border-color: var(--accent); background: var(--accent); }
.ifig-ack .ifig-clock, .ifig-ack span:last-child { color: var(--accent); }
/* The acknowledgement is not a rung: it is what the three above were climbing
   towards, so it arrives after them and the climb has nowhere else to go. */
.ifig-ack { animation-name: ifigAck; }
@keyframes ifigAck { 0%, 44% { opacity: 0; } 54%, 84% { opacity: 1; } 92%, 100% { opacity: 0; } }

/* ── projects: the one wire everything depends on ────────────────────────── */
.ifig-link { display: flex; align-items: center; gap: var(--space-3); }
.ifig-box {
  padding: 4px var(--space-3);
  border: 1px solid var(--border-bright);
  border-radius: var(--radius-s);
  background: var(--bg-1);
  color: var(--ink-muted);
}
.ifig-box-here { border-color: var(--accent); color: var(--accent-dim); }
.ifig-wire { position: relative; flex: 1; height: 9px; }
.ifig-wire::before {
  content: ""; position: absolute; left: 0; right: 0; top: 4px;
  border-top: 1px dashed var(--border-bright);
}
.ifig-packet {
  position: absolute; top: 1px; left: 0;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--accent);
  animation: ifigSend 3.2s linear infinite;
  animation-delay: calc(var(--i) * 1.06s);
}
@keyframes ifigSend {
  0% { left: 0; opacity: 0; }
  10% { opacity: 1; }
  90% { opacity: 1; }
  100% { left: calc(100% - 7px); opacity: 0; }
}
.ifig-dsnline { color: var(--ink-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ifig-dsnline b { color: var(--ink); }

/* ── credits: which channels spend, and what one page costs ──────────────── */
.ifig-tier { display: flex; align-items: baseline; gap: var(--space-3); }
.ifig-price {
  flex: none; min-width: 5.5em;
  padding: 1px 7px;
  border: 1px solid var(--border-bright);
  border-radius: var(--radius-s);
  text-align: center;
}
.ifig-price.is-free { border-color: var(--accent); color: var(--accent-dim); }
.ifig-price.is-paid { border-color: var(--level-warning); color: var(--level-warning); }
.ifig-tier-paid { animation: ifigSpend 6s ease-in-out infinite; }
@keyframes ifigSpend { 0%, 30% { opacity: 0.45; } 42%, 76% { opacity: 1; } 88%, 100% { opacity: 0.45; } }
.ifig-balance { display: flex; gap: 6px; color: var(--ink-muted); }
/* Two numbers in one place, one fading into the other. The slot is sized so the
   line does not reflow as the balance changes width. */
.ifig-swap { position: relative; width: 3.4em; height: 1.5em; }
.ifig-swap b { position: absolute; left: 0; top: 0; color: var(--ink); }
.ifig-swap b:first-child { animation: ifigWas 6s ease-in-out infinite; }
.ifig-swap b:last-child { animation: ifigBecomes 6s ease-in-out infinite; }

/* ── settings: people, what they may change, and the team ────────────────── */
.ifig-members { list-style: none; margin: 0; padding: 0; display: grid; gap: 3px; }
.ifig-members li {
  display: flex; align-items: baseline; gap: var(--space-2);
  padding-left: var(--space-3);
  color: var(--ink);
  animation: ifigArrive 7s ease-out infinite;
  animation-delay: calc(var(--i) * 0.4s);
}
.ifig-role { margin-left: auto; color: var(--ink-faint); }
/* The bracket is the team: it gathers the two rows above it rather than being
   a third thing in the list, because that is what a team is for. */
.ifig-members .is-team { box-shadow: inset 2px 0 0 var(--accent); }
.ifig-team {
  justify-self: start;
  padding: 1px 7px;
  border: 1px solid var(--accent);
  border-radius: var(--radius-s);
  color: var(--accent-dim);
  animation: ifigFilter 7s ease-in-out infinite;
}

/* ---------- the post-signup tour ---------- */
.tour { max-width: 720px; margin: 0 auto; }
.tour-progress {
  display: flex; gap: var(--space-2); align-items: center;
  margin-bottom: var(--space-5);
}
.tour-pip {
  flex: 1; height: 2px; background: var(--border); border-radius: 1px;
  transition: background 160ms linear;
}
.tour-pip.is-done { background: var(--accent); }
.tour-panel { display: none; }
.tour-panel.is-current { display: block; }
.tour-panel h2 { font-size: var(--fs-5); margin: 0 0 var(--space-3); line-height: 1.25; }
.tour-panel .lead { color: var(--ink-muted); font-size: 1rem; margin-bottom: var(--space-4); }
.tour-points { list-style: none; margin: 0; padding: 0; }
.tour-points li {
  position: relative; padding: var(--space-2) 0 var(--space-2) var(--space-5);
  border-top: 1px solid var(--border); color: var(--ink-muted);
}
.tour-points li::before {
  content: "◉"; position: absolute; left: 0; color: var(--accent); opacity: 0.7;
}
.tour-actions {
  display: flex; gap: var(--space-3); align-items: center;
  margin-top: var(--space-6); padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}
.tour-actions .tour-skip { margin-left: auto; }
.tour-step-label { color: var(--ink-faint); }

@media (prefers-reduced-motion: reduce) {
  .tour-pip { transition: none; }
}


/* ---------- the routing circuit ----------
   Reuses the flow map's canvas, pan/zoom and pulse wholesale: same engine,
   same learned gestures. Only the node vocabulary is new, and it is new
   because routing nodes are not interchangeable the way journey steps are:
   a rung, a handset and a trigger fail in different ways, so they are told
   apart by colour rather than by reading the label. */
/* The canvas takes the full width and the inspector is a drawer over it,
   rather than a permanent column. A routing graph is wide (every rung and
   every contact is another column) and giving a third of the page to a panel
   that says "click something" is what forced the diagram off the screen on
   first load. The drawer is CSS-only: HTMX swaps content in, `:empty` decides
   whether it is there at all. */
.circuit-workspace { position: relative; }

/* ── routing, as one screen ──────────────────────────────────────────────
   The canvas is the page. Everything that used to sit under it (teams,
   ladders, rotations) is in a panel on the right that hides, because those are
   things you reach for while looking at the diagram rather than things you
   read after scrolling past it.

   Height is set by circuit-edit.js from the element's own top offset: the
   appbar is padding plus whatever a button measures, and a hardcoded guess
   here would be wrong the first time somebody changed the font. The value
   below is the no-script fallback. */
.rc-page .container { max-width: none; padding: 0; }

.rc { display: flex; flex-direction: column; height: 76vh; position: relative; }

.rc-bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
  background: var(--bg-1);
}
.rc-bar-title { display: flex; flex-direction: column; gap: 2px; font-size: var(--fs-1); }
.rc-bar-actions { display: flex; align-items: center; gap: var(--space-3); }

.rc-stage { position: relative; flex: 1; min-height: 0; }
.rc-stage .circuit-workspace,
.rc-stage .flow-stage { height: 100%; }
.rc-legend {
  margin: 0;
  padding: var(--space-2) var(--space-4);
  border-top: 1px solid var(--border);
}

/* The panel floats over the canvas rather than taking a column from it: the
   diagram is the page, and the tools are laid on top of it. */
.rc-panel {
  position: absolute;
  top: var(--space-3); right: var(--space-3); bottom: var(--space-3);
  width: 310px;
  display: flex; flex-direction: column;
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  overflow: hidden;
  z-index: 2;
}
.rc.is-panel-hidden .rc-panel { display: none; }

.rc-panel-tabs { display: flex; border-bottom: 1px solid var(--border); }
.rc-tab {
  flex: 1;
  padding: var(--space-3) var(--space-2);
  border: 0; background: none; cursor: pointer;
  color: var(--ink-faint); font: inherit; font-size: var(--fs-0);
}
.rc-tab.is-on { color: var(--ink); box-shadow: inset 0 -2px 0 var(--accent); }
.rc-panel-body { flex: 1; overflow-y: auto; padding: var(--space-3); }

/* The management sections, which used to be three cards. Collapsed by default
   except the one you are most likely to want, so the panel opens as a list of
   headings rather than a wall. */
.rc-group { border-top: 1px solid var(--border); padding: var(--space-3) 0; }
.rc-group:first-of-type { border-top: 0; padding-top: 0; }
.rc-group > summary {
  cursor: pointer;
  font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--ink-faint);
}
.rc-group[open] > summary { color: var(--ink); margin-bottom: var(--space-3); }
.rc-group .table { font-size: var(--fs-0); }
.rc-group .rule-form input, .rc-group .rule-form select { width: 100%; }
.rc-group .step-table { margin-top: var(--space-2); }

@media (max-width: 900px) {
  .rc { height: auto; }
  .rc-stage { height: 62vh; }
  .rc-panel { position: static; width: auto; margin: var(--space-3); }
  .rc.is-panel-hidden .rc-panel { display: flex; }
}

/* ── the circuit, made editable ──────────────────────────────────────────
   The + that turns the diagram into an editor, and the states a drag puts a
   node into. The panel that holds the pieces is .rc-panel above; this is only
   what lives on the canvas itself. */
.circuit-workspace.is-busy { opacity: 0.7; pointer-events: none; }

/* The first-run state. An org with nothing routing anywhere is the one that
   most needs the canvas, so it gets the same + it would get later, one per
   project, rather than a sentence pointing at another page. */
.rc-rung-form { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: flex-end; margin-top: var(--space-3); }
.rc-rung-form .lc-field { flex: 1 1 96px; margin-bottom: 0; }
.rc-rung-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-3);
}
.rc-drill { display: inline-block; margin: 0 4px 4px 0; }

/* "You are looking at one project." Quiet, because it is a label rather than a
   control: the switcher in the header is the control. */
.scope-note { margin: 0 0 var(--space-3); font-size: var(--fs-0); }
.rc-bar .scope-note { margin: 0; }

.circuit-blank-starts { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-3); }
.circuit-blank.is-busy { opacity: 0.7; pointer-events: none; }

/* The + that turns the diagram into an editor. Sits on the node's bottom-right
   corner, and is a sibling of the node group so a click on it never opens the
   inspector underneath. */
.circuit-plus { cursor: crosshair; }
.circuit-plus circle {
  fill: var(--bg-0);
  stroke: var(--border-bright);
}
.circuit-plus text {
  fill: var(--ink-muted);
  font-family: var(--font-mono);
  font-size: 13px;
  pointer-events: none;
}
.circuit-plus:hover circle, .circuit-plus:focus circle { stroke: var(--accent); }
.circuit-plus:hover text, .circuit-plus:focus text { fill: var(--accent); }

/* Only the nodes that can name somebody light up under a drag. */
.circuit-node.is-target rect { stroke: var(--accent); stroke-dasharray: 3 2; }

.circuit-problem {
  position: absolute;
  left: var(--space-3); bottom: var(--space-3);
  margin: 0;
  padding: 6px var(--space-3);
  background: var(--bg-1);
  border: 1px solid var(--level-error);
  border-radius: var(--radius-s);
  color: var(--level-error);
  font-size: var(--fs-0);
  z-index: 3;
}

@media (max-width: 780px) {
}
.circuit-stage { height: 360px; min-height: 0; }
/* What a node says when you click it. It lives in the panel's third tab now,
   so it is a block in a column that already scrolls, sized by that column.

   It used to be a drawer floating over the canvas, and kept the positioning to
   match: absolute, 320px, anchored to the right. Inside a 310px panel that put
   its left edge 22px outside the box, and the panel clips what overflows it, so
   every line in the inspector lost its first two characters: "WA" read as "IA"
   and "rung 1" as "ung 1". Nothing was broken about the content; it was being
   drawn just off the edge of its own container. */
.circuit-inspector { width: 100%; }
.circuit-inspector:empty { display: none; }
/* Journey steps are event names and read as identifiers; a rung is not one. */
.circuit-inspector .inspector-head h3::before { content: none; }
/* Sticky, not absolute: the drawer scrolls, and a close button that
   scrolls off the top of it is a trap. */
.circuit-close { position: sticky; top: 0; float: right; z-index: 1; }
.circuit-legend { margin-top: var(--space-3); }

.circuit-node text { pointer-events: none; }
.circuit-label { fill: var(--ink); font-size: 13px; }
.circuit-sub { fill: var(--ink-muted); font-size: 11px; }
.circuit-badge { fill: var(--ink-faint); font-size: 11px; text-anchor: end; }

/* The ground state. Kinds below only ever change the stroke, so a node that
   grows a new meaning cannot accidentally lose its fill or its radius. */
.circuit-node rect { fill: var(--bg-1); stroke: var(--border-bright); stroke-width: 1; }
.circuit-node:hover rect, .circuit-node:focus rect { stroke: var(--accent); }
.circuit-node:focus { outline: none; }

.circuit-node-source rect { fill: var(--bg-2); stroke: var(--border-bright); }
.circuit-node-source .circuit-label { fill: var(--ink-muted); }
.circuit-node-rule rect { stroke: var(--ink-muted); }
.circuit-node-ack rect { stroke: var(--accent); stroke-dasharray: 3 3; }
.circuit-node-ack .circuit-label { fill: var(--accent); }

/* Paid channels get the warm line, free ones stay quiet: the diagram should
   make where the money goes obvious without a legend. */
.circuit-node-rung rect { stroke: var(--level-warning); }
.circuit-node-contact rect { stroke: var(--accent); }
.circuit-node-channel-free rect { stroke: var(--border-bright); }
.circuit-node-channel-free .circuit-badge { fill: var(--accent); }

/* Three states that mean "this will not do what you think". A paused rule and
   an unverified handset are silent failures, which is the whole point of drawing the
   config is that they stop being invisible. */
.circuit-node-rule-off rect { stroke: var(--border); stroke-dasharray: 4 3; }
.circuit-node-rule-off .circuit-label,
.circuit-node-rule-off .circuit-sub { fill: var(--ink-faint); }
.circuit-node-rung-cold rect,
.circuit-node-rung-head-cold rect { stroke: var(--border); stroke-dasharray: 4 3; }
.circuit-node-rung-cold .circuit-label,
.circuit-node-rung-head-cold .circuit-label { fill: var(--ink-muted); }
.circuit-node-rung-head rect { stroke: var(--level-warning); }
.circuit-node-contact-unverified rect { stroke: var(--error-line); }
.circuit-node-contact-unverified .circuit-badge { fill: var(--level-error); }

/* The objects a rung names, rather than the people they resolve to. A team and
   a rotation are groups, so they take the quiet line: the loud ones are for
   things that page a phone, and these two only ever lead to one that does. */
.circuit-node-team rect,
.circuit-node-rotation rect { stroke: var(--ink-muted); }
.circuit-node-rotation .circuit-sub { fill: var(--accent); }
/* A whole ladder folded into one node. */
.circuit-node-ladder rect { stroke: var(--level-warning); stroke-dasharray: 6 2; }

/* Below the line: everything routing owns that leads nowhere. Dashed and dim,
   because these are not part of any path yet, and one selector rather than four
   so a new kind of loose object cannot arrive styled like a live one. */
.circuit-node[class$="-unwired"] rect {
  fill: none;
  stroke: var(--border);
  stroke-dasharray: 3 3;
}
.circuit-node[class$="-unwired"] .circuit-label { fill: var(--ink-muted); }
.circuit-node[class$="-unwired"] .circuit-sub { fill: var(--ink-faint); }

/* ── routing, as four decisions ──────────────────────────────────────────── */
/* The form that replaced the canvas as the way in. Plain rows on purpose: the
   only thing being asked is who, for what, on which channels and how long, and
   anything cleverer than a checkbox and a number would be the screen arguing
   with its own point. */
/* The decisions on the left, what they do on the right. Two columns because
   they are one thought: changing a box on the left is only meaningful if you
   can see the minute it moves on the right. Below 1040px the panel goes under
   the form rather than beside it, where it reads as the consequence instead. */
.sr-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 340px;
  gap: var(--space-5);
  align-items: start;
}
.sr-decisions { min-width: 0; }
@media (max-width: 1040px) {
  .sr-layout { grid-template-columns: minmax(0, 1fr); }
}

.sr-verdict { margin: var(--space-4) 0; border-bottom: 0; border-radius: var(--radius-m); }
.sr-block { padding: var(--space-5) 0; border-top: 1px solid var(--border); }
.sr-block:first-of-type { border-top: 0; padding-top: 0; }
.sr-checks { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); }
.sr-check { display: flex; align-items: center; gap: 6px; font-size: var(--fs-1); }
.sr-check input { margin: 0; }
/* Explanatory text on this screen is instruction, not decoration: it says what
   ticking a box will do. It reads at the secondary weight, never the faintest
   one, and stays inside a reading measure. */
.sr-note { color: var(--ink-muted); margin: 0 0 var(--space-3); max-width: 62ch; }
.sr-oncall { margin-top: var(--space-3); }
.sr-oncall b { color: var(--ink); }
.sr-groups { display: flex; flex-wrap: wrap; gap: var(--space-4); margin-top: var(--space-4); }
.sr-group { flex: 1 1 220px; max-width: 320px; margin-bottom: 0; margin-top: var(--space-4); }
.sr-group-pick { margin-top: var(--space-3); }
.sr-group-pick b { color: var(--ink); }
.sr-turns { margin-top: var(--space-4); }
.sr-turns-note { margin: 2px 0 0 22px; }

/* ── the people, and what can reach them ─────────────────────────────────── */
/* One row each. The channels on the right are `is_verified_for`, the same gate
   the send path goes through, so a row with nothing on it is exactly the row
   the panel opposite is reporting as reaching nobody. */
.sr-people {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
}
.sr-person {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-top: 1px solid var(--border);
}
.sr-person:first-child { border-top: 0; }
.sr-person:hover { background: var(--bg-2); }
.sr-person-pick {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  flex: 1;
  min-width: 0;
  cursor: pointer;
}
.sr-person-pick input { margin: 0; align-self: center; }
.sr-person-name { color: var(--ink); font-weight: 600; }
.sr-person-where {
  color: var(--ink-faint);
  font-size: var(--fs-0);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sr-person-on { display: flex; gap: 4px; flex-wrap: wrap; flex: none; }

/* A rule with a word in it, between two lists that answer one question. Quiet
   because nothing here is exclusive: it says "or you could", not "pick one". */
.sr-or {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: var(--space-4) 0;
  color: var(--ink-faint);
  font-size: var(--fs-0);
}
.sr-or::before, .sr-or::after { content: ""; flex: 1; height: 1px; background: var(--border); }

/* ── the teams, and who is behind the name ───────────────────────────────── */
.sr-teams {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
}
.sr-team {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-top: 1px solid var(--border);
}
.sr-team:first-child { border-top: 0; }
.sr-team:hover { background: var(--bg-2); }
.sr-team-pick { display: flex; align-items: baseline; gap: var(--space-2); flex: none; cursor: pointer; }
.sr-team-pick input { margin: 0; align-self: center; }
.sr-team-name b { color: var(--ink); font-weight: 600; }
.sr-team-on {
  flex: 1;
  min-width: 0;
  text-align: right;
  color: var(--ink-faint);
  font-size: var(--fs-0);
  overflow-wrap: anywhere;
}
/* A team with nobody on it is the shape that looks configured and pages no one,
   so it is the one thing in this list that is not quiet. */
.sr-team-empty { color: var(--level-warning); }

/* ── the cycle, and the arrows that change it ────────────────────────────── */
.sr-roster {
  list-style: none;
  margin: var(--space-3) 0 0;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  max-width: 460px;
}
.sr-turn {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: 5px var(--space-3);
  border-top: 1px solid var(--border);
}
.sr-turn:first-child { border-top: 0; }
.sr-turn-n { color: var(--ink-faint); font-size: var(--fs-0); width: 1.4em; }
.sr-turn-name { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.sr-turn-when { color: var(--ink-faint); font-size: var(--fs-0); white-space: nowrap; }
/* Whoever is holding it, marked on the rail rather than by colouring the row:
   holding the pager is a fact about now, not a warning. */
.sr-turn-now { box-shadow: inset 2px 0 0 var(--accent); }
.sr-turn-now .sr-turn-name { color: var(--ink); font-weight: 600; }
.sr-turn-now .sr-turn-when { color: var(--accent); }
.sr-turn-move { display: flex; gap: 2px; flex: none; }
.sr-turn-move .btn { padding: 0 7px; line-height: 1.6; }
.sr-turn-move .btn[disabled] { opacity: 0.35; cursor: default; }
.sr-roster-note { margin-top: var(--space-2); }
.sr-steps { margin-top: var(--space-3); }
.sr-steps input[type="number"] { width: 72px; }
.sr-steps td { vertical-align: middle; }
/* The wait and the price are two or three characters; wrapping "1 credit" onto
   two lines makes a four-row table look like an eight-row one. */
.sr-steps td:nth-child(3), .sr-steps td:nth-child(4) { white-space: nowrap; }
.sr-others { margin-top: var(--space-4); }
.sr-outgrown .sr-rungs { margin: var(--space-3) 0; padding-left: var(--space-4); font-size: var(--fs-1); }
.sr-outgrown .sr-rungs li { padding: 2px 0; }

/* ── the circuit, beside the form ────────────────────────────────────────── */
/* One vertical wire with a node per page the climb would send. Drawn in CSS
   rather than SVG because it is a list that happens to have a line down it: it
   has to reflow, wrap long names and be readable by a screen reader, and an
   SVG would give up all three to gain nothing. */
.sr-flow { position: sticky; top: 72px; padding: var(--space-4); }
.sr-flow-head { margin-top: 0; }
.sr-flow-trigger { margin: 0 0 var(--space-4); color: var(--ink); font-weight: 600; }
.sr-flow-idle { margin: 0; color: var(--ink-muted); }

.sr-circuit { list-style: none; margin: 0; padding: 0; }
.sr-beat {
  display: grid;
  grid-template-columns: 2.6rem 11px minmax(0, 1fr);
  gap: 0 var(--space-2);
  padding-bottom: var(--space-4);
}
.sr-beat:last-child { padding-bottom: var(--space-3); }
.sr-beat-clock {
  color: var(--ink-muted);
  font-size: var(--fs-0);
  text-align: right;
  line-height: 1.5;
  font-variant-numeric: tabular-nums;
}
/* The wire: full height through the middle beats, a half-length stub at each
   end, so the climb visibly starts and stops rather than running off the panel. */
.sr-beat-rail { position: relative; }
.sr-beat-rail::before {
  content: "";
  position: absolute;
  left: 50%; top: 0; bottom: 0;
  width: 1px;
  margin-left: -0.5px;
  background: var(--border-bright);
}
.sr-beat:first-child .sr-beat-rail::before { top: 0.7em; }
.sr-beat:last-child .sr-beat-rail::before { bottom: auto; height: 0.7em; }
.sr-beat-dot {
  position: absolute;
  left: 50%; top: 0.7em;
  width: 7px; height: 7px;
  margin: -4px 0 0 -4px;
  border: 1px solid var(--ink-muted);
  border-radius: 50%;
  background: var(--ink-muted);
}
.sr-beat-body { min-width: 0; }
.sr-beat-gap {
  margin: 0 0 2px;
  color: var(--ink-faint);
  font-size: var(--fs-0);
}
.sr-beat-line { display: flex; align-items: baseline; gap: var(--space-2); margin: 0; line-height: 1.5; }
.sr-beat-channel { color: var(--ink); font-weight: 600; }
.sr-beat-cost { margin-left: auto; color: var(--ink-faint); font-size: var(--fs-0); white-space: nowrap; }
.sr-beat-who { margin: 1px 0 0; color: var(--ink-muted); font-size: var(--fs-1); overflow-wrap: anywhere; }

/* A rung that costs money is drawn in the warning hue, because "this one bills
   you" is the thing worth noticing about it before an outage rather than after.
   A rung that reaches nobody is the error hue: it is the failure the whole
   screen exists to make visible. */
.sr-beat-paid .sr-beat-dot { border-color: var(--level-warning); background: var(--level-warning); }
.sr-beat-paid .sr-beat-cost { color: var(--level-warning); }
/* Hollow, because nothing leaves through it: a filled node on the wire means a
   page actually goes out there. */
.sr-beat-dead .sr-beat-dot { border-color: var(--level-error); background: var(--bg-1); }
.sr-beat-nobody { color: var(--level-error); }

.sr-flow-end {
  margin: 0;
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
  color: var(--ink-muted);
  font-size: var(--fs-1);
}
.sr-flow-silent { color: var(--level-error); }
.sr-flow-cost { margin: var(--space-2) 0 0; color: var(--ink-muted); font-size: var(--fs-1); }
.sr-flow-cost b { color: var(--ink); }

/* ── the verdict, and what is stopping it being true ─────────────────────── */
/* One sentence above the canvas answering the question the whole screen is
   about. Its colour is the state, and this is the one place on this page where
   colour means configuration health rather than incident severity: red is
   "nothing pages anybody", amber is "somebody is reached but it will not ring",
   and a working setup is quiet ink like everything else. */
.rc-verdict {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-2) var(--space-4);
  border: 0;
  border-bottom: 1px solid var(--border);
  background: var(--bg-1);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-0);
  text-align: left;
  cursor: pointer;
}
.rc-verdict:hover { background: var(--bg-2); }
.rc-verdict-line { flex: 1; }
.rc-verdict-count { color: var(--ink-faint); font-size: var(--fs-0); }
.rc-verdict-silent { box-shadow: inset 3px 0 0 var(--level-error); }
.rc-verdict-degraded { box-shadow: inset 3px 0 0 var(--level-warning); }
.rc-verdict-ok { box-shadow: inset 3px 0 0 var(--accent); }

.rc-tab-count {
  padding: 0 5px;
  border-radius: 8px;
  background: var(--level-warning);
  color: var(--bg-0);
  font-size: var(--fs-0);
}

.rc-findings { list-style: none; margin: 0 0 var(--space-3); padding: 0; }
.rc-finding {
  padding: var(--space-2) 0 var(--space-2) var(--space-3);
  border-top: 1px solid var(--border);
  border-left: 2px solid var(--border);
}
.rc-finding-silent { border-left-color: var(--level-error); }
.rc-finding-muted { border-left-color: var(--level-warning); }
.rc-finding-what {
  border: 0; padding: 0; background: none;
  color: var(--ink); font: inherit; font-size: var(--fs-0); text-align: left;
  cursor: pointer;
}
.rc-finding-what:hover { color: var(--accent); }
.rc-finding-why {
  margin: 2px 0 var(--space-2);
  font-size: var(--fs-0); color: var(--ink-faint); line-height: 1.5;
}

/* The bottom of the page: what to do, then what the drawing means. */
.rc-bottom {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-2) var(--space-4);
  border-top: 1px solid var(--border);
}
.rc-bottom .rc-legend { margin: 0; padding: 0; border: 0; }
.rc-quick { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }

/* A finding, put back on the node it is about. */
.circuit-mark-silent { fill: var(--level-error); }
.circuit-mark-muted { fill: var(--level-warning); }

/* A line in the manage tab: a name, a fact, and the way to it on the canvas.
   It replaced a table repeating every rung, which was a second rendering of a
   ladder that could disagree with the one being looked at. */
/* The line above a canvas that has pieces but no traces yet. */
.rc-note {
  position: absolute;
  top: var(--space-3);
  left: var(--space-4);
  right: 340px;
  z-index: 2;
  margin: 0;
  font-size: var(--fs-0);
}

.rc-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 6px 0;
  border-bottom: 1px solid var(--border);
  font-size: var(--fs-0);
}
.rc-row-name { flex: 1; }
.rc-row .btn { margin-left: auto; }

.circuit-divider line { stroke: var(--border); stroke-dasharray: 2 6; }
.circuit-divider text {
  fill: var(--ink-faint);
  font-family: var(--font-mono);
  font-size: 11px;
}

/* The chevron that folds a ladder shut and opens it again, drawn like the +
   because it is the same kind of thing: a handle on a node, not a control in a
   panel somewhere else. */
.circuit-fold { cursor: pointer; }
.circuit-fold circle {
  fill: var(--bg-0);
  stroke: var(--border-bright);
  stroke-width: 1;
}
.circuit-fold text {
  fill: var(--ink-muted);
  font-family: var(--font-mono);
  font-size: 12px;
}
.circuit-fold:hover circle, .circuit-fold:focus circle { stroke: var(--accent); }
.circuit-fold:hover text, .circuit-fold:focus text { fill: var(--accent); }

/* Carrying a team, a rotation or a ladder from the canvas onto a rule or rung.
   The ghost follows the pointer in page coordinates, so it lives on the body
   rather than inside the pannable, zoomable svg. */
.circuit-node[data-drag] { cursor: grab; }
body.is-carrying { cursor: grabbing; user-select: none; }
.circuit-ghost {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 60;
  padding: 4px 8px;
  border: 1px solid var(--accent);
  border-radius: var(--radius-s);
  background: var(--bg-1);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: var(--fs-0);
  pointer-events: none;
}

/* A trace nothing has travelled. Dashed rather than hairline-thin, because
   "never reached" has to survive being zoomed out. */
.circuit-edge-cold { stroke-dasharray: 4 4; opacity: 0.4; }


/* ---------- the journey builder ----------
   The discovered map, reused as a picker. Everything below is state on top of
   the flow map's own vocabulary: a node you have already used, a step marked
   for deletion, a step naming an event that has never been sent. */
.builder-stage { height: 320px; min-height: 0; }
.builder-node { cursor: pointer; }
.builder-tick { fill: var(--ink-faint); font-size: 14px; text-anchor: end; }
.builder-node:hover .builder-tick, .builder-node:focus .builder-tick { fill: var(--accent); }
/* Filled, not outlined. The flow map already spends its accent stroke on
   entry nodes, so outlining a chosen one makes "this is where sessions start"
   and "I picked this" the same picture. */
.builder-node.is-chosen rect { fill: var(--accent); stroke: var(--accent); }
.builder-node.is-chosen text { fill: var(--on-accent); }
.builder-node.is-chosen .builder-tick { fill: var(--on-accent); }

/* ---------- the journey, as a chain of blocks ----------
   What replaced a seven-column table.

   The table gave every field of a step the same weight, so the event name (the
   only part most steps ever need) sat between a role dropdown and a JSON box,
   and reading a journey back meant reading a spreadsheet. A block gives the
   name the line, keeps beside it only the two choices that change what gets
   matched, and folds the rest away.

   The trace between the blocks is the same visual language the flow map and
   the buy buttons already use for "something moves through here", and it is
   the reason the page has to say out loud that matching is unordered: draw a
   chain and people will assume the chain is the rule. See .builder-truth. */
.builder-steps { --step-gap: 24px; }
.step-chain {
  list-style: none;
  margin: var(--space-4) 0 var(--step-gap);
  padding: 0;
  counter-reset: step;
}

.step-block {
  position: relative;
  display: grid;
  grid-template-columns: 32px minmax(0, 1fr) auto;
  gap: var(--space-3);
  align-items: center;
  counter-increment: step;
  margin-bottom: var(--step-gap);
  padding: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-m);
  background: var(--bg-0);
}
.step-block:last-child { margin-bottom: 0; }
.step-block:focus-within { border-color: var(--border-bright); }

/* The trace comes in from above rather than going out below, so the formset's
   spare rows sitting at the end of the chain cannot leave one dangling into
   nothing. `is-head` is set by the builder on the first block anybody can see;
   `:first-of-type` is the same thing with the script off, where the spare rows
   are visible and are part of the chain. */
.step-block::before {
  content: "";
  position: absolute;
  left: 27px;
  bottom: 100%;
  width: 1px;
  height: var(--step-gap);
  background: repeating-linear-gradient(
    to bottom, var(--border-bright) 0 3px, transparent 3px 6px
  );
}
.step-block:first-of-type::before, .step-block.is-head::before { display: none; }

/* Current, running down the chain in the order the blocks are in. Each block
   is a third of a second behind the one above it, which is what makes it read
   as one thing travelling rather than five things blinking. */
.step-block::after {
  content: "";
  position: absolute;
  left: 25px;
  bottom: calc(100% + var(--step-gap));
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 6px var(--accent);
  animation: stepCurrent 2.6s linear infinite;
  animation-delay: calc(var(--step-i, 0) * 0.3s);
}
.step-block:first-of-type::after, .step-block.is-head::after { display: none; }
.step-block:nth-child(1) { --step-i: 0; }
.step-block:nth-child(2) { --step-i: 1; }
.step-block:nth-child(3) { --step-i: 2; }
.step-block:nth-child(4) { --step-i: 3; }
.step-block:nth-child(5) { --step-i: 4; }
.step-block:nth-child(6) { --step-i: 5; }
.step-block:nth-child(7) { --step-i: 6; }
.step-block:nth-child(8) { --step-i: 7; }
.step-block:nth-child(9) { --step-i: 8; }
@keyframes stepCurrent {
  0% { transform: translateY(0); opacity: 0; }
  12% { opacity: 1; }
  88% { opacity: 1; }
  100% { transform: translateY(var(--step-gap, 24px)); opacity: 0; }
}

.step-rail {
  display: grid;
  place-items: center;
  width: 32px; height: 32px;
  border: 1px solid var(--border-bright);
  border-radius: 50%;
  color: var(--ink-faint);
  font-size: var(--fs-0);
}
.step-ord::before { content: counter(step); }
/* The entry step is the one that decides whose sessions count, so it is worth
   being able to find at a glance rather than by reading five dropdowns. */
.step-block:has(select[name$="-role"] option[value="entry"]:checked) .step-rail {
  border-color: var(--accent);
  color: var(--accent);
}

.step-body { min-width: 0; }
.step-line { display: flex; gap: var(--space-2); align-items: center; flex-wrap: wrap; }
.step-name {
  flex: 1 1 18ch; min-width: 0;
  font: inherit; font-size: var(--fs-2);
  color: var(--ink); background: transparent;
  border: 0; border-bottom: 1px solid var(--border-bright);
  border-radius: 0;
  padding: 4px 2px;
}
.step-name:focus { outline: none; border-bottom-color: var(--accent); }
.step-pick {
  flex: 0 0 auto;
  font: inherit; font-size: var(--fs-0);
  color: var(--ink-muted); background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-s);
  padding: 3px 6px;
}
.step-reach { flex: 0 0 auto; font-size: var(--fs-0); }

/* Everything a step can have and most steps never need. Same fold as the one
   on the journey's own card, so "there is more here" looks the same twice. */
.step-more, .form-more { margin-top: var(--space-2); }
.step-more summary, .form-more summary {
  font-size: var(--fs-0); color: var(--ink-faint);
  cursor: pointer; list-style: none; width: max-content;
}
.step-more summary::-webkit-details-marker,
.form-more summary::-webkit-details-marker { display: none; }
.step-more summary::before, .form-more summary::before { content: "+ "; }
.step-more[open] summary::before, .form-more[open] summary::before { content: "− "; }
.step-more-body, .form-more-body {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-3);
  margin-top: var(--space-3);
}
.step-more-body .field { margin-bottom: 0; }
.form-more-body .field { margin-bottom: 0; }

.step-acts { display: flex; gap: var(--space-1); align-items: center; }
.step-move { padding: 2px 7px; }
.step-move:disabled { opacity: 0.3; cursor: default; }
.step-move:disabled:hover { border-color: var(--border-bright); background: transparent; }

/* The DELETE checkbox is what the server reads, so it stays submittable and
   stays reachable without JavaScript; it is only hidden once the buttons
   that tick it are known to be working. Same for the move buttons, which have
   nothing to set without the script. */
.step-delete { display: inline-block; }
[data-step-remove], .step-restore, .step-move { display: none; }
[data-builder="root"][data-bound="1"] .step-delete { display: none; }
[data-builder="root"][data-bound="1"] [data-step-remove] { display: inline-block; }
[data-builder="root"][data-bound="1"] .step-move { display: inline-flex; }
[data-builder="root"][data-bound="1"] .is-dropped [data-step-remove] { display: none; }
[data-builder="root"][data-bound="1"] .is-dropped .step-restore { display: inline-flex; }

/* Dropped, not gone: the row is still submitted, ticked, because that is how
   the server is told a saved step went away. It stops being counted by the
   chain's numbering, so the blocks below it do not read as 3, 4, 6. */
.step-block.is-dropped { counter-increment: none; opacity: 0.5; }
.step-block.is-dropped .step-name { text-decoration: line-through; }
.step-block.is-dropped .step-ord::before { content: "×"; }

/* Adding a step you already have is a no-op, so the block says so itself
   rather than raising an error for something that is not a mistake. */
@keyframes builder-repeat {
  from { border-color: var(--accent); background: var(--accent-glow); }
  to { border-color: var(--border); background: var(--bg-0); }
}
.step-block.is-repeat { animation: builder-repeat 900ms ease-out; }
@media (prefers-reduced-motion: reduce) {
  .step-block.is-repeat { animation: none; outline: 1px solid var(--accent); }
  .step-block::after { display: none; }
}

/* Blank formset extras, once the builder is running. They keep submitting (and
   Django keeps ignoring them); they just stop sitting in the chain above the
   blocks you actually chose. */
[data-builder="root"][data-bound="1"] .step-block.is-blank { display: none; }

/* The next link in the chain: one box that adds one block, or four if you
   paste four lines. It replaced a planning textarea in a card of its own plus
   a separate one-line box under the table, which were the same act performed
   twice in two places. */
.step-compose {
  position: relative;
  display: flex;
  gap: var(--space-2);
  align-items: center;
  flex-wrap: wrap;
  padding-left: 44px;
  min-height: 32px;
}
.step-compose::before {
  content: "";
  position: absolute;
  left: 27px;
  bottom: 100%;
  width: 1px;
  height: var(--step-gap, 24px);
  background: repeating-linear-gradient(
    to bottom, var(--border-bright) 0 3px, transparent 3px 6px
  );
  display: none;
}
/* Only when there is something for it to come down from. */
.step-chain:has(> .step-block:not(.is-blank)) ~ .step-compose::before { display: block; }
.step-compose::after {
  content: "+";
  position: absolute;
  left: 0; top: 0;
  display: grid; place-items: center;
  width: 32px; height: 32px;
  border: 1px dashed var(--border-bright);
  border-radius: 50%;
  color: var(--ink-faint);
  font-size: var(--fs-1);
}
.step-compose textarea {
  flex: 1 1 22ch; min-width: 0;
  resize: none; overflow: hidden;
  font: inherit; font-size: var(--fs-1); line-height: 1.5;
  color: var(--ink); background: var(--bg-0);
  border: 1px dashed var(--border-bright);
  border-radius: var(--radius-s);
  padding: 6px 10px;
}
.step-compose textarea:focus {
  outline: none;
  border-style: solid; border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-glow);
}
.step-compose-help { margin: var(--space-2) 0 0 44px; font-size: var(--fs-0); max-width: 68ch; }

/* Said where the drawing invites the wrong idea, rather than on the page that
   reports the results. A chain of blocks looks like a route, and this is the
   one thing about a journey somebody has to know before they build one. */
.builder-truth {
  margin: var(--space-4) 0 0;
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-left: 2px solid var(--accent);
  border-radius: var(--radius-m);
  background: var(--bg-0);
}
.builder-truth p { margin: 0 0 var(--space-3); max-width: 68ch; color: var(--ink-muted); font-size: var(--fs-1); }
.builder-truth p:last-child { margin-bottom: 0; }
.builder-truth .lead { color: var(--ink); font-size: var(--fs-2); font-weight: 600; }

/* Narrow: the actions drop under the block rather than squeezing the name into
   a column of its own. */
@media (max-width: 640px) {
  .step-block { grid-template-columns: 32px minmax(0, 1fr); }
  .step-acts { grid-column: 2; justify-content: flex-end; }
}

/* The picker, given the screen. The card becomes the page, so the map gets the
   height it has always wanted and the form is untouched underneath it. */
.flow-card:fullscreen {
  display: flex; flex-direction: column;
  border: 0; border-radius: 0; margin: 0;
  padding: var(--space-3) var(--space-4);
  background: var(--bg);
}
.flow-card:fullscreen .builder-stage { flex: 1 1 auto; height: auto; }


/* ---------- the active project switcher ----------
   In the app bar, on every org page, because the project you are looking at is
   global state and used to be re-asked in each section's own filter bar. */
.proj-switch { display: flex; gap: var(--space-2); align-items: center; margin-left: var(--space-4); }
.proj-switch-ico { color: var(--ink-faint); font-size: var(--fs-0); }
/* It is .filter-select first: the switcher should look like every other
   select in the product, not like a special one in the app bar. */
.proj-switch-select { max-width: 220px; }
/* The no-JavaScript path. app.js marks the form once it has bound the select,
   so this is hidden for the people who do not need it and present for the
   people who do, rather than absent for everybody. */
.proj-switch.is-autosubmit .proj-switch-go { display: none; }

/* Screen-reader-only, for the label above the select. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

@media (max-width: 720px) {
  /* The app bar is tight on a phone; the icon goes, the control stays. */
  .proj-switch { margin-left: var(--space-2); }
  .proj-switch-ico { display: none; }
  .proj-switch-select { max-width: 120px; }
}

/* ── docs ──────────────────────────────────────────────────────────────
   One public page carrying every SDK, so it needs navigation that the
   other public pages do not. The sidebar mirrors .explorer-facets rather
   than introducing a second sticky-nav idiom: same offset, same hover,
   same active treatment, so the two read as one system. */
.docs-grid { display: grid; grid-template-columns: 190px 1fr; gap: var(--space-5); align-items: start; }
.docs-nav { position: sticky; top: 62px; padding: var(--space-3); }
.docs-nav-title { margin: 0 0 var(--space-2); font-size: var(--fs-micro); font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-muted); }
.docs-nav-group { margin-bottom: var(--space-3); }
.docs-nav a { display: block; padding: 3px 6px; border-radius: var(--radius-s); color: var(--ink-muted); font-size: var(--fs-1); }
.docs-nav a:hover { background: var(--bg-2); color: var(--ink); text-decoration: none; }
/* Anchored headings must clear the sticky app bar, or a jump lands with the
   heading hidden underneath it and the reader thinks the link is broken. */
.docs-section { scroll-margin-top: 74px; }
.docs-section > h3::before { content: "❯ "; color: var(--accent); }
.docs-lede { color: var(--ink-muted); max-width: 66ch; }
.docs-what { margin: var(--space-3) 0 0; padding-left: 1.1em; color: var(--ink-muted); font-size: var(--fs-1); }
.docs-what li { margin-bottom: 3px; }
@media (max-width: 860px) {
  .docs-grid { grid-template-columns: 1fr; }
  .docs-nav { position: static; }
}

/* ── the routing panel's pieces ──────────────────────────────────────────── */
/* What the panel beside the routing canvas is built from: the draggable chips,
   the little labelled fields, and the template cards. These outlived the ladder
   page they were written for, which is now the canvas itself.

   Colour here means what it means everywhere else in this product: severity.
   The panel is ink and hairlines, and the accent marks what is selected. */
.lc-group {
  margin: var(--space-3) 0 var(--space-2);
  font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--ink-faint);
}
.lc-group:first-child { margin-top: 0; }
.lc-hint { margin: 0 0 var(--space-3); font-size: var(--fs-1); color: var(--ink-muted); line-height: 1.55; }

.lc-chip {
  display: flex; flex-direction: column; gap: 1px;
  padding: 6px var(--space-3);
  margin-bottom: 4px;
  background: var(--bg-0);
  border: 1px solid var(--border);
  border-radius: var(--radius-s);
  font-size: var(--fs-0);
  cursor: grab;
}
.lc-chip:active { cursor: grabbing; }
.lc-chip-note { font-size: var(--fs-0); color: var(--ink-faint); }

.lc-field { display: flex; flex-direction: column; gap: 3px; margin-bottom: var(--space-3); }
.lc-label { font-size: var(--fs-0); color: var(--ink-muted); }
.lc-field select, .lc-field input { width: 100%; }
/* A whole routing shape, offered as a card: what it builds, what it still
   wants, and whether the plan carries it. Same bones as a ladder template,
   because they are the same kind of offer at two different sizes. */
.rc-strategies { display: flex; flex-direction: column; gap: var(--space-2); }
.rc-strategy {
  padding: var(--space-3) 0;
  border-top: 1px solid var(--border);
}
.rc-strategy:first-child { border-top: 0; padding-top: 0; }
.rc-strategy h4 { margin: 0 0 4px; font-size: var(--fs-0); }
.rc-strategy.is-blocked { opacity: 0.75; }
.rc-strategy-why { margin-bottom: var(--space-2); }
.rc-strategy-why > summary {
  cursor: pointer;
  font-size: var(--fs-0);
  color: var(--ink-faint);
}

.lc-preset { padding: var(--space-3) 0; border-top: 1px solid var(--border); }
.lc-preset:first-of-type { border-top: 0; }
.lc-preset h4 { margin: 0 0 4px; font-size: var(--fs-0); }
.lc-preset-rungs { margin: 0 0 var(--space-2); padding-left: var(--space-4); font-size: var(--fs-0); }
.lc-preset-needs { margin: 0 0 var(--space-2); padding-left: var(--space-4); font-size: var(--fs-0); color: var(--level-warning); }
.lc-preset.is-blocked { opacity: 0.75; }



/* ---------- the upgrade dialog ----------
   Shown when a plan gate refuses something. It has one job the red flash
   underneath cannot do: say what the feature is for, to somebody who has just
   proved they want it by trying to use it.

   Everything animates once and settles. A loop would keep pulling the eye back
   to the picture while the words beside it are being read, and the picture is
   the least important thing on this dialog. */
.up {
  /* The close button is absolutely placed against this, so without it the
     button escapes to the viewport and lands outside the card. */
  position: relative;
  border: 1px solid var(--border-bright);
  border-radius: var(--radius-m);
  background: var(--bg-1);
  color: var(--ink);
  padding: 0;
  max-width: 560px; width: calc(100% - 2 * var(--space-5));
  max-height: calc(100vh - 2 * var(--space-5));
  box-shadow: var(--shadow-1), 0 24px 64px rgb(0 0 0 / 0.45);
  overflow: auto;
}
.up::backdrop { background: rgb(3 4 8 / 0.72); backdrop-filter: blur(3px); }
.up[open] { animation: up-in 0.28s cubic-bezier(0.16, 1, 0.3, 1); }
@keyframes up-in {
  from { opacity: 0; transform: translateY(10px) scale(0.985); }
  to   { opacity: 1; transform: none; }
}

/* Inset rather than flush, because `showModal` focuses the first control it
   finds and that is this one: a focus ring drawn on a button sitting in the
   corner reads as a box hanging off the dialog. Being first is correct, the
   way out should be, so the button moves instead of the focus. */
.up-close-form { position: absolute; top: var(--space-2); right: var(--space-2); z-index: 1; }
.up-close {
  border: 0; background: none; cursor: pointer;
  color: var(--ink-faint); font: inherit; font-size: 14px; line-height: 1;
  padding: 7px 9px; border-radius: var(--radius-s);
}
.up-close:hover { color: var(--ink); background: var(--bg-1); }

.up-figure {
  background: var(--bg-0);
  border-bottom: 1px solid var(--border);
  padding: var(--space-5) var(--space-5) var(--space-4);
}
.up-svg { width: 100%; height: 96px; display: block; }
.up-svg text {
  font-family: var(--mono, monospace); font-size: 9px;
  fill: var(--ink-faint); text-transform: lowercase;
}
.up-svg line, .up-svg path, .up-svg rect, .up-svg circle {
  stroke: var(--border-bright); fill: none; stroke-width: 1.5;
}

/* Each piece waits its turn: --i is its place in the sequence. */
.up-rung, .up-week, .up-person, .up-stop, .up-lane, .up-hour {
  opacity: 0.18;
}
.up.is-lit .up-rung, .up.is-lit .up-week, .up.is-lit .up-person,
.up.is-lit .up-stop, .up.is-lit .up-lane, .up.is-lit .up-hour {
  animation: up-lite 0.42s ease forwards;
  animation-delay: calc(var(--i) * 0.16s + 0.12s);
}
@keyframes up-lite { to { opacity: 1; } }

.up-spine { stroke: var(--border); stroke-dasharray: 3 4; }
.up.is-lit .up-rung-hit line, .up.is-lit .up-rung-hit circle { stroke: var(--ok); }
.up.is-lit .up-rung-hit circle { fill: var(--ok); }
.up.is-lit .up-rung-hit text { fill: var(--ok); }
.up.is-lit .up-week:last-child rect { stroke: var(--ok); }
.up.is-lit .up-person:last-child circle,
.up.is-lit .up-person:last-child path { stroke: var(--ok); }
.up-leak { stroke: var(--warn, #d08a2c) !important; }
.up.is-lit .up-lane-held rect { stroke: var(--warn, #d08a2c); stroke-dasharray: 4 3; }
.up.is-lit .up-hour { fill: var(--bg-1); }
.up.is-lit .up-hour-loud { stroke: var(--ok); fill: var(--accent-glow); }

/* The path draws itself rather than fading in: a journey is a sequence, and a
   line that appears whole says nothing about the order it happens in. */
.up-path { stroke: var(--border-bright); stroke-dasharray: 260; stroke-dashoffset: 260; }
.up.is-lit .up-path { animation: up-draw 0.9s ease forwards 0.1s; }
@keyframes up-draw { to { stroke-dashoffset: 0; } }

.up-body { padding: var(--space-5); }
.up-kicker {
  font-family: var(--mono, monospace); font-size: var(--fs-0);
  color: var(--warn, #d08a2c); margin: 0 0 var(--space-2);
}
.up-title { margin: 0 0 var(--space-3); font-size: var(--fs-4, 1.4rem); }
.up-stake { margin: 0 0 var(--space-4); color: var(--ink-muted); }

.up-benefits { list-style: none; margin: 0 0 var(--space-5); padding: 0; }
.up-benefits li {
  position: relative; padding-left: 22px; margin-bottom: var(--space-2);
  color: var(--ink); opacity: 0;
}
.up-benefits li::before {
  content: "✓"; position: absolute; left: 0; color: var(--ok);
}
.up.is-lit .up-benefits li {
  animation: up-rise 0.34s ease forwards;
  animation-delay: calc(var(--i) * 0.09s + 0.34s);
}
@keyframes up-rise {
  from { opacity: 0; transform: translateY(5px); }
  to   { opacity: 1; transform: none; }
}

.up-foot { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.up-per { opacity: 0.7; margin-left: -0.15em; }
.up-reassure {
  margin: var(--space-4) 0 0; font-size: var(--fs-0); color: var(--ink-faint);
}

/* Somebody who has asked for less motion still gets the whole dialog: it just
   arrives already settled, rather than not arriving. */
@media (prefers-reduced-motion: reduce) {
  .up[open] { animation: none; }
  .up.is-lit .up-rung, .up.is-lit .up-week, .up.is-lit .up-person,
  .up.is-lit .up-stop, .up.is-lit .up-lane, .up.is-lit .up-hour,
  .up.is-lit .up-benefits li { animation: none; opacity: 1; }
  .up.is-lit .up-path { animation: none; stroke-dashoffset: 0; }
}
