/* Reset & Base */
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }

/* ════════════════════════════════════════════════════════════════════
   Inter, self-hosted.

   --font-sans named Inter for a long time without anything ever shipping
   it: no @font-face, no font file, and a CSP whose font-src is 'self'
   and data: only, so a CDN could not have rescued it either. Every
   machine silently fell through to the next entry — Avenir Next on
   macOS, Segoe UI on Windows — which meant the type scale (700/750
   weights, the -0.01em on the wordmark, the 0.08em micro-labels) was
   tuned against a face nobody was looking at.

   One variable file, 48 KB, covering 100-900. Static cuts would have
   needed four files and still could not render the 750 the sidebar
   labels ask for; a variable axis hits it exactly. Italic is not
   shipped: the ten italic rules in the product are all hint, placeholder
   and empty-state text — never a formatting choice a user makes — and
   synthesising those costs nothing next to another 50 KB.

   swap, not optional: the fallback metrics are close enough that the
   swap is undramatic, and a first paint in Avenir Next beats blank text.
   ════════════════════════════════════════════════════════════════════ */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url('/static/fonts/inter-latin-wght-normal.woff2?v=a9d9e105b8d4') format('woff2-variations');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                   U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC,
                   U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ════════════════════════════════════════════════════════════════════
   Tributary design tokens — single light theme.

   Dark-ready: a future dark theme redefines these same tokens under
   `:root[data-color-scheme="dark"]`, and components must only ever read
   tokens.

   Note the attribute. The scheme goes on `data-color-scheme` and not on
   `data-theme` because those are two different axes, and conflating them
   is what the deleted theme subsystem did — it wrote a theme *id* to
   `data-theme` while no rule anywhere keyed off it. Nothing reads
   `data-theme` now; keeping the scheme off it leaves the name free for a
   real theme axis rather than borrowed for this one.
   ════════════════════════════════════════════════════════════════════ */
:root {
    /* Says out loud what every token below already assumes. Without it the
       browser is free to render the things this stylesheet does not paint —
       native selects, date pickers, scrollbar gutters, the canvas behind a
       reload — from the OS preference, which on a dark desktop puts dark
       widgets inside a light app. The dark theme, when it arrives, sets
       `light dark` here and flips the value on the attribute above. */
    color-scheme: light;

    /* type. --font-mono is the only monospace stack in the product: SQL, formulas,
       the diff viewer, keyboard hints and QA payloads all read it. It used to be
       defined here and used nowhere, while five different literal stacks fought
       across the two stylesheets — the formula editor asked for bare `monospace`
       (Courier New on Windows) beside a QA panel asking for Consolas.

       That claim is now measured rather than asserted: check_css_tokens.mjs
       fails the build on any font-family naming a real face outside a token.
       It had drifted back once already — the layout editor declared its own
       'SF Mono', 'Fira Code', 'Consolas' stack six times, and Fira Code's
       default ligatures were rendering `!=` in its condition rules as a
       single crossed-equals glyph on any machine that had the font. */
    --font-sans: 'Inter', 'Avenir Next', 'Segoe UI', system-ui, -apple-system, sans-serif;
    --font-mono: ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;

    /* layered neutrals: canvas → recessed → panel, with panel-strong caps */
    --canvas: #eff3f4;
    --panel: #ffffff;
    /* Not a neutral, despite matching --panel today. --panel is an app surface
       and follows the colour scheme; --paper is the sheet a report is printed
       on, and a report is white on every scheme because it leaves here as a
       PDF, an attachment and a sheet of paper. The designer's preview frame is
       the only thing that reads it. Separating them is what lets a dark theme
       darken every surface except the one that is standing in for paper. */
    --paper: #ffffff;
    --panel-strong: #f4f7f8;
    --recessed: #fafcfc;
    --line: #d9e2e6;
    --line-strong: #bfccd2;
    /* --line-control is the only hairline that must be *perceivable* rather
       than decorative: it bounds form controls, so WCAG 1.4.11 wants 3:1. */
    --line-control: #818a8f;
    --ink: #22313d;
    --ink-strong: #141f28;
    --ink-secondary: #40505c;
    --muted: #616f78;

    /* accent: refined teal — primary action, active nav, selection, focus.
       Hue is the decided one; lightness is set by the 4.5:1 floor for 13px
       white-on-accent button text, which the earlier #0e8d93 missed at 4.00. */
    --accent: #0a767b;
    --accent-strong: #085f64;
    --accent-soft: #e0f2f3;
    --accent-line: #bfe3e5;
    --row-hover: #f0f6f7;

    /* On an --accent fill. The query builder's table cards put secondary text
       and a live text input on the accent, and reached for white at 0.75, 0.5,
       0.2 and 0.15 alpha to do it — every one of which failed once the accent
       was darkened to #0a767b: the alias input's border landed at 1.46:1, which
       is not a boundary, it is an invisible control. Alpha over a coloured fill
       cannot be reasoned about at the point of use, so these are the resolved
       values, each checked against #0a767b.

       The field goes darker than its surface rather than lighter — that is what
       buys 8.2:1 for its text — and the boundary is carried by the line, at
       3.38:1. The ring is white because the rail's cyan ring composites to
       2.26:1 here, below the 3:1 a focus indicator owes WCAG 2.4.11. */
    --on-accent-ink: #ffffff;          /* 5.40:1 on --accent */
    --on-accent-muted: #dff1f1;        /* 4.62:1 on --accent */
    --on-accent-field: #08585c;        /* --on-accent-ink reads 8.20:1 here */
    --on-accent-line: #a3d6d8;         /* 3.38:1 on --accent */
    --on-accent-placeholder: #a8d3d5;  /* 5.05:1 on --on-accent-field */
    --focus-ring-on-accent: 0 0 0 2px var(--on-accent-ink);

    /* semantic triplets: text · soft fill · line.
       Text values clear 4.5:1 against BOTH the panel and their own soft fill,
       because badges render them at 11px/700 — bold, but not WCAG "large". */
    --success: #2a7d45;
    --success-soft: #ecf7f0;
    --success-line: #c2e3cd;
    --success-hover: #23693a;
    /* The success triplet's counterpart to --on-accent-ink, for the one
       control that fills with --success rather than tinting with it. */
    --on-success-ink: #ffffff;         /* 5.10:1 on --success */
    --danger: #b74a3e;
    /* The layout editor fills with --danger for the pivot card's error badge,
       the way --success fills the one control above. */
    --on-danger-ink: #ffffff;          /* 5.17:1 on --danger */
    --danger-soft: #fdf1ef;
    --danger-line: #f3cdc7;
    --danger-hover-soft: #fbe4e0;
    --danger-hover-line: #ecb8b0;
    --warning: #8f6116;
    /* Filled warning controls need their own foreground. Reusing --ink on this
       amber only reaches 2.47:1, while white clears the 4.5:1 text floor. */
    --on-warning-ink: #ffffff;         /* 5.40:1 on --warning */
    --warning-soft: #fdf6e8;
    --warning-line: #ecd9ab;
    --info: #466fa6;
    --info-soft: #eef4fb;
    --info-line: #c9dbf0;

    /* Categorical violet. Group bands, group tags and formula tokens each had
       their own near-identical violet or indigo across two stylesheets; this
       is the single value, dark enough to pass 4.5:1 as text on white and on
       its own 10-12% tint, which none of the originals did. */
    --band-group: #774ed5;
    /* The group level badge fills with the violet rather than tinting with it,
       so it needs the same treatment --accent and --danger get. */
    --on-band-group-ink: #ffffff;      /* 5.48:1 on --band-group */
    --formula: #774ed5;

    /* elevation is binary: hairlines separate surfaces, shadows are for
       resting cards (near-invisible) and floating layers (dramatic) only */
    --shadow-card: 0 1px 2px rgba(20, 36, 46, 0.05);
    --shadow-modal: 0 20px 56px rgba(12, 28, 38, 0.28);
    /* The floating tier again, cast sideways for the edge-anchored help drawer,
       which a downward shadow cannot describe. Same colour and same alpha as
       --shadow-modal on purpose: this is that tier rotated, not a third one. */
    --shadow-drawer: -16px 0 48px rgba(12, 28, 38, 0.28);
    /* 0.75 alpha, not 0.30: the ring is a focus indicator, so the *composited*
       result needs 3:1 against every surface it lands on (WCAG 2.4.11).
       At 0.30 it blended to #b7dddf — 1.46:1, effectively invisible. */
    --focus-ring: 0 0 0 3px rgba(10, 118, 123, 0.75);
    /* The same ring on the dark rail composites to #0d6167 — 2.27:1, which
       fails the identical rule the light ring was tuned to pass. The rail needs
       a ring drawn from its own palette, not the accent: this one lands at
       6.04:1 on the rail, 5.29:1 on hover, and 4.96:1 on an active item's
       tint, so focus stays obvious wherever it moves. */
    --focus-ring-rail: 0 0 0 3px rgba(85, 195, 201, 0.85);
    --transition: 120ms ease;

    /* The chevron every <select> in the product wears, as a token rather than
       as a literal inlined at the point of use.

       It carried #67767f — a grey in no scale, nearest --muted at #616f78 — and
       nothing could see it. check_css_tokens greps for `#rrggbb`, and inside a
       data: URI the hash is percent-encoded to %23, so the one literal colour
       left in this sheet was the one hiding behind an escape. The gate now
       decodes %23; this is the value it would have caught.

       url(), not currentColor: a data: URI is its own document and inherits
       nothing from the element, so currentColor there resolves to black. A
       token is what makes it themeable anyway — the dark scheme redefines this
       the same way it redefines --muted, and the arrow follows. */
    --select-chevron: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%23616f78' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");

    /* ── Type scale ───────────────────────────────────────────────────
       Colour in this product is tokenised to the last hex and measured by
       a script. Dimension was not tokenised at all, and drifted the way
       an unmeasured axis does: seventeen distinct font sizes across the
       four stylesheets, including 10.5, 12.5, 13.5 and a lone 17 — none
       of them decisions, all of them somebody nudging a number until one
       screen looked right. Six declarations sat at 9px.

       These are the tiers that were genuinely occupied, named for the job
       they do rather than the number they carry, so a declaration says
       what it means. The accidents were folded into the nearest tier:
       9→10, 10.5→11, 12.5→12, 13.5→13, 17→16, 22→24.

       --text-micro is the floor and it is a real floor, not a shade above
       one: 10px is already small for a dense tool, and check_css_tokens
       now fails the build on anything below it. */
    --text-micro: 10px;     /* dense chrome: layout-editor cells, inline meta */
    --text-label: 11px;     /* micro-labels, badges, table headers */
    --text-compact: 12px;   /* dense controls, secondary UI */
    --text-body: 13px;      /* table and panel body — the workhorse */
    --text-ui: 14px;        /* app body, form controls */
    --text-lg: 15px;
    --text-xl: 16px;        /* section headings, wordmark */
    --text-2xl: 18px;
    --text-3xl: 20px;       /* page h1 */
    --text-4xl: 24px;
    /* A 28px display step used to sit here, and its comment named its one
       reader — the pre-refresh dashboard's stat values. Those cards went when
       the dead-CSS sweep ran, and a scale step kept for a component that no
       longer exists is the shape --font-mono had before the note above it. */

    /* ── Spacing scale ────────────────────────────────────────────────
       Named for their value, unlike the type scale above. A 13px gap is
       not a different *kind* of gap from a 12px one, so there is nothing
       to name — the only thing worth enforcing is that it comes off the
       scale at all. Seventeen distinct gap values shipped before this,
       including 1, 3, 5, 7, 9, 13 and 18px.

       4px base, with 2, 6 and 10 as the half-steps the dense parts of
       this UI actually need. */
    --space-2: 2px;
    --space-4: 4px;
    --space-6: 6px;
    --space-8: 8px;
    --space-10: 10px;
    --space-12: 12px;
    --space-16: 16px;
    --space-20: 20px;
    --space-24: 24px;

    /* metrics. The radius scale was already here and already correct; it was
       simply bypassed — 38 raw px radii shipped alongside it, of which 6px
       (×13) and 4px (×10) were literal restatements of the two tokens below,
       and 1, 2, 3 and 5px were values the scale does not contain. */
    --radius: 6px;
    --radius-sm: 4px;
    --radius-md: 6px;
    --radius-lg: 8px;
    --radius-xl: 10px;
    --radius-pill: 999px;
    --control-h: 34px;
    /* Checkboxes and radios are the one control the user agent sizes. Left to
       Chrome they render 13px beside 34px fields, which is why two rules had
       already patched their own back up by hand (14px in the layout editor's
       props panel, 18px on the monitor enable card). Both still outrank this
       and keep the size they chose. */
    --control-check: 16px;

    /* ════════════════════════════════════════════════════════════════
       Stacking order.

       The fourth axis, and the last one that was still numbers. Colour,
       dimension and type all had a scale and a sweep; z-index had
       twenty raw values between 1 and 10000 spread over four
       stylesheets, and nothing reading them.

       Four sheets is the whole problem. Nothing between a popover in
       the designer and #modal-backdrop in the shell establishes a
       stacking context — .designer-panel, .designer-container,
       .designer-shell and <main> are all static or relative with
       z-index auto — so every one of those numbers competes in the root
       context against every other. A sheet that invents its own ladder
       is not choosing a local order, it is choosing an order against
       the rest of the product without saying so. That is how the query
       builder's join popover reached 10000, above the confirm dialog
       that asks whether to discard the join it is editing, and how the
       layout editor's context menu at 1600 landed above the same
       dialog raised from the same screen.

       The first five are in-component: they order an element against
       its own neighbours and never meet anything from another sheet.
       Two of them share a value, which is not a mistake — a sticky
       header and an underlay happen to need the same depth, and saying
       so twice is what keeps the next edit from assuming they are the
       same thing.

       The app ladder is spaced by ten so a new layer can land between
       two without renumbering. --z-menu sits one above its own
       backdrop because a menu and the click-catcher that dismisses it
       are one widget; --z-signin is above --z-dialog because a session
       can expire while a confirmation is open and the sign-in takeover
       has to cover it; --z-skip-link is last because the first Tab of
       the document has to clear whatever is up.
       ════════════════════════════════════════════════════════════════ */
    --z-underlay: 1;        /* drawn beneath the items it serves: the join canvas's connector lines */
    --z-stuck: 1;           /* sticky chrome over the body it scrolls with: table headers, panel headers, a frozen first column */
    --z-raised: 2;          /* lifted clear of the layer it sits on: a canvas card, a resizer's focus ring, an overlaid close button */
    --z-raised-edge: 3;     /* anchored to a raised item's own edge and must clear it: a join dot, a column resize grip */
    --z-surface-tool: 4;    /* a control floating over a whole editing surface, above everything on it */

    --z-rail-toggle: 10;    /* straddles the rail's edge and the content beside it */
    --z-toolbar: 20;        /* a page toolbar that outlives its own scroll */
    --z-menu-backdrop: 30;  /* the click-catcher under an open menu */
    --z-menu: 31;           /* that menu: dropdown, context menu, colour popover, autocomplete */
    --z-menu-nested: 32;    /* a submenu over the menu that opened it */
    --z-popover: 40;        /* a floating editor anchored to a control, dismissed without a backdrop */
    --z-overlay: 50;        /* a full-screen scrim that makes the shell inert */
    --z-dialog: 60;         /* a dialog raised from inside an overlay */
    --z-signin: 70;         /* the sign-in takeover, which can interrupt any of the above */
    --z-toast: 80;          /* transient, blocks nothing */
    --z-skip-link: 90;      /* the document's first Tab, over whatever is up */

    /* ════════════════════════════════════════════════════════════════
       The brand mark: four squares, light to dark.

       These were the only literal colours left in the product outside
       report CSS, and there are six copies of them — the two inline SVGs
       in index.html, the favicon in two forms (a file and a data: URI),
       the publish page's mark, and the tables that draw it in email.

       Only the first two can read a token. A favicon is fetched with no
       stylesheet; a publish page and an email leave here as standalone
       documents, for the same reason report CSS stays literal hex. So
       the other four stay literal on purpose — and check_css_tokens.mjs
       now measures them against these four values rather than trusting
       that six copies were changed together, which is the same
       instrument check_css_contrast.mjs already points at the report
       grid's four twins.
       ════════════════════════════════════════════════════════════════ */
    --brand-1: #7fccd1;
    --brand-2: #2aa4ab;
    --brand-3: #128c93;
    --brand-4: #0b5560;

    /* rail (dark) — the sidebar keeps its own muted token world */
    --rail-bg: #16212b;
    --rail-ink: #c6d2da;
    --rail-muted: #8296a4;
    --rail-line: rgba(255, 255, 255, 0.07);
    --rail-hover: rgba(255, 255, 255, 0.055);
    --rail-active-bg: rgba(30, 160, 167, 0.16);
    --rail-active-ink: #8fd7db;
    --rail-active-icon: #55c3c9;
    --rail-active-tint: rgba(85, 195, 201, 0.18);
    --rail-label: #758c9d;
    --rail-brand: #eef4f7;

    /* The rail's metrics. Named for the rail, like the colours above it — the
       whole family used to be --sidebar-*, which is what the components still
       call the element, but the token world calls it the rail and one name is
       better than two. */
    --rail-width: 240px;
    --rail-collapsed-width: 52px;

    /* Values of their own, not restatements of a token above.

       This block used to hold thirty-odd aliases as well — --bg for --canvas,
       --border for --line, --primary for --accent — introduced as a bridge so
       report-designer.css and the layout editor could keep working while the
       token system was rebuilt around them. The bridge became the road: 1,410
       reads went through the aliases against 120 through the names they aliased,
       including 358 inside this file. Two consequences, neither cosmetic. Every
       colour took a second hop to resolve, so nobody could tell from a
       declaration which generation they were reading. And nine tokens had no
       alias at all — --line-strong, --line-control, --ink-strong, --panel-strong,
       --recessed, --row-hover, --accent-soft, --accent-line, --accent-strong —
       so anything on the legacy path structurally could not reach them: the
       layout editor read zero of the nine across 3,375 lines, which is why it
       never used --line-control, the one hairline held to 3:1 because it bounds
       form controls. The aliases are gone and their readers renamed. */
    /* The selection/hover tint. Opaque, and that is the whole point.

       It was rgba(10, 118, 123, 0.1), and it is read as a background under
       --accent text in 36 rules across this file, report-designer.css and the
       layout editor. What an alpha fill composites to depends on the surface
       beneath it, so the same token pair passed or failed by location, and
       nothing at the point of use said which:

           over --panel        #e7f1f2  4.69  ok
           over --recessed     #e2efef  4.58  ok
           over --panel-strong #ddeaec  4.38  FAILS 1.4.3
           over --row-hover    #d9e9eb  4.32  FAILS
           over --canvas       #d8e7e8  4.25  FAILS

       Three rules were on the failing side — the active page-size button and
       both search-clear hovers — while thirty-three sat on --panel and were
       fine. This is the value they were already rendering; opaque, every
       instance renders it, and the pair below can be measured by a script that
       only reads hex, which is what check_css_contrast.mjs does. The tint is
       never used over the dark rail, which has --rail-active-tint of its own. */
    --accent-tint: #e7f1f2;
    --overlay-backdrop: rgba(12, 26, 34, 0.45);
    /* Lighter, for a scrim laid over a layer that is already dimmed and blurred
       — a confirmation over an open dialog. Full strength there stacks to near
       opacity and the question loses the context it is asking about. */
    --overlay-backdrop-stacked: rgba(12, 26, 34, 0.32);
    --editor-selection: rgba(10, 118, 123, 0.16);
    --editor-line-highlight: rgba(10, 118, 123, 0.05);
    /* Quieter than --line, for divisions inside a component rather than between
       components. The layout editor referenced this and three surface tokens
       without them ever being defined, so its Tailwind-slate fallbacks
       (#e2e8f0 / #fff / #f1f5f9 / #f8fafc) were what actually rendered. */
    --line-subtle: #e8eef0;

    /* SQL syntax colours. Previously literals in report-designer.css, scoped
       to a theme-id attribute selector that matched every theme the deleted
       theme subsystem shipped — so the scoping did nothing except guarantee
       the highlighting would vanish the day a theme called "dark" was added. */
    /* Measured against --canvas, not --panel. .CodeMirror sets its background to
       var(--canvas) in report-designer.css, so #eff3f4 is the surface every one
       of these actually lands on — the darkest of the three, and the one the
       contrast script used to skip. On it the previous values read 4.44 (number),
       4.18 (comment) and 4.15 (atom): every numeric literal, comment and NULL in
       every SQL editor in the product sat below AA while the check ran green. */
    --cm-number: #9c5f26;
    --cm-string: #2b7b4a;
    --cm-variable: #304a5b;
    /* Comments stay the quietest colour in the editor — but quietest still
       means legible, so this is the floor, not a shade below it. */
    --cm-comment: #636f78;
    --cm-atom: #b34b54;
    /* Two more that were never overridden, and so were still rendering
       CodeMirror's own defaults: brackets at #997 and type names at #085,
       which measure 2.62:1 and 4.04:1 on --canvas. Every parenthesis in every
       SQL editor in the product was the first of those. Same warm grey and
       same green family as the defaults they replace, moved to the AA floor. */
    --cm-bracket: #6b6a4e;
    --cm-type: #0a6b52;
}

