/* ==========================================================================
   Alertstore UI
   --------------------------------------------------------------------------
   Every dimension in this file comes from a token below. If a component needs
   a value that is not on one of these scales, extend the scale here rather
   than hard-coding a one-off number in the component.

   Two rules govern colour and type, and they are why the console looks the
   way it does:

   1. One saturated hue. --signal is reserved for "this will wake someone up".
      Everything else — open, acked, resolved, links, the primary button — is
      drawn from the ink ramp. A page of alerts where forty things are coloured
      is a page where nothing is.

   2. Machine values are monospaced, prose is not. Alert IDs, cluster IDs,
      fingerprints, ages and label values are things a machine emitted and a
      human copies; sentences are things a human reads. The split is a reading
      aid first and the console's signature second, so apply it strictly.

   3. Weights are 400, 500 and 700 — three, not five. The one exception is the
      uppercase pill, which takes 600 because caps at 11px need the extra
      weight to hold their shape; everything else picks from the three.
   ========================================================================== */

:root {
    color-scheme: light;

    /* ── Ink ramp: cool near-neutral, the substrate for everything ──
       Neither end of this ramp is pure. #ffffff against #000000 is roughly a
       21:1 contrast, which reads as glare in a bright room and as halation —
       letters bleeding into their background — in a dim one, and it is hardest
       on readers with astigmatism. So the page surface is a hair off white and
       the darkest ink a hair off black, which still leaves body text above
       16:1. Every step below is checked against WCAG AA at the size it is
       actually used: --ink-3 carries 12px column headings, so it has to clear
       4.5:1 on both --surface and --surface-2, not only on the lighter one. */
    --ink: #1b1e24;
    --ink-2: #565c68;
    --ink-3: #697080;
    --surface: #fcfcfd;
    --surface-2: #f3f4f6;
    --surface-3: #eaecf0;
    --rule: #e4e6eb;
    --rule-2: #d2d6de;

    /* ── The only saturated hue on the page ── */
    --signal: #d52525;
    --signal-strong: #b91c1c;
    --signal-bg: #fef2f2;
    --signal-border: #f3c0c0;

    /* Focus is the one exception, and it is deliberately a cold blue: a ring
       that shared the alarm's hue would make every tab stop look urgent. */
    --ring: #2f6feb;

    /* ── Surface & text: the names the rest of the file already uses ── */
    --bg: var(--surface-2);
    --bg-white: var(--surface);
    --bg-sidebar: var(--surface-2);
    --bg-hover: var(--surface-3);
    --border: var(--rule);
    --border-strong: var(--rule-2);
    --border-light: var(--rule);
    --text: var(--ink);
    --text-secondary: var(--ink-2);
    --text-muted: var(--ink-3);

    /* Solid actions are ink, not a brand colour, so that nothing on the page
       out-shouts an alarm. Links are ink too, and earn their affordance from
       an underline rather than from a hue. */
    --accent: var(--ink);
    --accent-hover: #000000;
    /* The one filled button on a page, and the only place besides links where
       the console spends a hue. Indigo puts it in the same family as the
       links: everything you can act on shares a colour, and the four state
       hues are left to describe what is rather than what you can do to it.
       Ink was tried and read as a slab — the single darkest object on an
       otherwise pale page, with nothing else like it.

       Light stops at #6159eb because that is as pale as indigo gets with a
       white label still at 5.08:1; indigo-500 lands at 4.47 and misses. Dark
       inverts instead, a pale fill under a dark label, since no mid indigo
       holds white text against #14181a. */
    --accent-solid: #6159eb;
    --accent-solid-hover: #4f46e5;
    --on-accent: #ffffff;
    /* Links get a hue back. #912 made them ink on the argument that an
       underline is affordance enough, which holds for a word inside a sentence
       and does not hold for "Clinic" — a destination that leaves the console,
       sitting in a row of boxes that look like buttons. Indigo rather than the
       obvious blue because #2563eb is already the open state; two blues a
       column apart would be read as related. */
    --link: #4f46e5;
    --link-hover: #4338ca;
    --link-rule: #c7c4f4;

    /* ── Severity ──────────────────────────────────────────────────────────
       A state pill has two jobs and they are given to two different things.
       The text carries the hue, so it is saturated and dark enough to read.
       The ground carries presence — "there is a chip here" — so it is a
       desaturated tint whose separation from the page comes from lightness,
       not from colour.

       That split is the whole point. Sizing the grounds by luminance alone
       forced yellow and green, which are light hues, to reach the target by
       piling on chroma, and they came out garish. At 45% saturation (62% for
       amber, which needs it to still read as yellow) they sit back and the
       four chips finally weigh the same: every ground is 1.10 against the
       page, every text is the lightest value that still clears AA at 11px —
       4.52 to 4.59, none of it spent on contrast nobody asked for. */
    --sev-crit: var(--signal);
    --sev-crit-bg: #f9efef;
    --sev-crit-border: var(--signal-border);
    --sev-warn: #aa570a;
    --sev-warn-bg: #f8f1e0;
    --sev-warn-border: #f1c36d;
    --sev-warn-fg: #aa570a;
    --sev-ok: #157e3d;
    --sev-ok-bg: #e6f5ec;
    --sev-ok-hover: #15803d;
    --sev-info: #2563eb;
    --sev-info-bg: #edf2f8;

    /* ── Spacing: the only spacing values allowed ── */
    --sp-1: 4px;
    --sp-2: 8px;
    --sp-3: 12px;
    --sp-4: 16px;
    --sp-5: 20px;
    --sp-6: 24px;
    --sp-7: 32px;
    --sp-8: 48px;

    /* ── Type: six steps, no more ──
       The scale has two defaults on purpose, and which one applies is a
       question about the text, not about the page it is on:

         --fs-md (13px)   things you scan. Table cells, filters, navigation,
                          labels, controls. Density is the point; a row you
                          read one word out of does not need to be comfortable.
         --fs-body (14px) sentences somebody wrote and somebody else has to
                          read end to end. Alert descriptions, comments,
                          delivery warnings. This is the product's own body
                          size, and 13px prose is genuinely harder to read.

       Anything that is neither — a value, a chip, a count — is --fs-md. */
    --fs-xs: 11px;   /* eyebrows, meta, field labels */
    --fs-sm: 12px;   /* secondary text, chips, dense controls */
    --fs-md: 13px;   /* dense default: everything you scan */
    --fs-body: 14px; /* prose default: everything you read */
    --fs-lg: 15px;   /* card lead text, dialog titles */
    --fs-xl: 20px;   /* page and detail titles */
    --lh-tight: 1.3;
    --lh: 1.5;
    --lh-relaxed: 1.6;

    /* System faces, and no webfont at all. An internal console is not worth a
       licence question or a download on first paint, and the platform UI faces
       — SF Pro, Segoe UI — are drawn for exactly this: small text, dense rows,
       screen rendering. They also mean the console never renders in a fallback
       nobody looked at.

       The trade is that it does not look identical on macOS, Windows and
       Linux. For a tool used inside one company on developer machines that is
       a price worth paying; if it ever has to be pixel-identical everywhere,
       that is the moment to revisit, not before. */
    /* macOS, then Windows, then whatever the desktop declares, then Linux and
       Android. Noto is the most widely distributed open family and, since
       fontconfig 2.14, the default sans on Linux ahead of DejaVu — but it is
       not the UI face of any platform, so it goes here rather than first.
       Listing it first would hand the whole console to Noto on every Mac and
       Windows box that happens to have it installed, which Chrome, Adobe and
       homebrew all do, and that is exactly the opposite of using the system
       font. It sits ahead of Roboto because Roboto is Android's UI face and
       `system-ui` has already answered for Android by this point; what reaches
       this part of the list is a Linux desktop.

       The generic goes last because nothing after it is ever reached. CJK is
       not listed: it turns up rarely enough here — a comment, a Jira summary —
       that the browser's own fallback is the right amount of effort. */
    --font-ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, 'Noto Sans', Roboto, Arial, sans-serif;
    --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Noto Sans Mono', 'Roboto Mono', monospace;
    /* Negative tracking only at display sizes. The system faces are already
       optically spaced for small text; tightening there undoes that work. */
    --track-title: -0.02em;

    /* ── Radius: three steps ── */
    --r-sm: 6px;     /* controls, tags, inputs — main's value */
    --r-md: 8px;     /* dialogs, popovers */
    --r-full: 999px; /* counters */

    --shadow-card: none;
    /* The product's own popover shadow. Wide and very faint — a menu should
       look lifted, not cut out and pasted on. */
    --shadow-pop: 0 8px 32px rgba(0, 0, 0, 0.08);

    /* ── Control heights: inputs and buttons on a row must match exactly ── */
    --control-h-sm: 28px;
    --control-h: 34px;
    --control-h-lg: 38px;

    /* Shared horizontal rail. Card padding and the table's outer cell padding
       use this so text starts on the same line on every page. */
    --card-px: 20px;
    --card-py: 20px;

    --sidebar-w: 212px;
    --sidebar-w-collapsed: 52px;
    --col-min: 190px;  /* min column width for auto-fill field grids */
    --status-col: 260px; /* the alert detail's status column */
    --kv-key: 150px;   /* key column floor, so adjacent kv blocks line up */
    --dot: 7px;        /* status dot */
    --marker-w: 12px;  /* disclosure triangle column */
    --tl-dot: 12px;    /* timeline dot */
    --row-py: 10px;    /* table row padding; the triage list lives or dies here */

    --dur: 0.12s;
}

/* ── Dark ──────────────────────────────────────────────────────────────────
   Not an inversion, and not black. The base surface sits near Material's
   #121212 rather than at #000000 for two reasons: a true black ground gives a
   hairline or a raised surface nothing to be lighter than, so depth has to be
   faked with shadow, which is invisible on black anyway; and white-on-black is
   the worst case for halation. The lightest ink stops short of #ffffff for the
   same reason — around 87% is the usual recommendation, and it still leaves
   body text above 13:1.

   The alarm also lifts several points of lightness here. A mid red that reads
   as urgent on paper-white reads as brown on a dark ground, and saturated hues
   vibrate against dark surfaces, so it is lightened and slightly desaturated
   rather than reused as-is.

   The values are TiDB Cloud's own: the ramp below is `carbon` and the alarm is
   `red` from @tidbcloud/uikit's dark palette, taken verbatim because that ramp
   already clears AA at every step this file uses it for. Note that the light
   scheme above is deliberately *not* carbon — it keeps a cooler blue cast. The
   two schemes are never on screen together, and each was picked on how it
   reads in the room it is for, so they are allowed to disagree. */

