/* Severino HQ: internal ops console. */

/* Cascade layers, declared once and in priority order. Every rule sits in one
   of them, so the layer it is in decides which rule wins rather than how many
   classes its selector names: a single-class component still beats a base
   element rule.

   There is deliberately no fourth layer for "things that must win": narrowing
   a component at a breakpoint is what "later in the same layer" means.
   `StyleContractTests` fails the build if a rule lands outside a layer. */
/* Inter, HQ's own copy, preloaded by base.html. Shown only if it is ready for
   the first paint (optional) and never swapped in after, so text never moves;
   cached a year, it is there from the second page on. */
@font-face { font-family: Inter; font-style: normal; font-weight: 400 700; font-display: optional; src: url("../fonts/inter/inter-variable-latin.woff2") format("woff2"); }
@layer tokens, base, components;

@layer tokens {

/* Two palettes, one set of names. Every colour token is written
   `light-dark(<light>, <dark>)`, so each pair is decided side by side and a
   token cannot exist in one theme only; `color-scheme` picks the half, and
   also moves the form controls, scrollbars and other native widgets with it.
   With no `data-theme` the page follows the operating system; the attribute,
   rendered by the server from the operator's choice, pins one or the other.

   The dark half is not the light half inverted. Surfaces step up in
   lightness as they come forward (page, card, raised row), because a shadow
   has nothing to fall on; hairlines are lighter than the surfaces they
   divide; status inks lighten and their fills deepen so a tone keeps its hue
   and its contrast; chart and category colours lift until they clear 5:1 on
   the card they sit on. Text, muted text included, clears 4.5:1 on every
   surface, and `core.browser_tests` samples it to keep that true. */
:root {
  color-scheme: light dark;
  accent-color: var(--accent);
  --bg: light-dark(#f6f6f3, #111215);
  --bg-tint: light-dark(#efefe9, #1c1d21);
  --surface: light-dark(#ffffff, #18191c);
  --surface-2: light-dark(#fafaf7, #1d1e22);
  --ink: light-dark(#18181b, #ececee);
  --ink-2: light-dark(#2a2a2e, #d6d6da);
  --muted: light-dark(#6b6b73, #a4a4ad);
  --muted-2: light-dark(#95959c, #8d8d96);
  --line: light-dark(#e6e6e1, #303238);
  --line-2: light-dark(#f0f0eb, #26272c);
  --accent: light-dark(#1f4d57, #8fd0dc);
  --accent-2: light-dark(#2a6976, #6fbccb);
  --accent-ink: light-dark(#ffffff, #0a1b1f);
  --accent-tint: light-dark(#e3eef0, #1a3238);
  --danger: light-dark(#a8261c, #f08276);
  --warn: light-dark(#a35a00, #e8a64a);
  --ok: light-dark(#1d6b35, #62c585);
  --attn: light-dark(#b0521a, #ef9258);

  /* Code surface. Dark by intent, not by theme: a block of code reads as a
     quoted artefact rather than as more page. On a dark page it sinks below
     the page instead, for the same reason. */
  --code-bg: light-dark(#161922, #0b0c0f);
  --code-ink: light-dark(#eaeef2, #e3e7ec);

  /* The border a control takes when pointed at: one step firmer than
     --line, which is the resting rule. */
  --line-strong: light-dark(#c4c4c0, #4c4e56);

  /* Category identity. Like --series-* these are a set, not a list: they are
     chosen against each other and against the page for separation under
     normal and colour-deficient vision. Changing one means re-checking all. */
  --cat-homelab: light-dark(#2e7d8c, #5bb4c4);
  --cat-automation: light-dark(#6c4ab5, #a18ae6);
  --cat-wordpress-security: light-dark(#1b6b3f, #4fb47b);
  --cat-cloudflare: light-dark(#c2691c, #e39150);
  --cat-dns-email-security: light-dark(#7a3886, #bf82cc);
  --cat-vps: light-dark(#21525c, #6aa6b4);
  --cat-tailscale: light-dark(#2a5cb0, #7399e6);
  --cat-adguard: light-dark(#1e5e3a, #56a87a);
  --cat-networking: light-dark(#4c4f5c, #9a9fb0);
  --cat-tls-certificates: light-dark(#8c5a16, #cc9a4c);
  --cat-cybersecurity-labs: light-dark(#9b2a2a, #e57373);
  --cat-other: light-dark(#6b6b73, #a4a4ad);

  /* Topology node kinds: the same kind of set as --cat-*, for the graph. */
  --topology-controller: light-dark(#6c7480, #9aa2ae);
  --topology-ability: light-dark(#7b68a6, #a996d8);
  --topology-target: light-dark(#b5762d, #d99a52);
  --topology-lane-head: 44px;

  /* Print is a different medium: its ink is ink, not the screen palette. */
  --print-ink: #000;
  --print-rule: #666;
  --print-faint: #999;
  --print-muted: #333;

  /* Route bands, ordered warm to cool so adjacent hops stay distinguishable. */
  --route-band-1: light-dark(#b4531f, #de7a45);
  --route-band-2: light-dark(#c98a2b, #e2aa55);
  --route-band-3: light-dark(#9a9a34, #c0c05c);
  --route-band-4: light-dark(#3f9268, #62bc8d);
  --route-band-5: light-dark(#0e7f9c, #42b3cf);

  /* Typefaces: one for reading, one for anything read character by
     character. Named so a vendored widget can be handed the same two. */
  --font: 'Inter', -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, "Segoe UI", sans-serif;
  --font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;

  /* Space scale: the even rungs only. An odd value is drift, since a one-pixel
     change to padding is visible in a dense table in a way a half-pixel change
     to type is not. 1px is deliberately absent: it is a hairline rule, not a
     space. */
  --space-1: 2px;
  --space-2: 4px;
  --space-3: 6px;
  --space-4: 8px;
  --space-5: 10px;
  --space-6: 12px;
  --space-7: 14px;
  --space-8: 16px;
  --space-9: 18px;
  --space-10: 20px;
  --space-12: 24px;
  --space-14: 28px;
  --space-16: 32px;
  --space-20: 40px;
  --space-24: 48px;
  --space-32: 64px;
  --space-40: 80px;

  /* Type scale. These are the steps; a size that is not one of them is drift,
     and the guard says so. */
  --text-3xs: 10px;
  --text-2xs: 11px;
  --text-xs: 12px;
  --text-sm: 13px;
  --text-md: 14px;
  --text-lg: 15px;
  --text-xl: 16px;
  --text-2xl: 18px;
  --text-3xl: 20px;
  --text-4xl: 22px;
  --text-display: 40px;

  /* Tones. A status colour is never one value: it is a fill, ink that stays
     legible on that fill, and a border a shade deeper than both. Those three
     have to agree, so each tone is defined once here for every component.

     Named once here, a component asks for a tone and a new variant chooses
     one. `--tone-bg/-ink/-line` are the contract a component reads; the
     `--tone-*-*` sets below are what a variant assigns to it. The neutral
     values are the fallbacks, so a component with no tone still renders. */
  --tone-neutral-bg: var(--bg-tint);
  --tone-neutral-ink: var(--ink-2);
  --tone-neutral-line: var(--line);

  --tone-positive-bg: light-dark(#e1efe5, #16301f);
  --tone-positive-ink: light-dark(#16522a, #9adcb0);
  --tone-positive-line: light-dark(#b8dac3, #2a5238);

  --tone-caution-bg: light-dark(#fbf1d6, #302810);
  --tone-caution-ink: light-dark(#6c5300, #ebcd6e);
  --tone-caution-line: light-dark(#ecdba0, #574619);

  --tone-critical-bg: light-dark(#f0dcdc, #3a1c1c);
  --tone-critical-ink: light-dark(#6a1f1f, #f4afaf);
  --tone-critical-line: light-dark(#dfb4b4, #653232);

  /* The second fill level. A chip is small enough to carry a saturated tint;
     a banner covering half the page is not, and the same value that reads as
     a label at 20px reads as an alarm at full width. Ink and border are
     shared with the tone above (only the fill lightens) so a banner and
     the chip beside it are recognisably the same status. Tones without a
     wash have no large-surface use yet; add one when they do. */
  --tone-positive-wash: light-dark(#e8f5ec, #14261a);
  --tone-caution-wash: light-dark(#fbf1d6, #272110);
  --tone-critical-wash: light-dark(#fbeaea, #2d1818);
  --tone-info-wash: light-dark(#eef2f7, #172030);

  /* The third fill, for marks that carry the strong ink rather than the muted
     one: a tick inside a decision step, a kind badge on a hop. Paired with
     `--ok`/`--warn`/`--danger` above, which are darker and more saturated than
     the tone inks because they sit on less fill and often stand alone. */
  --tone-positive-tint: light-dark(#e2efe6, #193422);
  --tone-caution-tint: light-dark(#f4ead8, #342b15);
  --tone-critical-tint: light-dark(#f6e2e0, #3a1d1d);

  --tone-severe-bg: light-dark(#fcdcd0, #3a2212);
  --tone-severe-ink: light-dark(#843300, #f7bb94);
  --tone-severe-line: light-dark(#ffbfa8, #693b1d);

  --tone-info-bg: light-dark(#e4ebf3, #1a2536);
  --tone-info-ink: light-dark(#1f3656, #b0c8ea);
  --tone-info-line: light-dark(#c9d3e3, #2f425e);

  --tone-notable-bg: light-dark(#dfe6f1, #1b2640);
  --tone-notable-ink: light-dark(#1c3463, #b0c2f2);
  --tone-notable-line: light-dark(#c5d2e9, #314368);

  --tone-special-bg: light-dark(#ebe0f0, #2a1e3a);
  --tone-special-ink: light-dark(#4a2a72, #d4bff2);
  --tone-special-line: light-dark(#d3c2e8, #4a3666);

  /* Categorical series palette. Assigned in fixed order, never cycled: colour
     follows the entity, not its rank, so filtering a series must not repaint
     the survivors. These are validated as a set (lightness band, chroma floor,
     colour-vision separation, normal-vision floor) against the white chart
     surface: do not hand-tune a slot without re-validating the whole set.
     Slots 3-5 sit under 3:1 contrast on white, so any chart using them must
     also carry visible labels or its data table (both provided by the
     chart primitive). Status colours above are reserved and never used as a slot.
     The dark halves keep each hue and its order and lift it until every slot
     clears 7:1 on the dark card. */
  --series-1: light-dark(#0e88a8, #3fb8d8);
  --series-2: light-dark(#eb6834, #f28a5e);
  --series-3: light-dark(#1baf7a, #42c992);
  --series-4: light-dark(#eda100, #f0b93e);
  --series-5: light-dark(#e87ba4, #f098bc);
  /* The calendar draws more sources than a chart draws series. */
  --series-6: light-dark(#7b5cc4, #a68ef0);
  --series-7: light-dark(#4f6b8f, #93acd0);
  --series-8: light-dark(#a0603a, #d39470);
  --series-9: light-dark(#2f8f8a, #6cc9c2);
  --series-10: light-dark(#8a8f2f, #c2c76c);
  /* A calendar source's colour, set by its series class; the accent until then. */
  --source: var(--accent);
  --radius: 8px;
  --radius-sm: 5px;
  /* Every square control in the header bar (the collapsed search button and
     the sections toggle), sized once so the two cannot drift apart. */
  --control-size: 34px;
  /* Every control in the header bar reads these, so none can be drawn as a
     different kind of thing from the ones beside it. */
  --bar-control-bg: var(--surface-2);
  --bar-control-bg-hover: var(--bg-tint);
  --bar-control-line: var(--line);
  /* Measured by the enhancement layer because the global header changes shape
     with its contents. Dense-page navigation and floating table headings both
     consume the same inset instead of guessing it independently. */
  --site-header-height: 55px;
  --page-nav-height: 0px;
  --motion-fast: 150ms;
  /* Fluid sizes. These scale with the viewport instead of stepping at a
     breakpoint, so the page has no width at which padding or a heading jumps. */
  --content-pad: clamp(14px, 2.2vw, 28px);
  /* The page uses the window: content runs to the gutters up to this, and only
     a very wide monitor sees margins. */
  --content-max: 1600px;
  /* A paragraph's measure, whatever the column: past about 80 characters a
     line is hard to track back to the start of the next. */
  --measure: 80ch;
  --font-h1: clamp(19px, 0.55vw + 17.2px, 22px);
  --font-h2: clamp(12px, 0.18vw + 11.4px, 13px);
  /* Two spacing steps, and only two. `--layout-gap` separates siblings inside a
     grid; `--stack-gap` separates stacked page sections. */
  --layout-gap: var(--space-8);
  /* How much of an identifier a table cell shows before cutting it; the row
     expander shows the rest. */
  --identifier-max: 32ch;
  /* Track floors for a band of a fixed number of cells, so it lays out as a
     whole number of even rows at any width: 4, 2 or 1 columns (never 3+1),
     or 3 or 1. Read where they are used, so 100% is that grid's width.
     `(x - 100%) * 999` is a step, hugely positive below x and negative above
     it, so clamp() lands on one end or the other. For gapless bands. */
  --tracks-4-2-1: clamp(clamp(25%, (54rem - 100%) * 999, 50%), (24rem - 100%) * 999, 100%);
  --tracks-3-1: clamp(33.333%, (36rem - 100%) * 999, 100%);
  --stack-gap: var(--space-9);
  /* The widest a chart drawing with type inside it renders. Its SVG scales
     with its card, so uncapped its axis type and marks inflate in a wide card
     and two charts on one page stop matching. Bar and line charts draw their
     type as HTML over the plot, so they are exempt.

     There is deliberately no matching floor: `.two-col` lays out at
     `minmax(320px, 1fr)`, so a floor would only ever add a scrollbar. Label
     collision is instead measured: `Chart.dense` sums the width the labels
     need, and a container query asks how wide the card is. */
  --chart-max: 640px;
  /* Where one axis label sits, as a share of the drawing it labels. Set per
     label on the element itself, because there is one per data point and the
     position is computed from the data: this declaration is the default a
     label falls back to if it ever arrives without one, and the reason the
     property is a declared part of the contract rather than an invention of a
     template. */
  --at: 0%;
  /* Where a chart's plot begins, as a share of its width; set per chart. */
  --gutter: 0%;
  /* How much of a whole one segment of an allocation is, as a share. Set per
     segment for the same reason `--at` is set per label: it is computed from
     the data, so the template supplies the number and this file decides what a
     share looks like. Declared here so it is part of the contract. */
  --part: 0%;
  /* On a dark page a soft shadow has nothing to fall on, so there it is
     darker and tighter, and the edge is carried by the surface's own hairline
     and its step up in lightness. */
  --shadow-sm: 0 1px 2px light-dark(rgba(20, 20, 30, .05), rgba(0, 0, 0, .35)), 0 1px 1px light-dark(rgba(20, 20, 30, .03), rgba(0, 0, 0, .25));
  --shadow: 0 1px 2px light-dark(rgba(20, 20, 30, .06), rgba(0, 0, 0, .4)), 0 4px 14px light-dark(rgba(20, 20, 30, .07), rgba(0, 0, 0, .3));
  /* A panel that floats over the page (a dropdown, the sections drawer)
     rather than sitting on it. */
  --shadow-pop: 0 8px 24px light-dark(rgba(20, 20, 30, .12), rgba(0, 0, 0, .55));
  /* The small lift under a switch knob. */
  --shadow-knob: 0 1px 2px light-dark(rgba(0, 0, 0, .18), rgba(0, 0, 0, .5));
  /* What a modal dims the page with. */
  --scrim: light-dark(rgba(24, 24, 27, .38), rgba(0, 0, 0, .6));
  /* Selected text: the accent's tint, dark enough on a dark page that the
     ink over it still reads. */
  --selection: light-dark(#c9e0e5, #24505a);
}

/* The operator's choice, when there is one. */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }
/* Paper is light whatever the screen was. */
@media print { :root { color-scheme: light; } }

}

@layer base {

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
/* iOS inflates text in a block wider than the viewport, scaled by how far it
   overruns, so equal type needs this. */
/* A link to a part of the page lands it below the sticky header, whatever
   the part is; a part under a page's own nav asks for more itself. */
html {
  -webkit-text-size-adjust: 100%; text-size-adjust: 100%;
  scroll-padding-top: calc(var(--site-header-height) + var(--space-5));
}
body {
  font-family: var(--font);
  font-size: var(--text-md);
  line-height: 1.5;
  color: var(--ink);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-feature-settings: "cv11", "ss01", "ss03";
  /* A word too long for its line (a URL, a key, a digest) breaks rather than
     pushing the page wide. It breaks only where it would overflow, and does
     not change how wide a table cell asks to be. */
  overflow-wrap: break-word;
}
a { color: var(--accent-2); text-decoration: none; }
a:hover { color: var(--accent); text-decoration: underline; }
code { font-family: var(--font-mono); font-size: .88em; background: var(--bg-tint); padding: 1px var(--space-2); border-radius: 4px; color: var(--ink-2); }
pre code { background: transparent; padding: 0; color: inherit; }
/* A chip that is a link reads as one: the link colour, underlined on hover. */
a code { color: var(--accent-2); }
a:hover code { color: var(--accent); text-decoration: underline; }
pre { background: var(--code-bg); color: var(--code-ink); padding: var(--space-7) var(--space-8); border-radius: var(--radius); overflow-x: auto; font-size: var(--text-sm); line-height: 1.55; }
::selection { background: var(--selection); color: var(--ink); }
pre ::selection { color: var(--code-ink); }

/* A node key is one long unbroken token. Stacked so the machine's name sits
   above it rather than competing for width, and allowed to break anywhere so
   it wraps inside its panel instead of widening the page. */
.conn-keys { display: grid; gap: var(--space-1); margin: var(--space-3) 0 var(--space-4); }
.conn-keys dt { color: var(--muted); font-size: var(--text-xs); }
.conn-keys dd { margin: 0 0 var(--space-3); }
.conn-keys dd code, .node-key { font-size: var(--text-xs); overflow-wrap: anywhere; word-break: break-all; }

/* A mark, not an alarm. It has to be visible while the mode is on (an
   operator who forgets can screenshot fiction and file it as fact) and it
   sits beside the profile control rather than shouting over the header. */
.demo-flag {
  font-size: var(--text-2xs); font-weight: 600; white-space: nowrap;
  padding: var(--space-1) var(--space-4); border-radius: 20px;
  background: var(--bg-tint); color: var(--muted);
  border: 1px solid var(--line);
}
/* The header is one row at every width, and on the narrowest screens there is
   no room for this. Dropped rather than shrunk: the switch in the profile menu
   still carries the state, so the mode is a tap away from being visible. */
@media (max-width: 400px) {
  .demo-flag { display: none; }
}

/* ── Header ─────────────────────────────────────────────────────── */

/* Off-screen rather than `display: none`, which would take it out of the tab
   order and leave nothing to skip with. It sits above the sticky header's
   stacking context so it is not painted underneath the thing it jumps past. */
.skip-link {
  position: absolute; left: 12px; top: -100px; z-index: 20;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: var(--space-5) var(--space-7); font-size: var(--text-sm); font-weight: 600;
  transition: top .12s ease-out;
}
.skip-link:focus { top: 12px; text-decoration: none; outline: 2px solid var(--accent); }
/* The target takes focus so the next Tab continues from the content, but it is
   only ever focused programmatically: an outline there would be a box drawn
   around the whole page. */
.content:focus { outline: none; }
@media (prefers-reduced-motion: reduce) { .skip-link { transition: none; } }

/* One row, always. The sections are the header's job; the search field is the
   part that gives way. It collapses to a button that expands back over the nav
   on focus, so a cramped header costs a click rather than a second row, and
   the nav is never handed less room than its own content. */
.site-header {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--space-5) var(--space-7);
  padding: var(--space-5) var(--space-14);
  background: color-mix(in srgb, var(--surface) 80%, transparent);
  border-bottom: 1px solid var(--line);
  position: sticky; top: 0; z-index: 10;
  backdrop-filter: saturate(180%) blur(12px);
}
.brand {
  font-weight: 700; font-size: var(--text-lg); color: var(--ink);
  letter-spacing: -.01em;
  display: inline-flex; align-items: center; gap: var(--space-4);
  white-space: nowrap;
}
.brand::before {
  content: ""; width: 9px; height: 9px; border-radius: 2px;
  background: var(--accent); display: inline-block;
}
.brand:hover { text-decoration: none; }
.primary-nav {
  display: flex;
  /* Single row, deliberately: a control wrapped onto a second row would sit
     beneath an open dropdown, where the panel covers it and swallows its
     clicks. Grouping keeps the bar short enough that one row is enough. */
  flex-wrap: nowrap;
  gap: var(--space-1);
  /* Never squeezed below its own content. `overflow` must stay visible, so a
     nav handed less room than it needs has nothing to clip or scroll it: its
     tail would paint underneath the search field. */
  flex: 0 1 auto;
  /* Must stay visible: an overflow scroll container clips absolutely
     positioned descendants, the group dropdowns included. */
  overflow: visible;
}
.primary-nav a {
  color: var(--muted); font-weight: 500; font-size: var(--text-sm);
  padding: var(--space-2) var(--space-5); border-radius: 6px;
  white-space: nowrap;
  transition: background .12s ease, color .12s ease;
}
.primary-nav > a, .nav-group > summary { color: var(--muted); font-weight: 500; font-size: var(--text-sm); padding: var(--space-2) var(--space-5); border-radius: 6px; white-space: nowrap; }
.primary-nav a:hover { color: var(--ink); background: var(--bg-tint); text-decoration: none; }
.primary-nav a.active { color: var(--ink); background: var(--bg-tint); }

/* Grouped sections. A <details> dropdown keeps the bar to a fixed number of
   controls as sections are added, instead of overflowing behind a hidden
   scrollbar where the tail becomes unreachable. */
.nav-group { position: relative; }
/* Safari stops toggling <details> when `display` on <summary> is changed away
   from list-item, so the summary keeps its default display and an inner span
   does the layout. Chromium toggles either way, so this has to be checked in
   Safari rather than assumed. */
.nav-group > summary {
  display: list-item; list-style: none;
  cursor: pointer; user-select: none;
  transition: background .12s ease, color .12s ease;
}
.nav-group > summary::-webkit-details-marker { display: none; }
.nav-group > summary::marker { content: ""; }
.nav-group-label { display: inline-flex; align-items: center; gap: var(--space-2); }
.nav-group > summary:hover, .nav-group[open] > summary { color: var(--ink); background: var(--bg-tint); }
.nav-group > summary.active { color: var(--ink); background: var(--bg-tint); }
.nav-caret { fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; opacity: .7; }
.nav-group[open] .nav-caret { transform: rotate(180deg); }
.nav-group-panel {
  position: absolute; z-index: 40; top: calc(100% + 6px); left: 0;
  min-width: 176px; display: flex; flex-direction: column; gap: 1px;
  padding: var(--space-2); background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.nav-group-panel a { padding: var(--space-3) var(--space-4); border-radius: var(--radius-sm); font-size: var(--text-sm); }

/* Gives way before the sections do: below the breakpoint it is an icon. */
.global-search {
  flex: 0 1 210px;
  min-width: 150px;
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--space-4);
  height: var(--control-size);
  padding: 0 var(--space-3) 0 var(--space-5);
  border: 1px solid var(--bar-control-line);
  border-radius: var(--radius-sm);
  background: var(--bar-control-bg);
  color: var(--muted);
  font-size: var(--text-sm);
  text-decoration: none;
}
.global-search:hover { background: var(--bar-control-bg-hover); color: var(--ink-2); text-decoration: none; }
.search-icon { flex: none; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
.search-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.search-key { padding: 0 var(--space-2); border: 1px solid var(--line); border-radius: 4px; background: var(--surface); color: var(--muted-2); font: inherit; font-size: var(--text-3xs); line-height: 1.6; }

@media (max-width: 1280px) {
  .global-search { flex: none; min-width: 0; width: var(--control-size); justify-content: center; padding: 0; }
  .search-label, .search-key { display: none; }
}

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

/* ── Layout ─────────────────────────────────────────────────────── */

/* overflow-x: clip, not hidden: clip contains sideways overflow without making
   this a scroll container, so position: sticky and the chart tooltip still
   work. A single wide table stays behind its own card's scroller instead of
   widening the whole document. */

/* Every form control looks like HQ's wherever it sits, not only inside .field.
   Components still override this; it is the floor. */
input:where([type=text], [type=url], [type=number], [type=password], [type=email], [type=date], [type=time], [type=datetime-local], [type=search], [type=file], :not([type])),
select,
textarea {
  padding: var(--space-3) var(--space-5);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--text-sm);
  transition: border-color .12s ease, box-shadow .12s ease;
}
input:where([type=text], [type=url], [type=number], [type=password], [type=email], [type=date], [type=search], :not([type])):focus,
select:focus,
textarea:focus { outline: none; border-color: var(--accent-2); box-shadow: 0 0 0 3px var(--accent-tint); }

}

@layer components {

/* ── Tone assignment ─────────────────────────────────────────────────
   The one place a status name maps to a tone, for every component that has
   statuses. Grouped by tone rather than by component, so `published`,
   `reachable` and `success` cannot become three different greens.

   A component decides which fill level it reads (`--tone-bg` for a chip,
   `--tone-wash` for a banner, `--tone-tint` for a mark carrying strong ink);
   this section decides only what the tone *is*. Adding a status means adding
   its class to one of these lists. Adding a component means reading the
   contract, not restating the colours. */

.pill-active, .pill-published, .pill-created, .pill-uploaded, .pill-public,
.pill-replied, .pill-reachable, .pill-good, .msg-success, .conn-kind-tailnet,
.connection-state-reachable, .connection-state-ready, .connection-state-good, .tone-good {
  --tone-bg: var(--tone-positive-bg);
  --tone-wash: var(--tone-positive-wash);
  --tone-tint: var(--tone-positive-tint);
  --tone-ink: var(--tone-positive-ink);
  --tone-line: var(--tone-positive-line);
}
.pill-paused, .pill-editing, .pill-drafting, .pill-researching,
.pill-internal, .pill-attention, .msg-warning, .notice-attention, .conn-decision-step-unknown,
.conn-kind-declined, .connection-state-attention, .connection-state-stale, .tone-attention {
  --tone-bg: var(--tone-caution-bg);
  --tone-wash: var(--tone-caution-wash);
  --tone-tint: var(--tone-caution-tint);
  --tone-ink: var(--tone-caution-ink);
  --tone-line: var(--tone-caution-line);
}
.pill-archived, .pill-deprecated, .pill-retired, .pill-sold, .pill-returned,
.pill-replaced, .pill-deleted, .pill-login_failed, .pill-denied, .pill-spam,
.pill-unreachable, .msg-error, .notice-serious, .conn-decision-step-does-not,
.connection-state-unreachable, .connection-state-revoked, .connection-state-unauthorized,
.conn-kind-elsewhere, .conn-kind-public, .conn-kind-hop-ignored {
  --tone-bg: var(--tone-critical-bg);
  --tone-wash: var(--tone-critical-wash);
  --tone-tint: var(--tone-critical-tint);
  --tone-ink: var(--tone-critical-ink);
  --tone-line: var(--tone-critical-line);
}
.pill-idea, .pill-draft, .pill-viewed, .msg-info {
  --tone-bg: var(--tone-info-bg);
  --tone-wash: var(--tone-info-wash);
  --tone-ink: var(--tone-info-ink);
  --tone-line: var(--tone-info-line);
}
.pill-completed, .pill-unread {
  --tone-bg: var(--tone-notable-bg);
  --tone-ink: var(--tone-notable-ink);
  --tone-line: var(--tone-notable-line);
}
.pill-sensitive, .pill-restricted, .pill-serious, .connection-state-serious, .tone-serious {
  --tone-bg: var(--tone-severe-bg);
  --tone-ink: var(--tone-severe-ink);
  --tone-line: var(--tone-severe-line);
}
.pill-imported, .pill-exported {
  --tone-bg: var(--tone-special-bg);
  --tone-ink: var(--tone-special-ink);
  --tone-line: var(--tone-special-line);
}
/* Neutral is already every component's fallback, so these assign nothing.
   They are still named: a status with no rule and a status deliberately
   neutral look identical and are not the same claim, and the class has to
   exist for the template check to find it. */
.pill-read, .pill-unprobed, .pill-neutral, .connection-state-unprobed,
.connection-state-neutral, .connection-state-configured {
  --tone-bg: var(--tone-neutral-bg);
  --tone-ink: var(--tone-neutral-ink);
  --tone-line: var(--tone-neutral-line);
}

.content { max-width: var(--content-max); margin: 0 auto; padding: var(--content-pad); overflow-x: clip; }
/* One rhythm for every stacked page section, set in one place. Adjacent sibling
   margins collapse, so a section that already declares its own bottom margin
   keeps whichever is larger rather than adding to this. `dialog` is excluded:
   an open modal centres itself with `margin: auto`, which a margin-top here would quietly override. */ .content > * + *:not(dialog){ margin-top: var(--stack-gap); }

h1 { font-size: var(--font-h1); margin: 0 0 var(--space-2); letter-spacing: -.012em; font-weight: 650; }
/* A section heading reads as a heading: ink, sentence case, a step above body,
   so it outranks the tracked-capital labels around it. */
h2 { font-size: var(--text-xl); margin: 0 0 var(--space-6); color: var(--ink); font-weight: 650; letter-spacing: -.01em; }
h3 { font-size: var(--text-md); margin: var(--space-8) 0 var(--space-3); font-weight: 600; }

.page-lede { color: var(--muted); font-size: var(--text-sm); margin: 0 0 var(--space-12); max-width: var(--measure); }
.muted { color: var(--muted); }

/* One head at every width. The copy steps aside (`display: contents`) so its
   parts can be ordered: the trail on its own row, the title and the controls
   sharing the next row while they fit (the browser decides, not a count of
   controls), and the lede the full width under both. Actions wrap under the
   title only when they cannot fit beside it. */
.page-head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3) var(--space-5); margin-bottom: var(--stack-gap); }
.page-head-copy { display: contents; }
.page-head .page-trail { flex: 1 1 100%; order: 0; margin-bottom: calc(-1 * var(--space-2)); }
/* The title yields width before the controls do: a long name wraps in its
   own column rather than pushing the lead control onto a row of its own. */
.page-head .page-title-row { flex: 1 1 0; order: 1; min-width: 0; }
.page-head .page-title-row > h1 { overflow-wrap: anywhere; }
.page-head > .page-actions { order: 2; }
/* Full width so it always takes its own row; the reading measure is kept by
   padding rather than a width cap, which would let it fit beside the title. */
.page-head .page-lede { flex: 1 1 100%; order: 3; max-width: none; margin-bottom: 0; padding-inline-end: max(0px, 100% - var(--measure)); margin-top: calc(-1 * var(--space-1)); }
.page-head h1 { margin: 0; }
.page-head p { margin: 0; }
.page-trail { margin: 0 0 var(--space-2); color: var(--muted); font-size: var(--text-xs); }
.page-trail a { color: var(--muted); }
.page-trail a:hover { color: var(--ink); }

/* Never squeezed by the lede beside it. A flex item defaults to shrinking at
   its content, which would wrap a control's label onto two lines and leave the
   controls beside it different heights. */
.page-head .page-actions { position: relative; margin-left: auto; display: flex; gap: var(--space-4); flex: 0 1 auto; flex-wrap: wrap; justify-content: flex-end; min-width: 0; }
.page-head .page-actions > * { white-space: nowrap; }
/* Ambient readings beside the greeting: no frame of their own, each panel a
   number worth reading at a distance and one quiet line under it. A panel
   opens in place into a popover; the strip never changes height. */
/* The strip's own box and its row of panels lay nothing out: each panel is an
   item of the dashboard's one row of readings (`.dash-readings`), beside the
   readings about HQ itself, so all of them share a size, a spacing and a line. */
.dashboard-glance { display: contents; }
.page-head:has(.dashboard-glance) { align-items: center; }
.glance-panels { display: contents; }
.glance-panel { position: relative; min-width: 0; }
.glance-panel-head {
  display: grid;
  grid-template-columns: 22px minmax(0, auto);
  align-items: center;
  gap: 0 var(--space-5);
  padding: var(--space-3) var(--space-7);
  cursor: pointer;
  list-style: none;
  border-radius: var(--radius);
}
.glance-panel-head:hover, .glance-panel[open] > .glance-panel-head { background: var(--surface-2); }
.glance-panel-head::-webkit-details-marker { display: none; }
.glance-panel-head::marker { content: ""; }
.glance-panel-head:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.glance-symbol { grid-row: 1 / span 3; align-self: center; color: var(--muted-2); }
.glance-symbol .icon { width: 22px; height: 22px; }
.glance-lead { color: var(--muted); font-size: var(--text-xs); white-space: nowrap; }
.glance-lead strong { color: var(--ink); font-size: var(--text-4xl); font-weight: 650; line-height: 1.15; letter-spacing: -.01em; font-variant-numeric: tabular-nums; }
.glance-lead-empty { color: var(--muted); font-size: var(--text-sm); font-weight: 600; }
/* One line, its parts divided by a middle dot the browser draws, so a part
   that is absent leaves no stray separator. */
.glance-caption { display: flex; min-width: 0; gap: var(--space-2); color: var(--muted); font-size: var(--text-2xs); white-space: nowrap; }
/* A run of short things on one line (links, values, figures): one separator
   for all of them, drawn here, so no list writes its own commas or gaps. */
.dot-list { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-1) var(--space-3); margin: 0; padding: 0; list-style: none; }
:is(.dot-list > li, .glance-caption > span) + :is(li, span)::before { content: "·"; margin-right: var(--space-3); color: var(--muted-2); }
.glance-alert { color: var(--attn); font-weight: 650; }
.glance-freshness { color: var(--warn); font-size: var(--text-3xs); white-space: nowrap; }
.glance-panel[data-refreshing] .glance-freshness { color: var(--muted-2); }
.glance-expanded { position: absolute; top: calc(100% + 6px); right: 0; z-index: 5; display: grid; gap: var(--space-6); width: min(340px, calc(100vw - 32px)); padding: var(--space-6) var(--space-7); background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); }
.glance-expanded-head { display: grid; gap: 1px; font-size: var(--text-xs); color: var(--muted); }
.glance-expanded-head strong { color: var(--ink); font-size: var(--text-sm); }
.glance-expanded-head time { color: var(--muted-2); font-size: var(--text-3xs); }
.glance-note { margin: 0; color: var(--muted); font-size: var(--text-xs); line-height: 1.4; }
.glance-metrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-5) var(--space-7); margin: 0; }
.glance-metrics > div { display: grid; gap: 1px; min-width: 0; }
.glance-metrics dt { color: var(--muted); font-size: var(--text-3xs); font-weight: 500; white-space: nowrap; }
.glance-metrics dd { margin: 0; overflow: hidden; color: var(--ink); font-size: var(--text-sm); font-weight: 650; text-overflow: ellipsis; white-space: nowrap; }
.glance-metrics dd.glance-metric-detail { color: var(--muted); font-size: var(--text-3xs); font-weight: 400; }
/* The hours ahead, one column each, scrolling sideways in the popover. */
.glance-hours { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(2.6rem, 1fr); margin: 0; padding: var(--space-4) 0 0; overflow-x: auto; overflow-y: hidden; list-style: none; border-top: 1px solid var(--line-2); }
.glance-hours li { display: grid; justify-items: center; gap: 1px; font-size: var(--text-3xs); color: var(--muted); }
.glance-hours strong { color: var(--ink); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
.glance-hours small { color: var(--muted-2); }
.glance-hours small.is-wet { color: var(--accent); font-weight: 600; }
.glance-link { justify-self: start; font-size: var(--text-xs); }
.glance-expanded-foot { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--space-3) var(--space-6); padding-top: var(--space-4); border-top: 1px solid var(--line-2); font-size: var(--text-xs); }
.glance-expanded-foot > span { display: flex; gap: var(--space-5); margin-left: auto; }
/* A link in look: the panel's one action besides going somewhere. */
.glance-refresh { min-height: 0; padding: 0; border: 0; background: none; box-shadow: none; color: var(--accent); font: inherit; cursor: pointer; }
.glance-refresh:hover { border: 0; background: none; text-decoration: underline; }

/* A page declares its sections; the shell owns how they are traversed. Like
   the global navigation this is one row at every width, horizontally
   scrollable when necessary, so adding a section cannot make a page taller
   before its actual content begins. */
.page-nav {
  position: sticky;
  inset-block-start: var(--site-header-height);
  z-index: 9;
  display: flex;
  align-items: center;
  gap: var(--space-6);
  min-inline-size: 0;
  padding-block: var(--space-4);
  background: color-mix(in srgb, var(--bg) 92%, transparent);
  border-block-end: 1px solid var(--line);
  backdrop-filter: saturate(160%) blur(8px);
}
/* Named for screen readers; to the eye a row of section links already says it. */
.page-nav-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.page-nav-list {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-inline-size: 0;
  margin: 0;
  padding: 0 var(--space-1);
  overflow-x: auto;
  overflow-y: clip;
  overscroll-behavior-inline: contain;
  scrollbar-width: none;
  list-style: none;
}
.page-nav-list::-webkit-scrollbar { display: none; }
.page-nav-list li { flex: none; }
.page-nav-list a {
  display: inline-flex;
  align-items: center;
  min-block-size: 32px;
  padding-inline: var(--space-5);
  border-radius: 999px;
  color: var(--muted);
  font-size: var(--text-xs);
  font-weight: 550;
  text-decoration: none;
  transition:
    color var(--motion-fast) ease,
    background-color var(--motion-fast) ease;
}
.page-nav-list a:hover,
.page-nav-list a:focus-visible { color: var(--ink); background: var(--surface); }
.page-nav-list a[aria-current="location"] {
  background: var(--accent-tint);
  color: var(--accent);
}
.page-section { scroll-margin-block-start: var(--page-nav-height); }
/* Most heads wrap their controls in .page-actions; list pages hang a single
   control straight off the head. Both should sit at the far edge, otherwise
   the same button lands beside the title on one page and against the right
   margin on the next. */
.page-head > .btn, .page-head > form { margin-left: auto; }
.page-head > .btn + .btn, .page-head > form + form { margin-left: 0; }

/* ── Sections menu ──────────────────────────────────────────────── */
/* Both controls belong to the phone layout only. Nothing here hides the nav:
   the collapse is added by the narrow breakpoint, so every wider screen (and
   any screen whose stylesheet failed to load) gets the links unconditionally. */
.nav-toggle, .nav-backdrop { display: none; }

/* ── Buttons ────────────────────────────────────────────────────── */

.btn, button {
  display: inline-flex; align-items: center; gap: var(--space-3);
  padding: var(--space-3) var(--space-6);
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink);
  border-radius: var(--radius-sm);
  cursor: pointer;
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 500;
  /* Stated rather than inherited, so a button holding an icon is exactly as
     tall as one holding a word, and the two match wherever they sit side by
     side. */
  line-height: 1.45;
  min-height: 34px;
  text-decoration: none;
  transition: border-color .12s ease, background .12s ease, box-shadow .12s ease;
  box-shadow: var(--shadow-sm);
}
/* An icon inside a button is a glyph, not a block: it must not stretch the
   box or shrink when the row runs out of room. */
.btn > svg, button > svg { flex: none; width: 1.15em; height: 1.15em; }
.btn:hover, button:hover { border-color: var(--line-strong); background: var(--surface-2); text-decoration: none; }
.btn.primary, button.primary { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); box-shadow: none; }
.btn.primary:hover, button.primary:hover { background: var(--accent-2); border-color: var(--accent-2); }
.btn.danger, button.danger { background: var(--danger); color: var(--accent-ink); border-color: var(--danger); }
.btn:disabled, button:disabled { cursor: not-allowed; opacity: .52; }

/* ── User menu ────────────────────────────────────────────────────
   This whole component lives after the generic button primitive in the same
   layer. A POST row and a link row therefore share `.menu-item`; their HTML
   element never changes their visual hierarchy. */
/* The control's one line holds the mark and the name. On a phone, where every
   other control in the bar is as small as it gets, this one gives way first,
   down to the mark alone: squeezed, the name wraps to a second line that the
   control does not show. So the name is there on every phone that fits it and
   on none that does not, all of it or none of it, without a width being named. */
.user-menu { position: relative; flex: 0 0 auto; }
.action-badge { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); min-width: var(--control-size); min-height: var(--control-size); padding: var(--space-2) var(--space-4); border: 1px solid var(--bar-control-line); border-radius: var(--radius-sm); background: var(--bar-control-bg); color: var(--ink-2); font-size: var(--text-sm); font-weight: 600; font-variant-numeric: tabular-nums; text-decoration: none; white-space: nowrap; }
.action-badge:hover { background: var(--bar-control-bg-hover); color: var(--ink); text-decoration: none; }
.action-badge svg { flex: none; width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.user-menu > summary {
  display: flex;
  flex-flow: row-reverse wrap;
  align-content: flex-start;
  justify-content: center;
  column-gap: var(--space-3);
  height: var(--control-size);
  overflow: hidden;
  padding: 0 var(--space-5);
  border: 1px solid var(--bar-control-line);
  border-radius: var(--radius-sm);
  background: var(--bar-control-bg);
  color: var(--ink-2);
  cursor: pointer;
  font-size: var(--text-sm);
  font-weight: 500;
  list-style: none;
  white-space: nowrap;
}
.user-menu > summary::-webkit-details-marker { display: none; }
/* Each fills the control's one line exactly, so the line after it starts
   below the control's edge and is not shown. */
.user-menu > summary > * { flex: none; height: calc(var(--control-size) - 2px); }
/* The name asks for only a few characters of the line and takes what there
   is, up to its own length: a long one is cut short beside the mark rather
   than wrapping away and leaving the control wide and empty. Only when not
   even those few characters fit does it leave the line. */
.user-menu .user-name { flex: 1 1 5ch; min-width: 0; max-width: max-content; overflow: hidden; line-height: calc(var(--control-size) - 2px); text-overflow: ellipsis; white-space: nowrap; }
/* Who the menu belongs to, above what it offers. */
/* The picture, when there is one, stands beside both lines; the name and
   what the person may do share the first, when they signed in is the second. */
.menu-identity { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; padding: var(--space-4) var(--space-5) var(--space-5); border-bottom: 1px solid var(--line-2); }
.menu-identity > * { grid-column: 2; }
.menu-identity-picture { grid-column: 1; grid-row: 1 / span 2; width: 36px; height: 36px; margin-right: var(--space-5); border-radius: 50%; object-fit: cover; background: var(--bg-tint); }
.menu-identity-who { display: flex; align-items: center; gap: var(--space-4); min-width: 0; }
.menu-identity-who strong { overflow: hidden; color: var(--ink); font-size: var(--text-sm); text-overflow: ellipsis; white-space: nowrap; }
.menu-identity-when { color: var(--muted); font-size: var(--text-2xs); }
/* What an account may do, said once beside its name. Not a status: it has no
   tone, only the accent, and it never changes while you watch. */
.role-mark { flex: none; padding: 0 var(--space-3); border: 1px solid var(--accent); border-radius: 999px; color: var(--accent); font-size: var(--text-2xs); font-weight: 600; letter-spacing: .04em; line-height: 1.5; text-transform: uppercase; }
/* The person's own picture as the control's mark. Its margins make it as tall
   as the control's one line, like everything else in it. */
.user-menu .user-avatar { width: 22px; height: 22px; margin-block: calc((var(--control-size) - 2px - 22px) / 2); border-radius: 50%; object-fit: cover; background: var(--bg-tint); }
.user-menu > summary:hover,
.user-menu[open] > summary { background: var(--bar-control-bg-hover); color: var(--ink); }
.user-menu .user-icon {
  fill: none;
  stroke: var(--muted);
  stroke-linecap: round;
  stroke-width: 1.8;
  transition: stroke .12s ease;
}
.user-menu > summary:hover .user-icon,
.user-menu[open] > summary .user-icon { stroke: var(--ink); }
.user-menu-panel {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 20;
  display: flex;
  /* As wide as what it holds, up to a width that still fits beside the
     controls to its right on the narrowest phone; a longer name is cut. */
  width: max-content;
  min-width: 200px;
  max-width: 248px;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  box-shadow: var(--shadow);
}
.user-menu-panel form { margin: 0; }
.menu-item {
  display: flex;
  width: 100%;
  min-height: 0;
  align-items: center;
  margin: 0;
  padding: var(--space-3) var(--space-5);
  border: 0;
  border-radius: 4px;
  background: none;
  box-shadow: none;
  color: var(--ink);
  cursor: pointer;
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 500;
  line-height: inherit;
  text-align: left;
}
.menu-item:hover {
  border: 0;
  background: var(--bg-tint);
  color: var(--ink);
  text-decoration: none;
}
.menu-item-split { display: grid; grid-template-columns: 1fr 1fr; }
/* The menu is two columns, and every row keeps to them: what starts a row
   starts at the first column's edge, and what answers it starts at the
   second's, half-way across. The switches, the theme's buttons, the
   connection's network and the second of two links all share that one edge,
   so nothing is centred in one row and pushed to the side in the next. */
.menu-settings { display: grid; grid-template-columns: 1fr 1fr; }
.menu-setting { flex-direction: column; align-items: flex-start; gap: var(--space-3); }
/* What a row's status says in a word, after its mark, and light. */
.menu-item-note { overflow: hidden; color: var(--muted); font-size: var(--text-2xs); font-weight: 400; text-overflow: ellipsis; white-space: nowrap; }
/* Where the menu leads, set off from what it sets. */
.menu-places { display: grid; gap: 1px; padding-top: var(--space-2); border-top: 1px solid var(--line-2); }
/* One trailing column for counts, connection state and switches. New menu
   statuses align by taking this slot instead of inventing their own margin. */
.menu-item-status {
  display: inline-flex;
  min-width: 0;
  align-items: center;
  gap: var(--space-3);
  padding-left: var(--space-5);
}
/* A switch in the profile menu. The track carries the state, so the label
   never has to rename itself to say what is currently true. */
.switch-track {
  width: 26px; height: 15px; border: 1px solid var(--line); border-radius: 20px;
  background: var(--line);
  transition: background .12s ease-out, border-color .12s ease-out;
}
.switch-knob {
  display: block; width: 11px; height: 11px; margin: 1px; border-radius: 50%;
  background: var(--surface); box-shadow: var(--shadow-knob);
  transition: transform .12s ease-out;
}
[aria-checked="true"] .switch-track { background: var(--accent); border-color: var(--accent); }
[aria-checked="true"] .switch-knob { transform: translateX(11px); }
@media (prefers-reduced-motion: reduce) {
  .switch-track, .switch-knob { transition: none; }
}
/* The theme choice: a label in the row's own inset, then three buttons that
   share one track. The pressed one is the choice in force. */
.menu-theme { display: grid; grid-template-columns: 1fr 1fr; align-items: center; padding: 0 var(--space-5); }
.menu-theme-label { color: var(--ink); font-size: var(--text-sm); font-weight: 500; }
/* Three icons in one small track: a screen for the system's choice, a sun, a
   moon. Named for a screen reader and in their tooltip; the words would make
   a menu row a paragraph. */
.menu-theme-options { justify-self: start; margin-left: var(--space-5); display: inline-flex; gap: var(--space-1); padding: var(--space-1); border-radius: 999px; background: var(--bg-tint); }
.menu-item.menu-theme-option {
  display: grid; place-items: center; width: 26px; height: 26px; min-height: 0; padding: 0;
  border: 0; border-radius: 999px; background: none; box-shadow: none;
  color: var(--muted); font-size: var(--text-md); cursor: pointer;
}
.menu-item.menu-theme-option:hover { background: var(--surface); color: var(--ink); border: 0; }
.menu-item.menu-theme-option[aria-pressed="true"] { background: var(--surface); color: var(--ink); box-shadow: var(--shadow-sm); }
/* A band: one surface whose cells are divided by hairlines. Each cell draws
   its own left and top edge and the band clips the ones on its outer rim, so
   a row that wraps keeps its dividers at any width and a short last row ends
   in plain surface. One technique for every band; none needs a breakpoint. */
/* `:is`, not `:where`: a cell that is also a surface (a control metric, a
   control card) has its shadow and inset cleared by the frame rule, and the
   cell rule has to win that. A band that needs a gap (stats inside a card)
   says so with a more specific selector. */
:is(.kpi-grid, .insight-grid, .machine-telemetry-metrics, .service-band, .fact-band, .control-summary, .connection-control-grid, .sweep-grid) {
  gap: 0;
  overflow: clip;
  & > * {
    box-shadow: -1px 0 0 var(--line-2), 0 -1px 0 var(--line-2);
    /* A cell is not a frame nested in a frame: it keeps its own padding. The
       frame rule zeroes the inset of anything inside a surface, and a band is
       one. */
    --surface-inset: initial;
  }
}
.control-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr)); margin: 0 0 var(--stack-gap); }
.control-summary > *, .two-col > * { min-width: 0; }
.machine-telemetry {
  padding: var(--surface-inset, var(--space-8) var(--space-9));
}
.machine-telemetry .section-head { margin-bottom: var(--space-6); }
.machine-telemetry .section-head h2 { margin-bottom: var(--space-1); }
.machine-telemetry .section-head p { margin: 0; font-size: var(--text-xs); }
.machine-telemetry .section-head time { font-size: var(--text-2xs); white-space: nowrap; }
.machine-telemetry-metrics {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  margin: 0;
}
.machine-telemetry-metrics > div { min-width: 0; padding: var(--space-5) var(--space-6); background: var(--surface-2); }
.machine-telemetry-metrics dt { color: var(--muted); font-size: var(--text-3xs); text-transform: uppercase; letter-spacing: .05em; }
.machine-telemetry-metrics dd { margin: var(--space-1) 0 1px; font-size: var(--text-2xl); font-weight: 650; font-variant-numeric: tabular-nums; }
.machine-telemetry-metrics span { color: var(--muted); font-size: var(--text-2xs); }

.control-metric, .control-card { padding: var(--surface-inset, var(--space-6) var(--space-7)); }
/* Label and headline share a line when they fit, so a short value does not
   spend a whole row on whitespace. Anything further down (a caption, a
   readout, a pill) takes a full line of its own, and a headline too long to
   sit beside its label wraps to one as well. */
/* `space-between` rather than `margin-left: auto`, so the pairing survives a
   long value. A short headline sits opposite its label; one too wide to share
   the line wraps to its own and stays left-aligned there, because a lone item
   on a `space-between` line starts at the beginning of it. Pushed right with
   `auto`, a wrapped value would stay right-aligned and leave a ragged column. */
/* `align-content: flex-start` so a card's rows sit at the top whatever height
   the card ends up. Cards share a height with the tallest in the row;
   stretched, the spare space would spread between the rows and float the value
   in the middle of nothing. */
.control-metric { display: flex; flex-wrap: wrap; align-items: baseline;
  align-content: flex-start; justify-content: space-between; gap: var(--space-2) var(--space-6); }
/* A fixed line box, so the label sits at the same height whether its headline
   shares the line or wraps below. */
.control-metric > .control-label { flex: 0 1 auto; line-height: 20px; }
.control-metric > strong { text-align: left; }
.control-metric > *:not(.control-label):not(strong) { flex: 1 1 100%; }
/* Sized so most values sit opposite their label and the card stays the height
   of what it contains. */
.control-metric strong { font-size: var(--text-lg); line-height: 1.35; overflow-wrap: anywhere; }
.control-metric > span:last-child { color: var(--muted); font-size: var(--text-sm); line-height: 1.4; overflow-wrap: anywhere; }
/* A summary is one raised band, not a box per fact. Each fact is its label
   over its value, divided from its neighbours by a hairline; a row that wraps
   keeps its hairlines because each cell draws its own left and top edge and
   the band clips whatever falls outside it. */
.control-summary > .control-metric {
  flex-direction: column;
  flex-wrap: nowrap;
  align-items: flex-start;
  justify-content: flex-start;
  gap: var(--space-2);
  border: 0;
  border-radius: 0;
  background: none;
  padding: var(--space-8) var(--space-9);
}
.control-summary > .control-metric > * { flex: none; max-width: 100%; }
/* A supporting row inside a control metric. Several stack under one headline,
   so they carry their own muted styling rather than relying on the :last-child
   rule above, which dresses exactly one caption. */
.readout { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); color: var(--muted); font-size: var(--text-sm); line-height: 1.5; overflow-wrap: anywhere; }
.readout-label { color: var(--ink-2); font-weight: 600; }
/* Inside a table, a readout keeps its line. `.table-scroll td` breaks words so
   a long cell wraps rather than widening the page: right for prose, wrong for
   an address. The wrapper already scrolls, so the row is as wide as it is and
   the reader swipes. */
td > .readout { flex-wrap: nowrap; white-space: nowrap; overflow-wrap: normal; }
/* A provider offered as a starting point, laid out in the same card grid.
   align-items keeps every child its own width: stretched, a pill spans the card
   and reads as a banner rather than a tag. */
.kind-choice { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-3); text-decoration: none; }
.kind-choice strong { color: var(--ink); font-size: var(--text-xl); font-weight: 650; }
.kind-choice-summary { color: var(--ink-2); font-size: var(--text-sm); line-height: 1.45; }
.kind-choice:hover { background: var(--surface-2); text-decoration: none; }
/* Switching between domains. Links rather than a select, because the whole set
   is small enough to show and a select would hide every other domain behind a
   click and then need a second one to arrive anywhere. Wraps rather than
   scrolls, so a narrow screen stacks them instead of hiding the tail. */
.zone-switch {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin: 0 0 var(--stack-gap);
}
.zone-switch a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-6);
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink-2);
  font-size: var(--text-sm);
  font-weight: 550;
  text-decoration: none;
}
.zone-switch a:hover { border-color: var(--ink-2); color: var(--ink); }
.zone-switch a.active {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--surface);
}
.zone-switch-note { font-size: var(--text-2xs); font-weight: 500; opacity: 0.75; }
/* A record's value is data, not prose: an IPv4 address broken across lines to
   fit a narrow column is unreadable, and the table already scrolls. */
.zone-value { font-family: var(--font-mono); font-size: var(--text-xs); overflow-wrap: normal; }
/* A status pill is a label, not a paragraph: wrapped, it grows the row it sits
   in. The table already scrolls horizontally, so the label stays whole. */
.data-table .pill { white-space: nowrap; }
/* A name in a table cell takes two lines at most, and so does a `.cell-line`:
   one fact written as a line. Clamped, it still breaks where it must, so its
   column can narrow to fit the page, and the row stays within the height
   core/browser_tests.py holds it to. The whole name is the record's own page. */
.data-table td [data-entity],
.data-table td .cell-line {
  display: -webkit-inline-box;
  max-width: 100%;
  overflow: hidden;
  vertical-align: top;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
}
/* The row's own name is what tells it from the next row, and names share
   their prefixes ("example-worker-01", "example-worker-02"), so it is never
   cut: it wraps at its hyphens, and the names beside it give way instead. */
.data-table td strong > [data-entity],
.data-table td strong > .cell-line { display: inline; overflow: visible; -webkit-line-clamp: none; line-clamp: none; }
/* A cell that leads with the row's name keeps room to show it, rather than
   letting the table's arithmetic narrow the column until each name stands
   several lines tall. */
.data-table td:has(> strong:first-child) { min-width: 11rem; }
/* A name in the muted line under it is context the row already shares with
   its neighbours ("on example-host"): one line, cut at the cell's edge. */
.data-table td .muted [data-entity] { -webkit-line-clamp: 1; line-clamp: 1; }
/* Two links in one cell need to read as two, not as one control with a strange
   name. The class sits on a span in one place and on a td in two others, and a
   cell given a flex display leaves table layout, so only the span gets the box
   and the cells space their children directly. */
.row-actions { white-space: nowrap; vertical-align: middle; }
span.row-actions { display: inline-flex; gap: var(--space-6); }
td.row-actions > * + * { margin-left: var(--space-6); }
.chart-grid { stroke: var(--line-2); stroke-width: 1; }
/* Axis labels are HTML over the plot, not <text> inside it.

   Text in an SVG is drawn in the SVG's own coordinates, so it scales with the
   drawing and shrinks to illegibility in a phone-width card. Compensating by
   enlarging the type in chart units means guessing the scale, and the guess is
   wrong the moment a card changes width.

   Positioned by percentage of the same viewBox the geometry uses, so a label
   sits exactly over its bar at any size, and rendered by the browser at a real
   11px whatever that size is. The plot keeps the SVG, which is what SVG is
   for: the bars and lines are geometry and are supposed to scale. */
/* Capped with the drawing it labels. Every label is positioned as a percentage
   of this box, so this box must be the drawing's box. */
.chart-frame {
  position: relative;
  container-type: inline-size;
  max-width: var(--chart-max);
}
/* Bar and line charts have no cap and stretch to their card, so their frame
   must not have one either. */
.chart-frame:has(> .bar-chart), .chart-frame:has(> .line-chart) { max-width: none; }
/* A crowded axis in a card too narrow to hold it. The template knows the axis
   is crowded (it counted the labels); only the container query knows the card
   is narrow. Neither can decide it alone, and a viewport width would be wrong
   for the same chart in a half-width card on a desktop.
   The ends survive rather than every other one: first and last are the two
   labels that say what range is being looked at, and choosing them is a
   statement about an axis rather than an accident of how many bars there
   happen to be. Every period is still named in the data table under the chart. */
@container (max-width: 460px) {
  .chart-frame-dense .chart-category { display: none; }
  .chart-frame-dense .chart-cats .chart-category:first-child,
  .chart-frame-dense .chart-cats .chart-category:last-child { display: block; }
}
/* The one thing a stylesheet cannot know is where a given label goes, because
   there is one per data point and the position is computed from the data. That
   single coordinate arrives as `--at`; every decision about how the label
   looks (size, colour, weight, anchoring) is here. */
.chart-frame .chart-axis {
  position: absolute;
  color: var(--muted);
  font-size: var(--text-2xs);
  line-height: 1;
  white-space: nowrap;
  pointer-events: none;
}
/* Right-aligned against the plot's own left edge, wherever that has ended up.
   The strip it sits in is the drawing's gutter, given as the same share of the
   width the geometry uses, so the two cannot disagree at any card size. */
.chart-frame .chart-tick {
  top: var(--at);
  left: var(--gutter);
  padding-right: var(--space-3);
  transform: translate(-100%, -50%);
}
/* A layer of its own, so "first" and "last" mean the first and last date
   rather than the topmost of the y-axis ticks. */
.chart-cats { position: absolute; inset: 0; pointer-events: none; }
/* Every label is centred on the point it names, including the two at the ends:
   anchored inward, the end labels leave their own points and overlap their
   neighbours. The drawing keeps a strip either side of its plot for the
   overhang, and what a long label needs beyond that it takes from the card's
   padding, rather than from the label. */
.chart-frame .chart-category {
  left: var(--at);
  bottom: 0;
  transform: translateX(-50%);
}
/* <figure> carries a UA margin of 1em 40px, which would inset every chart card
   from the surrounding grid. */
.chart-card { margin: 0 0 var(--stack-gap); }
.chart-card figcaption h2 { margin: 0; }
/* Held to the plot's own width so the figure reads as one column instead of a
   full-bleed sentence sitting over a fixed-width drawing. */
.chart-card figcaption p { margin: var(--space-2) 0 0; max-width: var(--chart-max); color: var(--muted); font-size: var(--text-sm); line-height: 1.45; }
/* Two lines are reserved only when the pair sits side by side, so the drawings
   start level; a stacked card has no neighbour to line up with and reserves
   nothing. 656px is two 320px tracks and the 16px gap `.two-col` resolves to. */
/* A calendar is not a plot: its legend comes between the caption and the
   days, so nothing under its caption lines up with a neighbour's drawing and
   a reserved line there is only a blank band. */
@container pair (min-width: 656px) {
  .chart-card:not(.calendar-card) figcaption p { min-height: calc(2 * 1.45em); }
}
.chart-legend { display: flex; flex-wrap: wrap; gap: var(--space-6); margin: var(--space-6) 0 var(--space-1); color: var(--muted); font-size: var(--text-xs); }
.chart-legend span { display: inline-flex; align-items: center; gap: var(--space-2); }
.chart-legend i { inline-size: 9px; block-size: 9px; border-radius: 2px; }
/* `.chart-scroll`, the drawing's wrapper, has no rule of its own, and that is
   the rule. A chart has no floor (see `.bar-chart`), so it is never wider than
   its card and there is nothing to scroll. An end label hangs half its width
   past the drawing, into the card's padding, and the wrapper is exactly as
   wide as the card's content whatever the card's inset. */
/* Fills the card it is in, at a height this stylesheet decides rather than one
   the viewBox dictates. Its parts are rectangles and horizontal rules, so
   `preserveAspectRatio="none"` stretches them to the box without saying
   anything untrue, and because the labels are HTML over the top, none of the
   type stretches with it, so it needs no cap.
   No floor either, for the reason given at `--chart-max`: a half-width card is
   narrower than any floor worth setting, so a floor could only ever turn into
   a scrollbar. */
.bar-chart {
  display: block;
  width: 100%;
  height: 214px;
}

/* Series fills. Every custom property used here is defined in :root; an
   undefined one makes `fill` invalid and SVG falls back to black. The
   :root-variable test checks it. */
.chart-series-1 { fill: var(--series-1); background: var(--series-1); }
.chart-series-2 { fill: var(--series-2); background: var(--series-2); }
.chart-series-3 { fill: var(--series-3); background: var(--series-3); }
.chart-series-4 { fill: var(--series-4); background: var(--series-4); }
.chart-series-5 { fill: var(--series-5); background: var(--series-5); }
/* 2px surface gap between stacked segments so adjacent fills read as separate
   marks rather than one block; also carries the CVD relief for close hues. */
.bar-chart rect { stroke: var(--surface); stroke-width: 2; paint-order: stroke; transition: opacity .08s ease; }
/* A line chart is the same drawing box as a bar chart, so it borrows the
   sizing rather than restating it: two charts stacked in a column have to
   share an axis position, and two rules that nearly agree is how they stop. */
/* Filled to its card, like the bar chart: a line chart's axis is HTML over the
   top, so there is no type inside the drawing to upscale, only geometry, which
   is what SVG is for.
   `preserveAspectRatio="none"` in the template stretches it, so the stroke is
   pinned; otherwise a 2px line thickens with the card and the trend dashes
   stretch into a different dash. The dots are circles and go slightly oval at
   full width; at r=2.5 that still reads as a dot, and a capped width would
   leave a band of empty card. */
.line-chart {
  display: block;
  width: 100%;
  /* Shorter than the 260 the viewBox implies, so a chart does not push the
     thing it describes below the fold. */
  height: 220px;
}
.line-chart .chart-line,
.line-chart .chart-trend,
.line-chart .chart-grid,
.line-chart .chart-mark { vector-effect: non-scaling-stroke; }
.chart-line { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
/* Strokes, not fills: `.chart-series-N` sets `fill`, which draws a line's path
   as a filled blob rather than a line. The dots keep using the fill classes. */
.chart-line-1 { stroke: var(--series-1); }
.chart-line-2 { stroke: var(--series-2); }
.chart-line-3 { stroke: var(--series-3); }
.chart-line-4 { stroke: var(--series-4); }
.chart-line-5 { stroke: var(--series-5); }
/* Dashed, and thinner than the series it belongs to: a fitted line is a claim
   about the data, and it should not be mistaken for the data. */
.chart-trend { fill: none; stroke-width: 1.5; stroke-dasharray: 5 4; opacity: .65; }
.chart-dot { stroke: var(--surface); stroke-width: 1.5; pointer-events: none; }
/* The pointer target. A 3px dot cannot be hit with a mouse, so the tooltip is
   carried by an invisible circle four times its size sitting over it. */
.chart-hit { fill: transparent; stroke: none; cursor: default; }
.chart-hit:hover { fill: color-mix(in srgb, var(--ink) 7%, transparent); }
.chart-mark { stroke: var(--ink-2); stroke-width: 1; stroke-dasharray: 3 3; opacity: .55; }
.chart-mark-label { font-size: var(--text-3xs); font-weight: 600; }
/* A share-of-one drawing, sized to itself. It must not borrow .bar-chart:
   that is width:100% against a wide, short viewBox, and a square one under the
   same rule scales to the full card and takes its own width in height. */
.pie-chart { display: block; inline-size: min(100%, 260px); block-size: auto; flex: none; }
/* Legend beside the circle rather than above it. A pie is square and a card is
   wide, so stacking them leaves a band of empty card the width of the legend
   and the height of the drawing. Centred together so the pair sits in the
   card rather than against its left edge. */
.pie-row { display: flex; align-items: center; justify-content: center; gap: var(--space-10); flex-wrap: wrap; margin: var(--space-2) 0 var(--space-4); }
.pie-row .chart-legend { flex-direction: column; align-items: flex-start; gap: var(--space-3); margin: 0; }
.pie-chart path, .pie-chart circle { stroke: var(--surface); stroke-width: 2; paint-order: stroke; }
.chart-data { margin-top: var(--space-3); color: var(--muted); font-size: var(--text-xs); }
.section-readout { margin-top: var(--space-3); font-size: var(--text-sm); }
/* A raw reading holds paths and names of any length: wrapped, so only its
   height scrolls and never the page sideways. */
.section-readout pre { max-height: 18rem; margin-top: var(--space-4); white-space: pre-wrap; overflow-wrap: anywhere; }
.control-label { color: var(--muted); font-size: var(--text-xs); font-weight: 500; }
.control-good, .control-healthy { color: var(--ok); }
.control-warn, .control-degraded, .control-drifted { color: var(--warn); }
.control-unknown { color: var(--muted); }
/* Neither is a health reading: one is a responsibility nothing reconciles, the
   other is a resource between being declared and being applied. Both take the
   neutral colour rather than a warning one. */
.control-declared, .control-pending { color: var(--muted); }
.compact-table { box-shadow: none; }
/* A node's relationships: the phrase once per group with its ends beside it,
   and under it where there is no room beside. */
.relation-list { display: grid; margin: 0; }
.relation-group { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-6); padding: var(--space-5) var(--space-6); }
.relation-group + .relation-group { border-top: 1px solid var(--line); }
.relation-group dt { flex: 0 0 12rem; color: var(--muted); font-weight: 600; }
.relation-group dd { flex: 1 1 18rem; min-width: 0; margin: 0; }
.relation-ends { display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-5); margin: 0; padding: 0; list-style: none; }
.relation-ends li { min-width: 0; overflow-wrap: anywhere; }
.relation-source { margin: var(--space-2) 0 0; color: var(--muted); font-size: var(--text-sm); }
/* Policy names and ports: each chip whole, the row wrapping between them. */
.code-list { display: inline-flex; flex-wrap: wrap; gap: var(--space-1) var(--space-2); }
.code-list code { white-space: nowrap; }
.external-link::after { content: " \2197"; font-size: .85em; }
/* A band of cells in a grid lays out as whole rows, by how many cells it has:
   one takes the band, three are three or one, four are four, two or one. Read
   off the band itself, so no page has to say how many it holds and none is
   left with three over one. */
:is(.control-summary, .insight-grid):has(> :only-child) { grid-template-columns: minmax(0, 1fr); }
:is(.control-summary, .insight-grid):has(> :nth-child(3):last-child) { grid-template-columns: repeat(auto-fit, minmax(var(--tracks-3-1), 1fr)); }
:is(.control-summary, .insight-grid):has(> :nth-child(4):last-child) { grid-template-columns: repeat(auto-fit, minmax(var(--tracks-4-2-1), 1fr)); }

/* A service's overview: one raised band, not a box per fact. Each fact is its
   icon tile, then its label over its value; hairlines divide them. Health's
   tile takes its tone, so the state reads before the word does. */
.service-band, .fact-band {
  /* A wrapping row, not a grid: its facts share the width however many there
     are, and a note under them takes a line of its own. A grid would keep an
     empty column for each track a spanning note reaches. */
  display: flex;
  flex-wrap: wrap;
  margin: 0 0 var(--stack-gap);
  /* Each fact's share before it grows: a variant sets a step instead. */
  --band-cell: 220px;
}
:is(.service-band, .fact-band) > * { flex: 1 1 var(--band-cell); }
.band-fact { display: flex; gap: var(--space-6); align-items: flex-start; min-width: 0; padding: var(--space-8) var(--space-9); }
.band-icon {
  display: grid; place-items: center; flex: none;
  width: 36px; height: 36px; border-radius: var(--radius);
  background: var(--accent-tint); color: var(--accent-2); font-size: var(--text-3xl);
}
.band-fact:is(.tone-good, .tone-attention, .tone-serious) .band-icon { background: var(--tone-bg); color: var(--tone-ink); }
.band-fact > div { display: grid; gap: var(--space-1); min-width: 0; }
.band-label { color: var(--muted); font-size: var(--text-xs); }
.band-fact strong { font-size: var(--text-lg); font-weight: 650; line-height: 1.3; overflow-wrap: anywhere; }
.band-fact:is(.tone-good, .tone-attention, .tone-serious) strong { color: var(--tone-ink); }
.band-fact small { color: var(--muted); font-size: var(--text-sm); line-height: 1.4; }
.page-title-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3) var(--space-5); }
.page-title-row > h1 { margin: 0; }
.page-title-meta { margin: 0; color: var(--muted); font-size: var(--text-sm); }
/* What a thing is at a glance, as tiles at the end of its title's line:
   each tile as wide as what it says, no band of empty cells around them.
   Under the title when the line is too short for both. */
.page-title-row > .head-tiles { margin-left: auto; display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--space-3) var(--space-10); }
.head-tiles > .band-fact { flex: 0 1 auto; padding: 0; gap: var(--space-4); }
.head-tiles .band-icon { width: 2rem; height: 2rem; }
.head-tiles strong { font-size: var(--text-md); }
.heading-aside { color: var(--muted); font-size: var(--text-md); font-weight: 500; letter-spacing: 0; }
.band-meta { flex-basis: 100%; display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3) var(--space-5); padding: var(--space-5) var(--space-9); border-top: 1px solid var(--line-2); color: var(--muted); font-size: var(--text-sm); }
/* The origin reads under the facet row rather than as a fifth card in it: four
   is what a name needs to answer, and a fifth would wrap the row. */
.control-origin { margin: calc(var(--space-3) * -1) 0 22px; color: var(--muted); font-size: var(--text-sm); line-height: 1.5; }
.control-origin strong { color: var(--ink); font-weight: 600; }

/* The links chooser opens as a dialog; "Edit" is its trigger. */
.ext-edit { margin-left: auto; color: var(--muted); font-size: var(--text-xs); }
.ext-edit:hover { color: var(--ink); }
/* One row per choice, each the same height, with the label and its subtitle in
   fixed columns so the ticks line up whatever the words are. */
.ext-choices { list-style: none; margin: 0 0 var(--space-6); padding: 0; display: grid; gap: var(--space-1); max-height: 15rem; overflow-y: auto; }
.ext-choices label {
  display: grid; grid-template-columns: auto 1fr auto; align-items: center;
  gap: var(--space-4); font-size: var(--text-sm); padding: var(--space-2) var(--space-3); border-radius: 6px; cursor: pointer;
}
.ext-choices label:hover { background: var(--bg-tint); }
.ext-choice-label { overflow-wrap: anywhere; }
.ext-choice-sub { font-size: var(--text-xs); white-space: nowrap; }
.ext-chooser-actions { display: flex; align-items: center; gap: var(--space-4); }
.ext-chooser-status { font-size: var(--text-xs); }
.icon { width: 1em; height: 1em; flex: none; }
/* Each control is its own POST, so each is its own form, laid inline so the
   page actions row does not stack them as blocks. */
.page-actions form { margin: 0; display: inline-flex; }

.control-diagnostics { margin: var(--space-10) 0; }
.control-diagnostics > summary { color: var(--muted); cursor: pointer; font-size: var(--text-sm); font-weight: 600; }
.control-diagnostics[open] > summary { margin-bottom: var(--space-6); }
/* Names and paths of any length, in half the width: wrapped, never sideways. */
.control-diagnostics pre { white-space: pre-wrap; overflow-wrap: anywhere; }
/* Inside a metric card the disclosure is one more row, not a section. */
.control-metric > .control-diagnostics { margin: var(--space-2) 0 0; }
/* A short record list on a card reads at body size, not headline size. */
.control-metric .list-rows strong { font-size: var(--text-sm); font-weight: 600; }
.control-metric .list-rows li { padding: var(--space-2) 0; }
/* Stacked, not two columns. A metric card is 220px at its narrowest and the
   shared definition list reserves 200px for the term alone, so two columns
   would put every value outside the card. */
.control-metric .kv { grid-template-columns: 1fr; gap: var(--space-1) 0; padding: var(--space-5) var(--space-6); }
.control-metric .kv dt { margin-top: var(--space-4); font-size: var(--text-xs); }
.control-metric .kv dt:first-of-type { margin-top: 0; }
.control-metric .kv dd { overflow-wrap: anywhere; }
.control-metric > .control-diagnostics[open] > summary { margin-bottom: var(--space-3); }
.operation-result { max-width: 34rem; overflow-wrap: anywhere; }
.operation-result > strong { display: block; line-height: 1.35; }
.operation-guidance { margin: var(--space-2) 0; color: var(--muted); font-size: var(--text-sm); line-height: 1.45; }
.operation-evidence { margin-top: var(--space-3); color: var(--ink); }
.operation-evidence ul { margin: var(--space-3) 0 0; padding-left: var(--space-9); }
.operation-evidence li { margin: var(--space-1) 0; }
.operation-evidence pre { max-height: 18rem; margin-top: var(--space-4); }
.btn.ghost { background: transparent; border-color: transparent; color: var(--muted); box-shadow: none; }
.btn.ghost:hover { color: var(--ink); background: var(--bg-tint); border-color: transparent; }
/* A destructive action that should not be the loudest thing in its row. */
/* A button like its neighbours, in red: the same surface and lift, so it does
   not read as an outline with the page showing through beside solid ones. */
.btn.ghost.danger { background: var(--surface); border-color: color-mix(in srgb, var(--danger) 35%, var(--line)); color: var(--danger); box-shadow: var(--shadow-sm); }
.btn.ghost.danger:hover { background: color-mix(in srgb, var(--danger) 8%, var(--surface)); color: var(--danger); }

/* ── Filter bar ─────────────────────────────────────────────────── */

.filter-bar {
  display: flex; flex-wrap: wrap; gap: var(--space-4);
  padding: var(--surface-inset, var(--space-5));
  margin-bottom: var(--space-8);

}
.content input[type=search], .filter-bar select, .filter-bar input[type=text] {
  padding: var(--space-3) var(--space-4); border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface); font: inherit; font-size: var(--text-sm);
}
.filter-bar input[type=search] { flex: 1 1 240px; }
/* One height for every control in the bar, so a wrapped row still reads as one
   row of controls rather than a field, two shorter selects and a taller button. */
.filter-bar > input, .filter-bar > select, .filter-bar > .btn { height: var(--control-size); box-sizing: border-box; }
/* A select is as wide as its longest option unless told otherwise. */
.filter-bar > select { max-width: 100%; min-width: 0; }
.filter-bar > .btn { padding-block: 0; }
/* One field and its button on a line: the field takes the room. */
.field-row { display: flex; gap: var(--space-4); align-items: center; margin-top: var(--space-6); }
.field-row > input { flex: 1; min-width: 0; }
.content input[type=search]:focus, .filter-bar input:focus, .filter-bar select:focus { outline: 2px solid var(--accent-tint); outline-offset: -1px; border-color: var(--accent-2); }

/* ── Data table ─────────────────────────────────────────────────── */

/* min-width:0 so the scroller can actually be narrower than its table. Without
   it a grid or flex ancestor sizes to the table's content instead, and the
   overflow escapes to the page rather than scrolling inside its card. */
/* A wide table scrolls sideways rather than widening the page. `overflow-y`
   is stated because left alone it computes to `auto` and offers a scrollbar
   it has no content for. */
.table-scroll { overflow-x: auto; overflow-y: clip; -webkit-overflow-scrolling: touch; margin-bottom: var(--space-8); min-width: 0; max-width: 100%; }
.table-scroll .data-table { margin-bottom: 0; }
/* The surface is the scroll box's, not the table's. A box that scrolls clips
   what overflows it, the table's own shadow included, so the box carries the
   border, the radius and the shadow. */
.table-scroll > .data-table { border: 0; border-radius: 0; box-shadow: none; }
/* A run of like events in a history: one line, its events a click away. */
.history-run > summary { cursor: pointer; }
.history-run-list { margin: var(--space-2) 0 0; padding-left: var(--space-5); font-size: var(--text-sm); }
/* Nothing follows it, so the gap it reserves is dead card. The same is true of
   anything that ends a card: a trailing margin inside a padded box is a band
   of empty card. */
.table-scroll:last-child { margin-bottom: 0; }
/* Refreshes pin column widths (fixed layout); long unbroken tokens must
   wrap inside their cell instead of bleeding across the pinned columns. */
/* Before tables.js has fitted it, and where there is no script at all, a
   table is sized to its content and scrolls inside its box: squeezed to fit
   with nothing to hold its columns open, every value would stand up as a strip
   a word wide. The cap stops a prose column running to a four-screen swipe. */
.table-scroll > .data-table:not([data-fit]):has(tbody td:not([colspan])) { width: max-content; min-width: 100%; max-width: 1400px; }
.table-scroll td { overflow-wrap: break-word; }
/* Fitted, a long unbroken value has to count as breakable when the columns
   are sized, or one identifier pushes the table past its box, and only
   `anywhere` does that. Each column's floor (below) keeps it readable. */
.table-scroll > .data-table[data-fit] td { overflow-wrap: anywhere; }
.data-table {
  /* A cell's space above and below, named because the row toggle is placed
     by it. A narrow table's roomier rows set it and both follow. */
  --row-pad: var(--space-4);
  /* How narrow a column may get; tables.js says, per heading. */
  --column-floor: 0px;
  width: 100%; border-collapse: collapse; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow-sm);
}
.data-table th, .data-table td { padding: var(--row-pad) var(--space-6); text-align: left; vertical-align: top; border-bottom: 1px solid var(--line-2); font-size: var(--text-sm); }
.data-table thead th { background: var(--surface-2); font-size: var(--text-xs); color: var(--muted); font-weight: 600; }
/* A sticky table heading, the way the platform does it.

   `overflow-x: auto` on the wrapper makes the browser compute `overflow-y` to
   `auto` as well, so the wrapper is a vertical scroll container whether it was
   asked to be or not, and a sticky cell pins to its nearest scroll container:
   the heading would pin to the wrapper and be lost the moment the page scrolls
   past. But these tables fit: `width: 100%` with wrapping cells means the
   measured content width equals the client width exactly, so the wrapper does
   not need `auto`.

   With no scroll container in between, the heading pins to the viewport at the
   chrome's own height and holds that line for the whole page scroll. Nothing
   is bounded, nothing traps the wheel, and the browser does it on the
   compositor: no clone, no measuring, no scroll handler to arrive late.

   Three details are load-bearing and each is silent when missed: `overflow:
   hidden` on `.data-table` would make the *table* the nearest scroll container
   (a box with no scroll range, where the heading never moves); a sticky cell
   is transparent by default and rows scroll through it; and `border-collapse:
   collapse` drops borders from sticky cells, so the rule is an inset shadow.

   No `overscroll-behavior` on purpose: that belongs on dialogs and sheets; on
   a table it stops scroll chaining and traps the wheel at the last row. */
/* Wherever the table fits its box, which tables.js makes true at every width
   and marks (`is-fitted`). Unmarked, with no script or a table capped to a
   height, the wrapper stays a scroll container from the start. */
.table-scroll.is-fitted:not([data-chart]) { overflow: visible; }
.table-scroll:is(.is-fitted, .is-pane):not([data-chart]) .data-table { overflow: visible; }
.table-scroll:is(.is-fitted, .is-pane):not([data-chart]) .data-table thead th {
  position: sticky;
  z-index: 2;
  background: var(--surface-2);
  box-shadow: inset 0 -1px 0 var(--line);
}
/* The offset must name whichever box the heading actually sticks to. */
.table-scroll.is-fitted:not([data-chart]) .data-table thead th {
  inset-block-start: calc(var(--site-header-height) + var(--page-nav-height));
}
/* A wrapper that IS the scroll container: its top edge, not the chrome's,
   a chrome-height offset here floats the heading down inside the box and
   lets rows scroll above it. */
.table-scroll.is-pane:not([data-chart]) .data-table thead th { inset-block-start: 0; }
.table-scroll.is-pane:not([data-chart]):focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.table-sort-link { display: inline-flex; align-items: center; gap: var(--space-2); color: inherit; white-space: nowrap; }
.table-sort-link:hover { color: var(--ink); text-decoration: none; }
/* The same control where the page sorts its own rows (tables.js): a button,
   so everything the button primitive gives it is taken back off. It looks and
   reads exactly as a list its server sorts: one control, wherever the sorting
   happens. */
.data-table thead button.table-sort-link { min-height: 0; margin: 0; padding: 0; border: 0; border-radius: 0; background: none; box-shadow: none; font: inherit; text-align: inherit; cursor: pointer; }
.sort-indicator { color: var(--muted-2); font-size: var(--text-2xs); }
[aria-sort="ascending"] .sort-indicator, [aria-sort="descending"] .sort-indicator { color: var(--accent); }
.data-table tbody tr:last-child > * { border-bottom: 0; }
/* A heading row that opens a group of rows (a tbody per group), so grouped
   rows share one table and one set of column widths. */
.data-table .row-group > th { padding-top: var(--space-6); background: var(--surface-2); color: var(--ink-2); font-size: var(--text-xs); font-weight: 650; text-align: left; }
/* Inside a card the card is the frame, so the table drops its own. */
.card .data-table { border: 0; border-radius: 0; box-shadow: none; }
/* A totals row. Ruled off from the rows above it and weighted, because it is a
   conclusion about them rather than another one of them, and in a table it
   is the only way to get a total to land under the column it totals. */
.data-table tfoot td {
  border-top: 1px solid var(--line);
  border-bottom: 0;
  background: var(--surface-2);
  font-weight: 650;
}
.data-table tbody tr:hover { background: var(--surface-2); }
.data-table .select-column { width: 38px; text-align: center; padding-inline: var(--space-4); }
/* Measurements, right-aligned on a fixed digit width so a column of them can
   be compared by eye. Left-aligned, "10m" and "1m" start together and end
   wherever the digits run out, which is the one thing a column of numbers
   exists to make easy. Applied to the header too, so the label sits over the
   figures rather than off to their left. */
.data-table .date-col { white-space: nowrap; }
.num-col { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* An index or label column that should take only what it needs. Auto layout
   otherwise hands a column of single digits the same share as one of
   sentences, and the row reads as a gap with a number on either side. */
.data-table .tight-col { width: 1%; white-space: nowrap; }
.data-table input[type=checkbox] { inline-size: 16px; block-size: 16px; accent-color: var(--accent); }

/* One row: the search field takes the room the filters leave, and one button
   applies both. Enter in the field submits the same form. */
.table-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-4); margin: 0 0 var(--space-6); padding: var(--surface-inset, var(--space-5)); }
.table-search { flex: 1 1 260px; min-width: 0; }
.table-filters { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-4); }
.table-filter-menu { position: relative; }
.table-filter-menu summary, .table-toggle, .table-sort { min-height: var(--control-size); display: inline-flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface-2); color: var(--ink-2); font-size: var(--text-sm); cursor: pointer; }
.table-filter-menu summary { list-style: none; }
.table-filter-menu summary::-webkit-details-marker { display: none; }
/* A disclosure's panel that would run off the right of the screen hangs from
   the disclosure's right edge instead; app.js decides when it opens. */
details > [data-edge="end"] { left: auto; right: 0; }
.table-filter-options { position: absolute; z-index: 20; top: calc(100% + 4px); left: 0; min-width: 220px; max-width: calc(100vw - 32px); max-height: 280px; overflow: auto; display: grid; gap: var(--space-1); padding: var(--space-4); background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-sm); box-shadow: var(--shadow); }
.table-filter-options label { display: flex; gap: var(--space-4); align-items: center; padding: var(--space-3); border-radius: 4px; font-size: var(--text-sm); }
.table-filter-options label:hover { background: var(--surface-2); }
.filter-count { min-width: 18px; padding: 1px var(--space-2); border-radius: 999px; background: var(--accent); color: var(--accent-ink); font-size: var(--text-2xs); text-align: center; }
.table-sort select { border: 0; padding: 0; background: transparent; color: inherit; }
.table-selection { position: sticky; top: 8px; z-index: 15; display: flex; align-items: center; gap: var(--space-4); margin: 0 0 var(--space-4); padding: var(--space-4) var(--space-5); background: var(--ink); color: var(--surface); border-radius: var(--radius-sm); box-shadow: var(--shadow); }
.table-selection[hidden] { display: none; }
.table-selection strong { margin-right: auto; font-size: var(--text-sm); }
main[aria-busy="true"] .table-scroll,
main[aria-busy="true"] [data-search-results] { opacity: .55; transition: opacity 100ms ease; }

/* Global search results */
.search-summary { display: flex; align-items: baseline; gap: var(--space-7); flex-wrap: wrap; margin-bottom: var(--space-7); }
.search-summary p { margin: 0; }
.search-scopes { display: flex; gap: var(--space-3); flex-wrap: wrap; }
.scope-chip { display: inline-flex; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--space-5); border: 1px solid var(--line); border-radius: 999px; background: var(--surface-2); color: var(--ink-2); font-size: var(--text-xs); text-decoration: none; }
a.scope-chip:hover { color: var(--ink); border-color: var(--muted-2); text-decoration: none; }
.scope-chip .count { font-weight: 600; }
.search-group { margin-bottom: var(--space-7); }
.search-hits { list-style: none; margin: 0; padding: 0; }
.search-hit { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-8); padding: var(--space-5) 0; border-top: 1px solid var(--line); }
.search-hit:first-child { border-top: 0; }
.search-hit-main { min-width: 0; }
.search-hit-title { margin: 0; display: flex; align-items: baseline; gap: var(--space-4); flex-wrap: wrap; }
.search-badge { font-size: var(--text-2xs); color: var(--muted-2); background: var(--surface-2); border: 1px solid var(--line); border-radius: 4px; padding: 1px var(--space-2); }
.search-snippet { margin: var(--space-1) 0 0; font-size: var(--text-sm); color: var(--muted); overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.search-snippet mark { background: color-mix(in srgb, var(--accent) 20%, transparent); color: inherit; padding: 0 1px; border-radius: 3px; font-weight: 600; }
.search-hit .row-meta { flex-shrink: 0; }
.row-actions a { font-size: var(--text-sm); }
.actions-col { width: 1%; text-align: right; white-space: nowrap; }

/* A pill in a table fits the cell's line, so a row holding one is no taller
   than a row without. */
.data-table td .pill { vertical-align: top; line-height: 1.4; }
/* A time is one phrase; broken over two lines it doubles its row. */
.data-table td time { white-space: nowrap; }

/* Columns size to what they hold, and a table too wide for its box scrolls in
   it. An identifier in a cell never breaks across lines ("hq-proxy" is never
   "hq-" over "proxy") and never takes more than --identifier-max: past it, it
   is cut, and the row expander shows it whole. The cap is a length, not a
   percentage, because a percentage counts for nothing while the columns are
   sized, and one long URL would then take the width every other column needs. */
.data-table td code { white-space: nowrap; overflow-wrap: normal; display: inline-block; max-width: var(--identifier-max); overflow: hidden; text-overflow: ellipsis; vertical-align: bottom; }
/* A square for a glyph, and as wide as its word when it holds one. */
.icon-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 28px;
  height: 28px;
  padding-inline: var(--space-3);
  white-space: nowrap;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--accent-2);
  font-weight: 650;
  line-height: 1;
  box-shadow: var(--shadow-sm);
}
.icon-link:hover {
  color: var(--accent);
  background: var(--surface-2);
  border-color: var(--line-strong);
  text-decoration: none;
}

/* ── Allocation ─────────────────────────────────────────────────── */
/* Parts of one whole, as a single row rather than a chart. A pie needs a
   legend to be read at all and a stacked bar chart brings an axis with it;
   this answers "what is this made of" at a glance and takes one line of a
   card doing it. Segments carry `.chart-series-N`, which already sets
   `background` as well as `fill`, so a proportion and a plotted series of the
   same thing are the same colour without a second palette to keep in step. */
.allocation {
  display: flex;
  block-size: 10px;
  border-radius: 999px;
  overflow: hidden;
  background: var(--bg-tint);
  margin: var(--space-7) 0 var(--space-6);
}
/* Width arrives as a value, not as presentation: same contract as the chart
   axis's `--at`, so the template supplies a share and this file decides what
   a share looks like. */
.allocation > span { display: block; block-size: 100%; inline-size: var(--part, 0%); min-inline-size: 2px; }
/* A legend that is also the navigation. The parts of a total are exactly the
   things you want to open, so making the key clickable removes a row of tiles
   that said the same words a second time. */
.allocation-legend { display: flex; flex-wrap: wrap; gap: var(--space-4) var(--space-10); margin: 0; font-size: var(--text-sm); }
/* Stacked, for a narrow column. Wrapped inline the entries break at arbitrary
   points and the amounts stop lining up, which is most of what a legend of
   numbers is for. */
.allocation-legend.stacked { flex-direction: column; gap: var(--space-4); }
.allocation-legend.stacked > * { justify-content: flex-start; }
.allocation-legend.stacked b { min-inline-size: 5.5em; }
.allocation-legend > * { display: inline-flex; align-items: baseline; gap: var(--space-4); color: inherit; }
.allocation-legend a:hover { color: var(--accent); }
.allocation-legend i { inline-size: 9px; block-size: 9px; border-radius: 2px; flex: none; align-self: center; }
.allocation-legend b { font-weight: 650; font-feature-settings: "tnum"; }
.allocation-legend span { color: var(--muted); }

/* Supporting figures under a hero, where a KPI tile would be a bordered card
   inside a card. Label under value, so three of them read as one row of facts
   rather than as more legend entries. */
.figure-stats {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6) var(--space-12);
  margin: var(--space-8) 0 0;
  padding-top: var(--space-7);
  border-top: 1px solid var(--line-2);
}
.figure-stats > span { display: inline-flex; flex-direction: column; gap: var(--space-1); }
.figure-stats b { font-size: var(--text-lg); font-weight: 650; font-feature-settings: "tnum"; }
.figure-stats i {
  font-style: normal; color: var(--muted); font-size: var(--text-3xs);
  text-transform: uppercase; letter-spacing: .055em; font-weight: 600;
}

/* One number a page is built around, where a KPI tile would make it one of
   four equals. Tabular so it does not reflow as it changes. */
.figure-hero {
  font-size: clamp(32px, 4.4vw, 44px);
  font-weight: 660;
  letter-spacing: -.022em;
  line-height: 1.05;
  font-feature-settings: "tnum";
  margin: 0;
}
.figure-hero-note { margin: var(--space-3) 0 0; font-size: var(--text-sm); color: var(--muted); font-feature-settings: "tnum"; }

/* A table in a narrow box (a phone, or half a page beside a chart: tables.js
   marks it by its box's width, not the screen's). Its rows are something to
   put a thumb on, so they get the room a control in them needs; an identifier
   is cut sooner, whole again when the row is opened; and within a column that
   stays, what is marked `narrow-more` waits in the line beneath the row. */
.data-table[data-narrow] > tbody { --row-pad: var(--space-6); }
.data-table[data-narrow] > tbody > tr:not(.row-detail) .narrow-more { display: none; }
.table-scroll > .data-table:is([data-narrow], [data-snug]) { --identifier-max: 22ch; }
/* A column held to a width that suits a wide table gives it back here, so
   the room goes to the column that names the row. */
.data-table[data-narrow] .tight-col > * { min-width: 0; }
/* What a page says about a table's columns, for tables.js to read: which stay
   whatever happens, and which leave first. Named here because this sheet is
   the list of classes a template may use (an extension's templates are held
   to it); they carry no style of their own. */
.data-table :is(.key-col, .optional-col) { }
/* A table fits its box at every width, by one mechanism (tables.js): it
   takes the box's width and wraps, and where that would cramp a column it
   gives up columns, named here, until the rest are comfortable. Never the
   column that names the row or one marked `key-col`; one marked
   `optional-col` goes first, then from the far end. Whatever leaves is in the
   row's toggle. No screen width is asked anywhere. */
.data-table[data-drop~="2"] > * > tr > :nth-child(2):not([colspan]),
.data-table[data-drop~="3"] > * > tr > :nth-child(3):not([colspan]),
.data-table[data-drop~="4"] > * > tr > :nth-child(4):not([colspan]),
.data-table[data-drop~="5"] > * > tr > :nth-child(5):not([colspan]),
.data-table[data-drop~="6"] > * > tr > :nth-child(6):not([colspan]),
.data-table[data-drop~="7"] > * > tr > :nth-child(7):not([colspan]),
.data-table[data-drop~="8"] > * > tr > :nth-child(8):not([colspan]),
.data-table[data-drop~="9"] > * > tr > :nth-child(9):not([colspan]),
.data-table[data-drop~="10"] > * > tr > :nth-child(10):not([colspan]),
.data-table[data-drop~="11"] > * > tr > :nth-child(11):not([colspan]),
.data-table[data-drop~="12"] > * > tr > :nth-child(12):not([colspan]) { display: none; }
/* Each column's floor: tables.js says how narrow a column may get (its own
   one-line width, or a few words' worth, whichever is less; more for the
   column that names the row), and this holds it there without capping how
   wide it may grow. With every floor held, a table that is still wider than
   its box is one that has to give up a column. */
.data-table > thead > tr > th::after { content: ""; display: block; width: var(--column-floor, 0); height: 0; }
/* Nothing left to give up and still too wide (a row of identifiers on a
   phone): the columns share the box evenly and a value is cut at its
   column's edge, whole again when the row is opened. */
.table-scroll > .data-table[data-wrap="tight"] { table-layout: fixed; }
.table-scroll > .data-table[data-wrap="tight"] .tight-col { width: auto; white-space: normal; }
.table-scroll > .data-table[data-wrap="tight"] td .code-list { display: flex; }
.table-scroll > .data-table[data-wrap="tight"] td code { min-width: 0; max-width: 100%; }
.table-scroll > .data-table[data-wrap="tight"] > thead > tr > th::after { content: none; }
/* While tables.js asks what each column would take on one line. */
.data-table[data-measure] > * > tr > :is(td, th),
.data-table[data-measure] > * > tr > :is(td, th) * { white-space: nowrap !important; }
/* A finger is not a cursor. Where the pointer is coarse, a control that is
   only as tall as its line of text (a sort heading, a "view all" link, a
   disclosure, a tick box) is given room around it to be hit, without moving
   anything: the room is the control's own, drawn over the space beside it. */
@media (pointer: coarse) {
  :is(.table-sort-link, .subtle-link, .pagination a, .month-mark, .month-span),
  main summary:not(.glance-panel-head) { position: relative; }
  :is(.table-sort-link, .subtle-link, .pagination a, .month-mark, .month-span)::after {
    content: ""; position: absolute; inset: calc(-1 * var(--space-5)) calc(-1 * var(--space-3));
  }
  /* A disclosure's whole line is its target, above and below. */
  main summary:not(.glance-panel-head) { padding-block: var(--space-3); margin-block: calc(-1 * var(--space-3)); }
  input[type=checkbox], input[type=radio] { inline-size: 20px; block-size: 20px; }
}
/* The toggle's gutter: the cell that names a row keeps room at its start in
   every row of a table where any row opens, and in the heading over it, so
   the names share one edge. The toggle stands in that room. */
.data-table .row-gutter { position: relative; padding-left: calc(var(--space-6) + 1.5rem); }
.data-table .row-gutter > [data-row-toggle] { position: absolute; inset-inline-start: var(--space-3); inset-block-start: calc(var(--row-pad) - var(--space-1)); }
/* What an open row was holding back, in a line of its own beneath it: each
   part under its column's name. Beneath the row and the table's full width, so
   opening one moves nothing else: the row's own cells stay as they were, and
   tables.js holds the columns at their widths while any row is open. */
.row-columns { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--space-3) var(--space-6); margin: 0; }
.row-columns dt { color: var(--muted); font-size: var(--text-2xs); }
.row-columns dd { min-width: 0; margin: 0; overflow-wrap: anywhere; }
.data-table tr.is-expanded:has(+ .row-detail) > * { border-bottom: 0; }
.data-table .row-detail > td { padding-top: 0; }
.data-table .row-detail:hover { background: none; }
/* With its columns held, a value the open row no longer cuts short has to
   break inside its column rather than run under the next. */
.data-table tr.is-expanded > :is(td, th) { overflow-wrap: anywhere; }

/* ── Pills / tags ───────────────────────────────────────────────── */

/* Reads the tone contract, so every variant below is a choice of tone rather
   than three colours. Falls back to neutral, so an unrecognised status renders
   as a plain pill instead of an unstyled one. */
.pill {
  display: inline-flex; align-items: center; gap: var(--space-2);
  padding: 1px var(--space-4); border-radius: 999px; font-size: var(--text-2xs); font-weight: 500;
  background: var(--tone-bg, var(--tone-neutral-bg));
  color: var(--tone-ink, var(--tone-neutral-ink));
  border: 1px solid var(--tone-line, var(--tone-neutral-line));
  letter-spacing: .01em;
  /* A pill is one short state; it never wraps. */
  white-space: nowrap;
}

.pill-unread { font-weight: 650; }

/* ── Contact submissions ────────────────────────────────────────── */

.status-tabs { display: flex; flex-wrap: wrap; gap: var(--space-3); margin: 0 0 var(--space-6); }
.status-tab {
  display: inline-flex; align-items: center; gap: var(--space-3);
  padding: var(--space-2) var(--space-6); border-radius: 999px; font-size: var(--text-sm);
  border: 1px solid var(--line); background: var(--surface); color: var(--ink-2);
}
.status-tab:hover { background: var(--surface-2); text-decoration: none; }
.status-tab.is-current { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
.status-tab .count { font-size: var(--text-2xs); opacity: .75; font-variant-numeric: tabular-nums; }
.row-unread td { background: var(--accent-tint); }
.row-unread .msg-preview a, .row-unread td:first-child a { font-weight: 650; }
.msg-preview { max-width: 420px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row-actions form { display: inline-block; margin: 0; }
.inline-form { display: inline; margin: 0; }
.row-actions button.icon-link { cursor: pointer; font-size: var(--text-sm); }
.icon-link.danger-link { color: var(--danger); }
.quick-actions { display: flex; flex-wrap: wrap; gap: var(--space-4); margin: 0 0 var(--space-10); }
.quick-actions form { margin: 0; }

/* Category color dots: paired with project rows */
.cat-dot {
  width: 7px; height: 7px; border-radius: 2px; display: inline-block; flex-shrink: 0;
  background: var(--muted-2);
}
.cat-dot.cat-homelab { background: var(--cat-homelab); }
.cat-dot.cat-automation { background: var(--cat-automation); }
.cat-dot.cat-wordpress_security { background: var(--cat-wordpress-security); }
.cat-dot.cat-cloudflare { background: var(--cat-cloudflare); }
.cat-dot.cat-dns_email_security { background: var(--cat-dns-email-security); }
.cat-dot.cat-vps { background: var(--cat-vps); }
.cat-dot.cat-tailscale { background: var(--cat-tailscale); }
.cat-dot.cat-adguard { background: var(--cat-adguard); }
.cat-dot.cat-networking { background: var(--cat-networking); }
.cat-dot.cat-tls_certificates { background: var(--cat-tls-certificates); }
.cat-dot.cat-cybersecurity_labs { background: var(--cat-cybersecurity-labs); }
.cat-dot.cat-other { background: var(--cat-other); }

/* ── KPI grid ───────────────────────────────────────────────────── */

/* Wrapping row, not a track grid. The 1px hairlines are the container's own
   background showing through the gaps, so any track a grid left empty would
   paint as a solid grey block. A flex row has no cells to leave empty:
   whatever wraps grows to fill its row instead, at every width. */
.kpi-grid { display: flex; flex-wrap: wrap; margin: 0 0 var(--stack-gap); }
.kpi { flex: 1 1 150px; min-width: 0; padding: var(--space-7) var(--space-8); display: flex; flex-direction: column; gap: var(--space-2); }
/* A linked KPI is a whole tile target, so it should not also wear inline-link
   underlines on its label and value. */
a.kpi, a.kpi:hover { text-decoration: none; color: inherit; }
a.kpi:hover { background: var(--surface-2); }
.kpi .label { font-size: var(--text-2xs); text-transform: uppercase; letter-spacing: .06em; color: var(--muted); font-weight: 600; }
/* A mark for what the figure counts: quieter than the label it sits before.
   The label is a row, so a label too long for its column wraps beside its
   mark, never underneath it. */
.kpi .label:has(> .icon) { display: flex; align-items: flex-start; gap: var(--space-3); }
.kpi .label .icon { width: 14px; height: 14px; margin-top: 1px; color: var(--muted-2); }
/* The supporting figures of an overview stand three to a row. In a card too
   narrow for a mark and its label to fit one of those columns, the mark goes:
   a label running into its neighbour costs more than the mark is worth. */
.kpi-grid.is-featured > :not(:first-child) .label .icon { display: none; }
@container (min-width: 325px) {
  .kpi-grid.is-featured > :not(:first-child) .label .icon { display: inline; }
}
.kpi .value { font-size: var(--text-4xl); font-weight: 650; letter-spacing: -.018em; line-height: 1.1; font-feature-settings: "tnum"; }
.kpi.is-zero .value { color: var(--muted-2); font-weight: 500; }
.kpi .muted { font-size: var(--text-2xs); }
/* Nested in a card, the grid's own bottom margin becomes dead space inside the
   card's padding rather than separation between sections. */
.card > .kpi-grid:last-child, .card > .insight-grid:last-child { margin-bottom: 0; }

/* The tooltip: chart marks, and anything else carrying data-tip. Fixed to the viewport and inert to the pointer, so it can
   never sit between the cursor and the mark it is describing. */
.chart-tip {
  position: fixed;
  z-index: 60;
  pointer-events: none;
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-sm);
  background: var(--ink);
  color: var(--accent-ink);
  font-size: var(--text-xs);
  font-weight: 500;
  line-height: 1.45;
  /* One line for a chart mark; a line per fact where a tip has several. */
  white-space: pre;
  box-shadow: var(--shadow);
  font-feature-settings: "tnum";
}
.bar-chart:hover rect:not(:hover) { opacity: .78; }

/* ── Insight grid ───────────────────────────────────────────────────
   Decision-support cards: a reading, its interpretation, the next action.
   Extensions render against this rather than defining their own card class,
   an undefined class is silent at runtime and simply renders unstyled. */

.insight-grid {
  /* One band, like the KPI band: one frame, and each reading a cell of it
     rather than a floating card with its own margin and shadow. */
  display: grid;
  /* A narrower track ceiling lets four insights share one row at desktop
     widths instead of stranding the fourth alone beside two empty columns. */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, clamp(240px, 22%, 290px)), 1fr));
  margin: 0 0 var(--stack-gap);
}
.insight-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-7) var(--space-8);
  min-width: 0;
}
.insight-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-5); flex-wrap: wrap; }
.insight-value { font-size: var(--text-xl); font-weight: 650; letter-spacing: -.015em; font-feature-settings: "tnum"; white-space: nowrap; }
.insight-title { margin: 0; font-size: var(--text-2xs); font-weight: 650; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
.insight-title a { color: inherit; }
.insight-body { margin: 0; font-size: var(--text-sm); line-height: 1.5; color: var(--ink-2); }
/* Sits with the body it follows, not pinned to the card's floor: pinned, it
   leaves a hole in the middle of every card whose body is shorter than its
   tallest neighbour. Cards still share a height; the slack goes underneath. */
.insight-action { margin: 0; font-size: var(--text-sm); line-height: 1.5; color: var(--muted); }


/* ── Attention queue ────────────────────────────────────────────────
   Cross-surface list of things awaiting a decision. A row, not a card: these
   are scanned top-down in severity order, and cards would imply the grid
   position carries meaning when only the order does. */

.attention-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
/* The decision, then (where the reader sorts their own queue) the one thing
   they can do to the row itself. The second wraps under the first when the
   row is too narrow for both, at the row's own width. */
.attention-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--space-2) var(--space-7);
  padding: var(--space-5) 0;
  border-top: 1px solid var(--line-2);
}
.attention-body { flex: 1 1 24rem; }
/* What can be pressed, then the pages that help look into it, then the
   reader's own dismiss: beside what the card says, never a row of their own
   while the row is wide enough for both. */
.attention-end { flex: 0 1 auto; margin-left: auto; display: flex; flex-direction: column; align-items: flex-end; gap: var(--space-3); }
.attention-end > .attention-actions { margin: 0; justify-content: flex-end; }
.attention-end > .attention-links { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--space-2) var(--space-4); margin: 0; padding: 0; list-style: none; font-size: var(--text-xs); }
.attention-links .icon-link { min-width: 0; width: 1.6rem; height: 1.6rem; padding: 0; border: 0; background: none; box-shadow: none; color: var(--muted); }
.attention-links .icon-link:is(:hover, :focus-visible) { color: var(--ink); background: var(--surface-2); }
.attention-row[data-dismissed] > .attention-end > :not([data-triage]) { display: none; }
.attention-end form { margin: 0; }
.attention-row:first-child { border-top: 0; padding-top: var(--space-2); }

/* What is wrong, on one line with how bad it is and what it is about: the
   line a reader scans the queue by. */
.attention-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2) var(--space-5); }
.attention-meta { display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2) var(--space-5); color: var(--muted); font-size: var(--text-xs); }
.attention-meta > a { overflow-wrap: anywhere; }
.attention-title { min-width: 0; max-width: 100%; font-size: var(--text-md); font-weight: 650; color: var(--ink); letter-spacing: -.005em; overflow-wrap: anywhere; }
/* The card's face under its title: what it means, then what to do, then the
   reason it cannot be done from here, then what can be pressed. */
.attention-detail { max-width: var(--measure); margin: var(--space-3) 0 0; font-size: var(--text-sm); line-height: 1.65; color: var(--ink-2); overflow-wrap: anywhere; }
.attention-advice { max-width: var(--measure); margin: var(--space-3) 0 0; font-size: var(--text-sm); color: var(--muted); overflow-wrap: anywhere; }
.attention-reason { max-width: var(--measure); margin: var(--space-3) 0 0; font-size: var(--text-xs); line-height: 1.55; color: var(--muted); overflow-wrap: anywhere; }
.attention-actions { display: flex; flex-wrap: wrap; gap: var(--space-3); margin-top: var(--space-4); }
.attention-actions form { margin: 0; }
/* What raised a card, opened from its urgency: a small panel over the page. */
.modifier-held [data-origin] { cursor: pointer; text-decoration: underline; text-underline-offset: 2px; outline: 1px solid var(--tone-line, var(--line)); outline-offset: 1px; }
/* Above the page, so a surface of its own again whatever card drew it. */
.origin-panel {
  --surface-border: 1px solid var(--line); --surface-radius: var(--radius); --surface-bg: var(--surface);
  --surface-shadow: var(--shadow-sm); --surface-inset: var(--space-6) var(--space-8);
  max-width: min(36rem, calc(100vw - 2rem)); margin: auto; padding: 0; border: 0; background: transparent; overflow: visible;
}
.origin-panel::backdrop { background: var(--scrim); }
/* What was read that shows it: each label once, its values beside it. */
.attention-facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--space-1) var(--space-6); margin: var(--space-3) 0 0; font-size: var(--text-xs); color: var(--ink-2); }
.attention-facts > div { display: contents; }
.attention-facts dt { color: var(--muted); }
.attention-facts dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
/* The queue under its domains: a heading per domain and its count. */
.queue-group + .queue-group { margin-top: var(--space-8); }
.queue-group > h3 { display: flex; align-items: baseline; gap: var(--space-4); margin: 0; padding-bottom: var(--space-3); border-bottom: 1px solid var(--line); color: var(--muted); font-size: var(--text-xs); font-weight: 650; letter-spacing: .05em; text-transform: uppercase; }
.action-items-aside[open] > summary { margin-bottom: var(--space-6); }
/* A domain's entries: an item, or a family of them folded. Each after the
   first is ruled off from the one before, whichever it is. */
.queue-entries > * + * { border-top: 1px solid var(--line-2); }
.queue-entries .attention-row:first-child { padding-top: var(--space-5); }
/* A family, closed: how bad, what kind, how many, and the one thing that can
   be done to all of them. Open, its rows are set in from the fold's mark. */
.queue-family > summary { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-5); padding: var(--space-5) 0; cursor: pointer; font-size: var(--text-md); }
/* The fold's mark follows the count, so a family's pill starts where a
   single row's does; the family's own control ends the line. */
.queue-family > summary::before { order: 1; }
.queue-family > summary .queue-family-end { order: 2; margin-left: auto; }
.queue-family > .attention-list { padding-left: var(--space-8); border-top: 1px solid var(--line-2); }
/* A row the reader has just dismissed, until the page is next drawn: one quiet
   line in its place, with the way back. */
.attention-row[data-dismissed] > .attention-body { display: none; }
.attention-row[data-dismissed] { align-items: center; color: var(--muted); font-size: var(--text-sm); }
.attention-row[data-dismissed]::before { content: attr(data-dismissed); flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.attention-clear { margin: var(--space-2) 0 0; color: var(--muted); font-size: var(--text-sm); }
.action-items-card { padding-top: var(--space-5); }
/* The open problems about the thing a page shows: the queue's cards under one count. */
.problems-here { padding-block: var(--space-5); margin-bottom: var(--space-6); border-left: 3px solid var(--tone-ink); }
.problems-here .attention-row { padding-block: 0; }
.problems-here .attention-row + .attention-row { padding-top: var(--space-5); margin-top: var(--space-5); }
.problems-here .attention-detail { margin-top: var(--space-2); }
/* A problem that is not urgent: one unframed line, its button beside it. */
.problem-lines { list-style: none; margin: 0 0 var(--space-4); padding: 0; display: grid; gap: var(--space-3); }
.problem-line { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2) var(--space-4); }
.problem-line > p { flex: 1 1 24rem; margin: 0; font-size: var(--text-sm); color: var(--ink-2); }
.problem-line > .pill { flex: none; }
.problem-line > p > strong { color: var(--ink); }
.problem-line > .attention-actions { margin: 0 0 0 auto; }
.problem-line > :is(.resolution-workflow, .resolution-fold) { flex-basis: 100%; margin-top: 0; }
/* The short queue on the dashboard, under the greeting. */
.dash-needs { margin-bottom: var(--stack-gap); padding-top: var(--space-5); }

/* A card's steps (partials/_resolution_workflow.html): one stands alone,
   several are numbered, and long ones fold under "Steps". */
/* Each step's label beside its command, the commands in one column; a label
   takes at most two fifths of the row and wraps inside it. A step's number is
   its label's, since a list laid out as a grid draws no markers. */
.resolution-workflow {
  margin: var(--space-3) 0 0; padding: 0; list-style: none;
  display: grid; grid-template-columns: fit-content(40%) minmax(0, 1fr); gap: var(--space-2) var(--space-6);
  font-size: var(--text-sm); color: var(--ink-2); counter-reset: step;
}
.resolution-workflow li { min-width: 0; display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; align-items: baseline; row-gap: var(--space-2); counter-increment: step; }
.resolution-workflow li > :not(strong, .command),
.resolution-workflow li > .command:first-child { grid-column: 1 / -1; }
ol.resolution-workflow > li > strong::before { content: counter(step) ". "; }
.resolution-workflow li > strong { color: var(--ink); font-weight: 650; }
.resolution-workflow li > p { max-width: var(--measure); margin: 0; line-height: 1.65; overflow-wrap: anywhere; }
.resolution-fold { margin-top: var(--space-3); }
.resolution-fold > summary { width: fit-content; max-width: 100%; color: var(--ink-2); font-size: var(--text-sm); cursor: pointer; }

/* A viewport query, not a container one: inside a size container Chrome
   restyles every element once the container is measured and runs their
   transitions from nothing, so each button in the queue would fade in on load. */
@media (max-width: 720px) {
  /* The decision keeps the width; bookkeeping follows it in reading order. */
}

.quick-actions.vertical { display: grid; grid-template-columns: 1fr; }

/* ── Section cards / two-col ────────────────────────────────────── */

/* Side-by-side panels share a top and a bottom edge: a ragged lower edge reads
   as a rendering fault rather than as a layout. Anything short simply has room
   underneath. */
/* Two columns at most: a track is never narrower than half the row, so four
   cards pair up 2+2 instead of leaving one orphaned, and below two 320px
   tracks it is one column with no breakpoint. */
.two-col { container: pair / inline-size; display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, max(320px, (100% - var(--layout-gap)) / 2)), 1fr)); gap: var(--layout-gap); align-items: stretch; margin: 0 0 var(--stack-gap); }

/* A main panel with a companion. Unlike two equal siblings, the companion is
   sized to its own contents: stretching a three-row list to match a long one
   just produces a tall empty box, which reads worse than a short card that is
   honestly short. */
.split { margin: 0 0 var(--stack-gap); }
/* Main content and a narrower companion. They share a row while both keep a
   readable width and wrap to a column when they cannot, at whatever width the
   page or card gives them. One layout for every such pairing: no breakpoint,
   no per-page columns. */
:where(.split, .upgrade-detail) {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--layout-gap);
}
:where(.split, .upgrade-detail) > * { flex: 1 1 16rem; min-width: 0; }
:where(.split, .upgrade-detail) > :first-child { flex: 2 1 28rem; }
/* min-width:0 so a child that scrolls its own overflow (a wide table in its
   scroll wrapper) cannot instead force the column open and push the page
   sideways. A grid item defaults to min-width:auto, which means "as wide as my
   content", and the scroll wrapper never gets the chance to do its job. */
.split > * { margin: 0; min-width: 0; }
/* The wide column second. A drawing sized to itself beside a table of six
   columns wants the narrow half, and reading order puts the drawing first. */
.two-col > * { min-width: 0; }
/* Cards sized to their own content rather than to the tallest in the row.
   Stretching is right when both sides are lists that read as one block, and
   wrong when one is a short table beside a tall drawing: the short one grows a
   band of empty card under its last row. */
.two-col.align-top { align-items: start; }
/* The sections at the end of a thing's page (what else HQ holds, how it
   relates, what names it) are mostly short: each is a card holding its own
   heading, two to a row at one height where both fit, and one that is wide
   (a table of four columns or more, a folded section, a fold of raw
   readings) takes the whole row. */
.section-flow { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 34rem), 1fr)); gap: var(--layout-gap); align-items: stretch; margin: 0 0 var(--stack-gap); }
/* Short sections collect at the end, so they pair with each other rather than
   each sitting beside the gap a wide one leaves. */
.section-flow > * { min-width: 0; margin: 0; order: 1; }
.section-flow > :not(section, .page-section),
.section-flow > :is(section, .page-section):has(table > thead th:nth-child(4)),
.section-flow > :is(section, .page-section):has(> details.section-fold) { grid-column: 1 / -1; order: 0; }
/* Sections stacked in one column of a pair, at the pair's own gap. */
.stack { display: grid; gap: var(--layout-gap); align-content: start; min-width: 0; }
/* A paired row whose two cards end at different points reads as broken.
   `align-top` stops a short card stretching, which is exactly what makes the
   pair ragged; instead the cards stretch and the shorter one gets somewhere to
   put the difference. A card in a stretched pair is a column, and its drawing
   sits in the middle of whatever height the row ends up being rather than
   leaving the slack in one lump at the bottom. */
.two-col:not(.align-top) > .card,
.two-col:not(.align-top) > .chart-card {
  display: flex;
  flex-direction: column;
}
.two-col:not(.align-top) > .chart-card > .chart-scroll,
.two-col:not(.align-top) > .chart-card > .pie-row { margin-block: auto; }
.two-col:not(.align-top) > .card > .table-scroll:only-of-type { margin-bottom: auto; }
/* Panels that flow rather than pair. Assigning cards to a fixed left and right
   column means their heights are whatever the data made them, so one column
   ends early and the page carries the difference as a hole. Columns balance
   themselves; a card just has to not be split across the break. */
/* At most two columns, each at least this wide: the browser drops to one when
   the space is not there, so no breakpoint has to say when. */
.card-columns { columns: 22rem 2; column-gap: var(--layout-gap); margin: 0 0 var(--stack-gap); }
.card-columns > * { break-inside: avoid; margin: 0 0 var(--layout-gap); display: block; width: 100%; }
.dashboard-patterns { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 360px), 1fr)); gap: var(--layout-gap); margin-bottom: var(--stack-gap); }
/* Four cards are two rows of two, never three and one left alone. */
.dashboard-patterns:has(> :nth-child(4):last-child) { grid-template-columns: repeat(auto-fit, minmax(min(100%, 30rem), 1fr)); }
/* Overview cards share the available width, so three contributing domains
   stay in one scan on a desktop. Narrow containers naturally stack them. */
/* As many overviews to a row as fit, sharing it evenly, and a row's cards the
   same height. Flex rather than a grid of fixed tracks, so the last row's
   cards share the whole row too: three domains on a window that fits two put
   the third across the full width, not in half of it beside nothing. */
.dashboard-highlights { display: flex; flex-wrap: wrap; gap: var(--layout-gap); margin-bottom: var(--stack-gap); }
.dashboard-highlights > .highlight-card { flex: 1 1 320px; }
.highlight-card.card { display: flex; flex-direction: column; min-width: 0; padding: var(--space-8); container: featured / inline-size; }
.highlight-card > .kpi-grid { flex: 1; }
/* A card holding a band of stats is the container its columns are measured
   against, so the same count-aware columns hold in any card, not only the
   dashboard's. */
.card:has(> .kpi-grid) { container-type: inline-size; }
.card > .kpi-grid {
  --kpi-columns: 3;
}
.highlight-card .section-head { margin-bottom: var(--space-8); padding-bottom: var(--space-6); border-bottom: 1px solid var(--line); }
.highlight-card .kpi { flex: 1 1 calc(100% / var(--kpi-columns) - 1px); padding: var(--space-5); }
/* Columns follow the cell count and the card's width (110px a cell), so rows
   stay even: four cells are 4 or 2+2, six are 6 or 3+3, never 3+1. */
.card > .kpi-grid:has(> :only-child) { --kpi-columns: 1; }
.card > .kpi-grid:has(> :nth-child(2):last-child),
.card > .kpi-grid:has(> :nth-child(4):last-child),
.card > .kpi-grid:has(> :nth-child(6):last-child) { --kpi-columns: 2; }
/* Two rows of three wait for a card wide enough to fit them; in a phone's card
   three columns are cramped. */
@container (min-width: 360px) {
  .card > .kpi-grid:has(> :nth-child(6):last-child) { --kpi-columns: 3; }
}
@container (min-width: 440px) {
  .card > .kpi-grid:has(> :nth-child(4):last-child),
  .card > .kpi-grid:has(> :nth-child(7)) { --kpi-columns: 4; }
}
@container (min-width: 550px) {
  .card > .kpi-grid:has(> :nth-child(5):last-child) { --kpi-columns: 5; }
}
@container (min-width: 660px) {
  .card > .kpi-grid:has(> :nth-child(6):last-child) { --kpi-columns: 6; }
}
.dashboard-counts { display: flex; flex-wrap: wrap; gap: var(--space-5) var(--space-9); padding: var(--space-4) 0; margin-bottom: var(--stack-gap); border-block: 1px solid var(--line); }
.dashboard-counts .kpi { display: flex; flex: 0 1 auto; flex-direction: row; flex-wrap: wrap; align-items: baseline; gap: var(--space-3); padding: 0; background: none; }
.dashboard-counts .kpi .value { font-size: var(--text-md); order: -1; }
.dashboard-counts .kpi .label { font-size: var(--text-xs); text-transform: none; letter-spacing: 0; }
.dashboard-counts .kpi .muted { font-size: var(--text-2xs); }
.dashboard-patterns > .card { display: flex; flex-direction: column; min-width: 0; margin: 0; padding: var(--space-6); }
.dashboard-patterns .chart-card figcaption p { min-height: 0; }
/* A pair ends together, so a chart's data opens inside its card rather than
   growing it and stretching the card beside it: a pane of its own height. */
.dashboard-patterns .chart-data > .table-scroll { max-height: 12rem; overflow: auto; }
.dashboard-patterns .chart-data .data-table thead th { position: sticky; inset-block-start: 0; z-index: 1; background: var(--surface-2); }
.dashboard-patterns .chart-legend { margin-top: var(--space-3); }
.dashboard-patterns .calendar-day { height: 24px; padding: var(--space-1); border-radius: 5px; vertical-align: middle; }
.dashboard-patterns .calendar-date { display: inline-block; }
.dashboard-patterns .calendar-marks { display: inline-flex; margin: 0 0 0 var(--space-1); min-height: 6px; }
.dashboard-patterns .calendar-dot { width: 5px; height: 5px; }
.dashboard-patterns .calendar-legend { margin-block: var(--space-3); }
.two-col > section, .two-col > div:not(.col-stack), .section-flow > :is(section, .page-section), .card { padding: var(--surface-inset, var(--space-8) var(--space-9)); }
/* Inside a layout container the gap is the separation, so a section's own
   stacking margin is dead space. Declared after the section rules above so it
   wins on source order at equal specificity. */
.two-col > *,
.dashboard-patterns > *,
.search-results > * { margin-bottom: 0; }

/* What sits beside the heading takes the next line when the two do not fit. */
.section-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--space-3) var(--space-6); margin-bottom: var(--space-6); }
.section-head h2 { margin: 0; flex: 1; }
.section-head .count { font-size: var(--text-2xs); color: var(--muted-2); font-weight: 500; }

/* Rich plugins emit data; HQ owns how a domain workspace is composed. Keeping
   this layout in the host means every present and future private domain gets
   the same rhythm without shipping its own near-copy of the shell. */
.domain-stack { display: grid; gap: calc(var(--stack-gap) * 1.5); }
.domain-stack > * { min-width: 0; }
.domain-stack > section { display: grid; gap: var(--space-6); }
/* The section's gap spaces its parts; their own stack margins would double it. */
.domain-stack > section > * { margin-block: 0; }
.domain-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--layout-gap);
}
.domain-head h2 {
  margin: 0;
  color: var(--ink);
  font-size: var(--text-2xl);
  font-weight: 650;
  letter-spacing: -.01em;
  text-transform: none;
}
.domain-head-copy { min-width: 0; }
.domain-head-copy .eyebrow { display: block; margin-bottom: var(--space-2); }
.domain-head-copy p { margin: var(--space-2) 0 0; max-width: 680px; }
.domain-head > a { flex: 0 0 auto; }

/* A domain's featured analysis: the chart and the calendar beside it. Two
   explicit columns rather than .two-col's auto-fit, so the pair stays paired.
   A chart alone in this row either sits in a card twice its width or scales
   its viewBox across the row, so the other half holds something worth reading. */
.domain-charts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 30rem), 1fr));
  gap: var(--layout-gap);
  margin: 0 0 var(--stack-gap);
}
.domain-charts > * { min-width: 0; margin: 0; }
.domain-charts .chart-card figcaption p { max-width: 62ch; }

/* Activity calendar: one month of plan against execution. */
.calendar-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-6); }
.calendar-head h2 { margin: 0; }
.calendar-head-copy { min-width: 0; }
/* A column inside a layout cell: stack cards under one another where the
   neighbouring cell is taller. A bare wrapper, not a card: the frame rule
   leaves it out, so the cards it holds keep their own frame and padding. */
.col-stack {
  display: flex;
  flex-direction: column;
  gap: var(--layout-gap);
  min-width: 0;
}
.col-stack > * { margin: 0; }

/* The calendar. A source's colour is one custom property, so a dot, a line, a
   bar and a swatch all read it the same way. */
.series-1 { --source: var(--series-1); }
.series-2 { --source: var(--series-2); }
.series-3 { --source: var(--series-3); }
.series-4 { --source: var(--series-4); }
.series-5 { --source: var(--series-5); }
.series-6 { --source: var(--series-6); }
.series-7 { --source: var(--series-7); }
.series-8 { --source: var(--series-8); }
.series-9 { --source: var(--series-9); }
.series-10 { --source: var(--series-10); }
/* The month takes the room; the list of calendars is as wide as its names and
   never grows, wrapping under the month where both do not fit. */
.calendar-layout > :first-child { flex: 999 1 36rem; }
.calendar-layout > .calendar-aside { flex: 1 1 15rem; }
.calendar-month { container: month / inline-size; padding: var(--space-6) var(--space-6) var(--space-4); }
.calendar-month .section-head { align-items: center; margin-bottom: var(--space-5); }
.calendar-month .section-head h2 a { color: inherit; text-decoration: none; }
.calendar-month .section-head h2 a:hover { color: var(--accent); }
/* Seven equal columns: the one fixed grid here, because weeks must line up. */
.month-grid { width: 100%; border-collapse: collapse; table-layout: fixed; }
.month-grid th { padding: 0 0 var(--space-3); color: var(--muted); font-size: var(--text-2xs); font-weight: 600; text-align: left; text-transform: uppercase; letter-spacing: .04em; }
.month-day {
  height: 6.5rem;
  padding: var(--space-2);
  overflow: hidden;
  border-top: 1px solid var(--line-2);
  vertical-align: top;
}
.month-day + .month-day { border-left: 1px solid var(--line-2); }
.month-day.is-weekend { background: color-mix(in srgb, var(--bg-tint) 45%, transparent); }
.month-day.is-outside .month-date { color: var(--muted-2); }
.month-day.is-past :is(.month-span, .month-item, .month-mark) { opacity: .62; }
.month-day.is-selected { box-shadow: inset 0 0 0 2px var(--accent); }
/* The date and the day's marks share one row, so marks take no height of
   their own and the bars under them stay level across the week. */
.month-day-head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1); margin-bottom: var(--space-1); }
.month-date {
  display: inline-grid;
  place-items: center;
  min-width: 1.6rem;
  height: 1.6rem;
  margin-right: var(--space-1);
  padding: 0 var(--space-2);
  border-radius: 999px;
  color: var(--ink);
  font-size: var(--text-xs);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-decoration: none;
}
.month-date:hover { background: var(--bg-tint); }
.month-day.is-today .month-date { background: var(--accent); color: var(--surface); }
/* A mark is a dot in its source's colour: filled once done, a ring while
   planned, a ring struck through once missed. Shape carries the state, so the
   source's colour stays the only colour. */
.month-mark { width: 8px; height: 8px; border-radius: 50%; background: var(--source); }
.month-item-dot { display: none; }
.month-mark.is-planned { background: transparent; box-shadow: inset 0 0 0 1.5px var(--source); }
.month-mark.is-missed {
  background: linear-gradient(135deg, transparent 44%, var(--source) 44% 56%, transparent 56%);
  box-shadow: inset 0 0 0 1.5px var(--source);
  opacity: .6;
}
/* A span is one bar across its days: rounded only where it starts and ends,
   titled where it starts or where a week begins. A gap holds the lane open. */
.month-span {
  display: block;
  height: 1.25rem;
  margin: 0 calc(-1 * var(--space-2)) var(--space-1);
  padding: 0 var(--space-3);
  overflow: hidden;
  background: color-mix(in srgb, var(--source) 18%, var(--surface));
  color: var(--ink);
  font-size: var(--text-2xs);
  font-weight: 600;
  line-height: 1.25rem;
  text-decoration: none;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.month-span.is-start { margin-left: 0; border-left: 3px solid var(--source); border-radius: var(--radius-sm) 0 0 var(--radius-sm); }
.month-span.is-end { margin-right: 0; border-top-right-radius: var(--radius-sm); border-bottom-right-radius: var(--radius-sm); }
.month-span.is-gap { visibility: hidden; }
.month-item {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin-bottom: var(--space-1);
  overflow: hidden;
  color: var(--ink);
  font-size: var(--text-2xs);
  line-height: 1.25rem;
  text-decoration: none;
  white-space: nowrap;
}
.month-item > span { overflow: hidden; text-overflow: ellipsis; }
.month-item::before { content: ""; flex: 0 0 auto; width: 6px; height: 6px; border-radius: 50%; background: var(--source); transform: translateY(-1px); }
.month-item.is-planned::before { background: transparent; box-shadow: inset 0 0 0 1.5px var(--source); }
.month-item time { color: var(--muted); font-variant-numeric: tabular-nums; }
.month-item:hover > span { color: var(--accent); }
/* All day: a filled block in its colour, as a one-day bar. */
.month-item.is-all-day { padding: 0 var(--space-3); border-radius: var(--radius-sm); background: color-mix(in srgb, var(--source) 18%, var(--surface)); font-weight: 600; }
.month-item.is-all-day::before { content: none; }
.month-more { display: block; color: var(--muted); font-size: var(--text-3xs); font-weight: 600; text-decoration: none; }
.month-more:hover { color: var(--accent); }
/* The aside: the chosen day over the list of calendars. Built from the
   shared head, record rows and facts; only a source's swatch is the calendar's. */
/* Beside the month it keeps its basis: the month's grow outweighs it a
   thousand to one. Wrapped under the month it is alone on its line and takes
   the width, its day and its list of calendars side by side. */
.calendar-aside { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)); align-items: start; gap: var(--layout-gap); }
.day-panel, .calendar-sources { padding: var(--space-5); }
.day-panel .section-head { margin-bottom: var(--space-4); }
.day-panel-entry { margin-bottom: var(--space-5); padding-bottom: var(--space-5); border-bottom: 1px solid var(--line-2); }
.day-panel-entry h3 { margin: 0 0 var(--space-2); }
.day-panel-entry p { margin: 0 0 var(--space-2); font-size: var(--text-sm); }
.day-panel-entry .page-actions { margin: var(--space-4) 0 0; }
.day-panel-source, .calendar-sources h3 { display: flex; align-items: center; gap: var(--space-3); margin: var(--space-4) 0 var(--space-2); color: var(--muted); font-size: var(--text-2xs); font-weight: 650; text-transform: uppercase; letter-spacing: .04em; }
.calendar-sources h3:first-child { margin-top: 0; }
.calendar-swatch { width: 8px; height: 8px; border-radius: 2px; background: var(--source); }
.calendar-sources ul { display: grid; gap: var(--space-1); margin: 0; padding: 0; list-style: none; }
.calendar-sources form { margin: 0; }
/* A source is a toggle: the whole row is the button, its box filled in its colour when checked. */
.calendar-source {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  min-height: 0;
  padding: var(--space-1) var(--space-2);
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  box-shadow: none;
  color: var(--ink);
  font: inherit;
  font-size: var(--text-sm);
  text-align: left;
  cursor: pointer;
}
.calendar-source:hover { background: var(--bg-tint); border: 0; }
.calendar-source > span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.calendar-source small { color: var(--muted-2); font-size: var(--text-2xs); font-variant-numeric: tabular-nums; }
.calendar-source .calendar-source-failure { color: var(--warn); }
/* What a calendar shows, under its name, where a phone can read it. */
.calendar-source-note { display: block; white-space: normal; font-variant-numeric: normal; }
.calendar-source:has(.calendar-source-note) { align-items: start; }
.calendar-source:has(.calendar-source-note) .calendar-check { margin-top: var(--space-1); }
.calendar-check { flex: 0 0 auto; width: 14px; height: 14px; border-radius: 4px; box-shadow: inset 0 0 0 1.5px var(--source); }
.calendar-source[aria-pressed="true"] .calendar-check { background: var(--source); }
.calendar-source[aria-pressed="false"] > span { color: var(--muted); }
/* Compact, as the dashboard shows it: where things are, not what. Bars are
   slivers and lines fold away; the day opens the calendar for the rest. */
.calendar-month.is-compact .month-day { height: 2.5rem; padding: var(--space-1); }
.calendar-month.is-compact :is(.month-item, .month-more) { display: none; }
.calendar-month.is-compact .month-item-dot { display: inline-block; }
/* A bar reaches across its cell's padding to meet the next, and no further. */
.calendar-month.is-compact .month-span { height: 4px; margin: 0 calc(-1 * var(--space-1)) var(--space-1); padding: 0; background: var(--source); font-size: 0; }
.calendar-month.is-compact .month-span.is-end { margin-right: 0; }
.calendar-month.is-compact .month-span.is-start { margin-left: 0; border-left: 0; }
.calendar-month.is-compact .month-date { min-width: 1.4rem; height: 1.4rem; font-size: var(--text-2xs); }
.calendar-month.is-compact .month-mark { width: 6px; height: 6px; }
/* A phone shows where things are, not what: dots and slivers, and the day
   panel under the month says the rest. */
@container month (max-width: 34rem) {
  .month-day { height: 3.25rem; }
  :is(.month-item, .month-more) { display: none; }
  .month-item-dot { display: inline-block; }
  .month-span { height: 4px; padding: 0; background: var(--source); font-size: 0; }
  .month-date { min-width: 1.4rem; height: 1.4rem; }
  .month-of { display: none; }
}

/* Week plan: the recurring shape of a week, in the calendar's own columns. */
.week-plan-card { margin: 0; }
.week-plan {
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: var(--space-1);
}
.week-plan-day {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) 0 var(--space-5);
  border: 1px solid transparent;
  border-radius: 9px;
}
.week-plan-day.is-planned { background: var(--surface-2); border-color: var(--line-2); }
.week-plan-label { color: var(--muted-2); font-size: var(--text-3xs); font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
.week-plan-day.is-planned .week-plan-label { color: var(--ink); }
.week-plan-mark { display: flex; align-items: center; justify-content: center; gap: var(--space-1); min-height: 8px; }
.week-plan-mark i { width: 8px; height: 8px; border-radius: 50%; display: block; }
.week-plan-note { color: var(--muted); font-size: var(--text-3xs); line-height: 1.2; text-align: center; }
.week-plan-notes { list-style: none; margin: var(--space-6) 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--space-3) var(--space-9); }
.week-plan-notes li { display: flex; align-items: baseline; gap: var(--space-3); font-size: var(--text-sm); }

.calendar-controls { display: flex; align-items: center; gap: var(--space-3); flex: 0 0 auto; }
.calendar-period { color: var(--ink); font-size: var(--text-md); font-weight: 650; white-space: nowrap; }
.calendar-nav { display: flex; gap: var(--space-2); flex: 0 0 auto; }
/* Scoped to beat `.calendar-nav a`, which fixes the arrows at a 28px square;
   otherwise the label is forced into that box and overlaps the next arrow. */
.calendar-nav .calendar-now {
  width: auto;
  flex: 0 0 auto;
  padding: 0 var(--space-5);
  font-size: var(--text-2xs);
  font-weight: 600;
  letter-spacing: .01em;
  white-space: nowrap;
}
.calendar-nav a,
.calendar-nav span {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border: 1px solid var(--line);
  border-radius: 8px;
  color: var(--muted);
  font-size: var(--text-xl);
  line-height: 1;
  text-decoration: none;
}
.calendar-nav a:hover { color: var(--accent); border-color: var(--accent); }
/* The disabled end of the range keeps its box so the pair does not shift. */
.calendar-nav span { opacity: .35; }
.calendar-description { margin: var(--space-2) 0 0; color: var(--muted); font-size: var(--text-sm); line-height: 1.45; }
.calendar-legend {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin: var(--space-6) 0 var(--space-3);
}
.calendar-card .chart-legend { margin-bottom: 0; }
.calendar-key-missed {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: transparent;
  border: 1.5px solid var(--muted-2);
}
.activity-calendar { width: 100%; border-collapse: separate; border-spacing: 3px; table-layout: fixed; }
.calendar-weekday {
  padding: 0 0 var(--space-1);
  color: var(--muted-2);
  font-size: var(--text-3xs);
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  text-align: center;
}
.calendar-day {
  height: 46px;
  padding: var(--space-2) var(--space-1) var(--space-2);
  text-align: center;
  vertical-align: top;
  border: 1px solid transparent;
  border-radius: 9px;
}
.calendar-date { display: block; color: var(--muted); font-size: var(--text-2xs); line-height: 1; font-weight: 550; font-feature-settings: "tnum"; }
.calendar-marks { display: flex; align-items: center; justify-content: center; gap: var(--space-1); min-height: 10px; margin-top: var(--space-3); }
.calendar-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; }
.calendar-dot.is-missed { background: transparent; border: 1.5px solid var(--muted-2); }
/* A day still ahead is drawn faintly: information, not a verdict. Drawn like a
   miss, every plan would look broken early in the week. */
.calendar-dot.is-planned { background: transparent; border: 1.5px dashed var(--line-2); }
/* Trained days are the figure, everything else the ground. */
.calendar-day.is-done { background: var(--surface-2); border-color: var(--line-2); }
.calendar-day.is-done .calendar-date { color: var(--ink); font-weight: 650; }
.calendar-day.is-missed .calendar-date { color: var(--muted); }
.calendar-day.is-filler { border-color: transparent; }
.calendar-day[data-tip] { cursor: default; }

.list-rows { list-style: none; padding: 0; margin: 0; }
.list-rows li {
  padding: var(--space-4) 0;
  border-bottom: 1px solid var(--line-2);
  display: flex; gap: var(--space-5); align-items: center; flex-wrap: wrap;
  font-size: var(--text-sm);
}
.list-rows li:last-child { border-bottom: 0; }
.list-rows li:first-child { padding-top: var(--space-2); }
.list-rows .row-main { display: flex; align-items: center; gap: var(--space-4); min-width: 0; flex: 1; flex-wrap: wrap; }
.list-rows .row-main > a { min-width: 0; overflow-wrap: anywhere; }
/* The title keeps the line its marker is on. Wrapping places a flex item by
   its full content width, so a long name would drop below its dot; a modest
   basis keeps them together and the name wraps inside itself instead. */
.list-rows .row-main > :is(a, strong) { flex: 1 1 12rem; min-width: 0; overflow-wrap: anywhere; }
.list-rows .row-meta { color: var(--muted); font-size: var(--text-xs); margin-left: auto; white-space: nowrap; }
.list-rows a strong { color: var(--ink); font-weight: 600; }
.list-rows a:hover strong { color: var(--accent); }

/* Prose list: sentences, where .list-rows carries chips.
   `.row-main` is a flex container, which is right for a row assembled from a
   dot, a link and a badge and wrong for a sentence: every inline element in a
   flex container becomes a block-level flex item, so emphasis inside one is
   laid out as a separate box and the sentence comes apart at the seams. This
   is the other list: bullets or numbers, ordinary inline flow inside, and a
   trailing muted note. */
.prose-list { margin: var(--space-4) 0 0; padding-left: var(--space-10); font-size: var(--text-sm); }
.prose-list li { margin: 0 0 var(--space-4); line-height: 1.55; }
.prose-list li:last-child { margin-bottom: 0; }
.prose-list li::marker { color: var(--muted); }
.prose-list strong { font-weight: 650; }
/* A quiet link: a section head's trailing "All →", an inline "Edit". One class
   for both. */
.subtle-link {
  color: var(--muted);
  font-size: var(--text-xs);
  font-weight: 500;
}
.subtle-link:hover { color: var(--accent); }

.section-foot { margin: var(--space-6) 0 0; font-size: var(--text-sm); }
.section-foot a { color: var(--muted); font-weight: 500; }
.section-foot a:hover { color: var(--accent); }
.dashboard-patterns .section-foot { margin-top: auto; padding-top: var(--space-6); text-align: right; }

/* Empty state: make zero panels feel intentional, not broken */
.empty {
  padding: var(--space-7) 0 var(--space-2);
  color: var(--muted);
  font-size: var(--text-sm);
}
.empty.compact { padding: var(--space-2) 0 0; }
.empty p { margin: 0 0 var(--space-3); }
.empty .empty-cta { color: var(--accent-2); font-weight: 500; }
/* What is missing leads; what would fill it follows, quieter and at a
   reading width. */
.empty > p:first-child { color: var(--ink-2); font-weight: 600; }
.empty .empty-why { max-width: var(--measure); }

/* ── KV grid ────────────────────────────────────────────────────── */

/* Facts: each label beside its value, and as many pairs to a row as the
   width holds, so seven short facts are two rows rather than seven. A label
   wraps inside its own column; a value takes what is left of its pair. */
.kv { display: grid; grid-template-columns: repeat(auto-fill, minmax(5rem, 8rem) minmax(min(13rem, 100% - 5rem - var(--space-6)), 1fr)); gap: var(--space-4) var(--space-6); padding: var(--surface-inset, var(--space-6) var(--space-8)); font-size: var(--text-sm); }
.kv dt { color: var(--muted); font-weight: 500; }
.kv dd { min-width: 0; margin: 0; color: var(--ink); overflow-wrap: anywhere; }
.eyebrow { display: block; margin-bottom: var(--space-1); color: var(--muted-2); font-size: var(--text-3xs); font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }

/* ── Audit / activity list ─────────────────────────────────────── */

.audit-list { list-style: none; padding: 0; margin: 0; }
.audit-list li {
  padding: var(--space-4) 0;
  border-bottom: 1px solid var(--line-2);
  font-size: var(--text-sm);
  display: flex; gap: var(--space-5); align-items: baseline; flex-wrap: wrap;
}
.audit-list li:last-child { border-bottom: 0; }
.audit-list .ts { color: var(--muted-2); font-variant-numeric: tabular-nums; font-size: var(--text-xs); white-space: nowrap; }

/* Until the readings are one row (their own container says when), a popover
   hangs from the readings' right edge, which is the page's, rather than from
   a panel that may sit anywhere along it. */
@container (max-width: 899px) {
  .glance-panel { position: static; }
  .glance-expanded { right: 0; width: min(340px, 100%); }
}

@media (max-width: 640px) {
  .glance-panel-head { column-gap: var(--space-3); }
  .user-menu { flex-shrink: 1000; min-width: var(--control-size); }
  /* The line under a lead gives way at its end, never the lead. */
  .glance-caption { overflow: hidden; text-overflow: ellipsis; }
}

/* A description under a row's name: two lines at most, and never wider than
   a name column should be, so the columns beside it stay on screen. */
.table-clamp { display: -webkit-box; max-width: 28ch; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; }

/* ── Forms ──────────────────────────────────────────────────────── */

/* A form is two columns where there is room and one where there is not, in
   reading order, and a row's fields share its whole width: pairs sit side by
   side, and the odd one of a run takes its row rather than leaving a hole.
   Two, never three: a third column is where a run of four became three and a
   stray. Anything that is not a single short field (a textarea, a group of
   boxes, errors, the actions) takes the row. */
.form {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-7) var(--space-10);
  align-items: flex-start;
  padding: var(--surface-inset, var(--space-10));
}
/* More than a third of the row, so no more than two fit it. */
.form .field { flex: 1 1 max(18rem, 34%); min-width: min(100%, 18rem); }
.form > :not(.field) { flex-basis: 100%; }
.form > .field:has(textarea) { flex-basis: 100%; }
.form > .field:has(> div > * + *) { flex-basis: 100%; }
.form > .field { margin-bottom: 0; }
/* A lone checkbox sits beside its label, the help under both, rather than
   alone on a line under its own question. */
.field:has(> input[type=checkbox]) { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; column-gap: var(--space-4); }
.field:has(> input[type=checkbox]) > input { grid-column: 1; grid-row: 1; margin: 0; }
.field:has(> input[type=checkbox]) > label { grid-column: 2; grid-row: 1; margin: 0; }
.field:has(> input[type=checkbox]) > :not(label, input) { grid-column: 1 / -1; }
/* A group of boxes reads across, not one per line. */
.field > div:has(> div input[type=checkbox], > div input[type=radio]) { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-8); }
.field { margin-bottom: var(--space-7); }
/* A control inside a table cell or a row, where the gap that separates stacked
   form fields is just a taller row. `.field` has to stay the wrapper: the
   control styling hangs off `.field select`, so dropping it to kill the margin
   drops the border and padding with it and the control falls back to the
   browser's own. Same modifier convention as .kv.compact and .empty.compact. */
.field.compact { margin-bottom: 0; }
.field label { display: block; font-weight: 600; margin-bottom: var(--space-2); font-size: var(--text-sm); }
.field input, .field select, .field textarea { width: 100%; }
.field textarea { resize: vertical; min-height: 80px; }
/* A field that is the form's subject (a title, a note) takes the whole row. */
.form .field.is-wide { flex-basis: 100%; }
/* A field shown only when another's value calls for it (app.js). The wrapper
   adds no box, so the fields inside keep their place in the grid. */
.form [data-when]:not([hidden]) { display: contents; }
.field .help { color: var(--muted); font-size: var(--text-xs); margin: var(--space-2) 0 0; }
.field .check-row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
  margin: 0;
  line-height: 1.4;
}
/* A box to tick, never a field to fill: `.field input` stretches every input
   to the column, which would make a checkbox the width of its row and push its
   label past the edge. */
.field input[type=checkbox], .field input[type=radio] { width: auto; }
.check-row input { flex: 0 0 auto; margin-top: var(--space-1); }
.field.has-errors input, .field.has-errors textarea, .field.has-errors select { border-color: var(--danger); }
/* Django renders errors as a plain `<ul class="errorlist">`, unstyled by
   default. */
.errorlist {
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0;
  display: grid;
  gap: var(--space-1);
  color: var(--danger);
  font-size: var(--text-xs);
  font-weight: 600;
}
/* Errors with no field to sit under say what was wrong with the request as a
   whole, so they read as a notice rather than as a line under a control. */
.form-errors {
  margin: 0 0 var(--space-7);
  padding: var(--space-5) var(--space-6);
  border: 1px solid color-mix(in srgb, var(--danger) 35%, var(--line));
  border-left: 3px solid var(--danger);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--danger) 6%, var(--surface));
}
.form-errors .errorlist { margin: 0; }
.errors { color: var(--danger); margin: var(--space-2) 0 0; padding-left: var(--space-8); font-size: var(--text-sm); }
.form-actions { display: flex; flex-wrap: wrap; gap: var(--space-4); margin-top: var(--space-6); }

/* ── Dropzone ───────────────────────────────────────────────────────
   The real file input covers the zone, so the click target and the drop target
   are the control itself. That keeps the whole thing working with scripting
   unavailable and means no hidden state can disagree with what will submit. */

.field label.dropzone, .dropzone {
  position: relative;
  display: flex; flex-direction: column; align-items: center; gap: var(--space-2);
  font-weight: 400;
  margin-bottom: 0;
  padding: var(--space-12) var(--space-9);
  border: 1.5px dashed var(--line);
  border-radius: var(--radius);
  background: var(--surface-2);
  text-align: center;
  cursor: pointer;
  transition: border-color .12s ease, background .12s ease;
}
/* `.field label.dropzone` above is (0,2,1), so a bare `.dropzone:hover` at
   (0,2,0) would lose to it inside a form field, which is every dropzone on a
   form. Matched here rather than lowered there, because the base rule's
   specificity is what makes it beat the generic `.field label` styling in the
   first place. */
.field label.dropzone:hover, .dropzone:hover,
.field label.dropzone:focus-within, .dropzone:focus-within {
  border-color: var(--accent-2); background: var(--accent-tint);
}
/* The input is full-bleed and `opacity: 0`, so its own ring is invisible and
   the zone has to carry one. Without this a keyboard user tabbing onto the
   upload control gets no indication they are on it at all. */
.field label.dropzone:focus-within, .dropzone:focus-within {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
.field label.dropzone.is-dragging, .dropzone.is-dragging {
  border-color: var(--accent); background: var(--accent-tint);
}
/* Full-bleed and invisible: the input is the hit target, the text is chrome. */
.field .dropzone input[type=file] {
  position: absolute; inset: 0; width: 100%; height: 100%;
  padding: 0; border: 0; background: none;
  opacity: 0; cursor: pointer;
}
.dropzone-title { font-weight: 600; font-size: var(--text-sm); color: var(--ink); }
.dropzone-hint { font-size: var(--text-xs); color: var(--muted); }
.dropzone-files {
  list-style: none; margin: var(--space-4) 0 0; padding: 0;
  display: flex; flex-direction: column; gap: var(--space-1);
  font-size: var(--text-xs); color: var(--ink-2); font-feature-settings: "tnum";
}

/* ── Modal ──────────────────────────────────────────────────────────
   One short task, over the page that launched it. The trigger is always a real
   link to a page that does the same job, so this layer is never load-bearing. */

.modal {
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  background: var(--surface);
  color: var(--ink);
  width: min(520px, calc(100vw - 32px));
}
.modal::backdrop { background: var(--scrim); }
.modal-panel { padding: var(--space-9) var(--space-10) var(--space-10); }
.modal-panel:focus { outline: none; }
.modal-head { display: flex; align-items: center; gap: var(--space-6); margin-bottom: var(--space-2); }
.modal-head h2 { margin: 0; flex: 1; font-size: var(--text-xl); letter-spacing: -.01em; }
.modal-close {
  padding: 0 var(--space-3); line-height: 1; font-size: var(--text-3xl);
  background: none; border: 0; color: var(--muted); box-shadow: none;
}
.modal-close:hover { color: var(--ink); background: var(--bg-tint); border-color: transparent; }
.modal-lede { margin: 0 0 var(--space-7); font-size: var(--text-sm); color: var(--muted); line-height: 1.5; }
.modal .form { background: none; border: 0; padding: 0; box-shadow: none; max-width: none; }

.command-center-modal {
  width: min(780px, calc(100vw - 28px));
  max-height: min(760px, calc(100vh - 28px));
}
.command-center-modal .modal-panel { padding: var(--space-9) var(--space-9) var(--space-6); }
.command-center-modal .modal-head { margin-bottom: var(--space-1); }
.command-center-modal .modal-head h2 { font-size: var(--text-2xl); letter-spacing: -.02em; text-transform: none; }
.command-center-modal .modal-lede { margin-bottom: var(--space-6); }
.command-center-palette { display: grid; min-height: 0; }
.command-center-query {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-5);
  padding: 0 var(--space-6);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
}
.command-center-query:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-tint);
}
.command-center-query svg { fill: none; stroke: var(--muted); stroke-width: 1.8; }
.command-center-query input[type=search] {
  width: 100%;
  min-height: 46px;
  padding: 0;
  border: 0;
  outline: 0;
  box-shadow: none;
  background: transparent;
  font-size: var(--text-lg);
}
.command-center-query kbd { white-space: nowrap; }
.command-center-results {
  min-height: 120px;
  max-height: min(56vh, 520px);
  margin-top: var(--space-5);
  overflow: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
}
.command-center-results[aria-busy="true"] { opacity: .62; }
.command-center-options { display: grid; gap: var(--space-5); }
.command-center-group h3 {
  position: sticky;
  z-index: 1;
  top: 0;
  margin: 0;
  padding: var(--space-3) var(--space-4) var(--space-2);
  background: color-mix(in srgb, var(--surface) 94%, transparent);
  color: var(--muted);
  font-size: var(--text-3xs);
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
}
.command-center-option {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, auto) 16px;
  align-items: center;
  gap: var(--space-5);
  min-width: 0;
  padding: var(--space-4) var(--space-4);
  border-radius: var(--radius-sm);
  color: inherit;
  text-decoration: none;
}
.command-center-option.is-active {
  background: var(--accent-tint);
  color: inherit;
}
.command-center-copy { display: grid; gap: var(--space-1); min-width: 0; }
.command-center-copy > .muted {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--text-xs);
}
.command-center-title { display: flex; align-items: baseline; gap: var(--space-4); min-width: 0; }
.command-center-title strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.command-center-title code { color: var(--muted-2); font-size: var(--text-3xs); overflow-wrap: anywhere; }
.command-center-evidence {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}
.command-center-evidence .scope-chip { padding: var(--space-1) var(--space-3); font-size: var(--text-3xs); white-space: nowrap; }
.command-center-destination { color: var(--muted); font-size: var(--text-3xs); white-space: nowrap; }
.command-center-arrow { color: var(--muted-2); opacity: 0; transform: translateX(-3px); transition: 100ms ease; }
.command-center-option.is-active .command-center-arrow { opacity: 1; transform: none; }
.command-center-empty {
  display: grid;
  place-items: center;
  gap: var(--space-2);
  min-height: 120px;
  padding: var(--space-10);
  text-align: center;
}
.command-center-loading, .command-center-more { padding: var(--space-7) var(--space-4); font-size: var(--text-xs); }
.command-center-footer {
  display: flex;
  justify-content: space-between;
  gap: var(--space-6);
  margin-top: var(--space-4);
  padding-top: var(--space-5);
  border-top: 1px solid var(--line-2);
  color: var(--muted);
  font-size: var(--text-3xs);
}
.command-center-footer span { white-space: nowrap; }
.command-center-footer kbd { margin-right: var(--space-1); }
.command-center-palette.has-active-option .command-center-enter-hint { display: none; }