body {
    font-family: var(--font-sans);
    background: var(--canvas);
    color: var(--ink);
    font-size: var(--text-ui);
    line-height: 1.5;
    overflow: hidden;
    height: 100vh;
}

#app {
    display: flex;
    height: 100vh;
    background: var(--canvas);
    position: relative;
}

/* Off-screen rather than display:none — it has to stay focusable so the first
   Tab on any route reveals it. */
.skip-link {
    position: absolute;
    top: 8px;
    left: -9999px;
    z-index: var(--z-skip-link);
    padding: 9px 14px;
    border-radius: var(--radius);
    background: var(--accent);
    color: var(--on-accent-ink);
    font-size: var(--text-body);
    font-weight: 600;
    text-decoration: none;
}

.skip-link:focus {
    left: 8px;
    box-shadow: var(--focus-ring);
}

/* For text that supplies the unit a visual layout implies. A count chip reading
   "12" beside a folder name is unambiguous on screen and is two unrelated
   numbers in the accessibility tree; this carries the noun across.

   Clipped rather than moved off-screen, unlike .skip-link above: nothing here
   is ever focused, and a 1px clip cannot drag an RTL layout sideways the way a
   left:-9999px element can. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* The skip target takes focus programmatically; a ring around the whole main
   region would be noise, and the next Tab already lands inside the content. */
#content:focus {
    outline: none;
}

/* ════════════════════════════════════════════════════════════════════
   Baseline focus indicator.

   .btn, .tab, .nav-link and the form controls each declare their own ring
   below. Nothing else did — report-designer.css alone carried 52 :hover
   rules against 8 :focus rules, so most of the designer, the query
   builder and the style editor styled the mouse and left the keyboard
   with whatever the UA happened to draw, or with nothing at all where
   something had set outline: none.

   Wrapped in :where() so it contributes no specificity: every rule that
   names a component still wins, and this only ever fills a gap. Drawn as
   an outline rather than a box-shadow for the same reason — it cannot
   clobber a component that is already using its shadow for elevation,
   and it follows the border radius on its own.
   ════════════════════════════════════════════════════════════════════ */
:where(#content, #modal-container, #login-overlay, #help-overlay) :focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* The help panel takes focus programmatically when it opens, the same way
   #content does from the skip link. Ringing the whole dialog would be noise;
   the next Tab already lands on a control inside it that draws its own. */
.help-panel:focus {
    outline: none;
}

/* Sidebar */
#sidebar {
    width: var(--rail-width);
    background: var(--rail-bg);
    border-right: 1px solid var(--rail-line);
    display: flex;
    flex-direction: column;
    flex-shrink: 0;
    min-width: 0;
    overflow: hidden;
    /* The one layout property this app animates. Collapsing the rail has to
       reflow the content column, there is no transform that does it, and it
       runs once per explicit user action rather than continuously. */
    transition: width 180ms ease;
}

.sidebar-header {
    padding: 16px 16px 8px;
    display: flex;
    flex-direction: column;
    gap: var(--space-12);
}

.logo {
    display: flex;
    align-items: center;
    gap: var(--space-10);
    font-size: var(--text-xl);
    font-weight: 700;
    font-family: var(--font-sans);
    color: var(--rail-brand);
    letter-spacing: -0.01em;
    min-width: 0;
}

.logo svg {
    width: 22px;
    height: 22px;
    flex-shrink: 0;
}

.logo-label,
.nav-link-label {
    display: inline-block;
    overflow: hidden;
    white-space: nowrap;
    max-width: 180px;
    opacity: 1;
    transform: translateX(0);
    /* max-width snaps rather than animating: it collapses eleven labels at
       once, and transitioning it forced a layout pass per label per frame.
       The fade and slide cover the change, and the rail's own width
       transition supplies the motion. */
    transition: opacity 120ms ease, transform 180ms ease;
}

.sidebar-toggle {
    position: absolute;
    top: 13px;
    left: calc(var(--rail-width) - 38px);
    width: 28px;
    height: 28px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--rail-muted);
    cursor: pointer;
    /* Its resting place is the expanded rail; the collapsed position is reached
       by transform below rather than by animating top and left. Those two were
       running a layout animation per frame in lockstep with #sidebar's width —
       the one layout property this app knowingly animates — which made the rail
       toggle the most expensive motion in the product for no gain. */
    transition: transform 180ms ease,
                background 140ms ease,
                color 140ms ease;
    z-index: var(--z-rail-toggle);
}

.sidebar-toggle:hover,
.sidebar-toggle:focus-visible {
    background: var(--rail-hover);
    color: var(--rail-ink);
}

/* The rail's ring, not the canvas one. The toggle sits on the rail's edge, and
   --accent composites to 3.02:1 there — passing, but at the floor, while
   --focus-ring-rail exists for this exact surface and lands at 7.80:1. */
.sidebar-toggle:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring-rail);
}

.sidebar-toggle-icon {
    width: 20px;
    height: 20px;
}

.nav-links {
    list-style: none;
    padding: 8px 12px 12px;
    flex: 1;
    min-width: 0;
}

/* Sections are real nested lists so the rail's grouping reaches assistive
   tech, not just sighted users. Visually identical to the old flat list. */
.nav-sublist {
    list-style: none;
    min-width: 0;
}

.nav-section-label {
    padding: 14px 10px 5px;
    font-size: var(--text-micro);
    font-weight: 750;
    text-transform: uppercase;
    letter-spacing: 0.11em;
    color: var(--rail-label);
    white-space: nowrap;
    overflow: hidden;
    transition: opacity 120ms ease;
}

.nav-link {
    display: flex;
    align-items: center;
    gap: var(--space-10);
    min-height: 34px;
    padding: 0 10px;
    border-radius: var(--radius);
    color: var(--rail-ink);
    text-decoration: none;
    font-size: var(--text-body);
    font-weight: 500;
    transition: background var(--transition), color var(--transition);
    margin-bottom: 1px;
    min-width: 0;
    position: relative;
}