[data-theme='dark'] {
    color-scheme: dark;

    /* carbon.9 / .8 / .7 — 13.6:1, 8.9:1, 4.7:1 on --surface */
    --ink: #dae2e5;
    --ink-2: #acb9bf;
    --ink-3: #79848a;
    /* carbon.0 / .1 / .2 */
    --surface: #14181a;
    --surface-2: #111517;
    --surface-3: #1f2426;
    /* carbon.3 / .4 */
    --rule: #25292b;
    --rule-2: #313638;

    /* red.7 / .8 / .2 / .4 — 5.6:1 on --surface */
    --signal: #f08080;
    --signal-strong: #ee8d8d;
    --signal-bg: #3d181e;
    --signal-border: #70202e;

    /* The same split, inverted: on a dark ground the tint is lighter than the
       page rather than darker. It also needs a bigger step — 1.35 rather than
       1.10 — because a luminance difference that reads clearly on paper
       disappears at the bottom of the scale. Text is the product's dark ramp,
       lifted a shade for firing, which does not clear AA on its own ground. */
    --sev-crit-bg: #492626;
    --sev-crit-border: #70202e;
    --sev-warn: #f2aa18;
    --sev-warn-bg: #382f18;
    --sev-warn-border: #674711;
    --sev-warn-fg: #f2aa18;
    --sev-ok: #52cc7a;
    --sev-ok-bg: #1d3526;
    --sev-ok-hover: #86dba2;
    --sev-info: #6cbae0;
    --sev-info-bg: #243142;

    /* peacock.8, the product's accent, and the only place it appears. */
    --ring: #6cbae0;

    /* A shadow on a dark ground has to be several times more opaque than the
       same shadow on paper before it reads as depth at all. */
    --shadow-pop: 0 8px 32px rgba(0, 0, 0, 0.32);

    /* Ink is the page's darkest value in light mode and its lightest in dark,
       so a solid ink button has to flip its label with it. The fill stops one
       step short of full ink (carbon.8, not .9): the same button that reads as
       weighty on paper reads as a glowing slab on a dark ground. */
    --accent: var(--ink);
    --accent-hover: #ffffff;
    --accent-solid: #818cf8;
    --accent-solid-hover: #a5b4fc;
    --on-accent: #14181a;
    --link: #a5b4fc;
    --link-hover: #c7d2fe;
    --link-rule: #4a4b7c;
}

/* ── Base ───────────────────────────────────────────────────────────────── */

* { margin: 0; padding: 0; box-sizing: border-box; }

/* The browser's own `[hidden] { display: none }` is a user-agent rule, so any
   author `display` (for example .btn) silently beats it and the element stays
   visible. Restate it here so the hidden attribute keeps working. */
[hidden] { display: none !important; }

body {
    font-family: var(--font-ui);
    background: var(--surface);
    color: var(--text);
    font-size: var(--fs-md);
    line-height: var(--lh);
    -webkit-font-smoothing: antialiased;
}

/* Every number in this console is either compared to the number above it (ages,
   counts) or copied out of it (IDs, versions). Proportional digits do neither
   well, so they are switched off globally rather than per component. */
body { font-variant-numeric: tabular-nums; }

.mono {
    font-family: var(--font-mono);
    font-variant-ligatures: none;
}

/* One focus treatment for everything interactive. :where() keeps specificity
   at zero so components can still restyle borders without losing the ring. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
    outline: 2px solid var(--ring);
    outline-offset: 2px;
}

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

/* ── Primitive: button ──────────────────────────────────────────────────── */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-1);
    height: var(--control-h);
    padding: 0 var(--sp-4);
    border: 1px solid var(--border);
    border-radius: var(--r-sm);
    background: var(--bg-white);
    color: var(--text);
    font-family: inherit;
    font-size: var(--fs-md);
    font-weight: 500;
    line-height: 1;
    white-space: nowrap;
    cursor: pointer;
    transition: background var(--dur), border-color var(--dur), color var(--dur);
}
.btn:hover { background: var(--bg-hover); border-color: var(--border-strong); }
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
.btn:disabled:hover { background: var(--bg-white); border-color: var(--border); }

.btn-sm { height: var(--control-h-sm); padding: 0 var(--sp-3); font-size: var(--fs-sm); }

/* Three weights of button, and which one a control gets is a question about
   the page, not about the control:

     .btn-primary    the thing this screen exists to do. Acknowledge on an
                     alert, Confirm in a dialog, sign in on the sign-in screen.
                     One per screen; a second one means neither is primary.
     .btn-secondary  a real action that is not the reason you came. Resolve,
                     Send, New Subscription.
     .btn            everything else. Filter, Prev, Next, Cancel.

   Primary is ink rather than a brand colour on purpose: the loudest thing on
   an alert console should be the alert. */
.btn-primary { background: var(--accent-solid); border-color: var(--accent-solid); color: var(--on-accent); }
.btn-primary:hover { background: var(--accent-solid-hover); border-color: var(--accent-solid-hover); }
.btn-primary:disabled:hover { background: var(--accent-solid); border-color: var(--accent-solid); }

.btn-secondary { border-color: var(--rule-2); font-weight: 500; }
.btn-secondary:hover { background: var(--bg-hover); border-color: var(--ink-3); }
.btn-secondary:disabled:hover { background: var(--bg-white); border-color: var(--rule-2); }

/* Link-styled button for row-level actions. Links earn their affordance from
   an underline, not a hue — the hue budget is spent on the alarm. */
.inline-action {
    background: none;
    border: 0;
    padding: 0;
    margin-left: var(--sp-2);
    color: var(--link);
    font-family: inherit;
    font-size: var(--fs-sm);
    text-decoration: underline;
    text-decoration-color: var(--link-rule);
    text-underline-offset: 2px;
    cursor: pointer;
}
.inline-action:hover { text-decoration-color: currentColor; }
.inline-action.danger { color: var(--sev-crit); }

/* An icon where a word used to be. The value beside it is empty as often as
   not, and "reassign" is the wrong tense then — there is nothing to *re*-assign
   before anyone has it. An icon has no tense, so the accessible name carries
   the distinction instead, where making it conditional costs nothing.

   The button is larger than its glyph on purpose: a 14px pencil is a 14px
   target, and this sits inside a dense column of values. */
.icon-action {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    margin-left: var(--sp-1);
    border: 0;
    border-radius: var(--r-sm);
    background: transparent;
    color: var(--ink-3);
    cursor: pointer;
}
.icon-action:hover { background: var(--bg-hover); color: var(--text); }
.icon-action svg { width: 16px; height: 16px; }

/* The row centres its children rather than aligning them on the text baseline.
   A button with no text in it has no baseline, so flex fell back to its bottom
   edge and the whole control floated above the line. Every item in this row is
   one line of the same size, so centring costs nothing and fixes it. */

/* ── Primitive: form field ─────────────────────────────────────────────────
   Applied by class where practical, and by scoped selector where the markup
   is generated as plain <input>/<select> inside a known container. */

.field,
.filters input[type="text"],
.filters select,
.modal input,
.modal select,
.modal textarea,
.filter-grid input {
    width: 100%;
    border: 1px solid var(--border);
    border-radius: var(--r-sm);
    background: var(--bg-white);
    color: var(--text);
    font-family: inherit;
    font-size: var(--fs-md);
    padding: 0 var(--sp-3);
    height: var(--control-h);
}

.field:hover,
.filters input[type="text"]:hover,
.filters select:hover,
.modal input:hover,
.modal select:hover,
.filter-grid input:hover { border-color: var(--border-strong); }

.field:focus,
.filters input:focus,
.filters select:focus,
.modal input:focus,
.modal select:focus,
.modal textarea:focus,
.filter-grid input:focus { border-color: var(--accent); }

.field::placeholder,
.filters input::placeholder,
.modal input::placeholder,
.filter-grid input::placeholder { color: var(--text-muted); }

.field:disabled,
.modal input:disabled { background: var(--bg); color: var(--text-secondary); }

/* Textareas size to content rather than to a fixed control height. */
.modal textarea {
    height: auto;
    min-height: calc(var(--control-h) * 2);
    padding: var(--sp-2) var(--sp-3);
    line-height: var(--lh);
    resize: vertical;
}

/* ── Primitive: tag ────────────────────────────────────────────────────────
   One geometry for every small inline label: status pills, inline warnings
   and counters. */

.pill,
.agent-status,
.agent-verdict,
.history-warning,
.members-count {
    display: inline-flex;
    align-items: center;
    padding: 2px var(--sp-2);
    border: 1px solid transparent;
    border-radius: var(--r-sm);
    font-size: var(--fs-xs);
    font-weight: 500;
    line-height: var(--lh-tight);
    white-space: nowrap;
}


/* main's treatment, restored: uppercase at the smallest step, state as a
   filled tint, class as an outline. The two columns differ in form and not
   only in wording, so neither has to be read to know which is which.

   Caps are now the only uppercase left on the screen — the nav headings and
   column headings that used to compete with them are sentence case — so they
   read as a label rather than as shouting. */
.pill { text-transform: uppercase; letter-spacing: 0.02em; font-weight: 600; }
.pill-open { background: var(--sev-warn-bg); color: var(--sev-warn); }
.pill-acked { background: var(--sev-info-bg); color: var(--sev-info); }
.pill-firing { background: var(--sev-crit-bg); color: var(--sev-crit); }
.pill-resolved,
.pill-resolved-am { background: var(--sev-ok-bg); color: var(--sev-ok); }
.pill-logging,
.pill-neutral { background: var(--bg); color: var(--text-secondary); }

.pill-page { background: var(--bg-white); border-color: var(--sev-crit-border); color: var(--sev-crit); }
.pill-ticket { background: var(--bg-white); border-color: var(--sev-warn-border); color: var(--sev-warn); }

/* Every pill in a column is as wide as that column's longest label and centres
   inside it. The cell is sized to max-content, so the widest pill sets the
   measure and the rest fill it — there is no number here to fall out of step
   with the vocabulary when a new state is added. A ragged stack of chips reads
   as noise; two straight edges let the eye separate open from acked without
   reading either. */
.cell-pill { width: 1px; white-space: nowrap; }
.cell-pill .pill { width: 100%; justify-content: center; }

/* A subscription's delivery mode. Never an alarm, so never the signal colour —
   two steps of ink, and the label carries the meaning. */
.pill-mode { background: var(--surface-3); color: var(--text); }
.pill-mode-quiet { background: transparent; border-color: var(--rule); color: var(--ink-3); }

.history-warning {
    background: var(--sev-warn-bg);
    border-color: var(--sev-warn-border);
    color: var(--sev-warn-fg);
    font-weight: 500;
}

.members-count {
    border-radius: var(--r-full);
    background: var(--bg);
    border-color: var(--border);
    color: var(--text-secondary);
    font-weight: 500;
}

/* ── Primitive: card ───────────────────────────────────────────────────────
   One padding value for every card, so vertical rhythm does not shift as the
   page scrolls. */

.detail-card,
.timeline-card {
    background: var(--bg-white);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    box-shadow: var(--shadow-card);
    padding: var(--card-py) var(--card-px);
}
.detail-card { margin-bottom: var(--sp-3); }

/* Not a card. A table already has a shape — rows and rules — and wrapping it
   in a second rounded, shadowed rectangle on a grey field just draws a box
   around a box. The list runs full width on the page's own surface, and the
   hairlines do the separating. */
.table-card {
    background: transparent;
    border: 0;
    border-radius: 0;
    box-shadow: none;
}

.card-title {
    font-size: var(--fs-md);
    font-weight: 500;
    color: var(--text);
    margin-bottom: var(--sp-3);
    display: flex;
    align-items: center;
    gap: var(--sp-2);
}

/* Eyebrow above a block of content. Sentence case and a hairline underneath:
   the rule does the separating that the caps used to do, and does it without
   competing with the content for attention. */