@media (max-width: 640px) {
  .command-center-modal { width: calc(100vw - 16px); max-height: calc(100vh - 16px); }
  .command-center-modal .modal-panel { padding: var(--space-7) var(--space-6) var(--space-5); }
  .command-center-option { grid-template-columns: minmax(0, 1fr) 14px; }
  .command-center-evidence { grid-column: 1; justify-content: flex-start; overflow: hidden; }
  .command-center-copy > .muted { white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
  .command-center-arrow { grid-column: 2; grid-row: 1; }
  .command-center-footer { justify-content: center; }
  .command-center-footer span:nth-child(2) { display: none; }
}

.list-rows .row-empty { color: var(--muted); }

.tag { display: inline-block; padding: 1px var(--space-4); margin-right: var(--space-2); background: var(--bg-tint); border-radius: 999px; font-size: var(--text-2xs); color: var(--ink-2); }

.search-page-form input[type=search] { flex-basis: 420px; }
.search-results {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: var(--layout-gap);
  margin-top: var(--layout-gap);
}
.command-center-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr));
  align-items: start;
  gap: var(--layout-gap);
  margin: var(--layout-gap) 0;
}
.command-center-grid > * { margin: 0; min-width: 0; }
.discovery-group > .section-head { margin-bottom: var(--space-4); }
.discovery-group > summary.section-head {
  cursor: pointer;
  list-style: none;
  margin: 0;
}
.discovery-group > summary.section-head::-webkit-details-marker { display: none; }
.discovery-group > summary.section-head::after {
  content: "›";
  color: var(--muted-2);
  font-size: var(--text-3xl);
  transform: rotate(90deg);
  transition: transform 120ms ease;
}
.discovery-group[open] > summary.section-head { margin-bottom: var(--space-4); }
.discovery-group[open] > summary.section-head::after { transform: rotate(-90deg); }
.discovery-list { list-style: none; margin: 0; padding: 0; }
.discovery-list li { border-top: 1px solid var(--line); }
.discovery-list li:first-child { border-top: 0; }
.discovery-link {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-7);
  padding: var(--space-5) 0;
  color: inherit;
  text-decoration: none;
}
a.discovery-link:hover strong { color: var(--accent); }
.discovery-main { display: grid; gap: var(--space-1); min-width: 0; flex: 1 1 20rem; }
.discovery-main .muted { font-size: var(--text-xs); line-height: 1.4; }
.discovery-meta {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2);
  flex-wrap: wrap;
  flex: 1 1 16rem;
}
.discovery-meta code { font-size: var(--text-3xs); overflow-wrap: anywhere; }
.command-hint { margin: var(--space-2) 0 0; }
.command-runner { margin: 0; max-width: 56rem; }
/* What a command acts on, when the page was opened for one thing. */
.command-target p { margin: 0; }
.command-target .field-label { display: block; color: var(--muted); font-size: var(--text-xs); }
.command-fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
  gap: var(--space-7);
}
.command-fields .field { margin: 0; min-width: 0; }
.command-fields textarea,
.command-fields .command-confirm { grid-column: 1 / -1; }
.command-fields textarea { resize: vertical; }
.command-effect-note { margin: 0 0 var(--space-7); }
.command-confirm {
  padding: var(--space-6) var(--space-7);
  /* A callout, like .notice: inside a surface its tint and its bar, not a frame. */
  border: var(--surface-border, 1px solid var(--warn));
  border-left: 3px solid var(--warn);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--warn) 7%, var(--surface));
}
.command-result pre {
  margin: var(--space-7) 0 0;
  overflow: auto;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.command-result-note { margin: var(--space-3) 0 0; font-size: var(--text-sm); }
.command-result { margin-bottom: var(--layout-gap); border-left: 3px solid var(--ok); }
.command-result .section-head { align-items: flex-start; }
.command-result .section-head h2 { margin-bottom: 0; }
/* The projected result: facts as a small grid, the list as a plain data table
   with its count in the caption, and the JSON behind a disclosure below both. */
.command-result-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 1fr));
  gap: var(--space-4);
  margin: var(--space-6) 0 0;
}
.command-result-facts dt { font-size: var(--text-3xs); font-weight: 650; letter-spacing: .05em; text-transform: uppercase; color: var(--muted); }
.command-result-facts dd { margin: var(--space-1) 0 0; overflow-wrap: anywhere; font-weight: 600; }
.command-result .table-scroll { margin-top: var(--space-6); }
.command-result-table caption { caption-side: top; text-align: left; padding: 0 0 var(--space-3); font-size: var(--text-sm); font-weight: 600; }
.command-result-table td { overflow-wrap: anywhere; }
.command-result-json { margin-top: var(--space-6); }
.command-result-json summary { cursor: pointer; }
.command-result-json summary .muted { display: block; margin-top: var(--space-1); }
.command-result-json pre { margin-top: var(--space-4); }
.scope-read { border-color: var(--line); }
.scope-remote_write { border-color: var(--accent); color: var(--accent); }
.scope-infrastructure_change { border-color: var(--warn); color: var(--warn); }
.scope-destructive { border-color: var(--danger); color: var(--danger); }
.connection-summary {
  display: flex;
  gap: var(--space-4) var(--space-9);
  flex-wrap: wrap;
  margin: 0 0 var(--layout-gap);
  color: var(--muted);
  font-size: var(--text-xs);
}
.connection-summary strong { color: var(--ink); }
.connection-summary .is-attention strong { color: var(--attn); }
.connection-inspect { margin-left: auto; }
/* A line of its own size: inline after a <br>, its links would align to the
   cell's larger line and sit above the words beside them. */