.nav-link svg {
    flex-shrink: 0;
    width: 16px;
    height: 16px;
    color: var(--rail-muted);
    transition: color var(--transition);
}

.nav-link .badge {
    flex-shrink: 0;
    transition: background var(--transition), border-color var(--transition), color var(--transition);
}

.nav-link:hover { background: var(--rail-hover); color: var(--rail-brand); }
/* Ring plus a surface change: the ring alone has to survive three different
   rail backgrounds, and the hover treatment costs nothing to reuse here. */
.nav-link:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring-rail);
    background: var(--rail-hover);
    color: var(--rail-brand);
}
.nav-link.active {
    background: var(--rail-active-bg);
    color: var(--rail-active-ink);
    font-weight: 600;
}
.nav-link.active svg { color: var(--rail-active-icon); }
.nav-link.active::before {
    content: "";
    position: absolute;
    left: -1px;
    top: 8px;
    bottom: 8px;
    width: 2.5px;
    border-radius: var(--radius-pill);
    background: var(--rail-active-icon);
}

.sidebar-footer {
    padding: 10px 12px 12px;
    border-top: 1px solid var(--rail-line);
    display: flex;
    flex-direction: column;
    gap: var(--space-8);
}

.sidebar-user {
    display: flex;
    align-items: center;
    gap: var(--space-10);
    padding: 6px 6px;
    border-radius: var(--radius);
    min-width: 0;
}

.sidebar-avatar {
    width: 28px;
    height: 28px;
    border-radius: var(--radius-pill);
    flex-shrink: 0;
    background: var(--rail-active-tint);
    color: var(--rail-active-ink);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: var(--text-label);
    font-weight: 700;
    letter-spacing: 0.02em;
}

/* Six of the 10px gap to its right, borrowed.
   Everything else in .sidebar-user is fixed — a 28px avatar and two 28px
   buttons — so this block gets exactly what is left, and what was left was 89px
   against a role label that measures 93.27. "ADMINISTRATOR" is the most common
   role in the product and it was four pixels too wide for its own box: before
   the ellipsis below it simply drew over the gap, and with the ellipsis it lost
   two letters. The negative margin hands those six pixels back to the flex line,
   which has only this item to give them to. Six rather than four so the ellipsis
   stays the exception, and the four that remain still separate this block from
   the help button. */
.sidebar-user-meta {
    flex: 1;
    min-width: 0;
    margin-right: -6px;
}

.sidebar-user-name {
    font-size: var(--text-compact);
    font-weight: 600;
    color: var(--rail-ink);
    line-height: 1.25;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Truncates the way .sidebar-user-name above it does. It had the nowrap and
   not the other two, so a role wider than the block — "Administrator" already
   is, by 4px — kept drawing past its own box and under the help and log-out
   buttons instead of ending in an ellipsis. */
.sidebar-user-role {
    font-size: var(--text-micro);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--rail-label);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.sidebar-iconbtn {
    width: 28px;
    height: 28px;
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: none;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--rail-muted);
    cursor: pointer;
    transition: background var(--transition), color var(--transition);
}

.sidebar-iconbtn:hover {
    background: var(--rail-hover);
    color: var(--rail-ink);
}

.sidebar-iconbtn:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring-rail);
    background: var(--rail-hover);
    color: var(--rail-ink);
}

body.sidebar-collapsed #sidebar {
    width: var(--rail-collapsed-width);
}

body.sidebar-collapsed .sidebar-header,
body.sidebar-collapsed .sidebar-footer {
    padding-inline: 8px;
}

body.sidebar-collapsed .sidebar-header {
    align-items: center;
    padding-bottom: 44px;
}

body.sidebar-collapsed .nav-links {
    padding-inline: 8px;
}

body.sidebar-collapsed .logo {
    gap: 0;
}

/* Offset from the expanded resting position: centred in the collapsed rail,
   and dropped below the logo now that there is no wordmark beside it. */
body.sidebar-collapsed .sidebar-toggle {
    transform: translate(
        calc((var(--rail-collapsed-width) - 28px) / 2 - (var(--rail-width) - 38px)),
        calc(48px - 13px)
    );
}

body.sidebar-collapsed .nav-link {
    justify-content: center;
    padding-left: 0;
    padding-right: 0;
}

body.sidebar-collapsed .logo-label,
body.sidebar-collapsed .nav-link-label {
    max-width: 0;
    opacity: 0;
    transform: translateX(-6px);
}

body.sidebar-collapsed .nav-section-label {
    height: 10px;
    padding: 0;
    opacity: 0;
}

body.sidebar-collapsed .sidebar-user {
    flex-direction: column;
    gap: var(--space-6);
    padding: 4px 0;
}

body.sidebar-collapsed .sidebar-user-meta {
    display: none;
}

body.sidebar-collapsed .nav-link .badge {
    position: absolute;
    top: 6px;
    right: 4px;
    margin-left: 0 !important;
    min-width: 18px;
    padding: 2px 4px;
    justify-content: center;
    font-size: var(--text-micro);
    line-height: 1;
    pointer-events: none;
}

/* Main Content */
#content {
    flex: 1;
    min-width: 0;
    overflow-y: auto;
    padding: 20px 24px 28px;
    background: var(--canvas);
}

/* Typography */
h1 { font-size: var(--text-4xl); font-weight: 700; }
h2 { font-size: var(--text-2xl); font-weight: 600; }
h3 { font-size: var(--text-lg); font-weight: 600; }

.page-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 18px;
    padding-inline: 6px;
}

.page-header h1 {
    margin: 0;
    font-size: var(--text-2xl);
    font-weight: 700;
    letter-spacing: -0.01em;
    color: var(--ink-strong);
}

/* Buttons — one primary per view; everything else stays neutral */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-8);
    min-height: var(--control-h);
    padding: 0 13px;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--panel);
    color: var(--ink);
    font-size: var(--text-body);
    font-weight: 600;
    line-height: 1.2;
    cursor: pointer;
    transition: background var(--transition), border-color var(--transition), color var(--transition);
    text-decoration: none;
    white-space: nowrap;
}

.btn:hover { background: var(--row-hover); border-color: var(--line-strong); }

.btn:focus-visible,
.btn-icon:focus-visible,
.tab:focus-visible,
.status-dot:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
}

.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent-ink); }
.btn-primary:hover { background: var(--accent-strong); border-color: var(--accent-strong); }
.btn-secondary { background: var(--panel); color: var(--ink); border: 1px solid var(--line); }
.btn-secondary:hover { background: var(--row-hover); border-color: var(--line-strong); }
.btn-danger { background: var(--danger-soft); border-color: var(--danger-line); color: var(--danger); }
.btn-danger:hover { background: var(--danger-hover-soft); border-color: var(--danger-hover-line); }
/* No page raises a success button today. Kept anyway, unlike the rules the dead
   sweep took out around it: those were residue left by subsystems that were
   deleted, and this is the fifth member of a semantic set whose other four are
   in use, with its two tokens already derived and measured. A gap in that set is
   a hole someone fills with --success and a literal. */
.btn-success { background: var(--success); border-color: var(--success); color: var(--on-success-ink); }
.btn-success:hover { background: var(--success-hover); border-color: var(--success-hover); }
.btn-ghost { background: transparent; border-color: transparent; color: var(--muted); }
.btn-ghost:hover { color: var(--ink); background: var(--row-hover); border-color: transparent; }
.btn-sm { min-height: 28px; padding: 0 10px; font-size: var(--text-compact); border-radius: var(--radius); }
.btn-full { width: 100%; justify-content: center; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }

.btn-group { display: flex; gap: var(--space-8); }

/* Cards */
.card {
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    padding: 20px;
    box-shadow: var(--shadow-card);
}

.card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    gap: var(--space-16);
}

.reports-filters .form-row {
    align-items: end;
}

.report-card .btn-group {
    flex-wrap: wrap;
}

.report-card {
    cursor: pointer;
    transition: border-color var(--transition), transform var(--transition);
}

.report-card:hover {
    border-color: var(--accent);
}

.reports-folder-card {
    min-height: 112px;
    background: var(--panel);
}

/* 14px for the same reason as .reports-folder-name's 13px: the heading's name
   is a button that pulls itself 4px left for its focus ring. */
.reports-folder-card-heading {
    display: flex;
    align-items: center;
    gap: var(--space-16);
    margin-bottom: 12px;
}

/* The cards are the grid's own sections, so they sit at h2 directly under the
   page's h1 — the grid had no h2 at all and jumped straight to h3, leaving a
   hole in the outline. Level is the semantics; the 15px is the presentation,
   and it is the size these titles have always been. */
.report-card h2 {
    font-size: var(--text-lg);
}

.reports-folder-card-heading h2 {
    margin: 0;
}

.report-meta {
    margin-top: 10px;
    display: flex;
    flex-direction: column;
    gap: var(--space-6);
}

.report-folder {
    color: var(--muted);
    font-size: var(--text-compact);
}

.report-tags {
    display: flex;
    gap: var(--space-6);
    flex-wrap: wrap;
}

/* Reports file browser */
.reports-browser,
.resource-browser {
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-card);
}

.reports-browser-nav,
.resource-browser-nav {
    display: flex;
    align-items: center;
    gap: var(--space-16);
    min-height: 62px;
    padding: 10px 14px;
    border-bottom: 1px solid var(--line);
    background: var(--panel);
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}

.reports-history-buttons {
    display: inline-flex;
    flex: 0 0 auto;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    overflow: hidden;
    box-shadow: var(--shadow-card);
}

.reports-history-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    padding: 0;
    border: 0;
    border-right: 1px solid var(--line);
    background: var(--panel);
    color: var(--ink-secondary);
    cursor: pointer;
    transition: background var(--transition), color var(--transition);
}

.reports-history-btn:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
}

.reports-history-btn:last-child {
    border-right: 0;
}

.reports-history-btn:hover:not(:disabled) {
    background: var(--row-hover);
    color: var(--accent);
}

.reports-history-btn:disabled {
    color: var(--line);
    cursor: default;
}

.reports-browser-actions,
.resource-browser-actions {
    display: flex;
    align-items: center;
    gap: var(--space-8);
    margin-left: auto;
    flex: 0 0 auto;
}