.content-label,
.instance-section-title,
.form-section {
    color: var(--ink-3);
    font-size: var(--fs-sm);
    font-weight: 500;
    letter-spacing: 0;
    text-transform: none;
}

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

.shell { display: flex; height: 100vh; height: 100dvh; }
.main-pane { position: relative; flex: 1; min-width: 0; display: flex; flex-direction: column; }
main { flex: 1; min-height: 0; overflow-y: auto; padding: var(--sp-5) var(--sp-6); }

/* ── Sidebar ────────────────────────────────────────────────────────────── */

.sidebar {
    width: var(--sidebar-w);
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    padding: var(--sp-3) var(--sp-2);
    background: var(--bg-sidebar);
    border-right: 1px solid var(--border);
    overflow-y: auto;
}
.sb-brand {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--sp-1) var(--sp-2) var(--sp-5);
    font-size: var(--fs-lg);
    font-weight: 500;
    letter-spacing: var(--track-title);
}
.sb-logo {
    display: block;
    width: 148px;
    height: auto;
    max-height: 48px;
    object-fit: contain;
    object-position: left center;
}
.sidebar-close,
.sidebar-collapse,
.sidebar-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--control-h);
    height: var(--control-h);
    flex: 0 0 auto;
    border: 0;
    border-radius: var(--r-sm);
    background: transparent;
    color: var(--text-secondary);
    cursor: pointer;
}
.sidebar-close { display: none; margin-right: calc(var(--sp-2) * -1); }
.sidebar-collapse { margin-right: calc(var(--sp-2) * -1); }

/* ── Collapsed navigation ──────────────────────────────────────────────────
   Down to a rail of icons rather than away entirely: the column still says
   where you are and stays one click from anywhere, which a hidden column does
   not. The labels and counts go, so each row keeps its name in `title`. */
.sidebar { transition: width 0.16s ease; }
.sidebar.collapsed { width: var(--sidebar-w-collapsed); }
.sidebar.collapsed .sb-title,
.sidebar.collapsed .sb-label,
.sidebar.collapsed .sb-count,
.sidebar.collapsed .sb-logo,
.sidebar.collapsed .sb-account > span,
.sidebar.collapsed .sb-caret { display: none; }
.sidebar.collapsed .sb-brand,
.sidebar.collapsed .sb-item,
.sidebar.collapsed .sb-bottom { justify-content: center; }
.sidebar.collapsed .sidebar-collapse { margin-right: 0; }
/* The headings are gone, so the groups need another way to read as groups. */
.sidebar.collapsed .sb-section + .sb-section {
    margin-top: var(--sp-2);
    padding-top: var(--sp-3);
    border-top: 1px solid var(--border);
}
/* At rail width the account row is just the avatar, and the menu — which is
   positioned in the viewport, not in the sidebar — still opens beside it. The
   two controls stack, because side by side they do not fit in 52px. */
.sidebar.collapsed .sb-bottom { flex-direction: column; gap: var(--sp-1); }
.sidebar.collapsed .sb-account { flex: 0 0 auto; justify-content: center; }
.sidebar-close:hover,
.sidebar-collapse:hover,
.sidebar-toggle:hover { background: var(--bg-hover); color: var(--text); }

.sidebar-backdrop,
.mobile-header { display: none; }

.sb-section { margin-bottom: var(--sp-4); }
.sb-title {
    padding: 0 var(--sp-2);
    margin-bottom: var(--sp-1);
    color: var(--ink-3);
    font-size: var(--fs-sm);
    font-weight: 500;
    text-transform: none;
    letter-spacing: 0;
}
.sb-item {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-1) var(--sp-2);
    min-height: var(--control-h-sm);
    border-radius: var(--r-sm);
    color: var(--text-secondary);
    font-size: var(--fs-md);
    text-decoration: none;
    cursor: pointer;
}
.sb-item:hover { background: var(--bg-hover); color: var(--text); }

/* Bundled rather than fetched: an icon that silently fails to load leaves the
   row it labels looking like a bug. Sized in em so it tracks the label. */
.sb-icon {
    /* A fixed size, not an em: the icon is a fixed mark beside the label, and
       tying it to the font size made it drift a pixel between the sidebar and
       the drawer, which use different type scales. */
    width: 16px;
    height: 16px;
    flex: 0 0 auto;
    color: var(--text-muted);
    opacity: 0.85;
}
.sb-item.on .sb-icon { color: var(--text-secondary); }
/* The label takes the slack, so the counts line up on one edge instead of
   sitting wherever each label happens to end. */
.sb-label {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.sb-item.on {
    background: var(--surface-3);
    color: var(--text);
    font-weight: 500;
}
.sb-count {
    padding: 0 2px;
    color: var(--ink-3);
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    font-weight: 500;
    font-variant-numeric: tabular-nums;
    min-width: 20px;
    text-align: right;
}
.sb-item.on .sb-count { color: var(--ink-2); }
.sb-bottom {
    display: flex;
    align-items: center;
    margin-top: auto;
    padding: var(--sp-1);
    border-top: 1px solid var(--border);
    color: var(--text-secondary);
    font-size: var(--fs-sm);
}

/* ── Account menu ──────────────────────────────────────────────────────────
   The trigger is a nav row like any other, so the footer reads as part of the
   column rather than as a strip bolted under it. */

.sb-account {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    flex: 1;
    min-width: 0;
    padding: var(--sp-1) var(--sp-2);
    min-height: var(--control-h-sm);
    border: 0;
    border-radius: var(--r-sm);
    background: transparent;
    color: var(--text-secondary);
    font-family: inherit;
    font-size: var(--fs-sm);
    text-align: left;
    cursor: pointer;
}
.sb-account:hover { background: var(--bg-hover); color: var(--text); }
.sb-account[aria-expanded='true'] { background: var(--surface-3); color: var(--text); }
.sb-avatar,
.sb-caret { flex: 0 0 auto; color: var(--ink-3); }
.sb-avatar { width: 16px; height: 16px; }
.sb-caret { width: 12px; height: 12px; }
.sb-account > span {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Fixed, and placed from JS. The sidebar scrolls, which makes it a clipping
   box, so an absolutely positioned menu would be cut off — and completely so
   at the collapsed rail's 52px. */
.user-menu {
    position: fixed;
    z-index: 40;
    padding: var(--sp-1);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    background: var(--bg-white);
    box-shadow: var(--shadow-pop);
}
.user-menu-email {
    padding: var(--sp-2) var(--sp-2) var(--sp-1);
    color: var(--ink-3);
    font-size: var(--fs-xs);
    overflow-wrap: anywhere;
}
.user-menu-item {
    display: block;
    width: 100%;
    padding: var(--sp-2);
    border-top: 1px solid var(--border);
    border-right: 0;
    border-bottom: 0;
    border-left: 0;
    border-radius: var(--r-sm);
    background: transparent;
    color: var(--text);
    font-family: inherit;
    font-size: var(--fs-md);
    text-align: left;
    cursor: pointer;
}
.user-menu-item:hover { background: var(--bg-hover); }

/* ── Theme cycle ── */

.theme-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: var(--control-h-sm);
    height: var(--control-h-sm);
    border: 0;
    border-radius: var(--r-sm);
    background: transparent;
    color: var(--ink-3);
    cursor: pointer;
}
.theme-toggle:hover { background: var(--bg-hover); color: var(--text); }
.theme-toggle svg { width: 15px; height: 15px; display: none; }
.theme-toggle[data-mode='light'] .icon-light,
.theme-toggle[data-mode='dark'] .icon-dark,
.theme-toggle[data-mode='auto'] .icon-auto { display: block; }

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

/* The bar sits on the table's left rail, not floating inboard of it, so the
   first field, the column headings and the first cell all start on one line. */
.filters {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-2);
    padding-bottom: var(--sp-3);
    margin-bottom: var(--sp-1);
}
/* Dense row: every control on it is exactly --control-h-sm tall. */
.filters input[type="text"],
.filters select {
    width: auto;
    height: var(--control-h-sm);
    padding: 0 var(--sp-2);
    font-size: var(--fs-sm);
}
.filter-meta {
    margin-left: auto;
    color: var(--ink-3);
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    font-variant-numeric: tabular-nums;
}

/* ── Filter bar: multi-select ──────────────────────────────────────────────
   A checkbox dropdown standing in for a native <select> where a control takes
   several values at once (class, state, team). Shaped after shadcn/ui's popover
   + command menu, built from this sheet's own tokens: the trigger matches the
   dense controls beside it, the menu is the same popover the resolve and combo
   menus use, and a chosen option is marked by a check rather than a fill. */
.ms { position: relative; }
.ms-btn {
    display: inline-flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-2);
    /* A fixed width, not auto: the label shifts between the "any" placeholder, a
       single value and an "N selected" count, and an auto-width button would
       resize on every change and jostle the controls beside it. Wide enough for
       the count and most single values; a longer one (a team name) is clipped
       rather than allowed to grow the row — the title attribute carries it in
       full. This is why the label truncates below. */
    width: 8.5rem;
    height: var(--control-h-sm);
    padding: 0 var(--sp-2);
    border: 1px solid var(--border);
    border-radius: var(--r-sm);
    background: var(--bg-white);
    color: var(--text);
    font-family: inherit;
    font-size: var(--fs-sm);
    cursor: pointer;
    white-space: nowrap;
    transition: background var(--dur), border-color var(--dur);
}
/* min-width:0 lets the label shrink below its content so text-overflow can
   clip it; without it a flex item refuses to go narrower than its text. */
.ms-value {
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
}
.ms-btn:hover { border-color: var(--border-strong); }
.ms-open .ms-btn { border-color: var(--accent); }
/* A control holding a selection reads in full ink; an empty one shows its
   "any" placeholder in muted ink, the way a native select's default sits back.
   The border stays quiet so a filled facet isn't louder than an empty one. */
.ms-value-empty { color: var(--text-muted); }
/* The chevron follows the panel: down at rest, flipped while it is open — the
   one animated affordance shadcn's trigger carries. */
.ms-caret {
    flex: none;
    color: var(--text-muted);
    transition: transform var(--dur);
}
.ms-open .ms-caret { transform: rotate(180deg); }
.ms-menu {
    position: absolute;
    top: calc(100% + var(--sp-1));
    left: 0;
    z-index: 110;
    min-width: max(100%, 11rem);
    max-height: calc(var(--control-h) * 6);
    overflow-y: auto;
    margin: 0;
    padding: var(--sp-1);
    background: var(--bg-white);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    box-shadow: var(--shadow-pop);
    list-style: none;
    transform-origin: top;
    animation: ms-menu-in var(--dur) ease;
}
/* shadcn's popover entrance: a short fade with a slight rise and zoom, so the
   menu looks lifted into place rather than snapped on. Silenced under the
   reduced-motion rule above like every other transition here. */
@keyframes ms-menu-in {
    from { opacity: 0; transform: translateY(-4px) scale(0.98); }
    to   { opacity: 1; transform: translateY(0) scale(1); }
}
.ms-option {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-2);
    border-radius: var(--r-sm);
    color: var(--text);
    font-size: var(--fs-sm);
    white-space: nowrap;
    cursor: pointer;
    transition: background var(--dur);
}
.ms-option:hover { background: var(--bg-hover); }
.ms-option.on { font-weight: 500; }
/* The check holds its column whether or not it shows, so labels line up and a
   toggle doesn't shift the row — only the tick fades in. */