.connection-row-meta { display: block; color: var(--muted); font-size: var(--text-2xs); }
/* Its own section under the table, not a second compartment of a card around it. */
.connection-available { margin-top: var(--space-10); }
.connection-available h2 { margin: 0 0 var(--space-4); }
.connection-available ul { display: grid; gap: var(--space-3); margin: 0; padding: 0; list-style: none; }
.connection-available li { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2) var(--space-5); font-size: var(--text-sm); }
.connection-sight,
.connection-more-scope { margin-top: var(--space-2); }
.connection-sight-tally { display: block; font-size: var(--text-xs); color: var(--muted); }
.connection-sight ul,
.connection-more-scope ul,
.cell-notes { display: grid; gap: var(--space-1); margin: var(--space-2) 0 0; padding: 0; list-style: none; }
.connection-sight li,
.connection-more-scope li { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-1) var(--space-3); font-size: var(--text-xs); }
.connection-sight-state { font-weight: 600; }
.connection-sight-refused .connection-sight-state,
.connection-sight-unreadable .connection-sight-state,
.connection-sight-refusal { color: var(--danger); }
.connection-sight-refusal { margin: var(--space-2) 0 0; font-size: var(--text-xs); }
.connection-sight-partial .connection-sight-state { color: var(--warn); }
.connection-credential-fix { margin-top: var(--space-2); font-size: var(--text-xs); }
.connection-credential-fix-headline { display: block; color: var(--warn); }
.operator-step { margin-top: var(--space-2); }
.operator-step p { margin: var(--space-1) 0 0; }
/* A command and its copy button, which sits in its corner rather than on a
   line of its own; the command leaves room for it. */