.resource-browser-title {
    display: flex;
    align-items: center;
    min-width: 0;
    min-height: 38px;
    padding: 5px 6px;
    color: var(--ink);
    font-size: var(--text-xl);
    font-weight: 700;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.resource-browser-toolbar {
    position: relative;
    top: auto;
    z-index: var(--z-toolbar);
    margin: 0;
    border-bottom: 1px solid var(--line);
    border-radius: 0;
    background: var(--panel-strong);
    box-shadow: none;
}

.resource-browser-results > .table-container {
    border: 0;
    border-radius: 0 0 var(--radius-lg) var(--radius-lg);
    box-shadow: none;
}

.resource-browser-results > .spinner {
    display: block;
    margin: 32px auto;
}

.resource-browser-results > .empty-state {
    min-height: 180px;
}

.resource-browser-workspace > .data-browser {
    height: calc(100vh - 118px);
    min-height: 420px;
    border: 0;
    border-radius: 0 0 var(--radius-lg) var(--radius-lg);
    box-shadow: none;
}

.reports-toolbar {
    padding: 12px 14px;
    display: flex;
    flex-direction: column;
    gap: var(--space-10);
    position: relative;
    z-index: var(--z-toolbar);
    border-bottom: 1px solid var(--line);
    background: var(--panel-strong);
}

.reports-toolbar-row {
    display: flex;
    align-items: center;
    gap: var(--space-10);
    flex-wrap: wrap;
}

.reports-search {
    position: relative;
    flex: 1 1 280px;
    min-width: 220px;
    display: flex;
    align-items: center;
}

.reports-search-icon {
    position: absolute;
    left: 12px;
    top: 50%;
    transform: translateY(-50%);
    color: var(--muted);
    pointer-events: none;
}

.reports-search input[type="text"] {
    padding-left: 36px;
    padding-right: 32px;
    height: 34px;
}

.reports-search-clear {
    position: absolute;
    right: 8px;
    top: 50%;
    transform: translateY(-50%);
    /* 24px, the WCAG 2.2 target-size floor. It sits inside a 34px field, so
       there is room for it without crowding the text. */
    width: var(--space-24);
    height: var(--space-24);
    border-radius: var(--radius-pill);
    border: none;
    background: var(--row-hover);
    color: var(--muted);
    font-size: var(--text-xl);
    line-height: 1;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.reports-search-clear:hover {
    background: var(--accent-tint);
    color: var(--accent);
}

/* ════════════════════════════════════════════════════════════════════
   `hidden` loses to any authored `display`.

   The page toggles these four with the hidden attribute, but the UA's
   `[hidden] { display: none }` carries no specificity to speak of, so
   the `display: flex` each rule declares above wins and the attribute
   does nothing: the ✕ sat beside an empty search box on seven pages —
   and took a Tab stop — the pagination kept its stale page buttons
   under "no reports match", and the breadcrumb held a 38px blank strip
   at the root. Three siblings already carry this guard
   (.reports-dropdown-panel, .preview-width-control,
   .qb-column-filter-clear); these are the four that missed it.
   ════════════════════════════════════════════════════════════════════ */
.reports-search-clear[hidden],
.admin-list-search-clear[hidden],
.reports-pagination[hidden],
.reports-breadcrumb[hidden] {
    display: none;
}

.reports-filter-select {
    position: relative;
}

.reports-filter-trigger {
    display: inline-flex;
    align-items: center;
    gap: var(--space-8);
    padding: 0 12px;
    height: 34px;
    background: var(--panel);
    border: 1px solid var(--line-control);
    border-radius: var(--radius);
    color: var(--ink);
    font-size: var(--text-body);
    cursor: pointer;
    transition: border-color var(--transition), background var(--transition);
    min-width: 160px;
    max-width: 240px;
}

.reports-filter-trigger:hover {
    border-color: var(--accent);
}

.reports-filter-trigger[aria-expanded="true"] {
    border-color: var(--accent);
    background: var(--accent-tint);
}

.reports-filter-trigger-label {
    color: var(--muted);
    font-weight: 500;
}

.reports-filter-trigger-value {
    color: var(--ink);
    font-weight: 500;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    flex: 1;
    text-align: left;
}

.reports-filter-trigger svg {
    color: var(--muted);
    flex-shrink: 0;
}

.reports-dropdown-panel {
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    min-width: 240px;
    max-width: 320px;
    max-height: 320px;
    overflow-y: auto;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--shadow-modal);
    padding: 6px;
    z-index: var(--z-menu);
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.reports-dropdown-panel[hidden] {
    display: none;
}

.reports-dropdown-panel-tags {
    min-width: 220px;
}

.reports-dropdown-option {
    display: flex;
    align-items: center;
    gap: var(--space-8);
    padding: 7px 10px;
    background: transparent;
    border: none;
    border-radius: var(--radius-sm);
    color: var(--ink);
    font-size: var(--text-body);
    cursor: pointer;
    text-align: left;
    width: 100%;
    transition: background var(--transition);
}

.reports-dropdown-option:hover {
    background: var(--row-hover);
}

.reports-dropdown-option.is-active {
    background: var(--accent-tint);
    color: var(--accent);
}

.reports-dropdown-option-label {
    flex: 1;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.reports-dropdown-option-count {
    font-size: var(--text-label);
    color: var(--muted);
    background: var(--panel);
    padding: 2px 8px;
    border-radius: var(--radius-pill);
    flex-shrink: 0;
}

/* Translucent white is a dark-surface idiom, and this surface is not dark. Over
   --accent-tint it composited to rgb(234,243,244) against a parent of
   rgb(231,241,242) — a three-count difference, so the chip lost its shape in
   the one state where it has to keep it. Opaque --panel, as at rest. */
.reports-dropdown-option.is-active .reports-dropdown-option-count {
    background: var(--panel);
    color: var(--accent);
}

.reports-dropdown-check {
    width: 16px;
    display: inline-flex;
    justify-content: center;
    color: var(--accent);
    font-weight: 700;
    flex-shrink: 0;
}

.reports-dropdown-empty {
    padding: 14px 10px;
    text-align: center;
    color: var(--muted);
    font-size: var(--text-compact);
}

/* Segmented control — related toggles share one bordered pill */
.reports-view-toggle {
    display: inline-flex;
    gap: var(--space-2);
    background: var(--recessed);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    padding: 3px;
    margin-left: 0;
}

.reports-view-btn {
    min-height: 26px;
    padding: 0 9px;
    background: transparent;
    border: none;
    border-radius: var(--radius);
    color: var(--muted);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: background var(--transition), color var(--transition);
}

.reports-view-btn:hover {
    color: var(--ink);
}

.reports-view-btn:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
}

.reports-view-btn.is-active {
    background: var(--panel);
    color: var(--ink-strong);
    box-shadow: var(--shadow-card);
}

/* The ring above is a box-shadow, and so is the active state's lift — and both
   selectors weigh (0,2,0), so the later one simply won and the *selected* half
   of the segmented control drew no focus indicator at all. One of the two is
   always selected, so a keyboard user lost focus on every pass across the pair.
   Restated here with both shadows on one declaration; the ring is listed second
   so it sits outside the card lift rather than under it. */
.reports-view-btn.is-active:focus-visible {
    outline: none;
    box-shadow: var(--shadow-card), var(--focus-ring);
}

.reports-filter-chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-6);
}

.reports-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-6);
    padding: 4px 10px;
    background: var(--accent-tint);
    color: var(--accent);
    border: 1px solid transparent;
    border-radius: var(--radius-pill);
    font-size: var(--text-compact);
    font-weight: 500;
    cursor: pointer;
    transition: background var(--transition), border-color var(--transition);
}

.reports-chip:hover {
    border-color: var(--accent);
}

.reports-chip-label {
    color: var(--muted);
    font-weight: 500;
}

.reports-chip-x {
    font-size: var(--text-ui);
    line-height: 1;
    opacity: 0.7;
}

.reports-chip-clear {
    background: transparent;
    color: var(--muted);
    border: 1px dashed var(--line);
}

.reports-chip-clear:hover {
    color: var(--danger);
    border-color: var(--danger);
}

.reports-breadcrumb {
    display: flex;
    align-items: center;
    gap: var(--space-8);
    min-width: 0;
    min-height: 38px;
    margin: 0;
    flex: 1 1 auto;
    overflow: hidden;
    color: var(--muted);
    font-size: var(--text-lg);
}

.reports-breadcrumb-link {
    padding: 5px 6px;
    border: 0;
    background: transparent;
    color: var(--accent);
    font: inherit;
    cursor: pointer;
    border-radius: var(--radius-sm);
    white-space: nowrap;
}

.reports-breadcrumb-link:hover {
    background: var(--accent-tint);
}

.reports-breadcrumb-current {
    padding: 5px 6px;
    color: var(--ink);
    font-size: var(--text-xl);
    font-weight: 700;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.reports-breadcrumb-separator {
    color: var(--muted);
    font-size: var(--text-3xl);
    line-height: 1;
}

/* Admin list toolbar — reports-style search shell for CRUD tables */
.admin-list-toolbar {
    padding: 12px 14px;
    display: flex;
    flex-direction: column;
    gap: var(--space-10);
    position: sticky;
    top: 0;
    z-index: var(--z-toolbar);
}

.admin-list-toolbar-row {
    display: flex;
    align-items: center;
    gap: var(--space-10);
    flex-wrap: wrap;
}

.admin-list-search {
    position: relative;
    flex: 1 1 280px;
    min-width: 220px;
    display: flex;
    align-items: center;
}

.admin-list-search-icon {
    position: absolute;
    left: 12px;
    top: 50%;
    transform: translateY(-50%);
    color: var(--muted);
    pointer-events: none;
}

.admin-list-search input[type="text"] {
    padding-left: 36px;
    padding-right: 32px;
    height: 34px;
}

/* How much of the list the search left on screen. Empty — and so drawing
   nothing — whenever the two numbers agree, which is why it carries no
   margin of its own: an empty box between the search field and the edge of
   the toolbar would still take space in the flex row. */
.admin-list-count {
    margin: 0;
    color: var(--muted);
    font-size: var(--text-compact);
    white-space: nowrap;
}

.admin-list-search-clear {
    position: absolute;
    right: 8px;
    top: 50%;
    transform: translateY(-50%);
    /* Matches .reports-search-clear: 24px, the WCAG 2.2 target-size floor. */
    width: var(--space-24);
    height: var(--space-24);
    border-radius: var(--radius-pill);
    border: none;
    background: var(--row-hover);
    color: var(--muted);
    font-size: var(--text-xl);
    line-height: 1;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.admin-list-search-clear:hover {
    background: var(--accent-tint);
    color: var(--accent);
}

.admin-list-toolbar-actions {
    display: inline-flex;
    align-items: center;
    gap: var(--space-10);
    flex-wrap: wrap;
    margin-left: auto;
}

.admin-list-toolbar-actions .btn {
    height: 34px;
}

.admin-list-toolbar-actions .form-control {
    width: auto;
    min-width: 150px;
    height: 34px;
}

/* ────────────────────────────────────────────────────────────────────
   On breakpoints below 1024px.

   Tributary is a desktop app. The supported range is roughly 1024–1920px,
   and the layouts that matter are 1280×800 / 1366×768 laptops. There is no
   phone or tablet target, and none of these blocks is an unfinished attempt
   at one — read them as the app surviving a *narrow window*: a half-screened
   1920 display lands at 960px, a half-screened 1440 at 720px.

   So they are kept, not removed, and no new one should be added below this
   comment on the theory that mobile is coming. It is not. Anything genuinely
   in scope belongs at 1024px and up.
   ──────────────────────────────────────────────────────────────────── */
@media (max-width: 900px) {
    .admin-list-toolbar-actions {
        width: 100%;
        margin-left: 0;
    }
}

/* Reports table */
#reports-list.is-loading {
    opacity: 0.6;
    pointer-events: none;
    transition: opacity 150ms ease;
}

#schedule-runs-log.is-loading {
    opacity: 0.6;
    pointer-events: none;
    transition: opacity 150ms ease;
}

.reports-results-table {
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    /* auto rather than hidden on the x axis: the table below declares a
       min-width, and hidden would clip the columns past it with no way to
       reach them. Still clips to the radius, which is what hidden was for. */
    overflow-x: auto;
    overflow-y: hidden;
    box-shadow: var(--shadow-card);
}

.reports-browser .reports-results-table {
    border: 0;
    border-radius: 0 0 var(--radius-lg) var(--radius-lg);
    box-shadow: none;
}

.reports-browser .reports-results-grid {
    padding: 16px;
}

.reports-browser #reports-list > .spinner {
    margin-block: 32px;
}

.reports-browser .reports-empty {
    border: 0;
    border-radius: 0 0 var(--radius-lg) var(--radius-lg);
}

.reports-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--text-body);
}

.reports-table thead th {
    text-align: left;
    padding: 9px 14px;
    background: var(--panel-strong);
    border-bottom: 1px solid var(--line);
    font-size: var(--text-label);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--muted);
}

.reports-col-sort {
    background: none;
    border: none;
    color: inherit;
    font: inherit;
    text-transform: inherit;
    letter-spacing: inherit;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: var(--space-4);
    /* 11px uppercase text is a 17px-tall click target. The padding lifts it
       past 24px and the matching negative margin gives the space back to the
       header cell, so the target grows without the row moving. */
    padding: var(--space-4) 0;
    margin: calc(var(--space-4) * -1) 0;
    min-height: var(--space-24);
}

.reports-col-sort:hover {
    color: var(--ink);
}

.reports-sort-indicator {
    color: var(--accent);
    font-size: var(--text-micro);
}

.reports-table tbody tr {
    border-bottom: 1px solid var(--line);
    cursor: pointer;
    transition: background var(--transition);
}

.reports-table tbody tr:last-child {
    border-bottom: none;
}

.reports-table tbody tr:hover {
    background: var(--row-hover);
}

.reports-table td {
    padding: 13px 14px;
    vertical-align: middle;
}

.reports-col-name {
    min-width: 220px;
}

.reports-col-last-run {
    width: 165px;
    color: var(--muted);
    white-space: nowrap;
}

.reports-last-run-by {
    max-width: 165px;
    margin-top: 2px;
    overflow: hidden;
    color: var(--ink-secondary);
    font-size: var(--text-label);
    text-overflow: ellipsis;
}

.reports-col-tags {
    width: 20%;
    max-width: 280px;
}

.reports-col-updated {
    width: 120px;
    color: var(--muted);
    white-space: nowrap;
}

/* 140px when this cell held a badge plus the access badge beside it. The state
   is a dot and a word now, and access has moved to its own column. */
.reports-col-status {
    width: 110px;
    white-space: nowrap;
}

.reports-col-access {
    width: 92px;
    white-space: nowrap;
}

.reports-col-actions {
    width: 1%;
    white-space: nowrap;
    text-align: right;
}

.reports-row-name {
    font-weight: 600;
    color: var(--ink);
}

.reports-row-description {
    font-size: var(--text-compact);
    color: var(--muted);
    margin-top: 2px;
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 1;
    -webkit-box-orient: vertical;
}

.reports-folder-row {
    background: var(--panel);
}

/* 13px, not 9: the name is a button, and button.entity-row-name pulls itself
   4px left so its focus ring clears the glyphs without moving the text. Adding
   that back here keeps the rendered gap at the 9px the row was drawn with. */
.reports-folder-name {
    display: inline-flex;
    align-items: center;
    gap: var(--space-12);
    color: var(--ink);
    font-weight: 600;
}

.reports-folder-icon {
    flex: 0 0 auto;
    color: var(--accent);
    fill: none;
}

.reports-folder-chevron {
    color: var(--muted);
    font-size: var(--text-4xl);
    line-height: 1;
}

.reports-row-actions {
    display: inline-flex;
    align-items: center;
    gap: var(--space-4);
    opacity: 0;
    transition: opacity var(--transition);
}

.reports-table tbody tr:hover .reports-row-actions,
.reports-table tbody tr:focus-within .reports-row-actions {
    opacity: 1;
}

/* Pagination */
.reports-pagination {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-16);
    padding: 14px 4px 4px;
    flex-wrap: wrap;
}

.reports-pagination-info {
    color: var(--muted);
    font-size: var(--text-body);
}

.reports-page-size {
    display: inline-flex;
    align-items: center;
    gap: var(--space-6);
}

.reports-page-size-btn {
    padding: 4px 10px;
    background: transparent;
    border: 1px solid var(--line);
    color: var(--muted);
    border-radius: var(--radius-sm);
    font-size: var(--text-compact);
    cursor: pointer;
    transition: background var(--transition), border-color var(--transition), color var(--transition);
}

.reports-page-size-btn:hover {
    color: var(--ink);
    border-color: var(--accent);
}

.reports-page-size-btn.is-active {
    background: var(--accent-tint);
    border-color: var(--accent);
    color: var(--accent);
}

.reports-pagination-controls {
    display: inline-flex;
    align-items: center;
    gap: var(--space-10);
}