.ms-check {
    display: inline-flex;
    flex: none;
    color: var(--accent);
    opacity: 0;
    transition: opacity var(--dur);
}
.ms-option.on .ms-check { opacity: 1; }
.ms-option-label { flex: 1; }
.ms-empty {
    padding: var(--sp-2) var(--sp-2);
    color: var(--text-muted);
    font-size: var(--fs-sm);
}

/* ── Table ──────────────────────────────────────────────────────────────── */

.table-scroll { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; min-width: 720px; }

/* Column widths: content-sized by default, proportional where it helps.
   Left to itself the browser pads every gap evenly and the row turns into
   seven items separated by rivers. Handing all the slack to one column instead
   just moves the whole river behind that column. So the short columns — id,
   state, class, age — stay exactly as wide as they need, and the three that
   hold variable-length text share the remainder by percentage.

   The percentages are shares of the table, not fixed widths: the columns track
   the window instead of leaving a desert on a wide screen. Each has a
   min-width for the other direction, so on a narrow window they stop
   shrinking, the table outgrows its container and .table-scroll takes over —
   an email broken across two lines to avoid a scrollbar is the worse trade. */
.table-fill th,
.table-fill td { width: 1px; white-space: nowrap; }

.table-fill .col-alert { width: 30%; min-width: 200px; white-space: normal; }
.table-fill .col-cluster { width: 18%; min-width: 160px; }
.table-fill .col-owner { width: 22%; min-width: 170px; }

/* Subscriptions: the filter summary is the one flexible cell. It absorbs the
   slack so the table fills the page, but with a cap — a wide screen would
   otherwise stretch the summary across it — and a low floor, so a narrow screen
   hands the width back to the other columns and wraps the summary instead of
   pinning it wide. */
.table-fill .col-filter { width: 100%; min-width: 160px; max-width: 560px; white-space: normal; }

/* When the table is wider than the viewport the row scrolls sideways, and the
   edit/delete actions are the last thing on the row — the first to leave the
   screen. Pin that column to the right edge so it stays put and the two actions
   are always reachable; the background matches the row so the scrolling cells
   pass behind it rather than showing through. */
.sub-table .col-actions {
    position: sticky;
    right: 0;
    background: var(--surface);
}
.sub-table tbody tr:hover .col-actions { background: var(--bg-hover); }

th,
td { padding: var(--row-py) var(--sp-3); }
/* The first column starts on the page's own left margin, so the table, the
   filter bar and the page title all share one vertical line. */
th:first-child,
td:first-child { padding-left: 0; }
th:last-child,
td:last-child { padding-right: var(--sp-2); }

th {
    text-align: left;
    border-bottom: 1px solid var(--rule-2);
    color: var(--ink-3);
    font-size: var(--fs-sm);
    font-weight: 500;
    text-transform: none;
    letter-spacing: 0;
    white-space: nowrap;
    padding-bottom: var(--sp-2);
}
td {
    border-bottom: 1px solid var(--rule);
    font-size: var(--fs-md);
    vertical-align: middle;
}
tbody tr:last-child td { border-bottom: none; }
tbody tr { cursor: pointer; transition: background var(--dur); }
tbody tr:hover td { background: var(--bg-hover); }

/* The row's leading cell is a real link. Quiet at rest — the row is the
   click target — and declared on hover. */
.row-link {
    color: inherit;
    text-decoration: underline;
    text-decoration-color: transparent;
    text-underline-offset: 2px;
    transition: color var(--dur), text-decoration-color var(--dur);
}
tbody tr:hover .row-link { text-decoration-color: var(--link-rule); }
.row-link:hover, .row-link:focus-visible { color: var(--link); text-decoration-color: var(--link); }

/* Page versus ticket is carried by the word in the class column and by nothing
   else — no row tint, no edge marker. It is the one red thing on the screen,
   which is enough to find, and it stays inside the column that is labelled
   with what it means. */

/* Resolved rows are history. They stay legible and stay in place — sorting
   them away would hide the thing you came to confirm — but they step back a
   tone so the open rows are what the eye lands on. */
tr.row-resolved td { color: var(--ink-3); }
tr.row-resolved .cell-alertname { color: var(--ink-2); font-weight: 400; }

.num { text-align: right; font-variant-numeric: tabular-nums; }

/* ── Subscription channel groups ──
   Subscriptions are grouped by their Feishu 群 (channel): a header row names the
   channel once and its subscriptions follow underneath. The header steps back
   from a normal data row — it is a section label. Grouping is carried by
   whitespace, not a fill: a generous gap above each header sets the section
   apart, and the name is a shade heavier so it reads as a heading over the
   rows beneath it. The name filters the list to this group; the link beside
   it opens it in Feishu. */
tr.sub-group-row { cursor: default; }
tr.sub-group-row:hover td { background: transparent; }
tr.sub-group-row td {
    border-bottom: none;
    padding-top: var(--sp-5);
    padding-bottom: var(--sp-1);
}
tr.sub-group-row:first-child td { padding-top: var(--sp-2); }
/* Nest the subscriptions under their group header — their leading cell steps in
   from the section name, so the rows read as belonging to the group above. */
.sub-table tbody tr:not(.sub-group-row) td:first-child { padding-left: var(--sp-4); }
/* The name is a link that opens the group in Feishu — it keeps the emphasized
   ink and weight of a heading, no default underline, gaining one on hover so it
   reads as clickable. */
.sub-group-name {
    font-weight: 600;
    text-decoration: none;
}
.sub-group-name:hover { text-decoration: underline; text-underline-offset: 2px; }

/* The channel picker and the New button ride together at the far edge of the
   header. The header keeps its default wrap (the detail-page nowrap rule does
   not apply here), so on a narrow screen the pair drops onto its own line under
   the title instead of crushing against it. */
.page-header.sub-header { flex-wrap: wrap; }
.sub-actions {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    margin-left: auto;
}
.sub-actions select {
    border: 1px solid var(--border);
    border-radius: var(--r-sm);
    background: var(--bg-white);
    color: var(--text);
    font-family: inherit;
    font-size: var(--fs-md);
    height: var(--control-h);
    padding: 0 var(--sp-2);
}
.sub-actions select:hover { border-color: var(--border-strong); }
.sub-actions select:focus { border-color: var(--accent); }

/* ── Cells ──
   The alertname is what someone scans for, so it is the only cell at full ink
   and full weight. The ID is what they copy into a ticket afterwards, so it is
   monospaced and quiet — in the old design it was the brightest thing in the
   row, which spent the accent colour on the least useful column. */
.cell-alertname {
    color: var(--text);
    font-weight: 500;
}
.cell-id {
    color: var(--ink-3);
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    font-weight: 400;
}
.cell-meta {
    color: var(--ink-2);
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
}
.cell-age {
    color: var(--text-secondary);
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    white-space: nowrap;
}
.cell-assignee { color: var(--text-secondary); }
.cell-dash { color: var(--rule-2); }


/* Paging is a follow-up action, not a destination, so it sits at the end of
   the rows on the reading edge rather than centred under them. */
.pager {
    display: flex;
    justify-content: flex-end;
    gap: var(--sp-2);
    padding-top: var(--sp-4);
    margin-top: var(--sp-1);
    border-top: 1px solid var(--rule);
}

/* ── Page header ────────────────────────────────────────────────────────── */

.page-header {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: var(--sp-2) var(--sp-3);
    margin-bottom: var(--sp-4);
}
/* Direct children only. Subscriptions and mute rules put a button straight
   into the header and it belongs on the far edge; the alert detail wraps its
   action in .detail-actions, and a descendant match pushed that button to the
   right edge of its own container too. Harmless while the container shrank to
   fit, but on a narrow screen the container goes full width and the button
   floated away from the title, alone on a line with a void beside it. */
.page-header > .btn,
.page-header > .detail-actions { margin-left: auto; }
/* Bold, matching the product's own title treatment. It is the one place in
   the console that uses the heaviest of the three weights. */
.page-title {
    font-size: var(--fs-xl);
    font-weight: 700;
    letter-spacing: var(--track-title);
    line-height: var(--lh-tight);
    overflow-wrap: anywhere;
    min-width: 0;
}
/* ── Alert / event detail ───────────────────────────────────────────────── */


/* Label above value. Nothing can push a value out of alignment, and the
   columns reflow on their own instead of needing a breakpoint. */
/* Two cards, two questions. The wide one is what the alert *is* — identity and
   labels, frozen when it was created. The narrow one beside it is what the
   alert is *doing*: the handful of fields that move while someone works it. In
   one grid they were the same kind of thing, and after a reload you had to find
   the state again among fifteen labels that had not changed.

   The two cards share one height (grid's default `stretch`, spelled out so it
   is not dropped as redundant): a status column ending mid-air beside a tall
   card reads as a fragment of the row rather than the other half of it. */
.detail-columns {
    display: grid;
    grid-template-columns: minmax(0, 1fr) var(--status-col);
    align-items: stretch;
    gap: var(--sp-3);
    margin-bottom: var(--sp-3);
}
.detail-columns .detail-card { margin-bottom: 0; }

/* One field per row: the column is narrow, and a status read at a glance must
   not depend on which of two columns the eye lands in. */
.status-list {
    display: grid;
    gap: var(--sp-4);
}

/* Label beside value on a shared key column, two pairs to a row once there is
   width for them. The key column is sized to its own longest label rather than
   to a fixed 150px: the longest label in this card is "Cluster version" at
   84px, so the fixed width was throwing away 66px on every row, and a short
   label like "ID" ended up 162px from its value. 
   Label-above-value spread thirteen fields down the page and left the right
   half of the card empty; the eye had to find each label before it could read
   the value under it. Beside it, the labels line up on one edge and the values
   on another, and the same content costs about a third of the height. */
.detail-grid {
    display: grid;
    grid-template-columns: max-content minmax(0, 1fr);
    gap: var(--sp-2) var(--sp-4);
    margin-bottom: var(--sp-4);
    align-items: baseline;
}
@media (min-width: 1100px) {
    .detail-grid {
        grid-template-columns: max-content minmax(0, 1fr) max-content minmax(0, 1fr);
        column-gap: var(--sp-6);
    }
}
/* Inside the identity grid the field is no longer a box of its own: its label
   and value are cells of that grid, so they align across rows rather than only
   within one field. Scoped, because the status column keeps stacking its label
   above its value — it is one narrow column, not a table. */
.detail-grid .detail-field {
    display: contents;
}

.detail-field {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
    min-width: 0;
}
.detail-label {
    color: var(--text-muted);
    font-size: var(--fs-xs);
    font-weight: 500;
    letter-spacing: 0.02em;
}
/* Flex so a trailing action wraps to the next line
   flush with the value instead of inheriting an inline margin. */
.detail-value {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 2px var(--sp-2);
    min-width: 0;
    color: var(--text);
    font-size: var(--fs-md);
    font-weight: 500;
    overflow-wrap: anywhere;
}
.detail-value .inline-action { margin-left: 0; }
.detail-value.mono { font-family: var(--font-mono); font-size: var(--fs-sm); }