.command { position: relative; width: fit-content; max-width: 100%; min-width: 0; }
.command > .command-copy { position: absolute; inset-block: 0; margin-block: auto; height: fit-content; inset-inline-end: var(--space-2); min-height: 0; padding: var(--space-1) var(--space-3); font-size: var(--text-2xs); color: var(--code-ink); border-color: transparent; background: transparent; opacity: .7; }
.command > .command-copy:is(:hover, :focus-visible) { opacity: 1; }
.command > .operator-command { margin: 0; padding-inline-end: 4.5rem; }
/* A command wraps rather than scrolling: selecting it copies it whole. */
.operator-command {
  width: fit-content;
  max-width: 100%;
  margin: var(--space-2) 0 0;
  padding: var(--space-2) var(--space-5);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  user-select: all;
  font-size: var(--text-xs);
}
.connection-sight-never_swept .connection-sight-state,
.connection-sight-not_connected .connection-sight-state { color: var(--muted); }
.connection-security {
  position: relative;
  overflow: hidden;
  margin-bottom: var(--layout-gap);
  padding: 0;
  border-top: 3px solid var(--accent);
}
.connection-security::after {
  content: "";
  position: absolute;
  inset: 0 0 auto auto;
  width: 280px;
  height: 180px;
  background: radial-gradient(circle at top right, color-mix(in srgb, var(--accent) 12%, transparent), transparent 68%);
  pointer-events: none;
}
.connection-security-serious { border-top-color: var(--danger); }
.connection-security-advanced { margin-top: calc(var(--layout-gap) * 1.5); padding: 0; overflow: hidden; }
.connection-security-advanced > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-9);
  padding: var(--space-8) var(--space-9);
  cursor: pointer;
  list-style: none;
}
.connection-security-advanced > summary::-webkit-details-marker { display: none; }
.connection-security-advanced > summary > span:first-child { display: grid; gap: var(--space-1); }
.connection-security-advanced > summary strong { font-size: var(--text-lg); }
.connection-security-advanced > summary small { color: var(--muted); font-size: var(--text-2xs); font-weight: 400; }
.connection-security-advanced > summary::after {
  content: "+";
  color: var(--muted);
  font-size: var(--text-3xl);
  line-height: 1;
}
.connection-security-advanced[open] > summary::after { content: "−"; }
.connection-security-advanced > summary .count { margin-left: auto; }
.connection-trust-path {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6) var(--space-12);
  align-items: center;
  padding: var(--space-9);
  border-top: 1px solid var(--line);
  background: var(--surface-2);
}
.connection-trust-path > :first-child { flex: 1 1 14rem; min-width: 0; }
.connection-trust-path > :last-child { flex: 3 1 24rem; min-width: 0; }
.connection-trust-path h3 { margin: var(--space-1) 0 var(--space-2); font-size: var(--text-lg); }
.connection-trust-path p { margin: 0; color: var(--muted); font-size: var(--text-xs); line-height: 1.45; }
.request-path {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
  gap: var(--space-8);
  margin: 0;
  padding: var(--space-2) var(--space-2) var(--space-4);
  list-style: none;
  max-width: 100%;
}
/* Each hop a card of its own, raised off the page, joined by an arrow: an icon
   tile that says what kind of thing it is, then what it is called. */