.reports-empty {
    background: var(--panel);
    border: 1px dashed var(--line);
    border-radius: var(--radius-lg);
}

@media (max-width: 820px) {
    .reports-browser-nav,
    .resource-browser-nav {
        flex-wrap: wrap;
        gap: var(--space-10);
    }
    .reports-browser-actions,
    .resource-browser-actions {
        width: 100%;
        padding-top: 10px;
        border-top: 1px solid var(--line);
        justify-content: flex-end;
    }
    .reports-toolbar-row {
        gap: var(--space-8);
    }
    .reports-view-toggle {
        margin-left: 0;
    }
    .reports-col-last-run,
    .reports-col-tags,
    .reports-col-status,
    .reports-col-access {
        display: none;
    }

    .resource-browser-workspace > .data-browser {
        height: calc(100vh - 170px);
    }
}

@media (max-width: 560px) {
    .reports-search {
        flex-basis: 100%;
    }
    .reports-filter-select {
        flex: 1 1 calc(50% - 4px);
    }
    .reports-filter-trigger {
        width: 100%;
        min-width: 0;
    }
    .reports-browser-actions .btn {
        padding-inline: 10px;
    }
    .reports-browser-actions {
        flex-wrap: wrap;
        row-gap: var(--space-8);
    }
    .reports-browser-actions .reports-view-toggle {
        margin-left: auto;
    }
}

/* Tables */
.table-container {
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    overflow-x: auto;
    box-shadow: var(--shadow-card);
}

/* ════════════════════════════════════════════════════════════════════
   Why tables have a floor width.

   `td` used to carry `word-break: break-word` globally, and the tables
   had no minimum width. Those two together meant the browser never
   reached for the horizontal scroll the container above offers — it
   just kept shrinking columns and breaking words inside them. At 1024px
   the schedules table rendered "Open OPI Purchase Orders" as eight
   lines in a 75px column, and the average row grew from 78px to 193px:
   three schedules visible where there had been eight. A half-screened
   1920 display is 960px, so this was not a hypothetical width.

   Nine column classes across five tables had already been patched back
   with `word-break: normal` to stop exactly this, one table at a time,
   and the columns carrying the longest content were the ones nobody had
   got to yet. So the default was wrong and the exception list was the
   real rule.

   Inverted: cells no longer break words, the table declares the width
   below which it would rather scroll than compress, and the two kinds
   of content that genuinely have no break opportunity — hosts, file
   paths, DSNs — opt *in* to `overflow-wrap: anywhere` by name.
   ════════════════════════════════════════════════════════════════════ */
table {
    width: 100%;
    border-collapse: collapse;
}

th {
    text-align: left;
    padding: 9px 14px;
    font-size: var(--text-label);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--muted);
    background: var(--panel-strong);
    border-bottom: 1px solid var(--line);
}

td {
    padding: 11px 14px;
    border-bottom: 1px solid var(--line);
    font-size: var(--text-body);
}

/* The cell that names its row.

   Every list in this product had a first column carrying the record's name and
   rendered it as a <td>, so a screen reader moving across a row announced
   "Acknowledged" or "2d ago" with nothing to say which alert or which report.
   scope="row" is what supplies that, and it costs a <th> — which the rule above
   styles as a column header: uppercase, 700, grey, on a filled background.

   So this undoes that. It is the whole visual half of the change: the markup
   says row header, the cell still reads as content. Everything here restates a
   td value rather than inventing one, deliberately — the two must not drift. */
tbody th[scope="row"] {
    padding: 11px 14px;
    font-size: var(--text-body);
    font-weight: 400;
    text-transform: none;
    letter-spacing: normal;
    color: var(--ink);
    background: transparent;
}

/* The two columns whose values have no space in them and no natural break
   point: hostnames, file paths, DSNs, connection strings. Everything else in a
   table cell is prose or a label and wraps at spaces.

   A .wrap-anywhere utility led this list and nothing had ever carried it. The
   two selectors below are the whole of what needs the behaviour; a lone utility
   with no family and no user is residue, not an affordance. */
.connections-table .conn-col-host,
.connections-table .conn-col-database {
    overflow-wrap: anywhere;
}

/* The width below which each list table would rather scroll than compress.

   Named per table rather than set on `table`, because this only applies to the
   full-width resource browsers. The product also renders small tables inside
   modals and designer panels, and a global floor would push those straight out
   of their container. */
.schedules-table { min-width: 940px; }
.reports-table { min-width: 920px; }
.monitors-table { min-width: 880px; }
.alerts-table { min-width: 880px; }
.connections-table { min-width: 820px; }
.users-table { min-width: 720px; }
.access-groups-table { min-width: 760px; }

/* Connections table: labels and badges stay on one line. The host and database
   columns opt into breaking above, with the rest of the unbreakable content. */
.connections-table .conn-col-name,
.connections-table .conn-col-type,
.connections-table .conn-col-actions,
.connections-table .conn-col-status {
    white-space: nowrap;
}

.connections-table .conn-col-status {
    width: 32px;
    padding-right: 0;
    text-align: center;
}

.connections-table .conn-col-actions {
    width: 48px;
    text-align: right;
}

.entity-row-name {
    font-weight: 600;
    color: var(--ink);
}

/* The row stays clickable for the mouse, but a click needs a keyboard
   equivalent (WCAG 2.1.1), and every one of these tables opened its record by
   row click alone. The name becomes the control that opens it — so the
   accessible name is the record's own, not a column of identical "Edit"s.
   Padding and matching negative margin keep the focus ring off the glyphs
   without moving the text a pixel. */
button.entity-row-name {
    display: inline-block;
    max-width: 100%;
    margin: -2px -4px;
    padding: 2px 4px;
    border: 0;
    border-radius: var(--radius-sm);
    background: none;
    font: inherit;
    font-weight: 600;
    color: var(--ink);
    text-align: left;
    cursor: pointer;
}

/* Underline on row hover too: the mouse target is the whole row — or the whole
   card, in the reports grid — so the affordance should light up wherever the
   pointer actually is. */
button.entity-row-name:hover,
tbody tr:hover button.entity-row-name,
.report-card:hover button.entity-row-name {
    text-decoration: underline;
    text-underline-offset: 2px;
}

button.entity-row-name:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
}

.users-table .users-col-username,
.users-table .users-col-role,
.users-table .users-col-created,
.users-table .users-col-actions {
    white-space: nowrap;
}

.users-table .users-col-actions {
    width: 48px;
    text-align: right;
}

.users-table tbody tr,
.connections-table tbody tr,
.schedules-table tbody tr.schedules-row,
.monitors-table tbody tr.monitors-row,
.alerts-table tbody tr.alerts-row {
    cursor: pointer;
}

.access-groups-table .access-groups-col-actions {
    width: 140px;
    white-space: nowrap;
}

.access-group-member-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-8);
    max-height: min(420px, 56vh);
    overflow-y: auto;
    padding-right: 4px;
}

.access-group-member-row {
    display: flex;
    align-items: flex-start;
    gap: var(--space-10);
    padding: 10px 12px;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--panel);
    cursor: pointer;
}

.access-group-member-row input {
    margin-top: 2px;
    accent-color: var(--accent);
}

.access-group-member-row span {
    min-width: 0;
}

.schedules-table .schedules-col-artifacts,
.schedules-table .schedules-col-last-run,
.schedules-table .schedules-col-actions {
    white-space: nowrap;
}

.schedules-table .schedules-col-artifacts,
.schedules-table .schedules-col-actions {
    width: 1%;
}

.schedules-table .schedules-col-actions {
    text-align: right;
}

.schedule-run-artifact {
    display: flex;
    align-items: center;
    gap: var(--space-8);
    min-width: 0;
}

.schedule-run-retention-action {
    flex: 0 0 auto;
    width: 28px;
    height: 28px;
}

.schedule-run-retention-action.is-protected {
    width: auto;
    padding: 0 8px;
    gap: var(--space-6);
    border-color: var(--accent-line);
    background: var(--accent-soft);
    color: var(--accent-strong);
    font-size: var(--text-label);
    font-weight: 700;
}

.schedule-run-retention-action.is-protected:hover {
    border-color: var(--accent);
    background: var(--accent-tint);
    color: var(--accent-strong);
}

@media (hover: hover) and (pointer: fine) {
    .schedule-run-retention-action:not(.is-protected) {
        opacity: 0;
        pointer-events: none;
        transition: opacity var(--transition), background 0.15s, color 0.15s, border-color 0.15s;
    }

    .schedules-table tbody tr:hover .schedule-run-retention-action,
    .schedules-table tbody tr:focus-within .schedule-run-retention-action {
        opacity: 1;
        pointer-events: auto;
    }
}

.monitors-table .monitors-col-last-run,
.monitors-table .monitors-col-last-value,
.monitors-table .monitors-col-actions {
    white-space: nowrap;
}

.monitors-table .monitors-col-actions {
    width: 1%;
    text-align: right;
}

.alerts-table .alerts-col-fired,
.alerts-table .alerts-col-status,
.alerts-table .alerts-col-delivery {
    white-space: nowrap;
}

.alerts-table .alerts-col-source {
    min-width: 220px;
}

/* ── Alert detail ─────────────────────────────────────────────────────
   The one modal in the product that was laid out entirely in inline
   styles, and in a unit nothing else here uses: 0.5em, 0.75em, 1em and
   1.5rem, five ad-hoc gaps in one dialog, against a spacing scale that
   is measured by check_css_tokens. Same rendered layout, off the scale. */
.alert-detail-meta {
    margin-bottom: var(--space-8);
}

.alert-detail-source {
    margin: var(--space-12) 0;
}

.alert-detail-heading {
    margin-top: var(--space-24);
}

.alert-detail-rules {
    width: 100%;
}

.alert-detail-note {
    margin-top: var(--space-16);
    color: var(--warning);
}

.alert-detail-ack {
    margin-top: var(--space-24);
}

.alert-detail-ack-hint {
    margin-top: var(--space-8);
}

/* The glyph is the state, so it carries the state's colour — and an
   accessible name, because "check mark" under a column headed Passed is
   a shape being read out, not an answer. */
.alert-detail-check-pass {
    color: var(--success);
}

.alert-detail-check-fail {
    color: var(--danger);
}

/* Status dot — a retest button, so the target is 24px even though the mark is
   10px (WCAG 2.2 SC 2.5.8). The dot lives on ::before; the button is the
   invisible hit area around it, and scaling on hover scales the mark, never
   the target. */
.status-dot {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    border: none;
    padding: 0;
    background: transparent;
    border-radius: 50%;
    cursor: pointer;
    vertical-align: middle;
}

.status-dot::before {
    content: '';
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--muted);
    box-shadow: 0 0 0 3px transparent;
    transition: box-shadow 0.15s, transform 0.15s;
}

.status-dot:hover::before {
    transform: scale(1.2);
}

/* The pulse scales rather than fades. At the old 0.35–0.75 opacity the dot
   spent most of the cycle near 2:1 against the row — below the 3:1 that a
   meaningful graphic owes (WCAG 1.4.11). Full colour holds 5.18:1 throughout
   and the size change still reads as "working". */
.status-dot-loading::before {
    animation: status-dot-pulse 1.2s ease-in-out infinite;
}

.status-dot-success::before {
    background: var(--success);
    box-shadow: 0 0 0 3px var(--success-soft);
}

.status-dot-danger::before {
    background: var(--danger);
    box-shadow: 0 0 0 3px var(--danger-soft);
}

@keyframes status-dot-pulse {
    0%, 100% { transform: scale(0.7); }
    50% { transform: scale(1); }
}

/* Icon button (table row actions) */
.btn-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    padding: 0;
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    color: var(--muted);
    cursor: pointer;
    transition: background 0.15s, color 0.15s, border-color 0.15s;
}

.btn-icon:hover {
    background: var(--row-hover);
    color: var(--ink);
}

.btn-icon-danger:hover {
    background: var(--danger-soft);
    color: var(--danger);
    border-color: var(--danger-soft);
}

tr:last-child td { border-bottom: none; }
tr:hover td { background: var(--row-hover); }

/* Forms */
.form-group {
    margin-bottom: 16px;
}

/* .field-label is the same micro-label for things a <label> may not name:
   a group of controls, or a read-only value. Identical type, no for=. */
.form-group label,
.form-group .field-label {
    display: block;
    font-size: var(--text-label);
    font-weight: 700;
    color: var(--muted);
    margin-bottom: 6px;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The visual half of `required`. The attribute already carries the fact to
   assistive tech, so this marker is decoration for the eye and nothing else. */
.form-group:has(> input[required], > select[required], > textarea[required]) > label::after {
    content: '*';
    margin-left: 3px;
    color: var(--danger);
}

/* Validation messaging. FormErrors renders .field-error and points the control
   at it with aria-describedby, so the message is part of the field rather than
   a toast in the corner. Colour is never the only signal: the message is text,
   and the control also carries aria-invalid. */
.field-error {
    margin-top: 5px;
    color: var(--danger);
    font-size: var(--text-compact);
    font-weight: 500;
}

input.is-invalid, select.is-invalid, textarea.is-invalid {
    border-color: var(--danger);
}

input.is-invalid:focus, select.is-invalid:focus, textarea.is-invalid:focus {
    border-color: var(--danger);
    box-shadow: 0 0 0 3px var(--danger-hover-line);
}

/* Server rejections name no field, so they sit at the top of the form the user
   is already looking at instead of only in the corner. */
.form-error-banner {
    margin-bottom: 16px;
    padding: 9px 12px;
    border: 1px solid var(--danger-line);
    border-radius: var(--radius);
    background: var(--danger-soft);
    color: var(--danger);
    font-size: var(--text-body);
}

.modal-prompt {
    margin: 0;
    color: var(--ink);
    font-size: var(--text-body);
}

/* Repeating editor rows: alert-policy rules and recipients, monitor
   recipients. Previously three copies of the same inline style, one of which
   reached for an undefined --surface and rendered with no fill at all. */
.editor-row {
    margin-bottom: 6px;
    padding: 8px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--panel-strong);
}