/* The alert's own sentence, at the top of the card.
   It used to sit under fifteen fields behind a rule, which put the one thing a
   person wrote below everything a machine derived. What fired reads first; the
   identity that qualifies it reads second. */
.detail-lead {
    margin: 0 0 var(--sp-4);
    padding-bottom: var(--sp-4);
    border-bottom: 1px solid var(--border-light);
    font-size: var(--fs-lg);
    line-height: var(--lh-relaxed);
}


.delivery-warning {
    margin-top: var(--sp-3);
    padding: var(--sp-2) var(--sp-3);
    border: 1px solid var(--sev-warn-border);
    border-radius: var(--r-sm);
    background: var(--sev-warn-bg);
    color: var(--sev-warn-fg);
    font-size: var(--fs-md);
}

.detail-links {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
    margin-top: var(--sp-3);
}
.detail-links a {
    display: inline-flex;
    align-items: center;
    height: var(--control-h-sm);
    padding: 0 var(--sp-3);
    border: 1px solid var(--border);
    border-radius: var(--r-sm);
    color: var(--text-secondary);
    font-size: var(--fs-sm);
    text-decoration: none;
}
.detail-links a { color: var(--link); }
.detail-links a:hover { border-color: var(--link); background: var(--bg); }

/* A reference, not a destination. These wore the same bordered box as the
   links beside them and so promised a click that never came — the more so once
   the real links took a colour. They read as what they are now: a label, and a
   machine value that selects whole on one click, because the only thing anyone
   does with it is paste it somewhere else. */
.detail-links .detail-ref {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    height: var(--control-h-sm);
    color: var(--ink-3);
    font-size: var(--fs-sm);
}
.detail-links .detail-ref .mono {
    color: var(--text-secondary);
    user-select: all;
}

/* ── Lifecycle actions ──────────────────────────────────────────────────── */

.detail-actions { display: flex; align-items: flex-start; justify-content: flex-end; flex-shrink: 0; gap: var(--sp-2); }
/* Mute is orthogonal to Ack/Resolve, so it sits a little apart. */
.action-mute { margin-left: var(--sp-2); }
.resolve-actions { position: relative; }
.resolve-menu-wrap { position: relative; display: inline-flex; flex-direction: column; align-items: flex-end; }
/* Popup surface. The resolve menu and the closed-set field menus hold
   different things but are the same box, so the box is declared once — a
   second popup that was nearly this one is how a stylesheet stops being a
   system. */
.resolve-dropdown,
.token-menu {
    position: absolute;
    right: 0;
    z-index: 30;
    padding: var(--sp-2);
    background: var(--bg-white);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    box-shadow: var(--shadow-pop);
}
.resolve-dropdown {
    top: calc(100% + var(--sp-2));
    display: grid;
    gap: var(--sp-1);
    min-width: 240px;
}
.resolve-option-button {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
    width: 100%;
    padding: var(--sp-2) var(--sp-3);
    border: 1px solid transparent;
    border-radius: var(--r-sm);
    background: transparent;
    color: var(--text);
    font-family: inherit;
    text-align: left;
    cursor: pointer;
    transition: background var(--dur), border-color var(--dur);
}
.resolve-option-button:hover { background: var(--bg); border-color: var(--border-light); }
.resolve-option-button strong { font-size: var(--fs-md); font-weight: 500; }
.resolve-option-button small { color: var(--text-muted); font-size: var(--fs-xs); line-height: var(--lh-tight); }

/* ── Agent analysis ─────────────────────────────────────────────────────────
   What an agent made of the alert. It sits beside the human record rather than
   inside it, and the styling says so: the status chip is the only colour, the
   verdict is a quiet outline, and nothing here borrows the alarm hue. An agent
   that is slow, wrong or down costs this card a paragraph and never the
   response, so it must never look like the alert's own state. */

/* One hairline between runs and nothing else — the same separation the
   occurrence rows below use. Each run is a block that starts with a header
   line, so there is never a paragraph or a timestamp whose owner has to be
   worked out from which line it sits nearest. */
.agent-run-list { display: flex; flex-direction: column; }
.agent-run { border-top: 1px solid var(--border-light); }
.agent-run:first-child { border-top: 0; }

/* The whole of a run that is not open: name, what it concluded, and when, on
   one line. Times sit right, in a column of their own, so a stack of runs can
   be read down either edge. */
.agent-run-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--sp-2);
    padding: var(--sp-2) 0;
    list-style: none;
    cursor: pointer;
}
.agent-run-head::-webkit-details-marker { display: none; }
.agent-run-head::before {
    content: '›';
    width: var(--marker-w);
    flex: 0 0 var(--marker-w);
    align-self: center;
    text-align: center;
    color: var(--text-muted);
    font-size: var(--fs-xl);
    line-height: 1;
    transition: transform var(--dur);
}
.agent-run[open] > .agent-run-head::before { transform: rotate(90deg); }
/* A run with nothing to open keeps the marker's width and drops its glyph, so
   every name on the card still starts on the same vertical. */
.agent-run-head.flat { cursor: default; }
.agent-run-head.flat::before { content: ''; }
/* The agent's name is an identifier it was registered under, not prose. */
.agent-run-name {
    min-width: 0;
    color: var(--text);
    font-family: var(--font-mono);
    font-weight: 500;
    overflow-wrap: anywhere;
}
/* Sentence case, unlike the state pills: these read as words rather than as an
   enum, and small caps on "Dispatched · 12m quiet" would shout a fact that is
   only mildly interesting. */
.agent-status { background: var(--bg); color: var(--text-secondary); }
.agent-status-running { background: var(--sev-info-bg); color: var(--sev-info); }
.agent-status-succeeded { background: var(--sev-ok-bg); color: var(--sev-ok); }
/* A failed run is a fact about the agent, never about the alert, so it takes
   the warning tint and leaves the alarm hue to things that wake people up. */
.agent-status-failed {
    background: var(--sev-warn-bg);
    border-color: var(--sev-warn-border);
    color: var(--sev-warn-fg);
}
/* The finding itself: outlined rather than tinted, because it is a prediction
   waiting to be checked against what the human eventually records, and a
   filled chip beside the state pills would read as one of them. */
.agent-verdict {
    border-color: var(--border-strong);
    background: transparent;
    color: var(--text);
}
/* Why the run happened, kept next to the chips rather than pushed to the far
   edge of a full-width card, where it would read as a column of its own. */
.agent-run-trigger {
    color: var(--text-muted);
    font-size: var(--fs-xs);
    white-space: nowrap;
}
/* When it ran: start time, then how long it took or that it has not stopped.
   Quiet and to the right, because it is a coordinate rather than a finding —
   the times a reader compares are the ones in the alert's own timeline. */
.agent-run-when {
    margin-left: auto;
    color: var(--text-muted);
    font-size: var(--fs-sm);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* Body aligns to the text column of its own header, like the occurrence rows,
   so the indent says which run this paragraph belongs to. */
.agent-run-body {
    padding: 0 0 var(--sp-3) calc(var(--marker-w) + var(--sp-2));
}
/* Prose an agent wrote for a person to read, so it takes the body size and the
   measure everything else on this page's prose takes. */
.agent-run-summary {
    max-width: 80ch;
    color: var(--text);
    font-size: var(--fs-body);
    line-height: var(--lh-relaxed);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}
.agent-run-detail { margin-top: var(--sp-2); }
.agent-run-detail > summary {
    cursor: pointer;
    color: var(--text-muted);
    font-size: var(--fs-sm);
}
.agent-run-detail > pre {
    margin-top: var(--sp-2);
    max-height: 24rem;
    overflow: auto;
    padding: var(--sp-2);
    background: var(--surface-sunken, var(--bg));
    border: 1px solid var(--border-light);
    border-radius: var(--radius-sm, 4px);
    font-size: var(--fs-sm);
    line-height: var(--lh-relaxed);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}
/* Directly under the summary it belongs to, never at the foot of the card. */
.agent-run-link {
    display: inline-block;
    margin-top: var(--sp-2);
    color: var(--link);
    font-size: var(--fs-sm);
    text-decoration: underline;
}
.agent-run-link:hover { color: var(--link-hover); }

/* ── Instances ──────────────────────────────────────────────────────────── */

.instance-list { display: flex; flex-direction: column; }
.instance-row { border-top: 1px solid var(--border-light); }
.instance-row:first-child { border-top: 0; }
.instance-row > summary {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-2) 0;
    font-size: var(--fs-sm);
    list-style: none;
    cursor: pointer;
}
.instance-row > summary::-webkit-details-marker { display: none; }
.instance-row > summary::before {
    content: '›';
    width: var(--marker-w);
    flex: 0 0 var(--marker-w);
    text-align: center;
    color: var(--text-muted);
    font-size: var(--fs-xl);
    line-height: 1;
    transition: transform var(--dur);
}
.instance-row[open] > summary::before { transform: rotate(90deg); }
.instance-name {
    min-width: 0;
    color: var(--text);
    font-family: var(--font-mono);
    font-weight: 500;
    overflow-wrap: anywhere;
}
.instance-summary-meta {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    margin-left: auto;
    color: var(--text-muted);
    white-space: nowrap;
}

/* Body aligns to the text column of the summary, not to a guessed offset. */
.instance-body { padding: 0 0 var(--sp-3) calc(var(--marker-w) + var(--sp-2)); }
.instance-facts {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(var(--col-min), 1fr));
    gap: var(--sp-3) var(--sp-5);
}
.instance-facts > span {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    color: var(--text-secondary);
    font-size: var(--fs-sm);
    overflow-wrap: anywhere;
}
.instance-facts strong {
    color: var(--text-muted);
    font-size: var(--fs-xs);
    font-weight: 500;
}
.instance-section-title { margin: var(--sp-4) 0 var(--sp-2); }
/* Points back at the common labels listed once above, so a row that looks short
   does not read as a row that lost context. */
.instance-common-note {
    margin-top: var(--sp-2);
    color: var(--text-muted);
    font-size: var(--fs-xs);
}
.instance-desc {
    max-width: 80ch;
    color: var(--text);
    font-size: var(--fs-body);
    line-height: var(--lh-relaxed);
    white-space: pre-wrap;
}
.instance-observations { display: flex; flex-direction: column; gap: var(--sp-1); }
.instance-observation {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    color: var(--text-secondary);
    font-size: var(--fs-sm);
}
.instance-observation.muted { color: var(--text-muted); }
.observation-dot {
    width: var(--dot);
    height: var(--dot);
    flex-shrink: 0;
    border-radius: var(--r-full);
    background: var(--text-muted);
}
.observation-dot.firing { background: var(--sev-crit); }
.observation-dot.resolved { background: var(--sev-ok); }

.instance-link { margin-top: var(--sp-2); }
.instance-link a { color: var(--link); font-size: var(--fs-sm); text-decoration: none; }
.instance-link a:hover { text-decoration: underline; }

/* Machine key/value pairs: one per row, shared key column. */
.kv {
    display: grid;
    grid-template-columns: minmax(var(--kv-key), max-content) minmax(0, 1fr);
    gap: var(--sp-2) var(--sp-4);
    align-items: baseline;
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    line-height: var(--lh-tight);
}
.kv dt { color: var(--text-muted); }
.kv dd { color: var(--text); min-width: 0; overflow-wrap: anywhere; }