/* Every hop the same shape, whatever it carries: a header row (icon, what kind
   of thing, its proof), the name on one line, then at most one line of facts.
   The hops share the width, each at least 8.5rem, and a route longer than one
   row continues on the next, since a strip that scrolls hides its last hops
   past the edge of a phone. */
.request-path li {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-rows: auto auto auto;
  align-content: start;
  align-items: center;
  column-gap: var(--space-4);
  row-gap: var(--space-2);
  min-width: 0;
  padding: var(--space-5) var(--space-6);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow);
}
.request-path li::after {
  content: "";
  position: absolute;
  top: 50%;
  right: calc(-1 * var(--space-8) + 3px);
  width: 6px;
  height: 6px;
  transform: translateY(-50%) rotate(45deg);
  border-top: 1.5px solid var(--line-strong);
  border-right: 1.5px solid var(--line-strong);
}
.request-path li:last-child::after { display: none; }
.request-path .hop-icon {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: var(--radius);
  background: var(--accent-tint);
  color: var(--accent-2);
  font-size: var(--text-lg);
}
.request-path .hop-label { min-width: 0; overflow: hidden; color: var(--muted); font-size: var(--text-xs); line-height: 1.3; white-space: nowrap; text-overflow: ellipsis; }
.request-path li > strong,
.request-path li > small {
  grid-column: 1 / -1;
  min-width: 0;
  overflow: hidden;
}
.request-path li > strong { display: flex; align-items: baseline; font-size: var(--text-sm); font-weight: 650; line-height: 1.3; }
/* A name takes a second line before it is cut, and breaks where it must; its
   certificate mark stays beside it. Cut, it is cut inside the hop. */
.request-path li > strong > :not(.cert-mark),
.request-path li > small {
  display: -webkit-box; min-width: 0; overflow: hidden; overflow-wrap: anywhere;
  -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2;
}
.request-path li > strong > .cert-mark { flex-shrink: 0; overflow: visible; }
.request-path li > small { margin: 0; color: var(--muted); font-size: var(--text-2xs); line-height: 1.35; }
/* A hop's certificate: a check in a circle after its name, in the name's colour
   once the controller has verified it, faint until then. */
.cert-mark { display: inline-flex; margin-left: .35em; font-size: 1.15em; color: var(--accent-2); vertical-align: -.15em; cursor: default; }
.cert-mark.is-pending { color: var(--muted-2); }
/* A hop this very request was proven to pass through: a dot in the corner,
   its reason on hover; the evidence is the connection page's. */
.hop-proof { grid-column: 3; grid-row: 1; width: 8px; height: 8px; border-radius: 50%; background: var(--line-strong); }
.request-path li.is-proven .hop-proof { background: var(--ok); box-shadow: 0 0 0 3px var(--tone-positive-bg); }
.request-path li.is-attention .hop-proof { background: var(--danger); }
.path-proof-note { display: flex; align-items: center; gap: var(--space-4); margin: var(--space-4) 0 0; color: var(--muted); font-size: var(--text-sm); }
.hop-proof-key { width: 8px; height: 8px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--tone-positive-bg); }
.request-path li.is-unknown { box-shadow: inset 0 3px 0 var(--muted-2), var(--shadow); }
.request-path li.is-unknown .hop-icon { background: var(--line-2); color: var(--muted-2); }
.request-path li.is-attention { box-shadow: inset 0 3px 0 var(--danger), var(--shadow); }
.connection-control-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(var(--tracks-3-1), 1fr));
  border-top: 1px solid var(--line);
}
.connection-control { min-width: 0; padding: var(--space-8) var(--space-9); }
.connection-control header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-5); margin-bottom: var(--space-6); }
.connection-control h3 { margin: 0; font-size: var(--text-xs); text-transform: uppercase; letter-spacing: .045em; color: var(--muted); }
.connection-control > strong { display: block; margin-bottom: var(--space-2); font-size: var(--text-md); }
.connection-control p { margin: 0; color: var(--muted); font-size: var(--text-xs); line-height: 1.5; }
.connection-control > a { display: inline-block; margin-top: var(--space-4); font-size: var(--text-xs); }
.connection-control-good { box-shadow: inset 3px 0 0 color-mix(in srgb, var(--ok) 65%, transparent); }
.connection-control-attention,
.connection-control-serious { box-shadow: inset 3px 0 0 color-mix(in srgb, var(--danger) 65%, transparent); }
/* Nothing here restates .data-table. The primitive already gives padding,
   alignment, a sticky head, break-word cells and .tight-col; this is only what
   is particular to connections. Cells keep their own display: grid or flex on
   a td drops it out of table layout and stacks the row into one column. */
.connection-table .muted,
.connection-table code { font-size: var(--text-2xs); }
/* An endpoint may be arbitrarily long and never owns the table's width: it
   caps itself, and the row's expander shows the rest. */
.connection-endpoint {
  display: inline-block;
  overflow: hidden;
  max-width: min(100%, var(--identifier-max));
  text-overflow: ellipsis;
  vertical-align: bottom;
  white-space: nowrap;
}
/* Here the name is one line, like the endpoint under it: the name leads five
   more facts in the same cell. */
.connection-table td > strong > [data-entity] { display: -webkit-inline-box; overflow: hidden; -webkit-line-clamp: 1; line-clamp: 1; }
/* An ability line's text track is minmax(0, 1fr), which reports no width of
   its own to the table: without a length here the column is squeezed until
   its words break, and the spare width goes to the columns with sentences. */
.connection-ability-list { display: grid; gap: var(--space-2); min-width: 20ch; margin: 0; padding: 0; list-style: none; }
.connection-ability-list li {
  display: grid;
  grid-template-columns: 5px minmax(0, 1fr);
  align-items: baseline;
  gap: var(--space-3);
  font-size: var(--text-2xs);
  line-height: 1.35;
}
.connection-ability-list li::before {
  content: "";
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--accent);
}
.connection-ability-list li.is-unavailable { color: var(--muted); }
.connection-ability-list li.is-unavailable::before { background: var(--muted-2); }
.connection-ability-list small { grid-column: 2; color: var(--danger); font-size: var(--text-3xs); font-weight: 650; }
/* Short of proof is not the same as refused: an undeclared or unverified
   requirement is a debt, said quietly; a missing or rejected grant is a stop. */
.connection-ability-list small.connection-evidence-undeclared,
.connection-ability-list small.connection-evidence-unverified,
.connection-ability-list small.connection-evidence-unknown { color: var(--muted); font-weight: 600; }
/* Lifecycle, authority and the reading's age, one under the other, in the
   table and in the stacked card alike. */
/* Wide enough to read a fact whole, as a length so the table counts it while
   sizing columns; each fact wraps inside it rather than being cut. */
.connection-standing { display: grid; gap: var(--space-1); justify-items: start; min-width: 18ch; }
.connection-standing .muted { font-size: var(--text-2xs); }
/* What was done names a resource, which can be any length: it holds one
   line within the standing's width and its age keeps a line of its own, so a
   long container name never sets the column. The title carries it whole. */
.connection-last-activity { display: grid; max-width: 26ch; font-size: var(--text-2xs); }
.connection-last-activity > span:first-child { overflow: hidden; text-overflow: ellipsis; }
.connection-authority { color: var(--muted); font-size: var(--text-2xs); }
.connection-problems { color: var(--attn); font-size: var(--text-2xs); font-weight: 650; }
/* Where a published thing runs, and a note with the names in it as links. */
.runs-on { margin: 0 0 var(--space-5); }
.linked-text { white-space: pre-line; overflow-wrap: anywhere; }
.connection-authority-proven { color: var(--muted); }
.connection-authority-missing { color: var(--danger); }
.connection-ability-more { display: block; margin-top: var(--space-2); font-size: var(--text-3xs); color: var(--muted); }
.connection-ability-list + .connection-ability-list { margin-top: var(--space-2); }
.connection-state {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  color: var(--tone-ink, var(--muted));
  font-size: var(--text-2xs);
  font-weight: 650;
  white-space: nowrap;
}
.connection-state::before {
  content: "";
  width: 7px;
  height: 7px;
  flex: none;
  border-radius: 50%;
  background: currentColor;
}
/* Each destination is its own target, so two commands on one row cannot read
   as one link. */
/* Spacing only. Setting `display` here would be (0,1,1) and would silently
   beat any single-class variant, un-centring the icon. */
.connection-next > a,
.connection-next > .btn { margin: 0 var(--space-2) var(--space-2) 0; }
.connection-next > a:last-child,
.connection-next > .btn:last-child { margin-bottom: 0; }
.connection-next .connection-direct-action {
  display: inline-block;
  font-size: var(--text-2xs);
}

.connection-ability { margin-bottom: var(--space-2); }
.connection-ability summary { cursor: pointer; }
.connection-ability[open] { display: grid; gap: var(--space-1); }
.connection-ability > .muted { font-size: var(--text-2xs); }
.connection-ability.is-unavailable summary strong { color: var(--danger); }
.connection-ability-state { color: var(--danger); font-size: var(--text-2xs); font-weight: 700; }
.connection-more { margin-top: var(--space-2); }
.connection-more summary { color: var(--accent); cursor: pointer; font-size: var(--text-xs); }
.connection-more[open] summary { margin-bottom: var(--space-2); }
/* Each thing to do next keeps to one line: the column is as wide as its
   longest, and the table gives the room from columns that can wrap. */
.connection-next { min-width: 0; }
.connection-next > :is(a, .btn) { white-space: nowrap; }
.connection-next > .muted { display: block; max-width: 240px; margin-top: var(--space-2); font-size: var(--text-2xs); line-height: 1.35; }
.connection-actions { margin-top: var(--space-3); }
.connection-actions > summary { color: var(--accent); cursor: pointer; font-size: var(--text-xs); }
.connection-actions > span { display: grid; justify-items: start; gap: var(--space-2); margin-top: var(--space-3); }
.connection-actions a { font-size: var(--text-xs); font-weight: 600; }
.connection-direct-action { font-size: var(--text-xs); font-weight: 600; }
.connection-direct-action.is-destructive { color: var(--danger); }
kbd {
  border: 1px solid var(--line);
  border-bottom-width: 2px;
  border-radius: 4px;
  background: var(--surface);
  padding: 1px var(--space-2);
  font: inherit;
  font-size: var(--text-2xs);
  color: var(--ink-2);
}

/* ── Messages ───────────────────────────────────────────────────── */

.messages { list-style: none; padding: 0; margin: 0 0 var(--space-8); }
/* Same tone contract as `.pill`, at the wash fill level. A success banner and
   a success chip therefore cannot drift apart: they read the same three
   tokens, and only the fill differs by design. */
/* A notice in the page: a titled callout in one of the tones, for anything
   that needs saying before the content it is about. Tone from the modifier
   (notice-attention, notice-serious, notice-info); the tone section colours it. */
.notice {
  margin: 0 0 var(--stack-gap);
  padding: var(--space-6) var(--space-7);
  /* A callout: inside a surface it keeps its tint and its bar, not a frame. */
  border: var(--surface-border, 1px solid var(--tone-line, var(--line)));
  border-left: 3px solid var(--tone-line, var(--line));
  border-radius: var(--radius-sm);
  background: var(--tone-wash, var(--surface));
  color: var(--tone-ink, inherit);
  font-size: var(--text-sm);
}
.notice > strong { display: block; }
.notice p { margin: var(--space-2) 0 0; }
.msg {
  padding: var(--space-4) var(--space-6); border-radius: var(--radius-sm); margin-bottom: var(--space-3);
  font-size: var(--text-sm);
  background: var(--tone-wash, transparent);
  color: var(--tone-ink, inherit);
  border: 1px solid var(--tone-line, var(--line));
}
/* Variants carry no colour of their own: the tone assignment section above
   gives them one. */

.pagination { display: flex; gap: var(--space-6); align-items: center; margin: var(--space-8) 0; font-size: var(--text-sm); }

.export-grid { display: flex; flex-wrap: wrap; gap: var(--space-4); margin-top: var(--space-4); }

.auth-card { max-width: 380px; margin: clamp(var(--space-20), 10vh, var(--space-40)) auto; padding: var(--surface-inset, var(--space-14)); text-align: center; }
.auth-logo { display: block; width: 64px; height: 64px; margin: 0 auto var(--space-7); }
.auth-card h1 { font-size: var(--text-2xl); margin-bottom: var(--space-9); }
.auth-card label { display: block; margin: var(--space-5) 0 var(--space-2); font-weight: 600; font-size: var(--text-sm); text-align: left; }
.auth-card input { width: 100%; padding: var(--space-4) var(--space-5); border: 1px solid var(--line); border-radius: var(--radius-sm); font: inherit; font-size: var(--text-md); }
.auth-card button { margin-top: var(--space-8); width: 100%; }
.auth-sso { display: block; text-align: center; margin-top: var(--space-2); }

.auth-note { margin: var(--space-8) 0 0; font-size: var(--text-xs); text-align: center; }
.notice-search { justify-content: center; margin: 0 0 var(--space-8); }