.editor-row-lg {
    margin-bottom: 8px;
    padding: 12px;
}

.editor-row:last-child {
    margin-bottom: 0;
}

/* ════════════════════════════════════════════════════════════════════
   Rule and recipient editors — alert policies and monitors.

   These two pages were built entirely in style attributes: .editor-row
   was the only class either of them had that this file defined, and
   .policy-rule-card and .monitor-recipient-row styled nothing at all,
   because no rule for them existed. Everything a reader would look for
   here — how a rule row lays out, how wide its fields are, what divides
   the webhook block from the row above it — lived in 54 inline
   declarations that no token, no media query and no theme could reach.

   The em-based scale went with them. Both pages sized in 0.5em/1.5em
   against a product that measures in px, so their gaps tracked whatever
   font-size they happened to inherit; 0.5em resolved to 6.5px inside a
   13px control and 7px next to a 14px one, where every other row in the
   app is on 8. The values below are the px scale those were reaching
   for, and they line up with .gap-2 / .mt-2.
   ════════════════════════════════════════════════════════════════════ */

/* One horizontal band of controls inside a rule or recipient row. */
.editor-controls {
    display: flex;
    align-items: center;
    gap: var(--space-8);
}

/* Rows whose controls exceed one line — a webhook recipient carries a
   channel, a destination, an enabled toggle and a remove button. */
.editor-controls-wrap {
    flex-wrap: wrap;
}

/* The lead band, when a configuration block follows it in the same row. */
.editor-controls-lead {
    margin-bottom: 8px;
}

/* Paired and tripled field groups: dataset/column, then
   aggregation/operator/threshold. */
.editor-fields-2,
.editor-fields-3 {
    display: grid;
    gap: var(--space-8);
}

.editor-fields-2 { grid-template-columns: 1fr 1fr; }
.editor-fields-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }

/* The field that takes the leftover width in a control band — a rule's
   label, a recipient's destination. The floor keeps an email address
   readable rather than letting it collapse next to a wide channel. */
.editor-field-grow {
    flex: 1;
    min-width: 180px;
}

/* A field sized by its own content: the channel and rule-type pickers,
   whose options are one short word. */
.editor-field-auto {
    width: auto;
}

/* A field held to the width its content actually needs. */
.editor-field-narrow {
    max-width: 220px;
}

/* A two-digit minute, inside the "At minute _ every _ hours" sentence. */
.cron-minute-field {
    width: 68px;
}

/* A block that belongs to the row above it — the webhook signing secret
   under its recipient. Dashed, because this is a continuation rather
   than a boundary between two rows. */
.editor-subsection {
    margin-top: 8px;
    padding-top: 8px;
    border-top: 1px dashed var(--line);
}

/* Explanatory line under the control it explains. */
.editor-note {
    margin-top: 4px;
}

/* A checkbox and its wording sitting beside a field rather than above it. */
.editor-inline-label {
    margin-left: 8px;
    font-size: var(--text-compact);
    color: var(--muted);
}

/* Separates Rules from Recipients — far enough apart to read as two
   sections of the form rather than two fields. */
.editor-section-heading {
    margin-top: 24px;
}

/* Pushes a destructive action to the opposite end of a dialog footer. */
.editor-footer-start {
    margin-right: auto;
}

/* A field that shares a line with others and so drops its own trailing gap. */
.form-group-inline {
    margin-bottom: 0;
}

/* A named division inside a long form — Recipients, Schedule. Weight and
   tracking rather than size, like every other micro-label in the product. */
.editor-section {
    margin: 24px 0 12px;
    padding-top: 12px;
    border-top: 1px solid var(--line);
}

.editor-section-title {
    font-size: var(--text-body);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--muted);
}

.editor-section-subtitle {
    margin-top: 3px;
}

/* Two fields at an uneven split, top-aligned so a field with a note under
   it does not drag its neighbour down. */
.editor-fields-wide {
    display: grid;
    grid-template-columns: 2fr 1fr;
    gap: var(--space-12);
    align-items: start;
}

/* A line of independent checkboxes, spaced further apart than fields so the
   label of one does not read as belonging to the next. */
.editor-checkbox-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-24);
    margin-top: 6px;
}

/* A badge sitting inside a label rather than in a cell of its own. */
.editor-badge-inline {
    margin-left: 6px;
}

/* ── Monitors ──────────────────────────────────────────────────────── */

/* The enable switch, given a surface of its own: it governs whether the
   monitor runs at all, and as a bare checkbox above the form it read as
   one more field. */
.monitor-enable-card {
    display: flex;
    align-items: center;
    gap: var(--space-10);
    margin-bottom: 20px;
    padding: 10px 12px;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--recessed);
}

.monitor-enable-card input[type="checkbox"] {
    width: 18px;
    height: 18px;
}

.monitor-enable-card label {
    margin: 0;
    font-weight: 600;
}

/* "Result <operator> <value>" reads as one sentence, so the parts sit on a
   shared baseline rather than being stacked as three labelled fields. */
.monitor-comparison {
    display: flex;
    align-items: end;
    flex-wrap: wrap;
    gap: var(--space-8);
    margin-bottom: 12px;
}

.monitor-comparison-lead {
    padding-bottom: 8px;
    color: var(--muted);
}

.monitor-comparison-operator { min-width: 170px; }
.monitor-comparison-value { width: 120px; }

/* The failure detail under a monitor row. Padding-top is zero because it
   continues the row above rather than starting a new one. */
.monitors-row-error {
    padding-top: 0;
    color: var(--danger);
}

/* Truncation notice. Deliberately not styled as an error — nothing failed, the
   grid is just showing a window onto a larger result. */
.data-grid-truncated,
.db-truncated-row td {
    padding: 10px 12px;
    background: var(--panel-strong);
    border-top: 1px solid var(--line);
    color: var(--ink-secondary);
    font-size: var(--text-compact);
}

/* The same notice, one level down: this one sits inside a group, between its
   last drawn row and its subtotal, and has to read as part of that group
   rather than as the end of the table. Inset and unruled does that; a second
   full-width band there would compete with the group header above it. */
.db-truncated-row.db-truncated-group-row td {
    padding-left: 24px;
    background: var(--recessed);
    border-top: 0;
    color: var(--muted);
    font-style: italic;
}

.form-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-16);
}

.reports-folder-input-help {
    margin: 6px 0 0;
}

input[type="text"], input[type="password"], input[type="number"], input[type="email"],
input[type="date"], input[type="time"], input[type="search"], input[type="url"],
input[type="datetime-local"], select, textarea {
    width: 100%;
    padding: 8px 11px;
    background: var(--panel);
    border: 1px solid var(--line-control);
    border-radius: var(--radius);
    color: var(--ink);
    font-size: var(--text-body);
    font-family: inherit;
    transition: border-color var(--transition), box-shadow var(--transition);
}

/* ════════════════════════════════════════════════════════════════════
   The one control colour nothing in this product had chosen.

   The rule above names nine input types and stops. Checkbox and radio are
   not among them, so all 95 of them across the app, the designer and the
   layout editor rendered at the user agent's size and, when checked, in the
   user agent's blue — a colour that appears nowhere in this palette, sitting
   beside teal buttons and teal focus rings. The alert policy editor showed
   ten of them at once.

   Four rules had set `accent-color` locally and were the only checkboxes in
   the product drawn in the brand: the access-group member rows, the sharing
   panel's SOX toggle, the parameter-options source choice, and the layout
   editor's props panel.

   No gate could see this, for the same reason none could see ::placeholder:
   check_css_tokens finds no literal because there is no declaration at all.
   check_css_contrast.mjs now asserts this rule exists — see its second half.
   ════════════════════════════════════════════════════════════════════ */
input[type="checkbox"], input[type="radio"] {
    width: var(--control-check);
    height: var(--control-check);
    accent-color: var(--accent);
    /* Rows put these in a flex line beside a label that is allowed to wrap;
       without this the box is what gives way. */
    flex-shrink: 0;
    cursor: pointer;
}

/* ════════════════════════════════════════════════════════════════════
   A label that wraps its own checkbox is the target, not the box.

   Clicking the wording toggles the control, so WCAG 2.5.8 measures the
   pair — and the pair was 21px tall, under the 24px minimum, which put it
   on the spacing exception instead. In the alert policy editor the
   exception failed: "Fire on schedule failure", "Fire when rules match"
   and "SOX-relevant" sit flush against the Rule combination select below
   them, so the 24px circle centred on each reached to within 10.5px of it
   where it needs 12. Meeting the minimum outright is the simpler answer
   than shimming a gap, and it covers the other seven of these in the
   product before they end up beside something too.

   min-height needs a box to apply to, hence inline-flex — which also puts
   a real gap between the control and its wording in place of the single
   collapsed text space they had been relying on.

   Split in two on purpose. The layout half sits in :where(), so it carries
   no specificity at all and every class-based rule outranks it — which is
   what a floor should do: `.access-group-member-row` deliberately aligns
   its box to the first line of a two-line name, and `.prop-checkbox` has
   already laid out the props panel. Both keep their decisions.

   The target size is not on the same footing, so it is not in :where().
   A control's hit area is not a layout preference to be overridden by
   whichever rule happens to come later.
   ════════════════════════════════════════════════════════════════════ */
:where(label:has(> input[type="checkbox"]), label:has(> input[type="radio"])) {
    display: inline-flex;
    align-items: center;
    gap: var(--space-8);
    cursor: pointer;
}

label:has(> input[type="checkbox"]),
label:has(> input[type="radio"]) {
    min-height: 24px;
}

input:focus, select:focus, textarea:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: var(--focus-ring);
}

/* ════════════════════════════════════════════════════════════════════
   The one text colour in this product that nothing had chosen.

   145 inputs carry a placeholder and four rules across two sheets styled
   one, so the rest rendered in the user agent's own grey — #757575 in
   Chrome, which measures 4.47:1 on this app's field background. Under
   the floor, and untouchable by every gate here: check_css_tokens sees
   no literal because there is no declaration, and check_css_contrast
   measures pairs that are declared. A colour supplied by the browser is
   not a pair anyone wrote down.

   --muted is the token for text that is present but subordinate, and it
   is measured against all five surfaces a field can sit on. It also puts
   the hint a clear step below --ink, which is the value the field will
   hold — the hierarchy the placeholder should have had all along.

   Unqualified, so a field added tomorrow is covered without anyone
   remembering this block is here.
   ════════════════════════════════════════════════════════════════════ */
::placeholder {
    color: var(--muted);
    opacity: 1;
}

/* ════════════════════════════════════════════════════════════════════
   Windows High Contrast.

   Forced-colors mode does not paint box-shadow, and box-shadow is where
   every focus indicator in this product lives: twenty-odd rules spend
   `outline: none` to buy a --focus-ring, eleven of them in this sheet
   and the rest in the designer's and the data browser's.

   Which is why this block is allowed `!important`, which almost nothing
   else here is. Without it the block was dead code — `:focus-visible`
   is specificity (0,1,0) and every rule it exists to correct is class-qualified at
   (0,2,0), so `.btn`, `.nav-link`, `.modal-close` and the rest kept
   their `outline: none` and, with the shadow unpainted, showed no focus
   indicator at all. The two lazily-loaded sheets would also have won on
   order at a tie. Importance is the only lever that reaches all of them
   from one place, and this is what it is for: a display mode the user
   chose overriding decoration the author chose, not a component
   fighting its own design system.

   Unqualified on purpose. A control added tomorrow is covered without
   anyone remembering this block is here.
   ════════════════════════════════════════════════════════════════════ */
@media (forced-colors: active) {
    :focus-visible,
    input:focus, select:focus, textarea:focus {
        outline: 2px solid Highlight !important;
        outline-offset: 2px !important;
    }
}

textarea {
    min-height: 80px;
    resize: vertical;
}

select {
    appearance: none;
    background-image: var(--select-chevron);
    background-repeat: no-repeat;
    background-position: right 12px center;
    padding-right: 32px;
}

/* ════════════════════════════════════════════════════════════════════
   Badges: two families, not one.

   There used to be four variants, all semantic, and the product used
   them as a categorical palette. --warning was the most-reached-for of
   the four and carried, in the same product: a monitor in an error
   state, a schedule that is simply switched off, a job that is
   currently running, the "User" role, and every free-form tag a user
   had ever typed on a report. --primary carried output formats, driver
   types, role names and delivery channels — the accent that also means
   primary action, active nav and selection, spent on "XLSX".

   Two states contradicted themselves outright across pages: "Error" was
   amber in monitors and red in schedules, and "Running" was teal in
   monitors and amber in schedules.

   So the semantic four are now states only, and a neutral family
   carries everything that is a category rather than a condition. The
   test at the point of use: does this word describe something that has
   gone right, gone wrong, or needs attention? Then it is semantic.
   Otherwise it is a label, and labels are grey.

   `.badge` on its own also used to set no background and no colour at
   all, so an unmodified badge — monitors' "Pending", alerts' "Info" —
   rendered as naked bold text in a column of pills. The base now *is*
   the neutral variant, so the unmodified case is the common case.
   ════════════════════════════════════════════════════════════════════ */