.raw-context { margin-top: var(--sp-3); }
.raw-context > summary {
    margin-bottom: var(--sp-2);
    color: var(--link);
    font-size: var(--fs-sm);
    cursor: pointer;
}

/* ── Event detail content blocks ────────────────────────────────────────── */

.content-section + .content-section {
    margin-top: var(--sp-4);
    padding-top: var(--sp-4);
    border-top: 1px solid var(--border-light);
}
.content-label { margin-bottom: var(--sp-2); }
.raw-event {
    margin-top: var(--sp-3);
    padding: var(--sp-3);
    max-height: 520px;
    overflow: auto;
    border: 1px solid var(--border-light);
    border-radius: var(--r-sm);
    background: var(--bg);
    color: var(--text-secondary);
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    line-height: var(--lh-relaxed);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

/* ── Timeline ───────────────────────────────────────────────────────────── */

.tl-tabs {
    display: flex;
    gap: var(--sp-4);
    margin-bottom: var(--sp-3);
    border-bottom: 1px solid var(--border);
}
.tl-tab {
    border: 0;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    padding: 0 0 var(--sp-2);
    background: transparent;
    color: var(--text-secondary);
    font-size: var(--fs-md);
    font-weight: 500;
    cursor: pointer;
}
.tl-tab:hover { color: var(--text); }
.tl-tab.active {
    color: var(--text);
    border-bottom-color: var(--text);
}
/* Debug is an escape hatch most people never need; keep it quiet. Opacity
   (not a grey token) so it recedes clearly in both light and dark, where the
   muted/secondary ink tokens sit too close to tell apart. */
.tl-tab.muted { color: var(--text-muted); font-weight: 400; opacity: 0.5; }
.tl-tab.muted:hover { opacity: 0.85; }
.tl-tab.muted.active { color: var(--text-secondary); border-bottom-color: var(--text-muted); opacity: 1; }
.tl-list { position: relative; }
.tl-line {
    position: absolute;
    left: calc(var(--tl-dot) / 2 - 0.5px);
    top: var(--sp-2);
    bottom: var(--sp-2);
    width: 1px;
    background: var(--border);
}
.tl-entry {
    display: flex;
    align-items: flex-start;
    gap: var(--sp-3);
    padding: var(--sp-1) 0;
    position: relative;
    font-size: var(--fs-md);
}
.tl-dot {
    width: var(--tl-dot);
    height: var(--tl-dot);
    flex-shrink: 0;
    /* Centre the dot on the cap height of the first line of text. */
    margin-top: var(--sp-1);
    border: 2px solid var(--border);
    border-radius: var(--r-full);
    background: var(--bg-white);
    z-index: 1;
}
.tl-dot.action { border-color: var(--accent); }
.tl-dot.system { border-color: var(--text-muted); }
.tl-dot.comment { border-color: var(--sev-info); background: var(--sev-info-bg); }
.tl-dot.agent { border-color: var(--accent); background: var(--accent); }
.tl-body { flex: 1; min-width: 0; }
.tl-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2); }
.tl-event { color: var(--text); font-weight: 500; }
.tl-actor { color: var(--text-secondary); font-size: var(--fs-sm); }
.tl-transition { color: var(--text-muted); font-size: var(--fs-xs); }
.tl-time {
    color: var(--text-muted);
    font-size: var(--fs-sm);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
/* A comment is the one thing on the timeline a person wrote for another
   person, so it reads at prose size and in full ink; the machine transitions
   around it stay small and grey. */
.tl-detail {
    margin-top: 2px;
    color: var(--text-secondary);
    font-size: var(--fs-body);
    line-height: var(--lh-relaxed);
    overflow-wrap: anywhere;
}
.tl-detail.recalled { text-decoration: line-through; }

/* Comments tab: the conversation as chat bubbles. */
.tl-chat { display: flex; flex-direction: column; gap: var(--sp-3); }
.chat-msg { display: flex; align-items: flex-start; gap: var(--sp-2); }
.chat-avatar {
    flex-shrink: 0;
    width: 28px;
    height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--r-full);
    background: var(--surface-3);
    color: var(--text-secondary);
    font-size: var(--fs-sm);
    font-weight: 600;
    text-transform: uppercase;
}
.chat-avatar.agent { background: var(--accent); color: var(--bg-white); }
.chat-body { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.chat-meta { display: flex; align-items: baseline; gap: var(--sp-2); }
.chat-name { color: var(--text); font-size: var(--fs-sm); font-weight: 600; }
.chat-badge {
    padding: 0 6px;
    border-radius: var(--r-sm);
    background: var(--accent);
    color: var(--bg-white);
    font-size: var(--fs-xs);
    font-weight: 600;
}
.chat-time {
    color: var(--text-muted);
    font-size: var(--fs-xs);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.chat-bubble {
    align-self: flex-start;
    max-width: 100%;
    padding: var(--sp-2) var(--sp-3);
    border-radius: var(--r-md);
    background: var(--surface-3);
    color: var(--text);
    font-size: var(--fs-body);
    line-height: var(--lh-relaxed);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}
.chat-bubble.agent { background: var(--sev-info-bg); }
.chat-bubble.recalled { text-decoration: line-through; opacity: 0.7; }

.tl-compose {
    display: flex;
    align-items: flex-end;
    gap: var(--sp-2);
    margin-top: var(--sp-4);
    padding-top: var(--sp-4);
    border-top: 1px solid var(--border-light);
}
.tl-compose textarea {
    flex: 1;
    min-height: var(--control-h);
    padding: var(--sp-2) var(--sp-3);
    border: 1px solid var(--border);
    border-radius: var(--r-sm);
    background: var(--bg);
    color: var(--text);
    font-family: inherit;
    font-size: var(--fs-md);
    line-height: var(--lh);
    resize: vertical;
}
.tl-compose textarea:hover { border-color: var(--border-strong); }
.tl-compose textarea:focus { border-color: var(--accent); background: var(--bg-white); }
.tl-compose textarea::placeholder { color: var(--text-muted); }

/* ── Modal ──────────────────────────────────────────────────────────────── */

.modal-overlay {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--sp-4);
    background: rgba(20, 22, 30, 0.35);
}
.modal {
    width: min(520px, 100%);
    max-height: calc(100vh - var(--sp-7));
    overflow-y: auto;
    padding: var(--sp-6);
    background: var(--bg-white);
    border-radius: var(--r-md);
    box-shadow: var(--shadow-pop);
}
.modal h3 { margin-bottom: var(--sp-4); font-size: var(--fs-lg); font-weight: 500; }
.modal label {
    display: block;
    margin-bottom: var(--sp-1);
    color: var(--text-muted);
    font-size: var(--fs-xs);
    font-weight: 500;
}
.modal > input,
.modal > select,
.modal > textarea { margin-bottom: var(--sp-3); }
.modal-buttons {
    display: flex;
    justify-content: flex-end;
    gap: var(--sp-2);
    margin-top: var(--sp-5);
}

/* The "also create a mute rule" opt-in: a compact checkbox on one line,
   overriding the muted, block .modal label above. */
.mute-also {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    margin: var(--sp-3) 0 0;
    color: var(--text);
    font-size: var(--fs-md);
    font-weight: 400;
    cursor: pointer;
}
/* Opt back out of the full-width, control-height .modal input sizing. */
.mute-also input[type="checkbox"] {
    width: auto;
    height: auto;
    flex: none;
    margin: 0;
    padding: 0;
    accent-color: var(--accent);
}

/* ── Combobox: a text field that suggests ───────────────────────────────────
   The field below the panel is plain free text, so nothing here may read as a
   required choice — the rows are a shortcut past typing an address in full.

   The panel is fixed, not absolute: absolute would be clipped by the dialog's
   own scrollport, and in-flow would shove the fields under it around on every
   keystroke. Its coordinates come from the field's box, set in app.js. */
.combo { margin-bottom: var(--sp-3); }
/* On the filter bar the combo is just one more inline control, not a stacked
   form row: drop the row spacing and let its input match the dense fields. */
.filters .combo { margin-bottom: 0; }
.combo-list {
    position: fixed;
    z-index: 110;  /* over .modal-overlay */
    margin-top: var(--sp-1);
    max-height: calc(var(--control-h) * 6);
    overflow-y: auto;
    padding: var(--sp-1);
    background: var(--bg-white);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    box-shadow: var(--shadow-pop);
    list-style: none;
}
.combo-option {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: var(--sp-2) var(--sp-3);
    border-radius: var(--r-sm);
    cursor: pointer;
}
/* One highlight for both lanes: the keyboard's active row and the pointer's
   hovered row are the same "this is what Enter or a click takes" state. */
.combo-option.on,
.combo-option:hover { background: var(--bg-hover); }
.combo-name { color: var(--text); }
.combo-detail { color: var(--text-muted); font-size: var(--fs-xs); }

.filter-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(var(--col-min), 1fr));
    gap: var(--sp-3);
    margin-bottom: var(--sp-3);
}
/* Bottom margin keeps a hint attached to the field above it rather than
   reading as the caption of the next label. */
.form-hint { margin: var(--sp-1) 0 var(--sp-3); color: var(--text-muted); font-size: var(--fs-xs); }

/* Section eyebrow inside a dialog: separates the promoted attributes from the
   raw labels, which are two different questions with overlapping names. The
   type comes from the shared eyebrow rule above; only the spacing is its own. */
.form-section { margin: var(--sp-4) 0 var(--sp-2); }
.form-section-note { font-weight: 400; text-transform: none; }

/* The one sentence that says fields narrow together, replacing two paragraphs
   that said it after the fact. */
.form-lead { margin: 0 0 var(--sp-3); color: var(--text-secondary); font-size: var(--fs-body); }
.field-note { margin: var(--sp-1) 0 0; }

/* Everything a filter does not usually name. The count is what keeps folding
   honest: a constraint hidden without a trace is a filter nobody can read. */
.more-filters {
    margin-top: var(--sp-3);
    padding: var(--sp-2) var(--sp-3);
    border: 1px solid var(--border);
    border-radius: var(--r-sm);
}
.more-filters > summary { cursor: pointer; font-size: var(--fs-md); font-weight: 500; }
.more-filters[open] > summary { margin-bottom: var(--sp-3); }
.more-count { color: var(--link); font-weight: 500; }

/* The filter in a sentence, directly above the button that saves it. A full
   border and a tint, like .delivery-warning and .label-collision: an accent
   stripe down one edge would be this stylesheet's only one, and a second idiom
   for "read this" is what the token block exists to prevent. An empty mute rule
   silences the fleet, so it is coloured like the hazard it is. */
.filter-summary {
    margin-top: var(--sp-4);
    padding: var(--sp-2) var(--sp-3);
    border: 1px solid var(--border);
    border-radius: var(--r-sm);
    background: var(--bg);
    color: var(--text);
    font-size: var(--fs-md);
    line-height: var(--lh);
}
.filter-summary.warn {
    border-color: var(--sev-crit-border);
    background: var(--sev-crit-bg);
    color: var(--sev-crit);
    font-weight: 500;
}

/* What the filter above would have done, under what it means. Attached to the
   summary rather than spaced away: the two are one statement. */