/* ── Dashboard: external links ─────────────────────────────────── */
.link-cell { white-space: nowrap; }
.link-cell a { font-size: var(--text-xs); font-weight: 500; margin-right: var(--space-5); }
.link-cell a:last-child { margin-right: 0; }

.ext-links { display: flex; flex-wrap: wrap; gap: var(--space-3); margin: 0; padding: 0; list-style: none; }
.ext-links a {
  display: inline-flex; align-items: center; gap: var(--space-3);
  padding: var(--space-2) var(--space-6);
  border: 1px solid var(--line); border-radius: 999px;
  background: var(--surface); color: var(--ink-2); font-size: var(--text-sm); font-weight: 500;
}
.ext-links a:hover { border-color: var(--line-strong); color: var(--ink); text-decoration: none; }
.ext-arrow { color: var(--muted-2); font-size: var(--text-xs); }
.ext-links a:hover .ext-arrow { color: var(--accent-2); }

/* ── Responsive ─────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition: none !important; animation: none !important; }
}


/* Sections behind one control. Below this width the bar cannot hold every
   section beside the brand and the account chip, and a second row is not the
   answer: it spends chrome on every page before any of them begins. The
   collapsed search above buys the headroom that keeps this width low; past it
   the sections move into a panel that opens on tap.

   `:target` rather than script or a `details` widget: nothing hides the nav by
   default, so a stylesheet that fails to load leaves every section plainly
   visible instead of none of them. */
@media (max-width: 1080px) {
  .nav-toggle-open { display: inline-flex; }
  .site-header:target .nav-toggle-open,
  .nav-is-open .nav-toggle-open { display: none; }
  .nav-toggle-close { display: none; }
  .site-header:target .nav-toggle-close,
  .nav-is-open .nav-toggle-close { display: inline-flex; }
  /* Under the header, over the page, and only while open. It has to be below
     the header's own z-index: the header is a stacking context, so a control
     inside it can never be raised above a backdrop that sits higher, and a
     backdrop covering the toggle means the button that closes the menu is not
     clickable, only the sheet on top of it. Below the header, tapping the
     toggle closes it deliberately and tapping anywhere else closes it too. */
  .site-header:target ~ .nav-backdrop,
  .nav-is-open .nav-backdrop {
    display: block; position: fixed; inset: 0; z-index: 9;
  }
  /* Matches the collapsed search button and the account chip exactly, so it
     reads as the same kind of control as the ones beside it. */
  .nav-toggle { align-items: center; justify-content: center;
    inline-size: var(--control-size); block-size: var(--control-size); order: 4;
    border: 1px solid var(--bar-control-line); border-radius: var(--radius-sm);
    background: var(--bar-control-bg); color: var(--ink-2);
  }
  .nav-toggle:hover { background: var(--bar-control-bg-hover); text-decoration: none; }
  .nav-toggle:focus:not(:focus-visible) { outline: none; }
  .nav-toggle:focus-visible {
    outline: 2px solid var(--accent-2);
    outline-offset: 2px;
  }
  .primary-nav { display: none; }
  .site-header:target .primary-nav,
  .nav-is-open .primary-nav {
    display: flex;
    position: absolute;
    top: calc(100% - 2px);
    right: 12px;
    z-index: 20;
    min-width: 190px;
    width: auto;
    flex-direction: column;
    align-items: stretch;
    gap: 1px;
    margin: 0;
    padding: var(--space-2);
    max-height: calc(100dvh - var(--site-header-height) - 12px);
    overflow-y: auto;
    overscroll-behavior: contain;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-pop);
  }
  .site-header:target .primary-nav > a,
  .site-header:target .primary-nav .nav-group > summary,
  .nav-is-open .primary-nav > a,
  .nav-is-open .primary-nav .nav-group > summary {
    min-height: 44px;
    padding: var(--space-4) var(--space-5);
    font-size: var(--text-md);
  }

  /* A section's own links fly out beside the list, not inside it: opened in
     place, one would push every section below it down the panel. The list is
     anchored to the right edge, so the room is to its left. */
  .site-header:target .primary-nav .nav-group-panel,
  .nav-is-open .primary-nav .nav-group-panel {
    top: -5px;
    right: calc(100% + 6px);
    left: auto;
    min-width: 168px;
  }
  .site-header:target .primary-nav .nav-group-panel a,
  .nav-is-open .primary-nav .nav-group-panel a {
    min-height: 44px;
    display: flex;
    align-items: center;
  }

}

/* The nav row is nowrap, so below roughly 830px the four grid tracks do not
   fit; the nav gets its own row here rather than overlapping the search. */
@media (max-width: 860px) {
  .site-header { gap: var(--space-4) var(--space-6); padding: var(--space-5) var(--space-7); }
  .brand { font-size: var(--text-md); }
  /* The sections live in the toggle's panel from 1080px down, so the bar stays
     a single row here and needs no wrapping rules of its own. */
  .primary-nav a { white-space: nowrap; padding: var(--space-3) var(--space-5); font-size: var(--text-sm); }

  /* The panel joins the flow instead of floating over it. On a phone there is
     no room to overlay, and a panel that pushes the page down cannot end up
     covering the controls beneath it and swallowing their taps. */
  .nav-group { position: static; }
  .nav-group[open] { width: 100%; }
  /* In flow, a closed panel keeps its box and would sit invisibly over the
     open one, catching its taps. */
  .nav-group:not([open]) .nav-group-panel { display: none; }
  .nav-group-panel {
    position: static;
    width: 100%;
    min-width: 0;
    margin-top: var(--space-2);
    box-shadow: none;
    border-radius: var(--radius-sm);
  }
  .user-menu { order: 2; }
}

/* ── Renewal timeline ───────────────────────────────────────────────
   A countdown list. The number leads because "how long have I got" is the
   question being asked; the date answers the follow-up. */

.renewal-list { list-style: none; margin: 0; padding: 0; }
.renewal-row {
  display: grid;
  grid-template-columns: 56px 1fr;
  align-items: baseline;
  gap: var(--space-7);
  padding: var(--space-5) 0;
  border-top: 1px solid var(--line-2);
}
.renewal-row:first-child { border-top: 0; padding-top: var(--space-1); }
.renewal-when {
  font-size: var(--text-2xl); font-weight: 650; letter-spacing: -.02em;
  color: var(--ink); text-align: right; font-feature-settings: "tnum";
}
.renewal-when.is-due { color: var(--attn); }
.renewal-unit { font-size: var(--text-xs); font-weight: 550; color: var(--muted); margin-left: 1px; }
.renewal-body { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.renewal-title { font-size: var(--text-sm); font-weight: 600; color: var(--ink); }
.renewal-meta { font-size: var(--text-xs); color: var(--muted); }

/* ── Planning timeline ─────────────────────────────────────────────
   One chronological primitive for every domain. A two-column horizon uses the
   desktop canvas without turning eight dates into a full page of vertical rail;
   source order remains chronological and collapses to one column on phones. */
.timeline-head { align-items: flex-start; }
.timeline-head > div { min-width: 0; }
.timeline-head .count { margin-left: auto; }
.timeline-description { margin: var(--space-2) 0 0; color: var(--muted); font-size: var(--text-xs); line-height: 1.45; }
.timeline-list {
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr));
  column-gap: var(--space-14);
  /* Each item draws the rule above it and the list clips the first row's, so
     the dividers are right in one column or two without counting items. */
  overflow: clip;
}
.timeline-item {
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr) auto;
  align-items: center;
  column-gap: var(--space-6);
  min-height: 62px;
  padding: var(--space-3) 0;
  box-shadow: 0 -1px 0 var(--line-2);
}
.timeline-item time { display: flex; flex-direction: column; align-items: center; font-feature-settings: "tnum"; }
.timeline-month { color: var(--muted); font-size: var(--text-3xs); font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
.timeline-day { color: var(--ink); font-size: var(--text-2xl); font-weight: 650; line-height: 1.1; }
.timeline-year { color: var(--muted-2); font-size: var(--text-3xs); font-weight: 600; letter-spacing: .04em; }
.timeline-copy { display: flex; flex-direction: column; min-width: 0; gap: var(--space-1); font-size: var(--text-sm); }
.timeline-copy a { color: var(--ink); }
.timeline-copy a:hover { color: var(--accent); }


/* ── Notice pages (404 / 500) ───────────────────────────────────────
   Deliberately plain: the operator is here by accident, so the page
   orients and offers one way out rather than decorating the mistake. */

.notice-page { max-width: 44ch; margin: 12vh auto 0; text-align: center; }
.notice-code {
  margin: 0 0 var(--space-3); font-size: var(--text-xs); font-weight: 650; letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted-2); font-feature-settings: "tnum";
}
.notice-page h1 { margin: 0 0 var(--space-5); font-size: var(--text-4xl); letter-spacing: -.02em; }
.notice-body { margin: 0 0 var(--space-10); color: var(--muted); line-height: 1.6; }
.notice-page .page-actions { justify-content: center; display: flex; }

/* In a narrow box (tables.js marks it by the box's width, not the screen's)
   the cadence matrix drops its oldest periods rather than scrolling. A
   sideways scrollbar under a five-column grid of dots is both ugly and
   useless: the columns only mean anything lined up against each other, and
   half of them parked off-screen are not lined up against anything. The
   three most recent survive, which is the end of the row an eye goes to
   first and the part a decision is made from; the full history is a tap away
   on the page this panel links to.
   Counted from the end, so it is "the last three" whatever the window
   length happens to be, rather than a fixed set of column numbers. */
.cadence-matrix[data-narrow] th:nth-child(n+2):nth-last-child(n+5),
.cadence-matrix[data-narrow] td:nth-child(n+2):nth-last-child(n+5) { display: none; }
/* The "last trained" line is what pushes the label column past a narrow box.
   The dots are the reading; the exact recency is on the split page. */
.cadence-matrix[data-narrow] .cadence-detail { display: none; }
.cadence-matrix[data-narrow] th, .cadence-matrix[data-narrow] td { padding: var(--space-3) var(--space-1); }

/* ── Cadence strip ──────────────────────────────────────────────────
   Whether a thing happened, period by period. A filled mark is a period it
   happened in; the gaps are the point, so they stay the same size and shape as
   the hits rather than collapsing. */

.cadence { display: flex; flex-direction: column; gap: var(--space-4); }
.cadence-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-5); }
.cadence-title { font-size: var(--text-sm); font-weight: 650; color: var(--ink); }
.cadence-count { font-size: var(--text-xs); color: var(--muted); font-feature-settings: "tnum"; white-space: nowrap; }
.cadence-strip { display: flex; flex-wrap: wrap; gap: var(--space-5); list-style: none; margin: 0; padding: 0; }
.cadence-week { display: flex; flex-direction: column; align-items: center; gap: var(--space-2); min-width: 34px; }
.cadence-mark {
  width: 26px; height: 26px; border-radius: 50%;
  border: 1.5px solid var(--line);
  background: var(--surface-2);
  display: flex; align-items: center; justify-content: center;
  font-size: var(--text-2xs); font-weight: 650; color: var(--muted);
  font-feature-settings: "tnum";
}
.cadence-mark.is-hit { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
/* A period nothing is known about. Dashed and unfilled with the missing-value
   dash inside, so it reads as "no reading" beside a period that was skipped,
   by shape as well as by colour. */
.cadence-mark.is-no-data { background: transparent; border-style: dashed; color: var(--muted-2); }
.cadence-mark.is-no-data::before { content: "\2013"; }
.cadence-label { font-size: var(--text-3xs); color: var(--muted-2); white-space: nowrap; }
.cadence-note { margin: 0; font-size: var(--text-xs); color: var(--muted); line-height: 1.5; }
/* Anchored at the top-left of its containing block. At its static position, a
   label in the last column of a wide table would sit past the page edge,
   outside both the table's scroll box and main's clip, and scroll the whole
   page sideways. */
.visually-hidden { position: absolute; inset-block-start: 0; inset-inline-start: 0; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

/* Cadence matrix: the same marks as a single strip, aligned in columns so the
   rows can be read against each other. Deliberately not stacked on a phone: a
   matrix that becomes a list of lists loses the alignment it exists for, and a
   row of five dots fits a narrow screen comfortably. */
.cadence-matrix th[scope="col"] { text-align: center; }
.cadence-matrix th[scope="col"]:first-child { text-align: left; }
.cadence-matrix th[scope="row"] { text-align: left; font-weight: 650; white-space: nowrap; }
.cadence-matrix th[scope="row"] a { color: var(--ink); }
.cadence-matrix th[scope="row"] a:hover { color: var(--accent); }
.cadence-detail { display: block; font-weight: 400; font-size: var(--text-2xs); }
.cadence-cell { text-align: center; }
.cadence-cell .cadence-mark { margin: 0 auto; }
.cadence-period { font-size: var(--text-2xs); color: var(--muted-2); font-weight: 500; white-space: nowrap; }
.cadence-total { text-align: center; font-feature-settings: "tnum"; color: var(--muted); white-space: nowrap; }
/* Compact: for a matrix sharing a row with a chart, where the default cell
   padding would push eight columns past a half-width card. */
.cadence-matrix.is-compact th,
.cadence-matrix.is-compact td { padding: var(--space-3) var(--space-2); }
.cadence-matrix.is-compact .cadence-period { font-size: var(--text-3xs); }
.cadence-matrix.is-compact th[scope="row"] { font-size: var(--text-sm); }
.cadence-matrix.is-compact .cadence-detail { font-size: var(--text-3xs); }

/* Tabbed charts. Several drawings of one thing in the space of one, built from
   radios so the panel works with no JavaScript and the arrow keys move between
   tabs for free. The inputs are hidden from sight but not from the accessibility
   tree, which is what keeps them focusable. */
.chart-tabs { padding-top: var(--space-6); }
.chart-tablist { display: flex; flex-wrap: wrap; gap: var(--space-3); margin: 0 0 var(--space-5); }
.chart-tab-input { position: absolute; opacity: 0; pointer-events: none; }
.chart-tab {
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: var(--space-2) var(--space-6);
  font-size: var(--text-xs);
  font-weight: 550;
  color: var(--muted);
  background: var(--bg-tint);
  cursor: pointer;
}
.chart-tab:hover { color: var(--ink); }
.chart-tab-input:checked + .chart-tab {
  background: var(--surface);
  border-color: var(--ink-2);
  color: var(--ink);
}
.chart-tab-input:focus-visible + .chart-tab { outline: 2px solid var(--accent); outline-offset: 2px; }
.chart-panel { display: none; }
/* The nth panel belongs to the nth radio. Written out rather than generated:
   the tab count is small, and `nth-of-type` on the panels would count the tab
   strip as well, since both are divs. Numbered classes say exactly which panel
   is meant and cannot be thrown off by a wrapper appearing between them. */
.chart-tabs .chart-tab-input:nth-of-type(1):checked ~ .chart-panel-1,
.chart-tabs .chart-tab-input:nth-of-type(2):checked ~ .chart-panel-2,
.chart-tabs .chart-tab-input:nth-of-type(3):checked ~ .chart-panel-3,
.chart-tabs .chart-tab-input:nth-of-type(4):checked ~ .chart-panel-4 { display: block; }
/* A chart inside a tab panel already sits in a card, so it must not draw a
   second border and a second block of padding inside the first. */
.chart-panel .chart-card { border: 0; padding: 0; margin: 0; box-shadow: none; background: none; }

/* What a reading was worth, in the outcome's own units. Coloured by whether it
   helped, with the sign and the number carrying the meaning: the colour is
   never the only thing saying which way it went. */
.worth { font-variant-numeric: tabular-nums; }
.worth-good { color: var(--ok); }
.worth-attention { color: var(--warn); }
.worth-neutral { color: var(--ink-2); }

/* A finding stated once, with its arithmetic underneath. The claim is what
   gets read; the numbers are what make it checkable. */
.finding { margin: var(--space-1) 0 var(--space-1); font-size: var(--text-lg); font-weight: 600; line-height: 1.4; }
.finding-evidence { margin: 0 0 var(--space-7); font-size: var(--text-xs); }

/* A route, drawn from its own points. No basemap, so the line carries
   everything: shape for where, colour for how fast. */
.route-frame { background: var(--bg-tint); border-radius: var(--radius); padding: var(--space-2); margin: var(--space-5) 0 var(--space-2); }
.route-map { display: block; width: 100%; height: auto; }
/* Semi-transparent so a retraced route shows its repeats as density rather
   than hiding every pass but the last under an opaque line. */
.route-line { fill: none; stroke-width: 3.5; stroke-linecap: round; stroke-linejoin: round; stroke-opacity: .55; transition: opacity .08s ease; }
/* A sequential ramp, slowest to fastest. Deliberately not the categorical
   series palette: these bands are ordered, and a reader has to be able to see
   which way the order runs without consulting the legend. */
.route-band-1 { stroke: var(--route-band-1); }
.route-band-2 { stroke: var(--route-band-2); }
.route-band-3 { stroke: var(--route-band-3); }
.route-band-4 { stroke: var(--route-band-4); }
.route-band-5 { stroke: var(--route-band-5); }
.route-swatch { inline-size: 9px; block-size: 9px; border-radius: 2px; }
.route-band-1.route-swatch { background: var(--route-band-1); }
.route-band-2.route-swatch { background: var(--route-band-2); }
.route-band-3.route-swatch { background: var(--route-band-3); }
.route-band-4.route-swatch { background: var(--route-band-4); }
.route-band-5.route-swatch { background: var(--route-band-5); }
.route-marker { stroke: var(--surface); stroke-width: 2.5; }
.route-start { fill: var(--ok); }
.route-finish { fill: var(--tone-severe-ink); }
.route-scale { stroke: var(--ink-2); stroke-width: 2; }
.route-map:hover .route-line:not(:hover) { opacity: .55; }

/* ── Background jobs ────────────────────────────────────────────── */
/* Work that outlives its request, reporting where it has got to. */
.job-progress { margin: 0 0 var(--stack-gap); }
.job-progress-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-6); }
.job-state { color: var(--muted); font-size: var(--text-xs); text-transform: uppercase; letter-spacing: .05em; }
/* The track. `overflow: hidden` so the fill's corners are the track's. */
.job-bar {
  position: relative;
  overflow: hidden;
  block-size: 6px;
  margin: var(--space-5) 0 var(--space-4);
  border-radius: 999px;
  background: var(--surface-2);
}
/* Filled to `--at`, set by the poller. Transitioned so a jump from 10% to
   60% between polls reads as progress rather than a glitch. */
.job-bar i {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: var(--at, 0%);
  border-radius: inherit;
  background: var(--accent);
  transition: inline-size .4s ease;
}
/* Work that cannot say how far along it is: a travelling band rather than a
   bar sitting at a number nobody computed. */
.job-bar-unknown i {
  inline-size: 30%;
  animation: job-sweep 1.4s ease-in-out infinite;
}
@keyframes job-sweep {
  0% { inset-inline-start: -30%; }
  100% { inset-inline-start: 100%; }
}
.job-note { margin: 0; color: var(--muted); font-size: var(--text-sm); }
.job-progress[data-state="failed"] .job-bar i,
.job-progress[data-state="lost"] .job-bar i { background: var(--danger); }
.job-progress[data-state="succeeded"] .job-bar i { background: var(--ok, var(--accent)); }
/* Reduced motion: the sweep is decoration. The bar still fills. */
@media (prefers-reduced-motion: reduce) {
  .job-bar-unknown i { animation: none; inline-size: 100%; opacity: .35; }
  .job-bar i { transition: none; }
  .page-nav-list a { transition: none; }
}

/* Shown only on paper. In the document rather than in CSS `content` so it
   can carry real values: a date, a name. */
.print-only { display: none; }

/* ── Print ──────────────────────────────────────────────────────── */
/* A page that leaves the building: the care brief is handed to a clinician on
   paper, in black and white. Three things break when a screen page is printed
   without help: backgrounds and borders are dropped, so cards dissolve;
   anything carried by colour is carried by nothing; and a chart lands half on
   one page and half on the next. */
@media print {
  /* Room for the browser's own header and footer. */
  @page { margin: 14mm 12mm; }

  html, body { background: var(--surface); color: var(--print-ink); font-size: 10.5pt; }
  .content { max-width: none; padding: 0; overflow: visible; }

  /* Chrome that means nothing on paper: navigation cannot be followed, the
     search box cannot be typed into, buttons cannot be pressed. */
  .site-header, .primary-nav, .global-search, .user-menu, .user-menu-panel,
  .page-nav,
  .table-filter-menu, .table-selection, .section-foot,
  .btn, button, .job-progress, .page-actions, form,
  /* `.pill` in a page head is a link ("Markdown", "Care →"). Status pills
     inside a table are content and are kept. */
  .page-head .pill, .section-head .pill,
  /* The app's warning banner addresses whoever is at the screen. Printed
     under a page of gait measurements it reads as an accusation. */

  /* Cards become ruled sections: backgrounds are dropped, so the separation
     has to be drawn in ink rather than in a tint. */
  .card, figure.chart-card, .two-col > section, .two-col > div {
    background: none;
    border: 1px solid var(--print-ink);
    box-shadow: none;
    padding: 10pt 12pt;
    margin: 0 0 8pt;
  }


  /* The card is the unit that moves whole. `table` deliberately does *not*
     avoid breaking: nested elements that each refuse to split are how a card
     starts on one page, its table jumps to the next, and the card's opening
     border is left behind as an empty rectangle. Rows still hold together. */
  .card, figure.chart-card, .section-head, tr { break-inside: avoid; }
  h1, h2, h3 { break-after: avoid; }

  /* Two across on paper, where the screen fits three: A4 gives about 186mm
     between the margins, so a pair is ~90mm each: close to the width these
     are legible at, and six measures in three rows rather than six pages. */
  /* Charts only. Two five-column tables at half width lose their last
     columns off the edge of the paper. Selected by what the row contains, so
     it cannot be applied to a row of tables by accident. */
  /* Inline-block columns, not a grid: Chromium fragments grid items across
     page breaks badly, printing the card as an empty box with no chart in it.
     49% rather than 50% because inline-block boxes are separated by the
     whitespace between the tags. */
  .two-col:has(> .chart-card) { display: block; font-size: 0; }
  .two-col:has(> .chart-card) > .chart-card {
    display: inline-block;
    width: 49%;
    vertical-align: top;
    font-size: 10.5pt;
    margin: 0 0 8pt;
  }
  .two-col:has(> .chart-card) > .chart-card:nth-child(odd) { margin-right: 1.5%; }
  /* Everything else stacks and keeps full width, which is how a wide table
     survives the margins. */
  .two-col:not(:has(> .chart-card)) { display: block; }
  .two-col:not(:has(> .chart-card)) > * { margin-bottom: 8pt; }
  .chart-frame, .line-chart, .bar-chart { max-width: none !important; }
  .chart-card { margin-bottom: 0; }

  /* These charts use colour to separate a declared measure from context; on
     paper that distinction lives in the caption instead and every line is
     drawn in the one ink there is. `print-color-adjust` is what keeps the
     grid a grey rather than nothing. */
  .chart-line, .chart-line-1, .chart-line-2, .chart-line-3, .chart-line-4,
  .chart-line-5 { stroke: var(--print-ink) !important; }
  .chart-dot, .chart-series-1, .chart-series-2, .chart-series-3,
  .chart-series-4, .chart-series-5 { fill: var(--print-ink) !important; }
  .chart-grid, .chart-mark {
    stroke: var(--print-faint) !important;
    print-color-adjust: exact;
    -webkit-print-color-adjust: exact;
  }
  .chart-trend { stroke: var(--print-rule) !important; }
  .chart-axis, .chart-mark-label { color: var(--print-ink); fill: var(--print-ink); }
  /* A hit area for a pointer paper does not have. */
  .chart-hit { display: none; }

  /* Muted greys print faint, and the small print is the part a careful
     reader wants. */
  /* Secondary greys (dates, reading counts, chart captions) recede on a
     backlit display and disappear on paper. On paper it is all one ink. */
  .muted, .lede, p, td, th, li, span, small, figcaption, .count,
  .page-lede, .chart-card figcaption p,
  /* Headings too: a muted grey reads as hierarchy on screen and as faded
     toner on paper. */
  h1, h2, h3, .section-head h2, .page-head h1 { color: var(--print-ink) !important; }
  .data-table th, .data-table td { border-bottom: 0.5pt solid var(--print-rule); padding: 4pt 6pt; }
  .data-table thead th { background: none; border-bottom: 1pt solid var(--print-ink); }
  .table-scroll { overflow: visible; }
  /* A heading pinned to a viewport that does not exist on paper. */
  .data-table thead th { position: static; box-shadow: none; }

  /* Disclosures print closed, silently dropping their contents, and those
     are the numbers behind every drawing. */
  details { display: block; }
  details > summary { display: none; }
  details > *:not(summary) { display: revert; }

  a { color: var(--print-ink); text-decoration: none; }

  /* A sheet of numbers with no date on it is one nobody can place. Rendered
     by the page, not here: `counter(page)` only resolves inside an `@page`
     margin box and silently produces nothing in ordinary flow. */
  .print-only { display: block !important; }
  .print-footer {
    margin-top: 10pt;
    padding-top: 6pt;
    border-top: 0.5pt solid var(--print-rule);
    font-size: 8pt;
    color: var(--print-muted);
  }
}


/* ---- Responsive and late component rules ------------------------------
   Same layer, later in source: a rule here beats the one it narrows by order
   alone, which is how CSS is meant to express 'and at this width, this
   instead'. Nothing here needs to outrank anything by specificity. */

/* The folded half of a zone. Its table keeps the same styling as the one above
   it, so opening it reads as more of the same list rather than a new thing. */
.zone-policy { margin-top: var(--stack-gap); }
.zone-policy summary { cursor: pointer; font-size: var(--text-sm); font-weight: 600; color: var(--ink-2); }
.zone-policy .table-scroll { margin-top: var(--space-5); }

/* A starred domain reads as chosen, not as merely another control. */
.btn.pinned { border-color: var(--accent); color: var(--accent); }
.zone-switch a > span[aria-hidden] { color: var(--accent); }

/* Facts above a form: what the thing is, before what can be changed. */
.form-facts { margin: 0 0 var(--stack-gap); padding: var(--space-5) var(--space-6); background: var(--bg-tint); border-radius: var(--radius-sm); }

/* Overflow menu: the actions worth having, without a second row of buttons.
   The panel is the nav's dropdown (same box, same shadow, same radius) so
   there is one dropdown in HQ rather than two that nearly match. */
.overflow-menu { position: relative; display: flex; align-self: center; }
/* Exactly a button's height, stated: its glyph comes from whatever face the
   device has for it, and must not set the size of the control. */
.overflow-menu > summary {
  display: inline-flex; align-items: center; justify-content: center;
  box-sizing: border-box; height: var(--control-size); min-width: 38px;
  padding: 0 var(--space-5);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--ink);
  box-shadow: var(--shadow-sm); cursor: pointer;
  font-size: var(--text-sm); font-weight: 500; line-height: 1;
  list-style: none;
}
.overflow-menu > summary::-webkit-details-marker { display: none; }
.overflow-menu > summary:hover,
.overflow-menu[open] > summary { border-color: var(--line-strong); background: var(--surface-2); }
.overflow-menu-panel {
  position: absolute; z-index: 40; top: calc(100% + 6px); right: 0;
  min-width: 200px; max-width: min(320px, calc(100vw - 32px));
  display: flex; flex-direction: column; gap: 1px;
  padding: var(--space-2); background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow);
  text-align: left;
}
/* The nav panel's own item metrics *and* its type: same colour, weight, size
   and padding, so the two dropdowns read as one object. */
.overflow-menu-panel a,
.overflow-menu-panel span,
.overflow-menu-panel button {
  padding: var(--space-3) var(--space-5); border-radius: var(--radius-sm);
  font-size: var(--text-sm); font-weight: 500; color: var(--muted);
  white-space: nowrap;
}
.overflow-menu-panel a:hover, .overflow-menu-panel button:hover { color: var(--ink); background: var(--bg-tint); text-decoration: none; }
/* A control from a page head, in the menu: a row like the others, whatever
   kind of button it was beside the title. Destructive keeps only its colour. */
.overflow-menu-panel :is(a, button, span).btn {
  display: flex; width: 100%; justify-content: flex-start; min-height: 0;
  border: 0; background: none; box-shadow: none;
}
.overflow-menu-panel .btn.danger { color: var(--danger); }

.name-list { display: flex; flex-direction: column; gap: var(--space-3); align-items: flex-start; max-width: 40rem; }
.name-list-row { display: flex; flex-wrap: wrap; gap: var(--space-3); width: 100%; }
/* Beside its Remove, whatever `.field input` says about width. */
.name-list-row input { flex: 1 1 12rem; width: auto; min-width: 0; }
.name-list-add { margin-top: var(--space-1); }
/* Says which values HQ can see for itself, beside the ones only this field
   records. Wraps under the input on a narrow screen rather than squeezing it. */
.name-list-note {
  align-self: center;
  color: var(--muted);
  font-size: 0.85em;
  white-space: nowrap;
}
/* A value a sweep reports. Shown as the fact it is rather than as an input,
   because HQ holds it whether or not this field does. */
.name-list-row-observed { align-items: center; }
.name-list-observed {
  flex: 1 1 auto;
  min-width: 0;
  padding: var(--space-3) 0;
  font-family: var(--font-mono);
  overflow-wrap: anywhere;
}

/* What a field will cause, said before it is caused. */
.field-effect { color: var(--warn); font-weight: 500; }
.form-will-act .form-actions button[type="submit"] { background: var(--accent-2); border-color: var(--accent-2); }

/* Email policy: four stages, then the one decision worth making. */
.mail-stages { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--layout-gap); margin: 0 0 var(--stack-gap); }
.mail-stage { padding: var(--surface-inset, var(--space-6) var(--space-7)); }
.mail-stage-concern { border-color: var(--warn); }
.mail-stage-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--space-2) var(--space-5); }
.mail-stage-head strong { font-size: var(--text-lg); overflow-wrap: anywhere; }
.mail-stage-question { margin: var(--space-3) 0 var(--space-1); font-size: var(--text-sm); font-weight: 500; }
.mail-stage-warning { margin: var(--space-4) 0 0; font-size: var(--text-sm); color: var(--warn); }
.mail-records { list-style: none; margin: var(--space-5) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-3); }
.mail-records li { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-3); font-size: var(--text-sm); }
.mail-record-value { color: var(--muted); overflow-wrap: anywhere; }

/* Choices side by side while each keeps a readable width. */
.policy-choices { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); gap: var(--space-4); }
.policy-choice { display: flex; gap: var(--space-5); align-items: flex-start; padding: var(--space-5) var(--space-6); border: 1px solid var(--line); border-radius: var(--radius-sm); cursor: pointer; background: var(--surface); }
.policy-choice:hover { border-color: var(--line-strong); background: var(--surface-2); }
.policy-choice:has(input:checked) { border-color: var(--accent); background: var(--bg-tint); }
.policy-choice input { margin-top: var(--space-1); flex: none; }
.policy-choice-body { display: flex; flex-direction: column; gap: var(--space-1); }
.policy-choice-label { font-weight: 600; font-size: var(--text-md); }
.policy-choice-consequence { color: var(--muted); font-size: var(--text-sm); line-height: 1.45; }

.mail-editor { padding: var(--surface-inset, var(--space-7) var(--space-8)); margin: 0 0 var(--stack-gap); }
.mail-editor h3 { margin: 0 0 var(--space-6); font-size: var(--text-lg); }
/* A number with its unit beside it, so a percentage never reads as a count. */
.input-suffix { display: flex; align-items: center; gap: var(--space-2); max-width: 9rem; }
.input-suffix input { flex: 1 1 auto; min-width: 0; }
.input-suffix span { color: var(--muted); font-weight: 600; }

/* A record shown inside help text is one long unbreakable token, and the
   content column clips rather than scrolls, so it breaks anywhere rather than
   losing its end on a phone. */
.help code, .mail-record-value { overflow-wrap: anywhere; }

/* ── Reachability answer ───────────────────────────────────────── */

.verdict { display: flex; align-items: baseline; gap: var(--space-4); margin: var(--space-7) 0 var(--space-2); }
.modal .filter-bar { margin-bottom: var(--space-2); align-items: center; }
.modal .filter-bar .control-label { margin: 0 var(--space-1) 0 0; }
/* ── Row-header tables ──────────────────────────────────────────────
   A `th[scope=row]` table with no `thead` is a description list in table
   markup. Its label column is as wide as its longest label and no wider,
   whatever the screen; the value takes the rest and breaks where it has to.
   The cadence matrix also uses row headers but is a real grid and sizes its
   own columns. */
.data-table th[scope=row] { width: 1%; padding-right: var(--space-10); font-weight: 600; color: var(--muted); white-space: nowrap; }

/* A long value the model puts no ceiling on: a policy document, a compose
   file. Monospace because it is structured text where alignment carries
   meaning, and nowrap-free so long lines wrap rather than hide. */
textarea.code {
  font-family: var(--font-mono);
  font-size: var(--text-xs); line-height: 1.5; tab-size: 2;
}

/* ----- This connection -------------------------------------------------- */

/* Wider than a normal dialog because this one is a table of readings rather
   than a short task; at the shared width every row would wrap onto several
   lines. */
.modal-wide {
  width: min(780px, calc(100vw - 32px));
  max-height: calc(100dvh - 32px);
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-y;
}
.modal-wide .modal-head {
  position: sticky;
  z-index: 5;
  top: 0;
  /* Cover the panel's top padding too. Without this, scrolled connection
     evidence paints in the strip above the sticky heading on mobile Safari. */
  padding: var(--space-9) 0 var(--space-5);
  margin-top: calc(var(--space-9) * -1);
  background: var(--surface);
}

.conn-dot {
  width: 8px; height: 8px; border-radius: 50%; flex: none;
  background: var(--muted-2);
}
.conn-badge-tailnet .conn-dot { background: var(--ok); }
.conn-badge-network .conn-dot, .conn-badge-loopback .conn-dot { background: var(--warn); }
.conn-badge-elsewhere .conn-dot { background: var(--danger); }

/* The panel is read in a dialog and on a page of its own, so what it does when
   it is narrow is asked of the panel, not the screen. */
.conn { display: grid; gap: var(--space-10); container: conn / inline-size; }
.conn-pending { padding: var(--space-10) 0; }
.conn h3 {
  font-size: var(--text-2xs); letter-spacing: .07em; text-transform: uppercase;
  color: var(--muted); margin: 0 0 var(--space-4);
}
.conn-note { color: var(--muted); font-size: var(--text-xs); margin: calc(var(--space-2) * -1) 0 var(--space-5); }