.badge {
    display: inline-flex;
    align-items: center;
    gap: var(--space-6);
    padding: 2px 8px;
    border-radius: var(--radius-pill);
    font-size: var(--text-label);
    font-weight: 700;
    letter-spacing: 0.01em;
    white-space: nowrap;
    /* The neutral default, which is also .badge-neutral below. */
    background: var(--panel-strong);
    color: var(--ink-secondary);
    border: 1px solid var(--line);
}

/* States. Something has gone right, gone wrong, or wants attention. */
.badge-success { background: var(--success-soft); color: var(--success); border-color: var(--success-line); }
.badge-danger { background: var(--danger-soft); color: var(--danger); border-color: var(--danger-line); }
.badge-warning { background: var(--warning-soft); color: var(--warning); border-color: var(--warning-line); }

/* In progress. Reads as active without claiming anything about the outcome —
   the state "Running" was in, before it was amber on one page and teal on
   another. Kept in the accent family because it is genuinely transient. */
.badge-progress { background: var(--accent-soft); color: var(--accent-strong); border-color: var(--accent-line); }

/* Categories: formats, roles, driver types, delivery channels, presets, tags.
   Grey by design — there are up to five of these in a single schedule row, and
   a row where everything is coloured is a row where nothing is. */
.badge-neutral { background: var(--panel-strong); color: var(--ink-secondary); border-color: var(--line); }

/* The quietest tier, for a category that is the unremarkable default —
   "Standard" access, an untagged report. */
.badge-muted { background: var(--row-hover); color: var(--muted); border-color: transparent; }

/* The viewer's own access role, which is the one badge in a list that is not
   about the record it sits on. It wore .badge-neutral, which is also what a tag
   wears, so on a reports row the tag "julie" and the role "admin" rendered as
   the same object one column apart. Outlined rather than filled: same quiet
   grey family, visibly a different kind of thing. */
.badge-role {
    background: transparent;
    color: var(--muted);
    border-color: var(--line-strong);
}

/* Login */
.login-overlay {
    position: fixed;
    inset: 0;
    background: var(--canvas);
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: var(--z-signin);
    /* The card is 498px tall with the SSO block, and body is overflow:hidden —
       so on a window shorter than that it was clipped at both ends with no way
       to reach either. Rare in the 1024–1920 band this app is built for, but
       the sign-in card is the one screen that must never be unreachable. */
    overflow-y: auto;
    padding: 24px;
}

.login-card {
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-xl);
    padding: 32px;
    width: 384px;
    box-shadow: var(--shadow-card);
}

.login-card-brand {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: var(--space-12);
    margin-bottom: 8px;
}

.login-card-logo {
    width: 26px;
    height: 26px;
    padding: 0;
    border-radius: 0;
    background: transparent;
    color: var(--accent);
    flex-shrink: 0;
}

/* h1 by level, 16px by role. The overlay's only heading has to be level 1 for
   the document outline, but it is a wordmark beside a 26px logo, not a page
   title, so it keeps the size it had as an h2. */
.login-card-brand h1 {
    margin: 0;
    font-size: var(--text-xl);
    font-weight: 700;
    letter-spacing: -0.01em;
    color: var(--ink-strong);
}
.login-card p { color: var(--muted); margin-bottom: 24px; text-align: left; }
.error-msg { color: var(--danger); font-size: var(--text-body); margin-bottom: 12px; }
.login-sso {
    margin-top: 20px;
}

.login-divider {
    position: relative;
    margin: 20px 0 18px;
    text-align: center;
}

.login-divider::before {
    content: "";
    position: absolute;
    inset: 50% 0 auto;
    border-top: 1px solid var(--line);
}

.login-divider span {
    position: relative;
    padding: 0 12px;
    background: var(--panel);
    color: var(--muted);
    font-size: var(--text-compact);
    text-transform: uppercase;
    letter-spacing: 0.08em;
}

.google-login-button {
    display: flex;
    justify-content: center;
    min-height: 44px;
}

.login-sso-hint {
    margin-top: 12px;
    margin-bottom: 0;
    font-size: var(--text-compact);
    line-height: 1.5;
}

/* Modal */
.modal-backdrop {
    position: fixed;
    inset: 0;
    background: var(--overlay-backdrop);
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: var(--z-overlay);
    backdrop-filter: blur(4px);
}

.modal-container {
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-xl);
    padding: 20px 22px 22px;
    width: 560px;
    max-width: 90vw;
    max-height: 85vh;
    overflow-y: auto;
    box-shadow: var(--shadow-modal);
}

.modal-container.modal-container-wide {
    width: min(960px, 94vw);
}

/* Confirmations sit above whatever raised them — including another dialog —
   so they get their own overlay and a higher stacking order. Narrower, because
   a one-line question in a 560px box reads as a form with the fields missing.
   No second blur: the layer below is already blurred, and stacking two makes
   the underlying dialog unreadable rather than merely recessed. */
.confirm-backdrop {
    z-index: var(--z-dialog);
    background: var(--overlay-backdrop-stacked);
    backdrop-filter: none;
}

.confirm-container {
    width: 420px;
}

.confirm-container .modal-header {
    margin-bottom: 12px;
}

.modal-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
}

.modal-header h2 {
    margin: 0;
    font-size: var(--text-lg);
    font-weight: 700;
    color: var(--ink-strong);
}
.modal-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    background: none;
    border: none;
    border-radius: var(--radius);
    color: var(--muted);
    cursor: pointer;
    font-size: var(--text-2xl);
    line-height: 1;
    padding: 0;
    transition: background var(--transition), color var(--transition);
}
.modal-close:hover {
    background: var(--row-hover);
    color: var(--ink);
}
.modal-close:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
}

.modal-footer {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-8);
    margin-top: 20px;
    padding-top: 16px;
    border-top: 1px solid var(--line);
}

/* Empty State — icon-led and calm */
.empty-state {
    text-align: center;
    padding: 48px 20px;
    color: var(--muted);
    font-size: var(--text-body);
}

.empty-state svg {
    margin-bottom: 12px;
    color: var(--accent);
    opacity: 1;
    width: 30px;
    height: 30px;
}

.empty-state p { margin-bottom: 14px; }

/* The not-found route is the one empty state that carries a page-level
   heading and a way out, so it needs a measure the bare centred variant
   above never did — a full-width line of prose at 1440 is unreadable. */
.empty-state h1 {
    font-size: var(--text-3xl);
    font-weight: 700;
    color: var(--ink-strong);
    letter-spacing: -0.01em;
    margin-bottom: var(--space-12);
}

.empty-state h1 + p {
    max-width: 52ch;
    margin-left: auto;
    margin-right: auto;
    margin-bottom: var(--space-20);
    line-height: 1.6;
}

.empty-state-path {
    font-family: var(--font-mono);
    font-size: var(--text-compact);
    padding: 1px 5px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--recessed);
    color: var(--ink-secondary);
    /* A pasted address is the one string here that can be arbitrarily long
       and has no spaces to break at. */
    overflow-wrap: anywhere;
}

/* Tabs */
.tabs {
    display: flex;
    border-bottom: 1px solid var(--line);
    margin-bottom: 20px;
    gap: 0;
}

.tab {
    padding: 10px 20px;
    cursor: pointer;
    color: var(--muted);
    font-size: var(--text-body);
    font-weight: 500;
    border-bottom: 2px solid transparent;
    transition: color var(--transition), border-color var(--transition);
    background: none;
    border-top: none;
    border-left: none;
    border-right: none;
}

.tab:hover { color: var(--ink); }
.tab.active { color: var(--accent); border-bottom-color: var(--accent); }

/* Utility */
.flex { display: flex; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.gap-2 { gap: var(--space-8); }
.gap-4 { gap: var(--space-16); }
.mt-2 { margin-top: 8px; }
.mt-4 { margin-top: 16px; }
.mb-2 { margin-bottom: 8px; }
.mb-4 { margin-bottom: 16px; }
.text-muted { color: var(--muted); }
.text-sm { font-size: var(--text-compact); }
/* Code in a form field: a monitor's SQL, a QA case's JSON payload. Both asked
   for monospace and neither got it — one named a literal Menlo/Consolas stack
   inline, and the other carried class="mono" that no stylesheet defined, so it
   rendered in the proportional body face. --font-mono is the product's one
   monospace stack; see the note beside it in :root. */
.mono { font-family: var(--font-mono); font-size: var(--text-body); }
.text-right { text-align: right; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hidden { display: none !important; }

/* Loading Spinner */
.spinner {
    width: 20px;
    height: 20px;
    border: 2px solid var(--line);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: spin 0.6s linear infinite;
    display: inline-block;
}

@keyframes spin { to { transform: rotate(360deg); } }

/* Shown while LazyBundle fetches the authoring bundles. */
.lazy-bundle-loading {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-12);
    padding: 64px 20px;
    color: var(--muted);
    font-size: var(--text-body);
}

/* Toast Notifications — bottom right; errors persist until dismissed */
.toast-container {
    position: fixed;
    right: 22px;
    bottom: 22px;
    z-index: var(--z-toast);
    display: flex;
    flex-direction: column;
    gap: var(--space-8);
    width: min(360px, calc(100vw - 44px));
}

.toast {
    display: grid;
    grid-template-columns: 18px minmax(0, 1fr) 22px;
    gap: var(--space-10);
    align-items: start;
    padding: 11px 10px 11px 14px;
    border-radius: var(--radius-lg);
    background: var(--panel);
    border: 1px solid var(--line);
    color: var(--ink);
    font-size: var(--text-compact);
    font-weight: 500;
    line-height: 1.45;
    box-shadow: var(--shadow-modal);
    animation: toast-in 220ms ease-out;
    position: relative;
    overflow: hidden;
}

.toast::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 4px;
}

.toast-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    margin-top: 1px;
}

.toast-message {
    min-width: 0;
    overflow-wrap: anywhere;
    padding-top: 1px;
}

.toast-dismiss {
    width: 22px;
    height: 22px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: none;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--muted);
    font-size: var(--text-lg);
    line-height: 1;
    cursor: pointer;
    padding: 0;
    transition: background var(--transition), color var(--transition);
}

.toast-dismiss:hover {
    background: var(--row-hover);
    color: var(--ink);
}

.toast-dismiss:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
}

.toast-success::before { background: var(--success); }
.toast-success .toast-icon { color: var(--success); }
.toast-error::before { background: var(--danger); }
.toast-error .toast-icon { color: var(--danger); }
.toast-info::before { background: var(--info); }
.toast-info .toast-icon { color: var(--info); }

@keyframes toast-in { from { transform: translateY(8px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }

/* Reduced motion keeps every state readable — it does not blank the UI.
   Indefinite animation stops (it is the only motion here that runs without a
   user action), entrances arrive instantly, and the shell stops sliding. The
   spinner slows rather than freezing, because a frozen spinner reads as a hung
   request; state that was carried by motion is handed to opacity instead. */
@media (prefers-reduced-motion: reduce) {
    .toast {
        animation: none;
    }

    /* Still legible as "busy", without a fast rotation. */
    .spinner {
        animation-duration: 2.4s;
    }

    /* The dot's meaning is carried by colour and its adjacent label; hold it
       at the pulse's full size so it stays clearly visible. */
    .status-dot-loading::before {
        animation: none;
        transform: scale(1);
    }

    /* Collapsing the rail is a layout-wide slide — the largest movement in the
       app. Snap it instead; the result is identical, just immediate. */
    #sidebar,
    .logo-label,
    .nav-link-label,
    .sidebar-toggle {
        transition: none;
    }

    .modal-container,
    .login-card {
        animation: none;
    }
}

/* Scrollbar */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--line); border-radius: var(--radius-sm); }
::-webkit-scrollbar-thumb:hover { background: var(--muted); }

/* ── Help System ──────────────────────────────────────────────────────── */
/* Overlay backdrop */
#help-overlay {
    position: fixed;
    inset: 0;
    background: var(--overlay-backdrop);
    z-index: var(--z-overlay);
    opacity: 0;
    pointer-events: none;
    transition: opacity 200ms ease;
    display: flex;
    justify-content: flex-end;
}
#help-overlay.help-visible {
    opacity: 1;
    pointer-events: all;
}

/* Slide-in panel */
.help-panel {
    display: flex;
    width: 560px;
    max-width: 90vw;
    height: 100%;
    background: var(--panel);
    border-left: 1px solid var(--line);
    transform: translateX(100%);
    transition: transform 220ms ease;
    box-shadow: var(--shadow-drawer);
    overflow: hidden;
}
#help-overlay.help-visible .help-panel {
    transform: translateX(0);
}

/* Left nav sidebar */
.help-sidebar {
    width: 200px;
    flex-shrink: 0;
    background: var(--panel-strong);
    border-right: 1px solid var(--line);
    display: flex;
    flex-direction: column;
    overflow-y: auto;
}
.help-sidebar-header {
    display: flex;
    align-items: center;
    gap: var(--space-8);
    padding: 18px 16px 14px;
    font-size: var(--text-body);
    font-weight: 600;
    color: var(--ink);
    border-bottom: 1px solid var(--line);
}
.help-sidebar-header svg { color: var(--accent); }
.help-nav {
    padding: 8px;
    flex: 1;
}
.help-nav-group {
    margin-bottom: 4px;
}
.help-nav-group-label {
    padding: 10px 8px 4px;
    font-size: var(--text-micro);
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--muted);
}
.help-nav-item {
    display: block;
    width: 100%;
    padding: 7px 10px;
    border: none;
    border-radius: var(--radius);
    background: transparent;
    color: var(--ink-secondary);
    font-size: var(--text-compact);
    font-weight: 500;
    text-align: left;
    cursor: pointer;
    transition: background var(--transition), color var(--transition);
    margin-bottom: 1px;
}
.help-nav-item:hover {
    background: var(--row-hover);
    color: var(--ink);
}
.help-nav-item.active {
    background: var(--accent-tint);
    color: var(--accent);
    font-weight: 600;
}