.filter-preview {
    margin-top: calc(var(--sp-1) * -1);
    padding: var(--sp-3);
    border: 1px solid var(--border);
    border-top: 0;
    border-radius: 0 0 var(--r-sm) var(--r-sm);
    background: var(--bg);
    font-size: var(--fs-sm);
    line-height: var(--lh);
}

/* Dimmed, not emptied: an answer being refreshed is still the answer. */
.filter-preview.pending { opacity: 0.5; }

.filter-preview-head { color: var(--text); font-size: var(--fs-md); }
.filter-preview-head strong { font-variant-numeric: tabular-nums; }
.filter-preview-unit { color: var(--text-muted); }

.filter-preview-note,
.filter-preview-empty {
    margin: 0;
    color: var(--text-muted);
}

.filter-preview-warn {
    margin: var(--sp-2) 0 0;
    color: var(--sev-crit);
    font-weight: 500;
}

/* Seven days, oldest to newest. Fixed height so the row does not resize as the
   filter narrows: a box growing under the cursor reads as motion, not data. */
.filter-preview-bars {
    display: flex;
    align-items: flex-end;
    gap: var(--sp-1);
    height: 32px;
    margin-top: var(--sp-3);
}
.filter-preview-bar {
    flex: 1;
    min-height: 2px;
    border-radius: 1px;
    background: var(--accent);
}
.filter-preview-bar.empty { background: var(--border); }

.filter-preview-row {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--sp-2);
    margin-top: var(--sp-3);
}
.filter-preview-key {
    color: var(--text-muted);
    font-size: var(--fs-xs);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.filter-preview-name { color: var(--text); }
.filter-preview-count {
    margin-left: var(--sp-1);
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

.filter-preview-latest {
    margin: var(--sp-2) 0 0;
    padding: 0;
    list-style: none;
}
.filter-preview-latest li {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--sp-2);
    padding: var(--sp-1) 0;
}
.filter-preview-latest a { color: var(--link); text-decoration: none; }
.filter-preview-latest a:hover { text-decoration: underline; }
.filter-preview-alertname { color: var(--text); }
.filter-preview-meta { color: var(--text-muted); font-size: var(--fs-xs); }

/* Said at the row being typed, which is the only place it can be acted on. */
.label-collision {
    margin: var(--sp-1) 0 var(--sp-2);
    padding: var(--sp-2);
    border: 1px solid var(--sev-warn-border);
    border-radius: var(--r-sm);
    background: var(--sev-warn-bg);
    color: var(--sev-warn-fg);
    font-size: var(--fs-xs);
    line-height: var(--lh);
}

/* A closed-set field's menu. It hangs off the field rather than the box so it
   is positioned against the whole control, tokens and all. */
.token-field { position: relative; }
.token-menu {
    top: calc(100% + var(--sp-1));
    left: 0;
    display: flex;
    flex-direction: column;
    gap: var(--sp-1);
    max-height: 180px;
    overflow-y: auto;
}
.token-menu[hidden] { display: none; }
.token-option {
    padding: var(--sp-1) var(--sp-2);
    border: none;
    border-radius: var(--r-sm);
    background: none;
    color: var(--text);
    font: inherit;
    font-size: var(--fs-md);
    text-align: left;
    cursor: pointer;
}
.token-option:hover:not([disabled]) { background: var(--bg-hover); }
.token-option[disabled] { color: var(--text-muted); cursor: not-allowed; }
.token-menu-empty { padding: var(--sp-1) var(--sp-2); color: var(--text-muted); font-size: var(--fs-xs); }

/* Values within one field are alternatives, which nothing else in the form
   said. The word sits in the gap the tokens already leave, so it costs no
   layout and no element of its own. */
.token + .token { position: relative; margin-left: 18px; }
.token + .token::before {
    content: "or";
    position: absolute;
    top: 50%;
    left: -17px;
    transform: translateY(-50%);
    color: var(--text-muted);
    font-size: var(--fs-xs);
}

/* Checkbox group: a closed option set small enough to show every choice at
   once, so it stands in for a multi-select. The control height matches the
   text fields beside it in the filter grid. */
.checkbox-group {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-3);
    min-height: var(--control-h);
}
.checkbox-group label {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-1);
    margin-bottom: 0;
    color: var(--text);
    font-size: var(--fs-md);
    font-weight: 400;
    cursor: pointer;
}
.checkbox-group input[type="checkbox"] {
    width: auto;
    height: auto;
    margin: 0;
    padding: 0;
    accent-color: var(--accent);
}
.checkbox-group label:has(input:disabled) {
    color: var(--text-muted);
    cursor: not-allowed;
}

/* Token field: an open value set, where the closed one above uses checkboxes.
   The box is the control — it carries the border and the focus ring — and the
   text input inside it is stripped bare, so committed values and the one being
   typed sit on one surface. It starts at the control height the fields beside
   it use and grows by rows from there, rather than scrolling values out of
   sight the way one line of comma-separated text did. */
.token-box {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-1);
    min-height: var(--control-h);
    padding: 3px var(--sp-2);
    border: 1px solid var(--border);
    border-radius: var(--r-sm);
    background: var(--bg-white);
    cursor: text;
}
.token-box:hover { border-color: var(--border-strong); }
/* The ring belongs to the box, since the input inside it has no edge of its
   own to show focus on. */
.token-box:focus-within { border-color: var(--accent); }

.modal .token-input {
    flex: 1;
    width: auto;
    min-width: 10ch;
    height: calc(var(--control-h) - var(--sp-2));
    padding: 0;
    border: none;
    background: none;
}
.modal .token-input:hover,
.modal .token-input:focus { border: none; outline: none; }

.token {
    display: inline-flex;
    align-items: center;
    padding: 2px var(--sp-1) 2px 2px;
    border: 1px solid var(--border-light);
    border-radius: var(--r-sm);
    background: var(--bg);
    color: var(--text);
    font-size: var(--fs-xs);
    line-height: var(--lh-tight);
}
.token-text,
.token-remove {
    border: none;
    background: none;
    line-height: 1;
    cursor: pointer;
}
/* Monospace and `pre`: a value is checked character by character, and a run of
   spaces inside one has to look like a run of spaces rather than collapsing to
   the width of a single one. Leading and trailing space cannot get this far —
   values are trimmed on the way in — so what a token shows is what will be
   sent. */
.token-text {
    padding: 2px var(--sp-1);
    border-radius: var(--r-sm);
    color: inherit;
    font-family: var(--font-mono);
    font-size: inherit;
    white-space: pre;
}
.token-text:hover { background: var(--bg-hover); }
.token-remove {
    padding: 0 2px;
    border-radius: var(--r-sm);
    color: var(--text-muted);
    font-size: var(--fs-md);
}
.token-remove:hover { background: var(--bg-hover); color: var(--text); }

/* A field is an operator over its values — the alertmanager matcher read for
   one label. The operator sits beside the label so the value box below keeps
   the full width of its cell; monospace so `=~` and `!~` read as the symbols
   they are. */
.field-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-2);
    margin-bottom: var(--sp-1);
    /* Reserve the operator's height whether or not this field renders one, so a
       field with a selector and one without keep their value boxes on the same
       line across a row. Kept close to the label's own line box so the operator
       adds no row height over a plain label. */
    min-height: 20px;
}
.field-head label { margin-bottom: 0; }
/* The operator is a quiet modifier on the value, not a control competing with
   it — no box, muted until hovered, keeping only its caret to say it is a
   picker. `.modal select` is outspecified so this wins over the filled default. */
.modal .match-op,
.modal .label-op {
    flex: none;
    width: auto;
    height: 20px;
    padding: 0 var(--sp-1);
    border: none;
    background: none;
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    cursor: pointer;
}
.modal .match-op:hover,
.modal .label-op:hover { color: var(--text); background: var(--bg-hover); border-radius: var(--r-sm); }

/* A regex value called out where the filter is stated in prose — the sentence
   under the form and the preview — so a pattern is not read as a literal. */
.filter-re { font-family: var(--font-mono); color: var(--accent-solid); }

/* The input matches the tokens, so a value being typed and one already
   committed are compared in the same shapes. */
.modal .token-input { font-family: var(--font-mono); }

/* Label rows: a filter on a label is name, operator, value — the same three
   parts the promoted fields carry, laid out in a line. The operator is a picker
   rather than a printed `=`, so a label can be negated or matched by regex the
   way alertmanager's matchers are. */
.label-rows { margin-bottom: var(--sp-2); }
.label-row {
    display: grid;
    grid-template-columns: 1fr auto 1fr auto;
    align-items: center;
    gap: var(--sp-2);
    margin-bottom: var(--sp-2);
}
.modal .label-row input { font-family: var(--font-mono); }
.label-remove {
    width: var(--control-h);
    height: var(--control-h);
    border: none;
    border-radius: var(--r-sm);
    background: none;
    color: var(--text-muted);
    font-size: var(--fs-lg);
    line-height: 1;
    cursor: pointer;
}
.label-remove:hover { background: var(--bg-hover); color: var(--text); }
.label-add { margin-bottom: var(--sp-3); }

/* ── Sign-in ───────────────────────────────────────────────────────────── */

.signin {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    min-height: 100dvh;
    padding: var(--sp-4);
}
.signin-card {
    width: min(360px, 100%);
    padding: var(--sp-7) var(--sp-6);
    background: var(--bg-white);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
    box-shadow: var(--shadow-card);
    text-align: center;
}
.signin-brand { font-size: var(--fs-xl); font-weight: 700; letter-spacing: -0.3px; }
.signin-hint { margin-top: var(--sp-2); color: var(--text-secondary); }
.signin-actions { display: grid; gap: var(--sp-2); margin-top: var(--sp-6); }
.signin-actions .btn { width: 100%; }
.signin-error { margin-top: var(--sp-4); color: var(--sev-crit); font-size: var(--fs-sm); }

/* ── Feedback ───────────────────────────────────────────────────────────── */

.toast {
    position: fixed;
    right: var(--sp-5);
    bottom: var(--sp-5);
    z-index: 200;
    padding: var(--sp-3) var(--sp-4);
    border-radius: var(--r-sm);
    background: var(--text);
    color: #fff;
    font-size: var(--fs-md);
    box-shadow: var(--shadow-pop);
}
.empty-state {
    padding: var(--sp-8) var(--sp-5);
    text-align: center;
    color: var(--text-muted);
    font-size: var(--fs-md);
}
.error-msg { margin-top: var(--sp-3); color: var(--sev-crit); font-size: var(--fs-md); }
.text-muted { color: var(--text-muted); font-style: italic; }

/* ── Dashboards ─────────────────────────────────────────────────────────── */

/* "Now" strip: current alert-table state, independent of the filters below —
   visually its own thing (smaller, denser, no chart/table toggle, nothing to
   click) so it doesn't read as one more windowed section. */