.conn-verdict {
  display: flex; align-items: center; gap: var(--space-6);
  padding: var(--space-6) var(--space-7); border-radius: var(--radius);
  background: var(--bg-tint); border-left: 3px solid var(--muted-2);
}
.conn-tailnet { border-left-color: var(--ok); }
.conn-network, .conn-loopback { border-left-color: var(--warn); }
.conn-elsewhere { border-left-color: var(--danger); }
.conn-verdict .conn-dot { margin: 0; width: 9px; height: 9px; }
.conn-tailnet .conn-dot { background: var(--ok); }
.conn-network .conn-dot, .conn-loopback .conn-dot { background: var(--warn); }
.conn-elsewhere .conn-dot { background: var(--danger); }
.conn-verdict-text { flex: 1; min-width: 0; }
.conn-headline { font-size: var(--text-xl); font-weight: 650; margin: 0; }
.conn-sub { margin: 1px 0 0; color: var(--muted); font-size: var(--text-xs); }
.conn-address { font-size: var(--text-sm); white-space: nowrap; }
.conn-decision, .conn-path { display: grid; gap: var(--space-4); }
.conn-findings { display: grid; gap: var(--space-2); }
.conn-findings .conn-agree { margin: 0; }
.conn-decision-head { display: flex; align-items: end; justify-content: space-between; gap: var(--space-7); }
.conn-decision-head h3 { margin-bottom: var(--space-1); }
.conn-decision-head p { margin: 0; color: var(--muted); font-size: var(--text-2xs); }
.conn-decision-head > strong { color: var(--muted); font-size: var(--text-2xs); font-weight: 550; white-space: nowrap; }
.conn-decision ol {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}
.conn-decision li {
  display: flex;
  flex: 1 0 132px;
  min-width: 0;
}
.conn-decision-step {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-3);
  align-items: center;
  min-height: 44px;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--ink-2);
  background: var(--surface);
  font-size: var(--text-3xs);
  line-height: 1.25;
  text-decoration: none;
  width: 100%;
}
.conn-decision-step:hover { border-color: var(--accent); text-decoration: none; }
.conn-decision-step-does-not { border-color: color-mix(in srgb, var(--danger) 42%, var(--line)); background: color-mix(in srgb, var(--tone-tint) 55%, var(--surface)); }
.conn-decision-step-does-not .conn-tick { background: var(--tone-tint); color: var(--danger); }
.conn-decision-step-unknown { border-color: color-mix(in srgb, var(--warn) 38%, var(--line)); background: color-mix(in srgb, var(--tone-tint) 45%, var(--surface)); }
.conn-decision-step-unknown .conn-tick { background: var(--tone-tint); color: var(--warn); }
.conn-related {
  display: flex;
  align-items: center;
  gap: var(--space-3) var(--space-6);
  flex-wrap: wrap;
  margin-top: -7px;
  padding-bottom: var(--space-5);
  border-bottom: 1px solid var(--line);
  font-size: var(--text-xs);
}
.conn-related > span { color: var(--muted); }
.conn-related a { font-weight: 600; }
.conn-protocol { border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); }
.conn-protocol > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-7);
  padding: var(--space-6) var(--space-7);
  cursor: pointer;
  list-style: none;
}
.conn-protocol > summary::-webkit-details-marker { display: none; }
.conn-protocol > summary > span:first-child { display: grid; gap: var(--space-1); }
.conn-protocol > summary strong { font-size: var(--text-sm); }
.conn-protocol > summary small { color: var(--muted); font-size: var(--text-2xs); }
.conn-protocol > summary .count { margin-left: auto; }
.conn-protocol-body { display: grid; gap: var(--space-10); padding: var(--space-8) var(--space-7); border-top: 1px solid var(--line); }

/* Two columns of label/value pairs. A grid rather than a table so the labels
   share one width across both columns and the values line up with each other. */
.conn-cols { display: grid; gap: var(--space-10) var(--space-14); grid-template-columns: 1fr 1fr; align-items: start; }
.conn-pairs { display: grid; grid-template-columns: auto 1fr; gap: var(--space-3) var(--space-7); align-items: baseline; }
.conn-pairs dt { color: var(--muted); font-size: var(--text-xs); white-space: nowrap; }
.conn-pairs dd { margin: 0; font-size: var(--text-sm); min-width: 0; overflow-wrap: anywhere; }
.conn-pairs dd code { font-size: var(--text-xs); }
.conn-pairs strong { font-weight: 600; }
.conn-agree { font-size: var(--text-xs); margin: var(--space-6) 0 0; }
.conn-agree-yes { color: var(--ok); }
.conn-agree-no { color: var(--warn); }

/* Address rows: value and kind hold their width, the note takes the rest and
   is the only thing allowed to wrap. */
.conn-rows { display: grid; gap: var(--space-1); }
.conn-row {
  display: grid; grid-template-columns: minmax(0, auto) max-content 1fr;
  gap: var(--space-6); align-items: center; padding: var(--space-3) 0;
  border-bottom: 1px solid var(--line-2);
}
.conn-row:last-child { border-bottom: 0; }
.conn-row code { font-size: var(--text-xs); overflow-wrap: anywhere; }
.conn-row-note { color: var(--muted); font-size: var(--text-xs); }
.conn-current code { font-weight: 650; }
.conn-kind {
  font-size: var(--text-3xs); letter-spacing: .04em; padding: var(--space-1) var(--space-4); border-radius: 20px;
  background: var(--bg-tint); color: var(--muted); white-space: nowrap;
}
.conn-kind-tailnet { background: var(--tone-tint); color: var(--ok); }
.conn-kind-elsewhere, .conn-kind-public { background: var(--tone-tint); color: var(--danger); }

/* The chain HQ walked to decide which address it is judging you by. */
.conn-hops { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-2); }
.conn-hop {
  display: grid; grid-template-columns: minmax(0, auto) max-content 1fr;
  gap: var(--space-6); align-items: center;
  padding: var(--space-4) var(--space-6); border: 1px solid var(--line); border-radius: var(--radius);
}
.conn-hop-judged { border-color: var(--accent); background: var(--accent-tint); }
.conn-hop-ignored { opacity: .6; }
.conn-kind-hop-judged { background: var(--accent); color: var(--accent-ink); }
.conn-kind-hop-ignored { background: var(--tone-tint); color: var(--danger); }

.conn-layers { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-2); }
.conn-layer { border: 1px solid var(--line); border-radius: var(--radius); }
.conn-layer summary {
  display: grid; grid-template-columns: auto minmax(88px, .55fr) minmax(150px, 1fr) minmax(0, .9fr);
  align-items: center; gap: var(--space-5);
  padding: var(--space-4) var(--space-6); cursor: pointer; list-style: none;
}
.conn-layer summary::-webkit-details-marker { display: none; }
.conn-tick {
  width: 17px; height: 17px; border-radius: 50%; flex: none;
  display: grid; place-items: center; font-size: var(--text-2xs);
  background: var(--tone-positive-tint); color: var(--ok);
}
.conn-layer-does-not .conn-tick { background: var(--tone-critical-tint); color: var(--danger); }
.conn-layer-unknown { border-color: color-mix(in srgb, var(--warn) 38%, var(--line)); }
.conn-layer-unknown .conn-tick { background: var(--tone-caution-tint); color: var(--warn); }
.conn-layer-boundary {
  color: var(--muted); font-size: var(--text-3xs); font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase;
}
.conn-layer-label { font-weight: 550; font-size: var(--text-sm); min-width: 0; overflow-wrap: anywhere; }
.conn-layer-evidence {
  justify-self: end; font-size: var(--text-2xs); color: var(--muted);
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.conn-layer-body { margin: 0 var(--space-6) var(--space-5) var(--space-20); display: grid; gap: var(--space-4); }
.conn-layer-body p { margin: 0; font-size: var(--text-sm); color: var(--ink-2); }
.conn-layer-body dl { display: flex; gap: var(--space-4); margin: 0; font-size: var(--text-2xs); }
.conn-layer-body dt { color: var(--muted); }
.conn-layer-body dd { margin: 0; font-weight: 600; }
.conn-scope-note {
  margin: 0; padding-top: var(--space-6); border-top: 1px solid var(--line-2);
  color: var(--muted); font-size: var(--text-2xs);
}

/* A live reading, so it reads as a measurement being taken rather than a
   number that was true once. */
.conn-live { display: inline-flex; align-items: center; gap: var(--space-4); }
.conn-pulse {
  width: 7px; height: 7px; border-radius: 50%; background: var(--ok);
  animation: conn-pulse 1.8s ease-in-out infinite;
}
@keyframes conn-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: .35; transform: scale(.72); }
}
@media (prefers-reduced-motion: reduce) { .conn-pulse { animation: none; } }
.conn-samples { display: inline-flex; align-items: flex-end; gap: var(--space-1); height: 13px; }
.conn-samples i {
  width: 3px; background: var(--accent-2); border-radius: 1px;
  height: calc(var(--at) * 1%); min-height: 2px;
}

@container conn (max-width: 36rem) {
  .conn-cols { grid-template-columns: 1fr; }
  .conn-verdict { flex-wrap: wrap; }
  .conn-address { width: 100%; }
  .conn-layer-evidence { display: none; }
  .conn-layer summary { grid-template-columns: auto minmax(0, .7fr) minmax(0, 1fr); }
  .conn-layer-body { margin-left: var(--space-6); }
  .conn-decision-head { align-items: start; }
  .conn-decision-head > strong { max-width: 130px; white-space: normal; text-align: right; }
  .conn-related > span { flex-basis: 100%; }
  /* Value and kind on one line, the note beneath, rather than three columns
     that cannot each hold an IPv6 address. */
  .conn-row, .conn-hop { grid-template-columns: minmax(0, 1fr) max-content; }
  .conn-row-note, .conn-hop .conn-row-note { grid-column: 1 / -1; }
}

/* The two ends and the link between them. */
.conn-peering {
  display: grid; grid-template-columns: 1fr auto 1fr; gap: var(--space-7); align-items: center;
  padding: var(--space-7) var(--space-8); border: 1px solid var(--line); border-radius: var(--radius);
}
.conn-peer { display: grid; gap: var(--space-1); justify-items: start; min-width: 0; }
.conn-peering > .conn-peer:last-child { justify-items: end; text-align: right; }
.conn-peer-role {
  font-size: var(--text-3xs); letter-spacing: .07em; text-transform: uppercase; color: var(--muted-2);
}
.conn-peer strong { font-size: var(--text-md); }
.conn-peer code { font-size: var(--text-xs); }
.conn-link { display: grid; gap: var(--space-2); justify-items: center; min-width: 120px; }
.conn-link-label { font-size: var(--text-2xs); font-weight: 600; color: var(--accent-2); }
.conn-link-line {
  width: 100%; height: 2px; border-radius: 2px;
  background: linear-gradient(90deg, var(--accent-2), var(--ok));
}
.conn-link-relayed .conn-link-line { background: repeating-linear-gradient(
  90deg, var(--warn) 0 5px, transparent 5px 10px); }
.conn-link-relayed .conn-link-label { color: var(--warn); }
.conn-link-idle .conn-link-line { background: var(--line); }
.conn-link-idle .conn-link-label, .conn-link-unknown .conn-link-label { color: var(--muted); }
@container conn (max-width: 36rem) {
  /* Stacked, the two ends read as one column rather than as two facing each
     other, so they share an axis instead of one hugging each edge. */
  .conn-peering { grid-template-columns: 1fr; text-align: center; }
  .conn-peer, .conn-peering > .conn-peer:last-child {
    justify-items: center; text-align: center;
  }
  .conn-link-line { height: 2px; }
}

/* On a phone the dialog stops being a dialog and becomes the page: this one
   is long enough that a floating box with margins on four sides wastes the
   only dimension there is. */
@media (max-width: 560px) {
  .modal-wide {
    position: fixed; inset: 0;
    width: 100vw; max-width: 100vw; height: 100dvh; max-height: none;
    margin: 0; border: 0; border-radius: 0;
  }
  .modal-wide .modal-panel {
    min-height: 100%;
    padding-top: 0;
  }
  .modal-wide .modal-head {
    margin-top: 0;
    padding-top: max(18px, env(safe-area-inset-top));
  }
}

.conn-kind-read { background: var(--accent-tint); color: var(--accent); }
.conn-row-used code { color: var(--ink); }
.conn-more > summary {
  cursor: pointer; font-size: var(--text-xs); color: var(--muted); padding: var(--space-4) 0 var(--space-2);
}
.conn-opaque { border-left-color: var(--danger); }
.conn-opaque .conn-dot { background: var(--danger); }
.conn-badge-opaque .conn-dot { background: var(--danger); }
.conn-rules { display: grid; gap: var(--space-2); margin: 0 var(--space-6) var(--space-5) var(--space-20); justify-items: start; }
.conn-rules code { font-size: var(--text-2xs); }

/* An address is as wide as the address. Stretching each one to the column
   turns a list of six values into six grey slabs. */
.conn-row > code, .conn-hop > code { justify-self: start; }
.conn-peer-name { font-size: var(--text-md); font-weight: 600; }
.conn-kind-declined { background: var(--tone-tint); color: var(--warn); }

/* A header value is long by nature, and beside it a three-column row squeezes
   the note into one word per line the moment the panel is a dialog rather than
   a page. The value keeps the width it needs and the note takes its own line. */
.conn-row-wide { grid-template-columns: minmax(0, 1fr) max-content; }
.conn-row-wide > code { overflow-wrap: anywhere; }
.conn-row-wide .conn-row-note { grid-column: 1 / -1; }

/* A row whose value is long and variable (a header, a policy) puts its
   note on its own line. Squeezed into a third column beside a value that can
   be three hundred characters, the note collapses to one word per line. */
.conn-row-stacked { grid-template-columns: minmax(0, 1fr) max-content; row-gap: var(--space-1); }
.conn-row-stacked > code { justify-self: stretch; }
.conn-row-stacked .conn-row-note { grid-column: 1 / -1; }

/* Which network the tailnet link is riding over. A definition list pairs one
   term with one value, and this value is three things (a verdict, why it
   matters, and sometimes an address worth asking about) so the cell stacks
   rather than fighting the grid for a single line. */
/* Named for the question it answers, not for the mechanism: `.conn-peering` is
   the You-to-HQ strip at the top of this panel, and reusing it here would
   override that element's own grid.

   Not `justify-items: start`. The cell inherits `overflow-wrap: anywhere` from
   `.conn-pairs dd`, so a grid item sized to fit-content resolves to
   min-content, and with that wrap rule min-content is one character: the
   verdict would render as a column of single letters. */
.conn-carriage { display: grid; gap: var(--space-1); }
.conn-carriage > strong { font-weight: 600; }
/* Riding over the internet is not a fault, so it is not red. It is the one
   state where the path is public and the operator may want to look further,
   which is what the accent marks. */
.conn-carriage-internet > strong { color: var(--accent); }
.conn-carriage-relay > strong { color: var(--warn); }
.conn-carriage-unknown > strong { color: var(--muted); }
.conn-carriage .conn-more { width: 100%; }
/* The verdict is the answer; the reasoning is available, not imposed. Prose
   under every row would turn a scannable panel into an essay, and the
   explanation is only wanted when the verdict is surprising. */
.conn-why > summary { padding: var(--space-1) 0; }
.conn-why > p { margin: var(--space-1) 0 0; }

/* ----- Favourites ------------------------------------------------------- */

.service-name { display: flex; align-items: center; gap: var(--space-4); }
/* The star keeps a full touch target but hangs into the cell's padding, so
   the name starts on the line every other cell in the row starts on. */
.service-name > .pin-toggle { margin-block: -7px; }
/* A hostname is one token: it is never broken, and past --identifier-max it is
   cut on the link itself, so nothing runs under the next column. */
.service-name strong { display: flex; min-width: 0; max-width: var(--identifier-max); }
.data-table td .service-name strong > [data-entity] { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.service-name > form { display: inline-flex; }
.pin-toggle {
  background: none; border: 0; box-shadow: none; padding: 0 var(--space-1); cursor: pointer;
  /* An empty star, not an absent one: quiet, but it has to be findable. The
     hairline colour would vanish on a dark page. */
  font-size: var(--text-lg); line-height: 1; color: var(--muted-2);
}
.pin-toggle:hover { color: var(--muted); }
.pin-toggle.pinned { color: var(--warn); }
.pin-move { display: inline-flex; gap: var(--space-1); margin-left: auto; }
.pin-move button {
  background: none; border: 0; box-shadow: none; padding: 0 var(--space-1); cursor: pointer;
  font-size: var(--text-xs); line-height: 1; color: var(--muted-2);
}
.pin-move button:hover { color: var(--ink); }

/* An empty table has nothing to head: the headings wait until there is. */
.data-table:not(:has(tbody td:not([colspan]))) thead { display: none; }

/* ----- Provider readings ------------------------------------------------ */

/* Wants more air than the stack gives its siblings. Written against `.content`
   because the rule it has to beat, `.content > * + *:not(dialog)`, is (0,1,1)
   and a bare `.sweep` is (0,1,0). */
.content > .sweep { margin-top: var(--space-12); }
/* Each item draws its own ring, as topology lanes do, rather than the grid
   painting its gaps: a short last row then leaves blank card, not a grey
   block. Auto-fit lets a single row of readings take the whole width. */
.sweep-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 228px), 1fr));
  border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden;
}
.sweep-item {
  display: grid; grid-template-columns: 1fr auto; gap: var(--space-1) var(--space-5);
  align-items: baseline; padding: var(--space-4) var(--space-6);
}
.sweep-label { font-size: var(--text-xs); color: var(--ink-2); }
/* The count is the number on the row, so it leads rather than trails. */
.sweep-count { font-size: var(--text-md); font-weight: 650; justify-self: end; }
.sweep-age { grid-column: 1 / -1; font-size: var(--text-2xs); color: var(--muted); }
.sweep-item-unreachable { background: var(--tone-critical-wash); }
.sweep-item-unreachable .sweep-age { color: var(--danger); }
.sweep-item-stale .sweep-age { color: var(--warn); }

.section-heading {
  font-size: var(--text-sm); color: var(--muted); margin: var(--space-8) 0 var(--space-5); font-weight: 600;
}
.unmanaged > summary { cursor: pointer; font-size: var(--text-sm); color: var(--muted); padding: var(--space-5) 0; }
.unmanaged > summary:hover { color: var(--ink); }

/* ----- Derived topology ------------------------------------------------- */

/* A lens is chosen by following a link, so these are the shared chip primitive
   with one modifier for the current choice. aria-current carries it for anyone
   the border weight does not reach. */
.topology-lens-note { margin: 0 0 var(--space-5); font-size: var(--text-sm); }
.topology-trace-bar {
  display: flex; flex-wrap: wrap; gap: var(--space-5) var(--space-8); align-items: center;
  margin-bottom: var(--space-6); padding: var(--space-6); border: 1px solid var(--accent);
  border-radius: var(--radius); background: var(--accent-tint);
}
.topology-trace-bar > div { display: grid; gap: 1px; margin-right: auto; }
.topology-trace-controls { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }
.topology-trace-controls .scope-chip { background: var(--surface); }
.topology-hop { display: flex; align-items: center; gap: var(--space-3); }

.topology-workspace {
  background: var(--surface-2);
  overflow: hidden;
}
.topology-toolbar {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-4) var(--space-8);
  padding: var(--space-5); border-bottom: 1px solid var(--line); background: var(--surface);
}
.topology-search { flex: 1 1 240px; }
.topology-search input { width: 100%; min-height: 36px; }
.topology-kind-filters {
  display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-6);
  min-width: 0; max-width: 100%; margin: 0; padding: 0; border: 0;
}
.topology-kind-filters label {
  display: inline-flex; align-items: center; gap: var(--space-2); color: var(--ink-2);
  font-size: var(--text-xs); white-space: nowrap;
}
.topology-kind-filters input { accent-color: var(--accent); }
.topology-hint { margin: 0; padding: var(--space-4) var(--space-6); border-bottom: 1px solid var(--line-2); font-size: var(--text-xs); }
/* Lanes wrap into rows, so every lane is on screen at any width. The 1px
   gap between lanes is drawn by each lane's own ring, which leaves the empty
   cells of a short last row blank. */
/* The map and the selected node's details. The details sit beside the lanes
   where there is room and below them where there is not; never in a lane. */
.topology-stage { display: grid; grid-template-columns: minmax(0, 1fr); min-width: 0; }
/* A viewport query, not a container one: the workspace holds buttons, and
   inside a size container Chrome restyles them once the container is measured
   and runs their transitions from nothing (docs/Design.md). */
@media (min-width: 1180px) {
  .topology-stage.has-detail { grid-template-columns: minmax(0, 1fr) minmax(320px, 34%); }
  .topology-stage.has-detail .topology-detail {
    max-height: min(68vh, 680px); overflow-y: auto; border-top: 0; border-left: 1px solid var(--line);
  }
}
/* Scroll padding clears the sticky lane headers, so a card scrolled to is
   never under one. */
.topology-map {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(218px, 1fr));
  align-items: stretch; gap: 1px; overflow: auto; background: var(--surface-2);
  min-width: 0; max-width: 100%; max-height: min(68vh, 680px);
  overscroll-behavior-inline: contain;
  scroll-behavior: smooth;
  scroll-padding-top: var(--topology-lane-head);
}
.topology-lane { min-width: 0; padding: var(--space-5); background: var(--surface-2); box-shadow: 0 0 0 1px var(--line); }
.topology-lane > header {
  position: sticky; top: 0; z-index: 2; display: grid;
  grid-template-columns: auto 1fr auto; align-items: center; gap: var(--space-3);
  padding: var(--space-1) var(--space-1) var(--space-4); background: var(--surface-2);
}
.topology-lane h2 { margin: 0; font-size: var(--text-xs); text-transform: uppercase; letter-spacing: .055em; color: var(--muted); }
.topology-kind-mark { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }
.topology-kind-controller { background: var(--topology-controller); }
.topology-kind-connection { background: var(--accent-2); }
.topology-kind-ability { background: var(--topology-ability); }
.topology-kind-resource { background: var(--ok); }
.topology-kind-target { background: var(--topology-target); }
.topology-kind-dependency { background: var(--muted-2); }
.topology-node-list { display: grid; gap: var(--space-3); }
.topology-node {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center;
  gap: var(--space-3); padding: var(--space-4) var(--space-5);
  border: 1px solid var(--line); border-left: 3px solid var(--muted);
  border-radius: var(--radius-sm); background: var(--surface); box-shadow: var(--shadow-sm);
  cursor: pointer; scroll-margin-top: var(--topology-lane-head);
  transition: border-color 100ms ease, box-shadow 100ms ease, opacity 100ms ease;
}
.topology-node.is-good { border-left-color: var(--ok); }
.topology-node.is-attention { border-left-color: var(--warn); }
.topology-node.is-serious { border-left-color: var(--danger); }
.topology-node-title { display: grid; gap: 1px; min-width: 0; }
.topology-node-title strong, .topology-node-title small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.topology-node-title strong { font-size: var(--text-sm); color: var(--ink); }
.topology-node-title small { font-size: var(--text-2xs); color: var(--muted); }
/* The count is the node's direct relationships; the link around it selects
   the node, and says both in its label. */
.topology-node-select {
  display: inline-flex; align-items: center; gap: var(--space-1); min-height: 28px;
  padding: 0 var(--space-2); border-radius: 999px; color: var(--muted); text-decoration: none;
}
.topology-node-select::after { content: "\203A"; font-size: var(--text-sm); line-height: 1; }
.topology-node-select:hover, .topology-node-select:focus-visible, .topology-node-select[aria-current] { color: var(--accent); }
.topology-node-degree {
  min-width: 20px; padding: var(--space-1) var(--space-2); border-radius: 999px; background: var(--surface-2);
  color: inherit; font-size: var(--text-3xs); text-align: center; font-variant-numeric: tabular-nums;
}
.topology-detail {
  min-width: 0; padding: var(--space-6); border-top: 1px solid var(--line); background: var(--surface);
}
.topology-detail[aria-busy="true"] { opacity: .6; }
.topology-detail-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2) var(--space-4); }
.topology-detail-head h2 {
  margin: 0; font-size: var(--text-md); letter-spacing: normal; text-transform: none; color: var(--ink); overflow-wrap: anywhere;
}
.topology-detail-head .muted { font-size: var(--text-xs); }
.topology-node-body { display: grid; gap: var(--space-5); min-width: 0; }
.topology-node-body p { margin: 0; font-size: var(--text-xs); }
.topology-node-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.topology-node-actions form { display: inline-flex; }
/* The name is the box that truncates, so its own edge stays inside the node. */
.topology-node-title strong > * { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.topology-node-link { color: inherit; text-decoration: none; }
.topology-node-link:hover, .topology-node-link:focus-visible { color: var(--accent); text-decoration: underline; }

/* Triage reads as a list of questions with answers, not a paragraph: each row
   is one fact an operator checks, so the term stays scannable when the answer
   runs long. */
.topology-facts { display: grid; gap: var(--space-3); margin: 0; }
.topology-fact { display: grid; gap: 1px; }
.topology-fact dt {
  color: var(--muted-2); font-size: var(--text-3xs); font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase;
}
.topology-fact dd { margin: 0; font-size: var(--text-xs); color: var(--ink-2); }
.topology-fact dd .pill { margin-right: var(--space-2); vertical-align: baseline; }
.topology-field-list { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: var(--space-1) 0; padding: 0; list-style: none; }
.topology-field-list code { font-size: var(--text-2xs); }

/* An edge stated from the node standing at one end of it. The arrow and the
   heading both carry the direction, because a verb alone reads the same in
   either, and the whole row is the link, so walking the graph is one click
   per hop. */
.topology-relation-set { display: grid; gap: var(--space-1); }
.topology-relation-heading { margin: var(--space-2) 0 0; }
.topology-relation-list { display: grid; gap: var(--space-1); margin: 0; padding: 0; list-style: none; }
/* One relationship: the link to the other end on its own line, then each
   value it carries on a line of its own with its source and age. */
.topology-relation { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); min-width: 0; }
.topology-relation-values {
  display: grid; gap: var(--space-2); flex: 1 1 100%; min-width: 0;
  margin: 0 0 var(--space-2); padding: 0 0 0 var(--space-7); list-style: none; font-size: var(--text-xs);
}
.topology-relation-values li { display: grid; gap: 1px; min-width: 0; }
.topology-relation-value { overflow-wrap: anywhere; }
.topology-relation-source { flex: 1 1 100%; margin: 0; font-size: var(--text-2xs); overflow-wrap: anywhere; }
.topology-relation-link {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 0 var(--space-3);
  align-items: center; flex: 1 1 auto; min-width: 0; padding: var(--space-2) var(--space-3);
  border: 1px solid var(--line-2); border-radius: var(--radius-sm);
  background: var(--surface-2); color: var(--ink-2); text-decoration: none;
  font-size: var(--text-xs); transition: border-color 100ms ease, background 100ms ease;
}
.topology-relation-link:hover, .topology-relation-link:focus-visible {
  border-color: var(--accent); background: var(--accent-tint); color: var(--ink);
}
.topology-relation-arrow { grid-area: 1 / 1 / 3 / 2; color: var(--muted); font-size: var(--text-sm); }
.topology-relation-out .topology-relation-arrow { color: var(--accent-2); }
.topology-relation-verb {
  grid-area: 1 / 2 / 2 / 4; font-size: var(--text-3xs); text-transform: uppercase;
  letter-spacing: .055em; color: var(--muted);
}
.topology-relation-other {
  grid-area: 2 / 2 / 3 / 3; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; color: var(--ink);
}
.topology-relation-kind {
  grid-area: 2 / 3 / 3 / 4; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--text-3xs); color: var(--muted-2);
  font-family: var(--font-mono);
}
.topology-relation .pill { flex: 0 0 auto; }
.btn.compact { min-height: 30px; padding: var(--space-2) var(--space-4); font-size: var(--text-xs); }
.topology-node.is-selected { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-tint); }
.topology-node.is-related { border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); }
.topology-node[hidden], .topology-lane[hidden] { display: none; }
.topology-relations { margin-top: var(--space-6); }
.topology-relations-head { display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: baseline; }
.topology-relations-head h2 { margin: 0; font-size: var(--text-sm); }
/* A fixed reading height, so the ledger is a pane. Scroll chaining is kept:
   reaching the last edge hands the wheel back to the page. */
.topology-relations .table-scroll { max-height: 480px; margin-top: var(--space-6); overflow-y: auto; }
.topology-relations tr.is-related { background: var(--accent-tint); }


@media (prefers-reduced-motion: reduce) {
  .topology-node { transition: none; }
  .topology-map { scroll-behavior: auto; }
}

/* ----- Tools ------------------------------------------------------------ */

/* One nav entry holds every tool, so the tabs are the only thing telling an
   operator what else is in here. They are links with a real href rather than
   scripted panels: a tool with a result is a URL worth keeping. */
.tool-tabs { display: flex; gap: var(--space-2); flex-wrap: wrap; margin: 0 0 var(--space-3); }
.tool-tab {
  padding: var(--space-3) var(--space-7); border-radius: 8px; font-size: var(--text-sm); font-weight: 550;
  color: var(--muted); text-decoration: none; border: 1px solid transparent;
}
.tool-tab:hover { background: var(--surface-2); color: var(--ink); }
.tool-tab-current {
  background: var(--surface-2); color: var(--ink); border-color: var(--line-2);
}
.tool-summary { max-width: 68ch; margin: 0 0 var(--space-9); }
/* Two tools side by side where there is room, stacked where there is not.
   Neither is primary, so neither takes the wider column. */
.lookup-grid { display: grid; gap: var(--space-9); grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); }
.lookup-form { display: flex; gap: var(--space-5); align-items: flex-end; margin: var(--space-6) 0 var(--space-8); flex-wrap: wrap; }
.lookup-form .field { flex: 1 1 200px; }
/* The input and its button share a line, so the spacing that separates stacked
   fields would knock them out of alignment: `flex-end` lines up margin boxes,
   so a margin under the field plus one above the actions drops the button
   below the box it belongs to. The field takes the existing `.compact`
   modifier; the actions give up their stack margin here. */
.lookup-form .form-actions { margin-top: 0; }
.tool-intro > :last-child, .tool-result > :last-child { margin-bottom: 0; }

/* Side-by-side tools have to agree on where their rows are, and left to
   themselves they do not: each card is its own grid, so a three-line
   explanation on one side pushes its input below the input on the other and
   the two forms sit at different heights.

   Subgrid is what makes the bands shared rather than coincidental: intro,
   form and result line up across every card in the row no matter how the
   prose wraps, at any width, without anyone measuring text.

   Stacked in one column it holds as well: each card takes the next three
   rows, which then belong to it alone, so no width has to be asked. */
@supports (grid-template-rows: subgrid) {
  .lookup-grid { grid-template-rows: auto auto auto; }
  /* No result yet in either card: two rows, not a third holding nothing. */
  .lookup-grid:not(:has(.tool-result)) { grid-template-rows: auto auto; }
  .lookup-grid:not(:has(.tool-result)) > .card { grid-row: span 2; }
  .lookup-grid > .card {
    display: grid; grid-row: span 3; grid-template-rows: subgrid;
    align-content: start;
  }
}

/* ---- Analytics ---------------------------------------------------------- */

/* The window an operator is reading. Links rather than a control, so the
   choice is in the URL and a reading can be sent to someone. */
.window-switch { display: flex; flex-wrap: wrap; gap: var(--space-4); margin: 0 0 var(--stack-gap); }
.chip {
  display: inline-block; padding: var(--space-2) var(--space-6); border: 1px solid var(--line);
  border-radius: 999px; font-size: var(--text-sm); color: var(--muted);
  text-decoration: none; background: var(--bg);
}
.chip:hover { color: var(--accent); border-color: var(--accent); }
.chip.is-current { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }

.reading-note { margin: calc(var(--space-3) * -1) 0 var(--stack-gap); font-size: var(--text-sm); }

/* Six breakdowns, each a card. Two columns where there is room and one where
   there is not: a bar list is unreadable below about 20rem. */
.breakdown-grid {
  display: grid; gap: var(--layout-gap); margin: 0 0 var(--stack-gap);
  grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
  /* Each card as tall as its own list. Stretched to the row, a breakdown with
     three entries carries the height of one with twelve, and the page reads as
     mostly empty boxes. */
  align-items: start;
}

/* A row is label, bar and number in one line, with the bar behind the text
   rather than beside it: a separate bar column costs the label the width it
   needs, and a path is the longest thing on the page. */
.bar-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-1); }
.bar-row {
  position: relative; display: flex; align-items: baseline; gap: var(--space-5);
  padding: var(--space-3) var(--space-5); border-radius: 4px; font-size: var(--text-sm); min-inline-size: 0;
}
/* Width arrives as a value, not as presentation: the same `--at` contract the
   chart marks use, so the template supplies a share and this decides how a
   share looks. */
.bar-track {
  position: absolute; inset: 0 auto 0 0; inline-size: var(--at, 0%);
  background: var(--bg-tint); border-radius: 4px; min-inline-size: 2px;
}
.bar-label {
  position: relative; flex: 1 1 auto; min-inline-size: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.bar-value { position: relative; flex: none; font-feature-settings: "tnum"; font-weight: 650; }

/* Vitals sit under the breakdowns, not above them: they are the check you run
   after the question you came to ask. */
.vitals-strip { display: flex; flex-wrap: wrap; gap: var(--space-6) var(--space-16); margin: 0; }
.vital { display: flex; flex-direction: column; gap: var(--space-1); }
.vital dt { font-size: var(--text-2xs); letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
.vital dd { margin: 0; display: flex; align-items: baseline; gap: var(--space-4); font-size: var(--text-lg); }
.vital-rate { font-weight: 650; font-feature-settings: "tnum"; color: var(--attn); }
/* 75% of samples good is what passing means; the colour says which side of it
   this metric is on rather than making the reader compare two numbers. */
.vital-rate.is-passing { color: var(--ok); }
.vital .unit { font-size: .8em; margin-inline-start: 1px; }

/* A held request: who asks, what would happen, the decision. */
.approval-card { border-left: 3px solid var(--line); }
.approval-card.is-pending { border-left-color: var(--warn); }
.approval-head h2 { margin: 0; font-size: var(--text-xl); font-weight: 500; line-height: 1.4; text-transform: none; letter-spacing: 0; color: var(--ink); }
.approval-what { font-weight: 650; }
.approval-head > p { margin: var(--space-2) 0 0; font-size: var(--text-sm); }
.approval-reason { margin: var(--space-6) 0 0; padding: var(--space-4) var(--space-6); border-left: 2px solid var(--line-strong); color: var(--ink-2); font-size: var(--text-sm); }
.approval-label { margin: var(--space-10) 0 var(--space-4); font-size: var(--text-2xs); font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); }
.approval-actions { display: flex; flex-wrap: wrap; gap: var(--space-4); margin-top: var(--space-10); }
.approval-actions form { margin: 0; }

/* Agents: the state and its one control, then every rule in a table. */
.agent-state { display: grid; gap: var(--space-6); }
.agent-state-main { display: flex; align-items: center; gap: var(--space-6); }
.agent-state-main > .btn { margin: 0 0 0 auto; }
.agent-state-dot { flex: none; width: 10px; height: 10px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 4px color-mix(in srgb, var(--ok) 16%, transparent); }
.agent-state.is-paused .agent-state-dot { background: var(--warn); box-shadow: 0 0 0 4px color-mix(in srgb, var(--warn) 16%, transparent); }
.agent-state-copy h2 { margin: 0; color: var(--ink); font-size: var(--text-lg); font-weight: 650; letter-spacing: 0; text-transform: none; }
.agent-state-copy p { margin: var(--space-1) 0 0; color: var(--muted); font-size: var(--text-sm); }
.agent-state-facts { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-10); margin: 0; padding-top: var(--space-6); border-top: 1px solid var(--line-2); color: var(--muted); font-size: var(--text-sm); }
.agent-state-facts a { color: var(--muted); }
.agent-state-facts a:hover { color: var(--ink); }
.policy-dormant-note { margin-left: auto; color: var(--muted-2); }
.agent-state-facts strong { color: var(--ink); font-variant-numeric: tabular-nums; }
.policy-agents { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: var(--space-4); margin: 0; padding: 0; list-style: none; }
.policy-agents li { display: flex; flex-direction: column; gap: var(--space-1); padding: var(--surface-inset, var(--space-5) var(--space-6)); font-size: var(--text-xs); }
.policy-agent-name { font-size: var(--text-md); font-weight: 600; color: var(--ink); }
.policy-none { margin: 0; font-size: var(--text-sm); }
.policy { display: grid; gap: var(--stack-gap); }
.policy-table th, .policy-table td { vertical-align: middle; }
.policy-table .policy-select { width: 100%; max-width: 11rem; }
.policy-off { display: block; color: var(--muted-2); font-size: var(--text-3xs); font-weight: 500; letter-spacing: 0; text-transform: none; }
.policy-table thead th:first-child { color: var(--ink-2); font-size: var(--text-xs); }
.policy-table tbody th { font-weight: 500; color: var(--ink); }
.policy-effect { margin-left: var(--space-3); font-size: var(--text-3xs); font-weight: 600; letter-spacing: .05em; text-transform: uppercase; color: var(--muted-2); }
.policy-effect.effect-destructive { color: var(--danger); }
.policy-effect.effect-infrastructure_change { color: var(--warn); }
.policy-default { color: var(--muted); white-space: nowrap; }
.policy-unavailable { font-size: var(--text-xs); white-space: nowrap; }
.policy-select { min-width: 7.5rem; padding: var(--space-2) var(--space-4); border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface); color: var(--muted); font: inherit; font-size: var(--text-xs); }
.policy-select:focus { outline: none; border-color: var(--accent-2); box-shadow: 0 0 0 3px var(--accent-tint); }
.policy-select.rule-allow { color: var(--ok); border-color: currentColor; }
.policy-select.rule-approve { color: var(--warn); border-color: currentColor; }
.policy-select.rule-deny { color: var(--danger); border-color: currentColor; }
.policy-select.is-dormant { opacity: .5; }
/* A save button pinned to the bottom of a long form. It shows once the form
   has changed: app.js marks the form clean on load and the first edit clears
   the mark. Without script the mark is never set, so the bar always shows. */