/* Main content area */
.help-main {
    flex: 1;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    position: relative;
}
.help-close-btn {
    position: absolute;
    top: 14px;
    right: 16px;
    width: 30px;
    height: 30px;
    border: none;
    border-radius: var(--radius);
    background: var(--panel);
    color: var(--muted);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background var(--transition), color var(--transition);
    z-index: var(--z-raised);
}
.help-close-btn:hover {
    background: var(--row-hover);
    color: var(--ink);
}
.help-content {
    flex: 1;
    overflow-y: auto;
    padding: 28px 32px 40px;
}

/* Topic title */
.help-topic-title {
    font-size: var(--text-2xl);
    font-weight: 700;
    color: var(--ink);
    margin-bottom: 16px;
    padding-right: 40px;
    line-height: 1.3;
}

.help-sections {
    display: flex;
    flex-direction: column;
    gap: var(--space-20);
}

/* Definition list */
.help-def-list {
    display: flex;
    flex-direction: column;
    gap: 0;
    margin: 0;
}
.help-def-row {
    display: flex;
    gap: var(--space-12);
    padding: 8px 0;
    border-bottom: 1px solid var(--line);
}
.help-def-row:last-child { border-bottom: none; }
.help-def-row dt {
    flex-shrink: 0;
    width: 120px;
    font-size: var(--text-compact);
    font-weight: 600;
    color: var(--ink);
    padding-top: 1px;
}
.help-def-row dd {
    margin: 0;
    font-size: var(--text-compact);
    line-height: 1.55;
    color: var(--ink-secondary);
}
.help-def-row dd strong { color: var(--ink); font-weight: 600; }
.help-def-row dd code, .help-def-row dd kbd {
    font-family: var(--font-mono);
    font-size: var(--text-label);
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 1px 4px;
    color: var(--accent-strong);
}

/* Sections (shortcuts, etc.) */
.help-section {
    margin-bottom: 0;
}
.help-section-label {
    font-size: var(--text-label);
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--muted);
    margin-bottom: 10px;
}
.help-shortcuts {
    display: flex;
    flex-direction: column;
    gap: var(--space-6);
}
.help-shortcut {
    display: flex;
    align-items: center;
    gap: var(--space-12);
    padding: 8px 12px;
    background: var(--panel-strong);
    border: 1px solid var(--line);
    border-radius: var(--radius);
}
.help-shortcut kbd {
    font-family: var(--font-mono);
    font-size: var(--text-label);
    font-weight: 600;
    background: var(--panel);
    border: 1px solid var(--line);
    border-bottom-width: 2px;
    border-radius: var(--radius);
    padding: 3px 8px;
    color: var(--ink);
    white-space: nowrap;
}
.help-shortcut span {
    font-size: var(--text-body);
    color: var(--ink-secondary);
}

.help-bullet-list {
    margin: 0;
    padding-left: 18px;
    display: flex;
    flex-direction: column;
    gap: var(--space-8);
    color: var(--ink-secondary);
}
.help-bullet-list li {
    font-size: var(--text-body);
    line-height: 1.6;
}
.help-bullet-list strong {
    color: var(--ink);
    font-weight: 600;
}
.help-bullet-list code {
    font-family: var(--font-mono);
    font-size: var(--text-compact);
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 1px 5px;
    color: var(--accent-strong);
}

.help-note {
    padding: 12px 14px;
    background: var(--panel-strong);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    font-size: var(--text-body);
    line-height: 1.65;
    color: var(--ink-secondary);
}
.help-note.is-info {
    background: var(--info-soft);
    border-color: var(--info-line);
}
.help-note.is-warning {
    background: var(--warning-soft);
    border-color: var(--warning-line);
}
.help-note strong {
    color: var(--ink);
    font-weight: 600;
}
.help-note code {
    font-family: var(--font-mono);
    font-size: var(--text-compact);
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 1px 5px;
    color: var(--accent-strong);
}

/* Footer hint */
.help-footer-hint {
    margin-top: 32px;
    padding-top: 16px;
    border-top: 1px solid var(--line);
    font-size: var(--text-label);
    color: var(--muted);
    text-align: center;
}
.help-footer-hint kbd {
    font-family: var(--font-mono);
    font-size: var(--text-micro);
    background: var(--panel);
    border: 1px solid var(--line);
    border-bottom-width: 2px;
    border-radius: var(--radius-sm);
    padding: 1px 6px;
    color: var(--ink-secondary);
}

/* ===== Dashboard ===== */

.attention-banner {
    display: flex;
    gap: var(--space-16);
    align-items: flex-start;
    padding: 16px 20px;
    margin-bottom: 20px;
    border-radius: var(--radius-lg);
    border: 1px solid var(--line);
    background: var(--panel);
}

.attention-banner.attention-ok {
    border-color: var(--success-line);
    background: var(--success-soft);
}

.attention-banner.attention-warn {
    border-color: var(--danger-line);
    background: var(--danger-soft);
}

.attention-icon {
    flex-shrink: 0;
    width: 30px;
    height: 30px;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: var(--text-lg);
    line-height: 1;
    border: 1px solid transparent;
}

.attention-ok .attention-icon {
    background: var(--panel);
    color: var(--success);
    border-color: var(--success-line);
}
.attention-warn .attention-icon {
    background: var(--panel);
    color: var(--danger);
    border-color: var(--danger-line);
}

.attention-body { flex: 1; }

.attention-title {
    font-weight: 600;
    font-size: var(--text-lg);
    margin-bottom: 4px;
}

.attention-subtitle {
    color: var(--muted);
    font-size: var(--text-body);
}

.attention-items {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-8);
    margin-top: 8px;
}

.attention-item {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: 8px 14px;
    border-radius: var(--radius);
    border: 1px solid var(--line);
    background: var(--panel);
    text-decoration: none;
    color: var(--ink);
    min-width: 160px;
    transition: background 0.1s;
}

.attention-item:hover { background: var(--row-hover); }

.attention-item-label {
    font-size: var(--text-label);
    font-weight: 700;
    color: var(--muted);
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

.attention-item-value {
    font-size: var(--text-xl);
    font-weight: 600;
    display: flex;
    gap: var(--space-6);
    align-items: center;
    flex-wrap: wrap;
}

.attention-chip {
    font-size: var(--text-label);
    padding: 1px 8px;
    border-radius: var(--radius-xl);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.3px;
}

.attention-chip-critical { background: var(--danger-soft); color: var(--danger); }
.attention-chip-warning { background: var(--warning-soft); color: var(--warning); }
.attention-chip-info { background: var(--row-hover); color: var(--muted); }

/* Health tiles */
.dashboard-tiles {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: var(--space-16);
    margin-bottom: 24px;
}

/* Three columns is the one count that does not divide these tiles.
   auto-fit resolves to three from 1024px until about 1224px — most of a laptop's
   width — and an administrator has four tiles, so the fourth sat alone at 232px
   with 480px of empty row beside it.

   Lowering the floor to fit four across was the obvious fix and it is the wrong
   one: measured at 1024, four columns are 170px each, which wraps "View all" onto
   its own line, breaks "5 published / 20 draft" across two, and turns the success
   tile's label into two lines. Two by two at 356px fits everything on one line.
   So ask for two directly, over exactly the band where auto-fit would pick three.

   :has() scopes this to the case that is actually broken. Someone without
   administrator access never sees the Connections tile, and three tiles across
   three columns has no orphan to correct. */
@media (max-width: 1223px) {
    .dashboard-tiles:has(> :nth-child(4)) {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

.health-tile {
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    padding: 16px 18px;
    box-shadow: var(--shadow-card);
    display: flex;
    flex-direction: column;
    gap: var(--space-8);
}

.health-tile-danger { border-color: var(--danger); }

.health-tile-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: var(--space-8);
}

.health-tile-label {
    font-size: var(--text-label);
    color: var(--muted);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    font-weight: 700;
}

/* 24px tall, not the 18px the type alone gives it (WCAG 2.2 SC 2.5.8). These
   sit alone in a tile header rather than inside a sentence, so the inline
   exception does not reach them. The padding grows the target without moving
   the text: the negative right margin puts the trailing edge back where the
   tile's own padding had it. */
.health-tile-link {
    font-size: var(--text-compact);
    color: var(--muted);
    text-decoration: none;
    display: inline-flex;
    align-items: center;
    min-height: 24px;
    padding: 0 var(--space-6);
    margin-right: calc(var(--space-6) * -1);
    border-radius: var(--radius-sm);
}

.health-tile-link:hover { color: var(--ink); background: var(--row-hover); }

.health-tile-main {
    font-size: var(--text-4xl);
    font-weight: 700;
    letter-spacing: -0.02em;
    line-height: 1.1;
    display: flex;
    align-items: center;
    gap: var(--space-8);
    flex-wrap: wrap;
    color: var(--ink-strong);
    font-variant-numeric: tabular-nums;
}

.health-tile-main.rate-good { color: var(--success); }
.health-tile-main.rate-warn { color: var(--warning); }
.health-tile-main.rate-bad { color: var(--danger); }

.health-tile-bar {
    display: flex;
    height: 5px;
    border-radius: var(--radius-pill);
    overflow: hidden;
    background: var(--panel-strong);
}

.health-tile-bar-fill { height: 100%; }
.health-tile-bar-success { background: var(--success); }
.health-tile-bar-muted { background: var(--line); }

.health-tile-sub {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-6) var(--space-16);
    font-size: var(--text-compact);
    color: var(--muted);
}

.health-tile-empty {
    font-size: var(--text-body);
    color: var(--muted);
    padding: 4px 0;
}

/* Next runs — a rail section, same shell as .my-schedules directly below it.
   The two lists answer neighbouring questions ("what runs next anywhere" and
   "what runs next of mine"), so they read as one column of two, not as two
   unrelated widgets. */
.next-run-list {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    overflow: hidden;
}

.next-run-row {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: center;
    gap: var(--space-10);
    padding: 10px 14px;
    border-bottom: 1px solid var(--line);
    font-size: var(--text-body);
}

.next-run-row:last-child { border-bottom: 0; }

.next-run-name {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.next-run-eta {
    color: var(--muted);
    font-variant-numeric: tabular-nums;
}

.dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    flex-shrink: 0;
}

.dot-success { background: var(--success); }
.dot-danger { background: var(--danger); }
.dot-warning { background: var(--warning); }
.dot-muted { background: var(--line); }

/* A record's normal state, in a list. The pairing of a dot with its own word is
   the design language's treatment for states that are not exceptions, and it is
   what the dashboard's summary lines already used; the list tables reached for a
   badge instead, which put one or two on every row and left the badge meaning
   nothing. The word carries the state — the dot is aria-hidden and never the
   only thing saying it. */
.entity-state {
    display: inline-flex;
    align-items: center;
    gap: var(--space-6);
    white-space: nowrap;
    color: var(--ink-secondary);
}

.sparkline { display: block; margin-top: 2px; }
.sparkline-bar-success { fill: var(--success); }
.sparkline-bar-skipped { fill: var(--warning); }
.sparkline-bar-fail { fill: var(--danger); }
.sparkline-bar-muted { fill: var(--line); }

/* Split activity + my schedules */
.dashboard-split {
    display: grid;
    grid-template-columns: 2fr 1fr;
    gap: var(--space-20);
    align-items: start;
}

/* The right column stacks Next runs above My schedules. */
.dashboard-rail {
    display: flex;
    flex-direction: column;
    gap: var(--space-24);
    min-width: 0;
}

@media (max-width: 960px) {
    .dashboard-split { grid-template-columns: 1fr; }
}

.dashboard-section-title {
    font-size: var(--text-body);
    font-weight: 700;
    margin: 0 0 10px;
    color: var(--ink-strong);
}

/* Activity feed */
.activity-feed {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    overflow: hidden;
}

.activity-entry {
    display: flex;
    gap: var(--space-12);
    align-items: flex-start;
    padding: 10px 14px;
    border-bottom: 1px solid var(--line);
}

.activity-entry:last-child { border-bottom: 0; }

.activity-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    margin-top: 6px;
    flex-shrink: 0;
    background: var(--line);
}

.activity-success .activity-dot { background: var(--success); }
.activity-error .activity-dot { background: var(--danger); }
.activity-skipped .activity-dot { background: var(--warning); }
.activity-alert-critical .activity-dot { background: var(--danger); }
.activity-alert-warning .activity-dot { background: var(--warning); }

.activity-body {
    flex: 1;
    min-width: 0;
}

.activity-line {
    font-size: var(--text-body);
    line-height: 1.4;
}

.activity-line a {
    color: var(--ink);
    font-weight: 500;
    text-decoration: none;
}

.activity-line a:hover { text-decoration: underline; }

.activity-error {
    font-size: var(--text-compact);
    color: var(--danger);
    margin-top: 2px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.activity-skipped-note {
    font-size: var(--text-compact);
    color: var(--warning);
    margin-top: 2px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.activity-meta {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: var(--space-2);
    flex-shrink: 0;
}

.activity-time {
    font-size: var(--text-label);
    color: var(--muted);
    font-variant-numeric: tabular-nums;
}

/* My schedules */
.my-schedules {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    overflow: hidden;
}

.my-sched-row {
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: center;
    gap: var(--space-10);
    padding: 10px 14px;
    border-bottom: 1px solid var(--line);
    font-size: var(--text-body);
}

.my-sched-row:last-child { border-bottom: 0; }

.my-sched-name {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.my-sched-next {
    color: var(--muted);
    font-variant-numeric: tabular-nums;
    font-size: var(--text-compact);
}