.now-strip {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--sp-5);
    /* Taller than a toolbar on purpose: this is the page's first answer to
       "what's going on", and at header height it read as chrome rather than
       as that. Its numbers also outrank the windowed KPI tiles below (32px
       vs .tile-value's 26px) for the same reason. No tile carries alarm
       styling — that treatment left with the Overdue tile it belonged to. */
    padding: var(--sp-4) var(--sp-5);
    margin-bottom: var(--sp-4);
    background: var(--bg-white);
    border: 1px solid var(--border);
    border-radius: var(--r-md);
}
.now-strip-head { display: flex; flex-direction: column; gap: 2px; }
.now-strip-title { font-size: var(--fs-sm); font-weight: 700; letter-spacing: var(--track-title); }
.now-strip-caption { font-size: var(--fs-xs); color: var(--text-muted); }
.now-tiles { display: flex; gap: var(--sp-5); flex-wrap: wrap; }
.now-tile { min-width: 110px; }
.now-tile-label { font-size: var(--fs-sm); color: var(--text-secondary); }
.now-tile-value {
    margin-top: var(--sp-1);
    font-size: 32px;
    font-weight: 700;
    line-height: 1.1;
    letter-spacing: var(--track-title);
    font-variant-numeric: tabular-nums;
}
/* Unresolved pages: the strip's alarm number — pages nobody has closed. */
.now-tile-value-critical { color: var(--sev-crit); }
/* A linked tile stays visually calm: no link-blue, no underline on the
   number itself (color/weight are the same as a plain tile — see
   .now-tile-value / -critical above). The hover affordance is the same
   whole-surface background shift .sb-item uses for its own nav links, plus
   an underline on the label only, so the tile still reads as clickable
   without dressing up the number. */
a.now-tile-link {
    display: block;
    color: inherit;
    text-decoration: none;
    border-radius: var(--r-sm);
    padding: var(--sp-1) var(--sp-2);
    margin: calc(var(--sp-1) * -1) calc(var(--sp-2) * -1);
    transition: background var(--dur);
}
a.now-tile-link:hover { background: var(--bg-hover); }
a.now-tile-link:hover .now-tile-label { text-decoration: underline; text-underline-offset: 2px; }

.dash-filters {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-3);
    margin-bottom: var(--sp-5);
}
.dash-filters-label { font-size: var(--fs-sm); color: var(--text-muted); }
.dash-range { display: inline-flex; gap: var(--sp-1); }
.dash-range-btn {
    height: var(--control-h-sm);
    padding: 0 var(--sp-3);
    border: 1px solid var(--border);
    border-radius: var(--r-sm);
    background: var(--bg-white);
    color: var(--text-secondary);
    font-family: inherit;
    font-size: var(--fs-sm);
    cursor: pointer;
    transition: background var(--dur), border-color var(--dur), color var(--dur);
}
.dash-range-btn:hover { border-color: var(--border-strong); }
/* The selected preset, not a hover state: heavier weight and the same
   border-strong a focused field uses, so "this is the one in effect" reads
   without needing colour the alarm hasn't already spent. */
.dash-range-btn[aria-pressed='true'] {
    color: var(--text);
    font-weight: 600;
    border-color: var(--border-strong);
    background: var(--bg-hover);
}
.dash-custom-range { display: inline-flex; align-items: center; gap: var(--sp-2); }
.dash-date {
    height: var(--control-h-sm);
    padding: 0 var(--sp-2);
    border: 1px solid var(--border);
    border-radius: var(--r-sm);
    background: var(--bg-white);
    color: var(--text);
    font-family: inherit;
    font-size: var(--fs-sm);
}
.dash-date:hover { border-color: var(--border-strong); }
.dash-date:focus { border-color: var(--accent); }
.dash-team-filter { display: inline-flex; align-items: center; gap: var(--sp-2); }
.dash-select {
    height: var(--control-h-sm);
    padding: 0 var(--sp-2);
    border: 1px solid var(--border);
    border-radius: var(--r-sm);
    background: var(--bg-white);
    color: var(--text);
    font-family: inherit;
    font-size: var(--fs-sm);
}
.dash-select:hover { border-color: var(--border-strong); }
.dash-select:focus { border-color: var(--accent); }
.dash-caption { color: var(--text-muted); font-size: var(--fs-sm); }
.dash-filters-error { color: var(--sev-crit); font-size: var(--fs-sm); font-weight: 600; }

/* While a fetch is in flight over data already on screen, the previous
   render stays put and just recedes — never a skeleton, never a blank frame. */
.dash-body { transition: opacity var(--dur); }
.dash-body-stale { opacity: 0.55; pointer-events: none; }

.dash-section { margin-bottom: var(--sp-7); }
.dash-section:last-child { margin-bottom: 0; }
.dash-section-head { display: flex; align-items: baseline; gap: var(--sp-2); margin-bottom: var(--sp-3); }
.dash-section-head h3 { font-size: var(--fs-lg); font-weight: 700; letter-spacing: var(--track-title); }
.dash-section-note { color: var(--text-muted); font-size: var(--fs-sm); }

.kpi-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--sp-3); margin-bottom: var(--sp-3); }
.tile { background: var(--bg-white); border: 1px solid var(--border); border-radius: var(--r-md); padding: var(--sp-4); }
.tile-label { font-size: var(--fs-sm); color: var(--text-secondary); }
.tile-value { font-size: 26px; font-weight: 700; margin-top: var(--sp-1); line-height: 1.1; letter-spacing: var(--track-title); font-variant-numeric: tabular-nums; }
.tile-delta { font-size: var(--fs-xs); color: var(--text-muted); margin-top: var(--sp-1); font-variant-numeric: tabular-nums; }
/* Down is good for a noise rate and for night pages, and both deltas say so
   explicitly in the label rather than trusting the colour alone. */
.tile-delta-good { color: var(--sev-ok); font-weight: 600; }
.tile-delta-bad { color: var(--sev-crit); font-weight: 600; }

.dash-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); }
.dash-grid-1 { display: grid; grid-template-columns: 1fr; gap: var(--sp-3); margin-top: var(--sp-3); }

/* min-width: 0 overrides the grid item's default auto minimum, which
   otherwise equals its min-content size. A card's chart view has no wide
   content, but its table view (table-fill's columns, particularly the
   monospace/name ones) can out-measure the card at typical widths — without
   this, that min-content size wins over the 1fr track and the grid column
   (and every card sharing its row) widens the instant "Table" is clicked, a
   width jump the .table-scroll wrapper's own overflow-x can't prevent since
   it isn't the grid item. With this, the card keeps the track's width and
   .table-scroll's horizontal scrollbar takes any real overflow instead. */
.chart-card { background: var(--bg-white); border: 1px solid var(--border); border-radius: var(--r-md); padding: var(--sp-4); min-width: 0; }
.chart-card-head { display: flex; align-items: baseline; gap: var(--sp-2); margin-bottom: var(--sp-2); }
.chart-card-title { font-size: var(--fs-md); font-weight: 600; color: var(--text); }
.chart-card-sub { font-size: var(--fs-sm); color: var(--text-muted); }
.chart-table-toggle { margin-left: auto; }
.chart-card-control { display: inline-flex; align-items: center; gap: var(--sp-1); margin-left: auto; }
.chart-card-control + .chart-table-toggle { margin-left: 0; }
.chart-container { width: 100%; height: 240px; }
.dash-row-link { cursor: pointer; }

@media (max-width: 900px) {
    .kpi-row { grid-template-columns: repeat(2, 1fr); }
    .dash-grid-2 { grid-template-columns: 1fr; }
}
@media (max-width: 480px) {
    .kpi-row { grid-template-columns: 1fr; }
}

/* ── Mobile ─────────────────────────────────────────────────────────────── */

@media (max-width: 720px) {
    body.sidebar-open { overflow: hidden; }
    .shell { height: 100vh; height: 100dvh; }
    .sidebar {
        position: fixed;
        inset: 0 auto 0 0;
        z-index: 100;
        width: min(300px, 86vw);
        height: 100vh;
        height: 100dvh;
        transform: translateX(-100%);
        transition: transform 0.18s ease;
        overscroll-behavior: contain;
    }
    .sidebar.open { transform: translateX(0); box-shadow: 2px 0 16px rgba(20, 22, 30, 0.18); }
    .sidebar-close { display: flex; }
    .sidebar-backdrop {
        display: block;
        position: fixed;
        inset: 0;
        z-index: 90;
        background: rgba(20, 22, 30, 0.38);
        opacity: 0;
        pointer-events: none;
        transition: opacity 0.18s ease;
    }
    .sidebar-backdrop.open { opacity: 1; pointer-events: auto; }
    .main-pane { width: 100%; }
    /* The drawer is the narrow-screen answer and owns its own open state. It
       floats over the content, so there is no width to reclaim and nothing for
       the rail to do — it opens in full whatever the desktop was left in. */
    .sidebar.collapsed { width: var(--sidebar-w); }
    .sidebar.collapsed .sb-title,
    .sidebar.collapsed .sb-label,
    .sidebar.collapsed .sb-count,
    .sidebar.collapsed .sb-logo,
    .sidebar.collapsed .sb-account > span,
    .sidebar.collapsed .sb-caret { display: revert; }
    .sidebar.collapsed .sb-brand { justify-content: space-between; }
    /* The drawer is full width, so the rail's stacked footer does not apply. */
    .sidebar.collapsed .sb-bottom { flex-direction: row; }
    .sidebar.collapsed .sb-account { flex: 1; justify-content: flex-start; }
    .sidebar.collapsed .sb-item { justify-content: flex-start; }
    .sidebar.collapsed .sb-section + .sb-section {
        margin-top: 0;
        padding-top: 0;
        border-top: 0;
    }
    .sidebar-collapse { display: none; }
    /* The bar is gone and what is left is the button. It used to be a 52px
       strip carrying the application's own name — the least useful thing that
       can occupy the scarcest space, and already in the drawer it opens — with
       a rule under it, so every screen began with two headers stacked and the
       detail screen with three.

       The button is lifted out of the flow into a gutter that the first row of
       each page leaves for it, so it shares that row instead of owning one.
       Parked against .main-pane rather than the viewport, it stays put when
       the page scrolls, which is what a control tied to this pane should do. */
    .mobile-header {
        display: block;
        position: absolute;
        top: calc(var(--sp-3) + env(safe-area-inset-top));
        left: var(--sp-2);
        z-index: 5;
        min-height: 0;
        padding: 0;
        background: none;
        border: 0;
    }
    main {
        padding: var(--sp-4);
        padding-top: calc(var(--sp-4) + env(safe-area-inset-top));
    }
    /* The gutter. Every page's first row starts clear of the button, whatever
       that row happens to be — a title, a back link, or a filter bar. */
    main > *:first-child { padding-left: 34px; }
    /* The title and the action share a line. The header wraps by default, and
       the action asked for the full width of it, which guaranteed the button
       was pushed onto a line of its own with the screen empty beside it — the
       longer the alert name, the more certain. Now the header does not wrap and
       the name wraps inside its own column instead, which is the one thing here
       that can be given less room without losing anything. */
    .page-header { flex-wrap: nowrap; }
    .page-header > .detail-actions { width: auto; justify-content: flex-end; }
    /* Too narrow to stand beside anything: the status card stacks under the
       facts, where it is still the block right above the timeline. */
    .detail-columns { grid-template-columns: minmax(0, 1fr); }
    .resolve-menu-wrap { width: 100%; align-items: stretch; }
    .resolve-dropdown { position: static; min-width: 0; width: 100%; margin-top: var(--sp-2); }
}

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