form[data-clean] .save-bar { display: none; }
.save-bar { position: sticky; bottom: 0; z-index: 2; display: flex; justify-content: flex-end; padding: var(--space-6) 0 calc(var(--space-6) + env(safe-area-inset-bottom, 0px)); background: linear-gradient(transparent, var(--bg) 35%); }

/* ── Dashboard ──────────────────────────────────────────────────────
   A header, then rows. Each row is a grid whose cards share one width and one
   height, so the page stays even whichever domains an installation has. */
.dash-head { align-items: flex-end; margin-bottom: calc(var(--stack-gap) + var(--space-4)); }
.dash-hello { display: grid; gap: var(--space-2); min-width: 0; }
.dash-hello h1 { font-size: clamp(20px, .6vw + 16px, 24px); letter-spacing: -0.02em; line-height: 1.1; }
.dash-date { margin: 0; color: var(--muted); font-size: var(--text-2xs); font-weight: 650; letter-spacing: .08em; text-transform: uppercase; }
.dash-status { margin: 0; color: var(--muted); font-size: var(--text-md); }
.dash-status a { color: var(--ink-2); }
.dash-status strong { color: var(--accent); font-variant-numeric: tabular-nums; }
/* What a page says while its own readings are being read, and once they are
   in: one quiet line, never a banner. */
.visit-status {
  position: absolute; inset-block-start: var(--content-pad); inset-inline-end: var(--content-pad); margin: 0;
  color: var(--muted); font-size: var(--text-xs); white-space: nowrap;
}
.content:has(.ask-quiet) { position: relative; }
/* An ask: a button and how the work it asked for stands (partials/_ask.html).
   Inline, so it sits wherever a button does, and exactly as large as its
   button: what it says is drawn under the button, over the page, so pressing
   it moves nothing. Nothing is drawn until something was asked. */
.ask { position: relative; display: inline-flex; align-items: center; min-inline-size: 0; }
/* One nobody presses: its line sits on the trail's line, at the far edge, so
   it comes and goes without moving anything. */
.ask-quiet { position: static; display: block; block-size: 0; }
.ask-said {
  position: absolute; inset-block-start: calc(100% + var(--space-2)); inset-inline-start: 0; z-index: 3;
  display: flex; flex-wrap: wrap; gap: var(--space-3);
  inline-size: max-content; max-inline-size: min(36ch, 100vw - 28px);
  padding: var(--space-2) var(--space-4); border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface); box-shadow: var(--shadow-sm);
  color: var(--muted); font-size: var(--text-xs); white-space: normal;
  --ask-shift: 0px; translate: var(--ask-shift) 0;
}
/* A head's controls end at the page's edge, so what one says ends there too.
   Anywhere else app.js sets --ask-shift to keep it on the screen. */
.page-head .page-actions .ask { position: static; }
.page-head .page-actions .ask-said { inset-inline: auto 0; }
.ask-said:has(.ask-note:empty):has(.ask-elapsed:empty) { display: none; }
.ask-note { overflow-wrap: anywhere; }
.ask-note:empty, .ask-elapsed:empty { display: none; }
.ask-elapsed { font-variant-numeric: tabular-nums; }
.ask[data-ask-state="failed"] .ask-note { color: var(--danger); }
/* Busy keeps the button focusable, so it is marked rather than disabled. */
.ask [aria-disabled="true"],
form[aria-busy="true"] button[aria-disabled="true"] { cursor: progress; opacity: .6; }
/* The dashboard's readings: how this request arrived, whether the controller
   still is, and what the estate reports. One row of equals: the same mark, the
   same lead and the same quiet line under it, spaced rather than ruled apart,
   since a rule between two that have wrapped onto different lines divides
   nothing.

   The row asks its own width what it can show, never the window's. With room
   it is one line at the right of the greeting. Without, the readings pair up,
   two to a line, and on a phone the connection keeps its name and its verdict
   and drops the line between: how it arrived is one tap away.

   It is a layer of its own so that a panel's popover opens over the cards
   below it; a container is otherwise painted beneath what follows it. */
.dash-head > .dash-hello { flex: 0 1 auto; }
.dash-readings { position: relative; z-index: 2; display: flex; flex-wrap: wrap; align-items: flex-start; flex: 1 1 22rem; min-width: 0; container-type: inline-size; }
/* Two to a row while each half can hold a reading's lead, and one to a row
   below that: a half narrower than a lead would push the lead past the page. */
/* Two to a row where both fit. A reading whose lead is wider than half the
   row takes a row of its own, so its lead is never drawn over its neighbour. */
.dash-readings :is(.dash-reading, .glance-panel) { flex: 1 1 max(50%, 11rem); min-width: min-content; max-width: 100%; }
.dash-readings .glance-panel-head { padding-inline: var(--space-3); grid-template-columns: 22px minmax(max-content, 1fr); }
/* One shape for all of them, so they line up: the lead on a line of one
   height, in one size and one colour, with the mark beside that line rather
   than centred on however many lines the reading happens to have (it keeps
   its own column for the whole reading, so nothing is ever set under it); then the
   quiet lines, which start level because the leads do. A reading that is
   empty or out of date says so in the line under its lead, not by being
   smaller or greyer than its neighbours. */
.dash-readings .glance-symbol { align-self: start; margin-top: var(--space-1); }
.dash-readings .glance-panel-head > :not(.glance-symbol) { grid-column: 2; }
.dash-readings .glance-lead { display: flex; align-items: baseline; gap: var(--space-3); min-height: 26px; color: var(--muted); line-height: 1; }
.dash-readings .glance-lead :is(strong, .conn-dot) { align-self: center; }
.dash-readings .glance-lead strong,
.dash-readings .glance-lead-empty { color: var(--ink); font-size: var(--text-4xl); font-weight: 650; line-height: 1.15; letter-spacing: -.01em; }
.dash-reading { color: inherit; }
.dash-reading:hover { color: inherit; text-decoration: none; }
.dash-reading .glance-caption { overflow: hidden; text-overflow: ellipsis; }
.dash-connection .glance-caption { display: none; }
.dash-controller.is-silent .glance-caption { color: var(--attn); font-weight: 650; }
.dash-controller.is-arriving .conn-dot { background: var(--ok); }
.dash-controller.is-silent .conn-dot { background: var(--attn); }
@container (min-width: 440px) {
  .dash-connection .glance-caption { display: flex; }
}
@container (min-width: 900px) {
  .dash-readings :is(.dash-reading, .glance-panel) { flex: 0 1 auto; }
  .dash-readings > :first-child { margin-left: auto; }
  .dash-readings .glance-panel-head { padding-inline: var(--space-7); }
}
.dash-connection-checks { color: var(--muted-2); font-size: var(--text-3xs); white-space: nowrap; }
.dash-connection-checks.is-attention { color: var(--attn); font-weight: 650; }
.dash-cell > .card { display: flex; flex-direction: column; min-width: 0; margin: 0; }
.dash-cell { display: contents; }
.section-period { margin-left: var(--space-3); color: var(--muted-2); font-weight: 500; letter-spacing: 0; text-transform: none; }
.empty-value { color: var(--muted-2); }
.dash-empty { margin: 0; color: var(--muted); font-size: var(--text-sm); }
.dash-links { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-4) var(--space-6); padding-top: var(--space-8); border-top: 1px solid var(--line); }
.dash-links h2 { margin: 0; color: var(--muted-2); font-size: var(--text-2xs); font-weight: 650; letter-spacing: .08em; text-transform: uppercase; }
}

@layer components {
  /* ----- Watching: your GitHub profile ----------------------------------- */
  /* One raised band that says whose this is: the avatar, the name, the account,
     and the counts beside them. */
  .gh-profile {
    display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-6) var(--space-9);
    margin: 0 0 var(--stack-gap); padding: var(--space-8) var(--space-9);
    border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow);
  }
  .gh-avatar { width: 64px; height: 64px; border-radius: 50%; box-shadow: 0 0 0 3px var(--accent-tint); }
  .gh-identity { flex: 1 1 260px; min-width: 0; }
  .gh-identity p { margin: 0; }
  .gh-name { font-size: var(--text-xl); font-weight: 650; }
  /* Four to a row, then two, then one: never three over one. */
  .fact-band { --band-cell: var(--tracks-4-2-1); }
  /* The band's lower half: the same repository's settings, under a hairline. */
  .github-facts { flex-basis: 100%; display: grid; gap: var(--space-4); margin: 0; padding: var(--space-7) var(--space-9); border-top: 1px solid var(--line-2); font-size: var(--text-sm); }
  .github-facts > div { display: grid; grid-template-columns: minmax(0, clamp(7rem, 30%, 160px)) minmax(0, 1fr); gap: var(--space-6); }
  .github-facts dt { color: var(--muted); }
  .github-facts dd { margin: 0; }
  .github-gap { color: var(--tone-caution-ink, var(--muted)); }
  /* A repository's lists: one surface, a row per item read left to right. */
  .github-list { list-style: none; margin: 0 0 var(--space-6); padding: var(--surface-inset, 0); }
  .github-list li { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-5); padding: var(--space-4) var(--space-7); font-size: var(--text-sm); }
  .github-list li + li { border-top: 1px solid var(--line-2); }
  /* Workflows as chips that wrap like tags: the mark in its colour, the name. */
  .workflow-chips { display: flex; flex-wrap: wrap; gap: var(--space-3); margin: 0 0 var(--space-6); }
  .workflow-chip { display: inline-flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-6); border: 1px solid var(--line); border-radius: 999px; background: var(--surface); box-shadow: var(--shadow-sm); color: var(--ink); font-size: var(--text-sm); }
  .workflow-chip .icon { color: var(--muted-2); }
  .workflow-chip.tone-good .icon { color: var(--ok); }
  .workflow-chip.tone-serious { border-color: var(--danger); }
  .workflow-chip.tone-serious .icon { color: var(--danger); }
  .workflow-chip.tone-attention .icon { color: var(--tone-caution-ink); }
  .workflow-chip:hover { border-color: var(--line-strong); }
  .github-mark { display: inline-flex; font-size: 1.15em; color: var(--muted-2); }
  .github-mark.tone-good { color: var(--ok); }
  .github-mark.tone-serious { color: var(--danger); }
  .github-mark.tone-attention { color: var(--tone-caution-ink); }
  /* What the deploy proved before it ran: its own verification steps. */
  .github-verified { display: inline-flex; align-items: center; gap: var(--space-2); color: var(--danger); cursor: default; }
  .github-verified.is-verified { color: var(--ok); }
  /* The same rows on their own surface, where no band carries them. */
  .account-facts { }
  /* Containers: what needs you, one row per image and version, then the table. */
  .container-needs { list-style: none; margin: 0 0 var(--space-7); padding: var(--surface-inset, 0); overflow: hidden; }
  .container-needs > li { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--space-3) var(--space-6); padding: var(--space-5) var(--space-7); border-left: 3px solid var(--line); }
  .container-needs > li + li { border-top: 1px solid var(--line-2); }
  .container-needs > li.is-serious { border-left-color: var(--danger); }
  .container-needs > li.is-attention, .container-needs > li.is-behind { border-left-color: var(--tone-caution-line); }
  .container-needs-icon { color: var(--tone-caution-ink); font-size: var(--text-lg); line-height: 1; }
  .container-needs > li.is-serious .container-needs-icon { color: var(--danger); }
  /* Never wider than the row beside its icon, so the icon keeps its line. */
  .container-needs-body { flex: 1 1 min(22rem, 100% - 3rem); min-width: 0; }
  .container-needs-body p { margin: var(--space-1) 0 0; color: var(--muted); font-size: var(--text-sm); }
  .container-arrow { color: var(--muted-2); font-weight: 400; }
  .container-advisories { list-style: none; margin: var(--space-3) 0 0; padding: 0; display: grid; gap: var(--space-2); font-size: var(--text-sm); }
  .container-needs-where { display: flex; flex-wrap: wrap; gap: var(--space-2); max-width: 100%; }
  .container-needs-where .workflow-chip { padding: var(--space-1) var(--space-5); font-size: var(--text-xs); box-shadow: none; }
  .container-standing { white-space: nowrap; }
  /* Words rather than a pill: a narrow column wraps them instead of widening. */
  .container-reach { color: var(--danger); font-size: var(--text-xs); font-weight: 500; }
  .container-standing .icon { vertical-align: -0.15em; }
  .container-standing.is-vulnerable { color: var(--danger); }
  .container-standing.is-behind { color: var(--tone-caution-ink); }
  .container-standing.is-current .icon { color: var(--ok); }

  /* A container's upgrade: four facts, then its steps beside what stands in the way. */
  .upgrade-band { margin-bottom: var(--space-6); }
  /* Three facts across the band's full width, not three narrow tiles and a gap. */
  .fact-band.readiness-band { --band-cell: var(--tracks-3-1); }
  /* align-content: start, or the grid spreads the steps to the height of the
     column beside it. */
  .upgrade-steps { margin: 0; padding-left: var(--space-10); display: grid; gap: var(--space-2); align-content: start; font-size: var(--text-sm); }
  .upgrade-install { margin-top: var(--space-4); font-size: var(--text-sm); }
  .upgrade-install > p { margin: 0 0 var(--space-3); }
  .upgrade-install-steps { margin: 0; padding-left: var(--space-8); display: grid; gap: var(--space-4); }
  /* A long step label wraps beside its Copy button rather than pushing it
     onto a line of its own. */
  .upgrade-install-steps .hardening-head { flex-wrap: nowrap; align-items: flex-start; }
  .upgrade-install-steps .hardening-head strong { flex: 1 1 auto; min-width: 0; }
  .upgrade-install-steps pre { margin: var(--space-2) 0 0; white-space: pre-wrap; overflow-wrap: anywhere; }
  .upgrade-blockers { font-size: var(--text-sm); }
  .upgrade-blockers p { margin: 0 0 var(--space-3); }
  .upgrade-blockers ul { margin: 0; padding-left: var(--space-9); display: grid; gap: var(--space-2); }
  .container-facts code { font-size: var(--text-xs); }
  /* Facts on their own raised surface, as every other section sits. */
  .boxed-facts { margin: 0 0 var(--space-8); padding: var(--surface-inset, var(--space-5) var(--space-7)); }
  /* Why not on its own: the band's own bottom row, a line each. */
  .upgrade-reasons { list-style: none; margin: 0; flex-direction: column; align-items: flex-start; gap: var(--space-2); }
  .upgrade-reasons li::before { content: "·"; margin-right: var(--space-3); color: var(--muted-2); }

  /* The compose change that meets its unmet checks: one block to paste, then
     what each line can break and what HQ could not write. */
  .hardening-patch { display: grid; gap: var(--space-4); min-width: 0; }
  .hardening-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-3); min-width: 0; }
  .hardening-head code { overflow-wrap: anywhere; }
  /* Wrapped rather than scrolled: a long path stays on screen, and the copy
     takes the text, not the wrapping. */
  .hardening-patch pre { margin: 0; max-width: 100%; white-space: pre-wrap; overflow-wrap: anywhere; }
  .hardening-patch > p { margin: 0; font-size: var(--text-sm); }
  .hardening-notes { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-3); font-size: var(--text-sm); }
  .hardening-notes li { min-width: 0; overflow-wrap: anywhere; }
  [data-copy-target][hidden] { display: none; }

  /* Posture, led by what is not met: a row per gap with the repositories
     that miss it, then a row per repository with a segment per check. */
  .posture-gaps, .posture-repos { list-style: none; margin: 0 0 var(--space-7); padding: var(--surface-inset, 0); overflow: hidden; }
  .posture-gaps li, .posture-repos li { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3) var(--space-6); padding: var(--space-5) var(--space-7); }
  .posture-gaps li + li, .posture-repos li + li { border-top: 1px solid var(--line-2); }
  .posture-gaps li { align-items: flex-start; border-left: 3px solid var(--tone-caution-line); }
  .posture-gaps li.is-serious { border-left-color: var(--danger); }
  /* Failed on purpose, with the reason: a fact, not a gap. */
  .posture-gaps li.is-intended { border-left-color: var(--line); }
  .posture-gaps li.is-intended .posture-gap-icon { color: var(--muted); }
  .posture-gap-icon { color: var(--tone-caution-ink); font-size: var(--text-lg); line-height: 1; }
  .posture-gaps li.is-serious .posture-gap-icon { color: var(--danger); }
  /* Never wider than the row beside its icon, so the icon keeps its line. */
  .posture-gap-body { flex: 1 1 min(22rem, 100% - 3rem); min-width: 0; }
  .posture-gap-body p { margin: var(--space-1) 0 0; color: var(--muted); font-size: var(--text-sm); }
  .posture-gap-body p .muted { color: var(--muted-2); }
  .posture-gap-repos { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); max-width: 100%; }
  .posture-gap-repos .workflow-chip { padding: var(--space-1) var(--space-5); font-size: var(--text-xs); box-shadow: none; }
  .posture-repo-name { flex: 0 1 14rem; min-width: 0; display: flex; flex-direction: column; }
  .posture-repo-name a { font-weight: 600; }
  .posture-meter { flex: 0 1 auto; min-width: 0; display: flex; gap: var(--space-1); }
  .posture-access { flex: 1 1 12rem; min-width: 0; color: var(--muted); font-size: var(--text-sm); }
  .posture-access .icon { color: var(--muted-2); vertical-align: -0.15em; }
  .posture-access .is-unmet { color: var(--danger); }
  /* Fixed segments, the shared checks first, so one check sits in the same
     column on every row and a gap reads straight down the list. */
  .posture-meter span { flex: 0 1 1.6rem; min-width: 0.4rem; height: 0.6rem; border-radius: 2px; background: var(--line); cursor: default; }
  .posture-meter .posture-meter-break { flex: 0 0 var(--space-6); background: none; }
  .posture-meter .is-met { background: var(--ok); }
  .posture-meter .is-unmet { background: var(--danger); }
  .posture-meter span:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
  .posture-score { flex: 0 0 4.5rem; font-size: var(--text-sm); font-variant-numeric: tabular-nums; }
  .posture-score.is-met { color: var(--ok); }
  .posture-score.is-unmet { color: var(--danger); }
  .posture-everywhere { color: var(--muted); font-size: var(--text-sm); }
  .posture-everywhere > summary { color: var(--ink); }

  /* A project's addresses under its name: one line, dots between. */
  .project-meta { display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2) var(--space-3); }
  .project-meta > * + *::before { content: "·"; margin-right: var(--space-3); color: var(--muted-2); }
  .project-tags { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: 0 0 var(--stack-gap); }
  /* An age or a count that reads wrong split across two lines. */
  .nowrap { white-space: nowrap; }
  /* A line that is a label, not prose: cut with an ellipsis, whole on hover. */
  .one-line { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; max-width: 100%; }
  /* The same mark as a verified certificate: a check in a circle, one colour. */
  .gh-verified { display: inline-flex; margin-left: var(--space-3); color: var(--accent-2); font-size: .9em; vertical-align: -.1em; cursor: default; }
  .gh-stats { display: flex; flex-wrap: wrap; gap: var(--space-9); margin: 0; }
  .gh-stats div { display: grid; gap: var(--space-1); }
  .gh-stats dt { color: var(--muted); font-size: var(--text-xs); }
  .gh-stats dd { margin: 0; font-size: var(--text-xl); font-weight: 650; font-feature-settings: "tnum"; }
  .gh-stats dd a { color: inherit; }
  .gh-stats dd a:hover { color: var(--accent-2); }

  /* Who can reach a name, in one word. Internet-facing is stated plainly,
     since a public site is meant to be; only what HQ could not read is a
     warning. Exposure raises urgency where it matters, on the finding. */
  .exposure { font-size: var(--text-sm); white-space: nowrap; color: var(--muted); }
  .exposure-open { color: var(--ink); font-weight: 600; }
  .exposure-unknown { color: var(--warn); }
  .path-reach { margin: 0 0 var(--space-4); }

  /* A compact list is short labels beside short values: a column each, at
     every width, so a few numbers do not spend a screen stacked label over
     value. A value too long for its column wraps inside it. */
  .kv.compact { margin-bottom: 0; grid-template-columns: minmax(5rem, max-content) minmax(0, 1fr); gap: var(--space-2) var(--space-7); padding: var(--surface-inset, var(--space-5) var(--space-6)); align-items: baseline; }
  .kv.compact dt { margin-top: 0; }

  /* ── Surface: the one frame ─────────────────────────────────────────────
     Every box that holds content draws its frame here and nowhere else: one
     border, one radius, one background, one shadow. A component names itself
     in this list and declares only its layout; it never spells out a frame.

     Nesting is handled once, by inheritance: a surface sets the frame
     properties to "none" for everything inside it, so a surface inside a
     surface (stats in a card, a list in a band) draws no second frame and adds
     no second inset. Nothing downstream has to remember that rule.

     A grid that draws its dividers with a coloured background behind 1px gaps
     keeps that background; the frame around it is still this one. */
  :where(
  .table-scroll:has(> .data-table), .card, .two-col > section, .two-col > div:not(.col-stack), .section-flow > :is(section, .page-section), .control-card,
  .control-metric, .control-summary, .service-band, .fact-band,
  .filter-bar, .table-toolbar, .kpi-grid, .insight-grid,
  .kv, .form,
  .auth-card, .mail-stage, .mail-editor,
  .topology-workspace, .github-list, .account-facts, .container-needs,
  .boxed-facts, .policy-agents li, .relation-list,
  .posture-gaps, .posture-repos, .machine-telemetry, .machine-telemetry-metrics) {
    border: var(--surface-border, 1px solid var(--line));
    border-radius: var(--surface-radius, var(--radius));
    background: var(--surface-bg, var(--surface));
    box-shadow: var(--surface-shadow, var(--shadow-sm));

    & > * {
      --surface-border: 0 none;
      --surface-radius: 0;
      --surface-bg: transparent;
      --surface-shadow: none;
      --surface-inset: 0;
    }
  }
  /* Stats inside a card are the card's content: an even grid with real gaps,
     no tile padding doubling the card's own. */
  :is(.card, .highlight-card) > .kpi-grid {
    display: grid;
    /* Exactly the card's --kpi-columns, which the card sets from how many
       stats it holds and how wide it is (4 or 2+2, 6 or 3+3, never 3+1). A
       fit-as-many-as-fit grid would not hold that. */
    grid-template-columns: repeat(var(--kpi-columns, 2), minmax(0, 1fr));
    gap: var(--space-6) var(--space-7);
    background: none;
  }
  /* A row of stats shares its lines. Each stat takes the row's three tracks
     (label, value, note), so the labels start level and the values stand
     level, however many lines one label wraps to. */
  :is(.card, .highlight-card) > .kpi-grid > .kpi {
    padding: 0;
    background: none;
    box-shadow: none;
    display: grid;
    grid-row: span 3;
    grid-template-rows: subgrid;
    row-gap: var(--space-2);
  }
  /* What a figure looks like, beside it: dots for a part of a whole, a line
     for a direction. A filled dot is solid and an empty one is a ring, so the
     count does not rest on colour. The line takes the room the value leaves
     and is drawn only in a card wide enough to give it some. */
  .kpi-figure { display: flex; align-items: center; gap: var(--space-5); min-width: 0; }
  .kpi-dots { display: inline-flex; flex-wrap: wrap; gap: var(--space-3); }
  /* More than fit beside a value in a narrow column are set in two even rows,
     filled column by column, rather than left to break wherever the column ends. */
  .kpi-dots:has(> :nth-child(7)) { display: inline-grid; grid-auto-flow: column; grid-template-rows: repeat(2, auto); }
  .kpi-dot { position: relative; display: block; width: 9px; height: 9px; border: 1.5px solid var(--muted-2); border-radius: 50%; }
  .kpi-dot.is-filled { border-color: var(--ok); background: var(--ok); }
  /* A dot that leads somewhere is aimed at through a larger target than it draws. */
  a.kpi-dot::after { content: ""; position: absolute; inset: -7px -3px; }
  a.kpi-dot:hover, a.kpi-dot:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
  .kpi a.value { color: inherit; text-decoration: none; }
  .kpi a.value:hover { text-decoration: underline; }
  .kpi-trend { display: none; flex: 1 1 0; min-width: 0; max-width: 260px; height: 28px; margin-left: auto; overflow: visible; }
  .kpi-trend polyline { fill: none; stroke: var(--accent); stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
  .kpi-trend-hit { fill: transparent; }
  .kpi-trend-dot { stroke: var(--accent); stroke-width: 7; stroke-linecap: round; vector-effect: non-scaling-stroke; opacity: 0; pointer-events: none; }
  .kpi-trend-dot.is-latest, .kpi-trend-hit:hover + .kpi-trend-dot { opacity: 1; }
  .kpi-grid.is-featured > :first-child .kpi-trend { height: 40px; }
  @container (min-width: 340px) {
    .kpi-trend { display: block; }
  }

  /* The contributor's first of four figures leads the overview. The same
     values and links appear once; supporting facts form a compact second row. */
  .kpi-grid.is-featured .value { color: var(--accent); }
  .kpi-grid.is-featured > .kpi { gap: var(--space-3); }
  .kpi-grid.is-featured:has(> :nth-child(4):last-child) {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    align-content: start;
    row-gap: var(--space-10);
  }
  .kpi-grid.is-featured:has(> :nth-child(4):last-child) > :first-child {
    grid-column: 1 / -1;
    padding-bottom: var(--space-8);
    border-bottom: 1px solid var(--line-2);
  }
  .kpi-grid.is-featured:has(> :nth-child(4):last-child) > :first-child .value {
    font-size: var(--text-display);
    letter-spacing: -.04em;
    overflow-wrap: anywhere;
  }
  .kpi-grid.is-featured:has(> :nth-child(4):last-child) > :not(:first-child) .value {
    font-size: var(--text-2xl);
    color: var(--ink);
    overflow-wrap: anywhere;
  }
  /* A lone overview card across the page (an HQ with one contributor) puts
     its four figures on one row, the first still the largest: led from a row
     of its own, the three under it would sit beside a page width of nothing. */
  @container featured (min-width: 46rem) {
    .kpi-grid.is-featured:has(> :nth-child(4):last-child) { grid-template-columns: repeat(4, minmax(0, 1fr)); align-items: end; }
    .kpi-grid.is-featured:has(> :nth-child(4):last-child) > :first-child { grid-column: auto; padding-bottom: 0; border-bottom: 0; }
  }

  /* The last thing in a card ends the card: its own bottom margin would be
     dead space inside the card's padding. */
  /* A frame's own padding is its edge: the first and last thing in it keep no
     margin of their own, or a heading's top margin doubles the padding above it. */
  :is(.card, .control-card, .boxed-facts, .two-col > section, .two-col > div:not(.col-stack), .section-flow > :is(section, .page-section)) > :last-child { margin-bottom: 0; }
  :is(.card, .control-card, .boxed-facts, .two-col > section, .two-col > div:not(.col-stack), .section-flow > :is(section, .page-section)) > :first-child { margin-top: 0; }


  /* ── Disclosure: one marker for everything that opens in place ─────────
     Every details that is not a menu shows the same mark, turned when open,
     so "this opens" looks the same everywhere: a folded section, raw
     readings, a list behind "+5 more", a table's own row toggle. Menus carry
     data-menu and dress their summary as a control instead. A connection's
     hops and layers show their state with their own tick, in a fixed grid
     an extra mark would shift, so they carry none. */
  main details:not([data-menu]):not([data-connection-layer]):not([data-connection-hop]) > summary { list-style: none; cursor: pointer; }
  main details:not([data-menu]):not([data-connection-layer]):not([data-connection-hop]) > summary::-webkit-details-marker { display: none; }
  main details:not([data-menu]):not([data-connection-layer]):not([data-connection-hop]) > summary::before {
    content: "▸"; display: inline-block; width: 1em; margin-inline-end: var(--space-1);
    color: var(--muted); font-size: 1.05em; line-height: 1; text-align: center;
    transition: transform .12s ease;
  }
  main details:not([data-menu]):not([data-connection-layer]):not([data-connection-hop])[open] > summary::before { transform: rotate(90deg); }
  @media (prefers-reduced-motion: reduce) { main details:not([data-menu]):not([data-connection-layer]):not([data-connection-hop]) > summary::before { transition: none; } }

  /* A section that folds: its heading is the toggle. */
  .section-fold > summary { display: flex; align-items: baseline; }
  .section-fold > summary:hover :is(h2, h3) { color: var(--accent-2); }
  .section-fold > summary :is(h2, h3) { margin: 0; }
  .section-fold[open] > summary { margin-bottom: var(--space-5); }

  /* The topology's view picker, first in its own filter row. */
  .topology-view { display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: center; margin: 0; }
  /* A view's name is a sentence: the picker takes the row and never more. */
  .topology-view, .topology-view select { min-width: 0; max-width: 100%; }

  /* What the filter bar currently shows, at its far end. */
  .filter-bar-summary { margin: 0 0 0 auto; align-self: center; color: var(--muted); font-size: var(--text-sm); }
  .filter-bar-summary strong { color: var(--ink); }

  /* A head's controls on a phone: the lead one beside the title, the rest in
     the overflow menu, which is hidden wherever every control fits. */
  .page-head .page-actions > .overflow-menu { display: none; }
  @media (max-width: 640px) {
    .page-head .page-actions > :not(:first-child):not(.overflow-menu) { display: none; }
    .page-head .page-actions > .overflow-menu { display: flex; }
    .page-head .page-actions { flex-wrap: nowrap; }
  }

  /* The row toggle tables.js adds where a row says more than it shows: the
     one disclosure a row has, so it is sized to be found and hit, and turns
     when open. */
  .row-expand {
    display: inline-grid; place-items: center;
    width: 1.5rem; height: 1.5rem; min-height: 0; padding: 0; margin: 0;
    border: 0; border-radius: var(--radius-sm); background: none; box-shadow: none;
    color: var(--muted); font-size: var(--text-sm); line-height: 1; cursor: pointer;
    vertical-align: middle;
  }
  .row-expand:hover { border: 0; background: var(--line-2); color: var(--ink); }
  /* What a row holds back until it is expanded, and the hint it replaces. */
  .data-table tr:not(.is-expanded) .row-more,
  .data-table tr.is-expanded .row-less { display: none; }
  .row-expand > svg { fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; transition: transform .12s ease; }
  .row-expand[aria-expanded="true"] > svg { transform: rotate(90deg); }
  @media (prefers-reduced-motion: reduce) { .row-expand > svg { transition: none; } }
  /* Every clamp in the row lifted at once. The rules it lifts are scattered and
     each is specific to its column, so this one outranks them on purpose: an
     expanded row shows everything, whatever clamped it. */
  .data-table tr.is-expanded :is(td, th),
  .data-table tr.is-expanded :is(td, th) *:not(.visually-hidden) {
    white-space: normal !important;
    overflow: visible !important;
    text-overflow: clip !important;
    -webkit-line-clamp: unset !important;
    line-clamp: none !important;
    max-height: none !important;
    max-width: none !important;
  }

  /* A band too narrow for two cells is a list, like every other summary:
     each fact one row, its label and its value at the right edge, the detail
     under both. Asked of the band's own width, so a band in a narrow card
     reads the same as one on a phone. 27.5rem is two 220px cells. */
  :is(.service-band, .fact-band, .control-summary) { container: band / inline-size; }
  @container band (max-width: 27.5rem) {
    .band-fact { gap: var(--space-4); padding: var(--space-5) var(--space-6); align-items: center; }
    .band-icon { width: 24px; height: 24px; font-size: var(--text-lg); border-radius: var(--radius-sm); }
    /* The label keeps its words; a long value wraps at the right instead of
       squeezing the label to a letter a line. */
    .band-fact > div { flex: 1 1 auto; grid-template-columns: max-content minmax(0, 1fr); align-items: baseline; column-gap: var(--space-4); }
    .band-fact > div > .band-label { grid-column: 1; grid-row: 1; font-size: var(--text-sm); }
    .band-fact > div > strong { grid-column: 2; grid-row: 1; text-align: right; font-size: var(--text-md); }
    .band-fact > div > :not(.band-label):not(strong) { grid-column: 1 / -1; }
    .control-summary > .control-metric {
      flex-direction: row;
      flex-wrap: wrap;
      align-items: baseline;
      justify-content: space-between;
      gap: var(--space-1) var(--space-5);
      padding: var(--space-5) var(--space-6);
    }
    .control-summary > .control-metric > :not(.control-label):not(strong) { flex: 1 1 100%; }
  }
}

/* Moving between pages. Both pages opt in, so a same-origin navigation fades
   the new document in over the old instead of flashing to blank between them.
   The header is its own layer and is not animated: the old one is dropped and
   the new one stands at once, so only what is under it changes. Outside any
   cascade layer: `@view-transition` is a rule about the document. Skipped
   whole for a reader who asks for less motion, and by a browser that does not
   know the rule. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }
  .site-header { view-transition-name: site-header; }
  ::view-transition-old(site-header) { display: none; }
  ::view-transition-new(site-header) { animation: none; }
  ::view-transition-old(root),
  ::view-transition-new(root) { animation-duration: 120ms; }
}
