/* =============================================================================
   Alot of Bees — operator UI
   Implements brand guide v1 (see AlotOfBees/brand/alot-of-bees-brand-guide.html).
   No build step, no framework; every page loads this one file via ui/_layout.php.

   THEMING
   -------
   The two palette blocks below are the only place a colour literal may appear.
   Everything else — including every tint, wash, scrim and shadow — is derived
   from those tokens with color-mix(), so a value is tuned once and both themes
   follow. If you find yourself typing a hex below the palette blocks, the token
   you need is missing; add it there instead.

   Dark is the default. Light is a [data-theme="light"] override, defined exactly
   once. Which one applies is decided in script, not by an @media block, because
   the palette must not exist twice — see the note above that selector.

   CONTRAST
   --------
   Every ratio in a palette comment is measured against that theme's own --bg
   using the WCAG 2.1 relative-luminance formula. Beware that a token measured
   against --bg is not thereby safe on --surface or --raised, which is where most
   components actually sit — see the --outline note below for the one place that
   bit. --border is a divider at 1.3–1.4:1 and is NOT safe for anything a user
   has to perceive: interactive boundaries use --outline, focus rings --accent.
   ========================================================================== */

/* ---------- Type ----------------------------------------------------------
   Inter and JetBrains Mono, self-hosted, two files each. Both are variable
   fonts (verified: fvar/gvar/STAT present), so one file per subset covers the
   whole weight range and the descriptor pins the axis.

   Latin and latin-ext only. The Cyrillic, Greek and Vietnamese subsets Google
   serves are 200 KB of nothing for an English-language operator tool.

   font-display: swap — the fallback stack is Segoe UI / Calibri, which is the
   parent brand, so the pre-swap paint is on-brand rather than a blank page.
   -------------------------------------------------------------------------- */

@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 400 600;
    font-display: swap;
    src: url('fonts/inter-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308,
                   U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0,
                   U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 400 600;
    font-display: swap;
    src: url('fonts/inter-latin.woff2') format('woff2');
    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+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
                   U+FEFF, U+FFFD;
}
@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('fonts/jetbrains-mono-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308,
                   U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0,
                   U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('fonts/jetbrains-mono-latin.woff2') format('woff2');
    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+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
                   U+FEFF, U+FFFD;
}

/* ---------- Palette · dark ground ---------------------------------------- */

:root,
[data-theme="dark"] {
    --bg:            #12171C;
    --surface:       #181E25;
    --raised:        #1F2730;

    --border:        #2B3440;   /* dividers only — 1.43:1 */
    --border-strong: #5A6875;   /* focus rings, input outlines — 3.15:1 */

    --text:          #E8EDF2;   /* 15.30:1 */
    --text-2:        #A3B0BD;   /*  8.15:1 */
    --text-3:        #7A8894;   /*  4.96:1 */

    --accent:        #45E0E6;   /* 11.21:1 — dark ground only */
    --accent-ink:    #12171C;   /* text ON solid accent — 11.21:1 */
    --accent-quiet:  #1B3B40;

    --mark-body:     #E8EDF2;
    --mark-carry:    #45E0E6;

    --ok:            #3DDC84;   /* 10.10:1 */
    --run:           #B79BFF;   /*  7.85:1 */
    --warn:          #F5B544;   /*  9.93:1 */
    --err:           #FF6B6B;   /*  6.50:1 */
    --idle:          #7A8894;   /*  4.96:1 */

    --ok-bg:         #10281C;
    --run-bg:        #211B33;
    --warn-bg:       #2C2312;
    --err-bg:        #2E1618;

    /* Derived, theme-dependent. No literals: see the two exceptions noted.
       --field is recessed below --surface on dark and level with it on light,
       which is the only way an input reads as an input on both grounds. */
    --field:   var(--bg);
    /* The scrim and the shadow both need near-black. On this ground that is
       --bg itself; the light block swaps in --text, which is the same value. */
    --scrim:   color-mix(in srgb, var(--bg) 82%, transparent);
    --shadow-ink: var(--bg);

    color-scheme: dark;
}

/* ---------- Palette · light ground ---------------------------------------
   Not the dark values lightened. The accent and all five state colours are
   independently chosen: #45E0E6 is 1.5:1 on white and unusable for text or
   linework, and the dark state hues are all far too pale to read. Every value
   below clears WCAG AA (4.5:1) for normal text against --bg.

   The light palette lives here ONCE. There is deliberately no
   @media (prefers-color-scheme) copy of it — ui/_theme.php sets data-theme in
   <head> before first paint and app.js keeps it live. Two copies of thirty
   values drift the moment somebody tunes a single shade, and the app cannot
   function without JS anyway: every mutation goes through AOB.post().

   The selector is [data-theme] rather than :root[data-theme] on purpose. Same
   specificity as :root and declared later, so it still wins on <html> — but it
   also lets a subtree carry its own ground, which is what makes a side-by-side
   preview of both palettes possible in one document.
   -------------------------------------------------------------------------- */

[data-theme="light"] {
    --bg:            #F4F6F8;
    --surface:       #FFFFFF;
    --raised:        #E9EDF1;

    --border:        #D7DDE4;   /* dividers only — 1.26:1 */
    --border-strong: #7E8C9A;   /* focus rings, input outlines — 3.17:1 */

    --text:          #12171C;   /* 16.63:1 */
    --text-2:        #45525F;   /*  7.38:1 */
    --text-3:        #5E6C7B;   /*  4.96:1 */

    --accent:        #0C767C;   /*  4.92:1 — light ground only */
    --accent-ink:    #FFFFFF;   /* text ON solid accent — 5.33:1 */
    --accent-quiet:  #DCEEEF;

    --mark-body:     #12171C;
    --mark-carry:    #0C767C;

    --ok:            #0F7A46;   /* 4.98:1 */
    --run:           #5B3FC4;   /* 6.56:1 */
    --warn:          #8A5300;   /* 5.84:1 */
    --err:           #B3261E;   /* 6.03:1 */
    --idle:          #5E6C7B;   /* 4.96:1 */

    --ok-bg:         #E3F3EA;
    --run-bg:        #ECE7FA;
    --warn-bg:       #F7EEDD;
    --err-bg:        #F9E7E5;

    --field:   var(--surface);
    --scrim:   color-mix(in srgb, var(--text) 45%, transparent);
    --shadow-ink: var(--text);

    color-scheme: light;
}

/* ---------- Derived tokens, ground-independent ---------------------------
   These hold in both themes because they mix against --text, which inverts
   with the ground. One declaration, correct twice — the alternative is two
   copies that quietly disagree.
   -------------------------------------------------------------------------- */

:root {
    --hover:        color-mix(in srgb, var(--text)  5%, transparent);
    --hover-strong: color-mix(in srgb, var(--text)  9%, var(--raised));
    --accent-hover: color-mix(in srgb, var(--accent) 85%, var(--text));
    --err-ink:      var(--bg);      /* text ON solid --err — 6.50:1 / 6.03:1 */

    /* The boundary of anything interactive.
       NOT --border-strong, despite the guide naming it for input outlines. Its
       3.15:1 is measured against --bg, and inputs and buttons do not sit on --bg
       — they sit on --surface inside a card (2.93:1 dark) and on --raised
       (2.64:1 dark), both of which miss the 3:1 a perceivable boundary needs.
       --text-3 is an existing palette value and clears 3:1 on all three grounds
       in both themes, so one token is correct everywhere instead of three
       conditional ones. --border-strong survives for the theme-switch frame and
       the resting stat rule, which do sit on --bg / --surface. */
    --outline:      var(--text-3);

    --shadow-lg: 0 18px 50px color-mix(in srgb, var(--shadow-ink) 55%, transparent);
    --shadow-sm: 0  8px 24px color-mix(in srgb, var(--shadow-ink) 40%, transparent);

    --radius:    6px;
    --radius-lg: 10px;
    --radius-sm: 5px;

    --sans: "Inter", "Segoe UI Variable Text", "Segoe UI", Calibri, system-ui, sans-serif;
    /* The status glyphs (● ◐ ▲ ■ ✕ ○) are U+25xx/U+2715 and sit outside both
       webfont subsets, so they resolve from the last three families here. Every
       target OS supplies them; they are geometric shapes, not emoji. */
    --mono: "JetBrains Mono", "Cascadia Mono", Consolas, "Segoe UI Symbol",
            "DejaVu Sans Mono", ui-monospace, monospace;
}

* { box-sizing: border-box; }

/* ---------- Base --------------------------------------------------------- */

html, body {
    margin: 0;
    padding: 0;
    background: var(--bg);
    color: var(--text);
    font-family: var(--sans);
    font-size: 15px;
    line-height: 1.62;
    /* Globally, not per component. A count column whose figures change width
       between rows cannot be compared at a glance, and comparison at a glance
       is the whole job of these screens. */
    font-variant-numeric: tabular-nums;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

h1 { font-size: 26px;   font-weight: 600; line-height: 1.25; letter-spacing: -.012em; margin: 0 0 .25rem; }
h2 { font-size: 17px;   font-weight: 600; line-height: 1.35; letter-spacing: -.006em; margin: 1.75rem 0 .75rem; }
h3 { font-size: 15px;   font-weight: 600; line-height: 1.4;  margin: 1.25rem 0 .5rem; color: var(--text-2); }

code, pre, .mono, .num { font-family: var(--mono); }

/* Every interactive element, one rule. Before this, only text inputs and the
   theme switch showed focus at all — buttons, nav links and clickable rows
   showed nothing, which makes the app unusable from the keyboard. --accent
   clears 3:1 on both grounds (11.21:1 / 4.92:1).
   :where() keeps specificity at zero so a component can still opt out. */
:where(a, button, [tabindex], summary, .list-item, tr.clickable):focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: 3px;
}

/* ---------- Masthead ----------------------------------------------------- */

.masthead {
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    padding: 0 1.5rem;
    display: flex;
    align-items: center;
    gap: 1.5rem;
    position: sticky;
    top: 0;
    z-index: 40;
}

/* Mark + wordmark. Clear space is 0.25 × mark height on all four sides; the
   .brand padding and the flex gap provide it. Nothing may enter that zone —
   which is why the fleet chip is a separate flex child, not part of .brand. */
.brand {
    display: flex;
    align-items: center;
    gap: .55rem;
    padding: .8rem .25rem .8rem 0;
    color: var(--text);
    white-space: nowrap;
}
.brand:hover { text-decoration: none; }

.mark { display: block; width: 20px; height: 20px; flex-shrink: 0; }
.mark-lg { width: 34px; height: 34px; }

/* The differently-set "of" is what makes the one-word "Alot" read as
   deliberate rather than as a missing space. Do not normalise the three parts
   to one weight, and do not write "A Lot of Bees" anywhere. */
.wordmark {
    display: inline-flex;
    align-items: baseline;
    gap: .3em;
    font-size: 15px;
    font-weight: 600;
    letter-spacing: .10em;
    line-height: 1;
}
.wordmark .of {
    font-weight: 400;
    font-size: .83em;
    letter-spacing: 0;
    text-transform: lowercase;
    color: var(--text-3);
}

/* The slot the HIVE lozenge used to occupy. Same real estate, now load-bearing:
   the fleet's worst current state, on every page. */
.fleet-chip {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    flex-shrink: 0;
    font-family: var(--mono);
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: .10em;
    text-transform: uppercase;
    padding: 4px 9px;
    border-radius: var(--radius-sm);
    border: 1px solid currentColor;
}
.fleet-chip.ok   { color: var(--ok);   background: var(--ok-bg); }
.fleet-chip.warn { color: var(--warn); background: var(--warn-bg); }
.fleet-chip.err  { color: var(--err);  background: var(--err-bg); }
.fleet-chip.idle { color: var(--idle); background: var(--bg); }
/* A silent agent is the condition this product exists to surface, so it is the
   one chip that fills rather than tints. */
.fleet-chip.silent { color: var(--err-ink); background: var(--err); border-color: var(--err); }
.fleet-chip:hover { text-decoration: none; }
.fleet-chip .glyph { font-size: 11px; letter-spacing: 0; }

.nav { display: flex; gap: .25rem; flex: 1; flex-wrap: wrap; }
.nav a {
    padding: .55rem .8rem;
    border-radius: var(--radius);
    color: var(--text-2);
    font-weight: 500;
    font-size: 13.5px;
    border-bottom: 2px solid transparent;
}
.nav a:hover { background: var(--hover); color: var(--text); text-decoration: none; }
.nav a.active { color: var(--accent); border-bottom-color: var(--accent); }

.nav-badge {
    display: inline-block;
    min-width: 1.15rem;
    padding: 0 .3rem;
    margin-left: .35rem;
    border-radius: 999px;
    background: var(--err);
    color: var(--err-ink);
    font-family: var(--mono);
    font-size: 10.5px;
    font-weight: 700;
    text-align: center;
    line-height: 1.15rem;
}

.whoami { color: var(--text-3); font-size: 13.5px; white-space: nowrap; }
.whoami a { color: var(--text-2); }

/* ---------- Theme switch --------------------------------------------------
   A three-state segmented control: light / follow system / dark. Rendered by
   aob_theme_switch() in ui/_theme.php and driven by AOB.setTheme().

   Sized down deliberately. It is chrome an operator touches once, so it should
   not compete with the nav — hence icon-only, with the label exposed to
   assistive tech and as a title tooltip.
   ------------------------------------------------------------------------- */

.theme-switch {
    display: inline-flex;
    flex-shrink: 0;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--field);
}
.theme-btn {
    background: none;
    border: none;
    border-right: 1px solid var(--border);
    color: var(--text-3);
    font-family: inherit;
    font-size: 13px;
    line-height: 1;
    padding: .32rem .45rem;
    cursor: pointer;
    transition: background .12s, color .12s;
}
.theme-btn:last-child { border-right: none; }
.theme-btn:hover { background: var(--hover); color: var(--text); }
.theme-btn.active { background: var(--accent-quiet); color: var(--text); }
/* Inset, because an outset ring would be clipped by the group's overflow. */
.theme-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Visually hidden but still announced. Used for the switch's text labels. */
.sr-only {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

/* ---------- Layout ------------------------------------------------------- */

.wrap { max-width: 1400px; margin: 0 auto; padding: 1.5rem; }

/* Data-grid pages opt into the full screen.
   1400px is a comfortable reading measure and the right default for the hub, settings
   and anything with prose in it — a 1700px-wide paragraph is genuinely worse. But the
   jobs table is ten columns wide before a client name is added to it, and capping it at
   1400 pushed the actions column out of view on a screen with room to spare. So the
   width is chosen per page (aob_head's 'wide' option) rather than globally. */
.wrap-wide { max-width: min(2000px, 100%); }
.page-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin-bottom: 1.25rem;
}
.page-head .sub { color: var(--text-2); font-size: 13.5px; }
.actions { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }

.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: 1.15rem;
    margin-bottom: 1.25rem;
}
.card > h2:first-child, .card > h3:first-child { margin-top: 0; }
.card-flush { padding: 0; overflow: hidden; }

.grid { display: grid; gap: 1rem; }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.grid-4 { grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }

.split { display: grid; grid-template-columns: 320px 1fr; gap: 1rem; align-items: start; }

/* min-width: 0 is load-bearing, not tidying.
   A grid item defaults to min-width: auto, which means it grows to fit its widest
   content rather than shrinking. So one very wide descendant — a sample-row table
   from a 144-column Sage table — pushed the 1fr track past the viewport, took the
   whole page into horizontal scroll, and stretched every OTHER card in the pane to
   that width too. The enclosing .table-scroll never scrolled, because it had been
   made as wide as its own content. Overriding min-width lets the track stay narrow
   and hands the overflow back to the scroll container that is there to absorb it. */
.split > * { min-width: 0; }

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

/* ---------- Stat tiles ---------------------------------------------------
   The left border carries state, so its resting value must NOT be the accent:
   teal is brand, never a status. An unmarked tile gets a neutral rule.
   ------------------------------------------------------------------------- */

.stat {
    background: var(--surface);
    border: 1px solid var(--border);
    border-left: 3px solid var(--border-strong);
    border-radius: var(--radius);
    padding: .8rem 1rem;
}
.stat .label {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: .09em;
    color: var(--text-2);
    font-weight: 600;
}
.stat .value { font-family: var(--mono); font-size: 26px; font-weight: 500; line-height: 1.25; }
.stat .value.sm { font-size: 15px; }
.stat.good { border-left-color: var(--ok); }
.stat.warn { border-left-color: var(--warn); }
.stat.bad  { border-left-color: var(--err); }

/* ---------- Tables ------------------------------------------------------- */

.table-scroll { overflow-x: auto; }

table.data {
    width: 100%;
    border-collapse: collapse;
    font-size: 13.5px;
    line-height: 1.45;
}
table.data th {
    text-align: left;
    padding: .55rem .7rem;
    background: var(--raised);
    border-bottom: 1px solid var(--border);
    color: var(--text-2);
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: .09em;
    font-weight: 600;
    white-space: nowrap;
    position: sticky;
    top: 0;
    /* Explicit, so the header always wins against a sticky actions column (z-index 1
       below). Sticky elements with z-index:auto are painted in document order, which
       would let the last column's cells slide over the header. */
    z-index: 2;
}
/* Right-align every number, left-align every word — including the header, or
   the column heading and its figures disagree about where the column is. */
table.data th.num { text-align: right; }
table.data td {
    padding: .5rem .7rem;
    border-bottom: 1px solid var(--border);
    vertical-align: top;
}
table.data tbody tr:hover { background: var(--hover); }
table.data tbody tr.clickable { cursor: pointer; }

/* ---------- Pinned actions column ---------------------------------------
   Opt in with <table class="data sticky-actions">.

   No page width makes a ten-column operations table fit every laptop, so widening the
   container is only half the fix: the column that got clipped was the one carrying
   "Run now" and "Edit". Pinning it right means the buttons are reachable at any window
   size and the rest of the row scrolls underneath.

   A sticky cell is transparent by default, so it needs an explicit opaque background or
   the scrolling content shows through it. --hover is a semi-transparent mix and would
   do exactly that on hover, hence the opaque --hover-strong for the hovered state.

   :not([colspan]) matters. An expanded detail row — the "why?" panel under a failed run
   — is a single full-width cell, so it is simultaneously the first and LAST child of its
   row. Pinning it would give a full-width panel a stray left border and a pointless
   sticky context; excluding spanning cells leaves those rows alone.
   ------------------------------------------------------------------------- */
table.data.sticky-actions th:last-child:not([colspan]),
table.data.sticky-actions td:last-child:not([colspan]) {
    position: sticky;
    right: 0;
    border-left: 1px solid var(--border);
}
table.data.sticky-actions tbody td:last-child:not([colspan]) { background: var(--surface); z-index: 1; }
table.data.sticky-actions tbody tr:hover td:last-child:not([colspan]) { background: var(--hover-strong); }
/* The header corner sits above both the header row and the pinned column. */
table.data.sticky-actions th:last-child:not([colspan]) { z-index: 3; }

/* Inside a .card-flush the row ground is --surface; a plain .card is the same, but a
   striped or raised container would need its own override here rather than a guess. */
table.data td.num { text-align: right; font-family: var(--mono); font-size: 13px; }
table.data td.nowrap, table.data th.nowrap { white-space: nowrap; }
table.data .muted { color: var(--text-2); }

.empty {
    padding: 2rem 1rem;
    text-align: center;
    color: var(--text-2);
}

/* ---------- Status pills -------------------------------------------------
   Six states, and every one renders as colour + glyph + word, always, all
   three. A bare coloured dot is never acceptable: under deuteranopia green,
   amber and red collapse to the same muddy yellow, and these are operations
   screens. The glyph and the word come from PHP — see aob_status_pill() in
   ui/_layout.php, which is the only thing allowed to emit these classes.

     .ok     ●  last run succeeded and reconciled
     .run    ◐  work in flight; never latches
     .warn   ▲  nothing failed, but something is later than it should be
     .err    ■  a run errored; destination table not modified
     .silent ✕  no heartbeat; state unknown
     .idle   ○  scheduled and not yet due, or never run
   ------------------------------------------------------------------------- */

.pill {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    padding: 4px 9px;
    border-radius: var(--radius-sm);
    border: 1px solid currentColor;
    font-family: var(--mono);
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: .10em;
    text-transform: uppercase;
    white-space: nowrap;
}
.pill .glyph { font-size: 11px; letter-spacing: 0; }

.pill.ok   { color: var(--ok);   background: var(--ok-bg); }
.pill.run  { color: var(--run);  background: var(--run-bg); }
.pill.warn { color: var(--warn); background: var(--warn-bg); }
.pill.err  { color: var(--err);  background: var(--err-bg); }
.pill.idle { color: var(--idle); background: var(--bg); }

/* SILENT is the state this product exists for: it is the only one permitted to
   persist across sessions, and it must not be mistaken for FAILED sitting next
   to it. It fills instead of tinting — the same hue, a louder voice. Ink is
   --bg on --err: 6.50:1 dark, 6.03:1 light. */
.pill.silent {
    color: var(--err-ink);
    background: var(--err);
    border-color: var(--err);
}

/* ---------- Tags ---------------------------------------------------------
   Not a status. A tag states a fact about a row — its driver, its load mode,
   whether the SQL is hand-written — and those facts have no state colour
   because they are not states. Routing them through the six-state palette
   would render "raw SQL" in RUNNING violet and destroy the one thing colour is
   for on these screens.
   ------------------------------------------------------------------------- */

.tag {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    padding: 4px 9px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
    background: var(--raised);
    color: var(--text-2);
    font-family: var(--mono);
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: .10em;
    text-transform: uppercase;
    white-space: nowrap;
}
/* For a count that belongs beside a tag or pill — "3 FAILING". Mono already,
   so the figure lines up with the same figure in the table below it. */
.tag.count, .pill.count { letter-spacing: .04em; }

/* ---------- Buttons ----------------------------------------------------- */

.btn {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    padding: .45rem .85rem;
    border-radius: var(--radius);
    border: 1px solid var(--outline);
    background: var(--raised);
    color: var(--text);
    font-family: inherit;
    font-size: 13.5px;
    font-weight: 500;
    cursor: pointer;
    transition: background .12s, border-color .12s;
}
.btn:hover { background: var(--hover-strong); text-decoration: none; }
.btn:disabled { opacity: .45; cursor: not-allowed; }
.btn-primary {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-ink);
    font-weight: 600;
}
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
.btn-danger { color: var(--err); border-color: var(--err); background: var(--err-bg); }
.btn-danger:hover { background: var(--err-bg); border-color: var(--err); filter: none; }
.btn-sm { padding: .25rem .55rem; font-size: 13px; }
.btn-link {
    background: none;
    border: none;
    color: var(--accent);
    padding: .15rem .3rem;
    font-weight: 500;
}
.btn-link:hover { background: none; text-decoration: underline; }

/* ---------- Forms ------------------------------------------------------- */

label { display: block; font-weight: 600; font-size: 13.5px; margin-bottom: .3rem; color: var(--text-2); }
.hint { font-size: 13px; color: var(--text-2); margin-top: .25rem; font-weight: 400; }

input[type=text], input[type=password], input[type=email], input[type=number],
input[type=time], input[type=search], select, textarea {
    width: 100%;
    padding: .5rem .6rem;
    background: var(--field);
    /* --border is a divider at 1.4:1 and cannot carry an input boundary. */
    border: 1px solid var(--outline);
    border-radius: var(--radius);
    color: var(--text);
    font-family: inherit;
    font-size: 13.5px;
}
textarea { font-family: var(--mono); font-size: 13px; line-height: 1.45; resize: vertical; }
input:focus-visible, select:focus-visible, textarea:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
    border-color: var(--accent);
}
input[type=checkbox], input[type=radio] { accent-color: var(--accent); width: auto; }
select option { background: var(--field); color: var(--text); }

.field { margin-bottom: 1rem; }
.field-row { display: flex; gap: 1rem; flex-wrap: wrap; }
.field-row > .field { flex: 1; min-width: 170px; }
.inline-check { display: flex; align-items: center; gap: .45rem; font-weight: 400; }
.inline-check label { margin: 0; font-weight: 400; color: var(--text); }

fieldset {
    border: 1px solid var(--outline);
    border-radius: var(--radius);
    padding: .9rem 1rem;
    margin: 0 0 1rem;
}
legend {
    padding: 0 .4rem;
    font-weight: 600;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: .09em;
    color: var(--text-2);
}

/* ---------- Code / pre --------------------------------------------------- */

pre.code, .code-block {
    background: var(--field);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: .7rem .85rem;
    overflow-x: auto;
    font-family: var(--mono);
    font-size: 13px;
    line-height: 1.45;
    color: var(--text-2);
    margin: 0;
    white-space: pre;
}
.code-block .kw { color: var(--accent); }

.copy-row { display: flex; gap: .5rem; align-items: stretch; }
.copy-row .code-block { flex: 1; }

/* One-time secret reveal (pairing codes, tokens) */
.secret {
    background: var(--accent-quiet);
    border: 1px solid var(--accent);
    border-radius: var(--radius);
    padding: 1rem;
    text-align: center;
    margin: .75rem 0;
}
.secret .code {
    font-family: var(--mono);
    font-size: 26px;
    font-weight: 700;
    letter-spacing: .18em;
    color: var(--text);
    user-select: all;
}

/* ---------- Notices ----------------------------------------------------- */

.notice {
    border-radius: var(--radius);
    padding: .7rem .9rem;
    margin-bottom: 1rem;
    border: 1px solid var(--border);
    background: var(--surface);
    font-size: 13.5px;
}
.notice-error { border-color: var(--err);    background: var(--err-bg); }
.notice-warn  { border-color: var(--warn);   background: var(--warn-bg); }
.notice-ok    { border-color: var(--ok);     background: var(--ok-bg); }
.notice-info  { border-color: var(--accent); background: var(--accent-quiet); }
.notice strong { display: block; margin-bottom: .15rem; }
.notice a { color: var(--text); text-decoration: underline; }

/* ---------- Modal ------------------------------------------------------- */

.modal-backdrop {
    position: fixed; inset: 0;
    background: var(--scrim);
    display: none;
    align-items: flex-start;
    justify-content: center;
    padding: 4rem 1rem 1rem;
    z-index: 100;
    overflow-y: auto;
}
.modal-backdrop.open { display: flex; }
.modal {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    width: 100%;
    max-width: 620px;
    box-shadow: var(--shadow-lg);
    min-width: 0;              /* same reason as .split > * — it is a flex item here */
}

/* For content that is genuinely wide and cannot be usefully narrowed: sample rows
   from a table with 144 columns. Takes the viewport rather than a fixed width,
   because the useful width here is however much screen there is.

   The 620px default is right for a form and wrong for a data grid, which is the
   whole reason this variant exists rather than the default being widened. */
.modal-wide { max-width: min(1600px, calc(100vw - 2rem)); }

/* The body scrolls in both directions itself, so the backdrop never does. Without
   the explicit min-height:0 the flex child refuses to shrink and the modal grows
   taller than the viewport instead. */
.modal-wide .modal-body {
    padding: 0;
    overflow: auto;
    max-height: calc(100vh - 14rem);
    min-height: 0;
}
.modal-head {
    padding: .9rem 1.1rem;
    border-bottom: 1px solid var(--border);
    display: flex; justify-content: space-between; align-items: center;
}
.modal-head h2 { margin: 0; font-size: 17px; }
.modal-body { padding: 1.1rem; }
.modal-foot {
    padding: .8rem 1.1rem;
    border-top: 1px solid var(--border);
    display: flex; justify-content: flex-end; gap: .5rem;
}
.modal-close {
    background: none; border: none;
    color: var(--text-2);
    font-size: 20px;
    cursor: pointer;
    line-height: 1;
}

/* ---------- Toast ------------------------------------------------------- */

#toasts { position: fixed; right: 1rem; bottom: 1rem; z-index: 200; display: flex; flex-direction: column; gap: .5rem; }
.toast {
    background: var(--surface);
    border: 1px solid var(--border);
    border-left: 3px solid var(--border-strong);
    border-radius: var(--radius);
    padding: .6rem .9rem;
    font-size: 13.5px;
    max-width: 380px;
    box-shadow: var(--shadow-sm);
    animation: toast-in .16s ease-out;
}
.toast.err { border-left-color: var(--err); }
.toast.ok  { border-left-color: var(--ok); }
@keyframes toast-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

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

.login-shell {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1.5rem;
}
.login-card {
    width: 100%;
    max-width: 380px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: 1.75rem;
}
.login-lockup {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .6rem;
    margin-bottom: .5rem;
}
.login-lockup .wordmark { font-size: 22px; }
.login-card .tagline {
    text-align: center;
    color: var(--text-2);
    font-size: 13.5px;
    margin-bottom: 1.5rem;
}
.byline {
    text-align: center;
    color: var(--text-3);
    font-size: 11px;
    letter-spacing: .09em;
    text-transform: uppercase;
    margin-top: 1.25rem;
}
/* As a page footer it needs its own breathing room; inside the sign-in card the
   card's own padding already provides it. */
main + .byline { padding: 0 1.5rem 2rem; }

/* ---------- Utilities --------------------------------------------------- */

.muted   { color: var(--text-2); }
.dim     { color: var(--text-2); }
.faint   { color: var(--text-3); }
.tiny    { font-size: 11px; }
.small   { font-size: 13px; }
.right   { text-align: right; }
.center  { text-align: center; }
.nowrap  { white-space: nowrap; }
.mt0 { margin-top: 0; } .mt1 { margin-top: .5rem; } .mt2 { margin-top: 1rem; } .mt3 { margin-top: 1.5rem; }
.mb0 { margin-bottom: 0; } .mb1 { margin-bottom: .5rem; } .mb2 { margin-bottom: 1rem; }
.hidden { display: none !important; }
.flex { display: flex; gap: .5rem; align-items: center; }
.flex-between { display: flex; gap: .5rem; align-items: center; justify-content: space-between; }
.flex-wrap { flex-wrap: wrap; }
.spin { display: inline-block; animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

.truncate { max-width: 34ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Scrollable list used by the schema browser */
.list-scroll { max-height: 62vh; overflow-y: auto; }
.list-item {
    display: block;
    padding: .45rem .6rem;
    border-bottom: 1px solid var(--border);
    color: var(--text);
    cursor: pointer;
    font-size: 13.5px;
}
.list-item:hover { background: var(--hover); text-decoration: none; }
.list-item.active { background: var(--accent-quiet); border-left: 2px solid var(--accent); }
.list-item .meta { color: var(--text-2); font-size: 11px; }

/* =============================================================================
   Client scope
   =============================================================================
   The app has two altitudes: the client list, and the inside of one client.
   Everything below exists to make which of the two you are looking at
   unmistakable, because the pages themselves are the same tables either way.

   The load-bearing decision: scope is carried in the URL, never in the session.
   A stored "current client" is how somebody runs a job against the wrong one,
   and it makes two clients side by side in two tabs impossible. So the crumb is
   a statement of what the URL says, not of remembered state.
   -------------------------------------------------------------------------- */

/* Client name in the masthead, immediately after the wordmark.

   The separator is a real element rather than a ::before on the name so it can
   be dimmed independently — at --text-3 it reads as punctuation, and the eye
   goes to the client name instead of stopping on the chevron. */
.crumb {
    display: flex;
    align-items: center;
    gap: .55rem;
    min-width: 0;              /* lets the name truncate instead of pushing nav */
    flex-shrink: 1;
}
.crumb-sep {
    color: var(--text-3);
    font-size: 15px;
    line-height: 1;
    flex-shrink: 0;
    user-select: none;
}

/* The switcher is a <select> wearing the crumb's clothes.

   Deliberately a native select: it is the one control that already works with a
   keyboard, a screen reader and a touch keyboard on every platform, and a
   hand-rolled dropdown here would be a bespoke focus-trap for no gain. The
   arrow is drawn by the browser; padding-right leaves room for it. */
.tenant-switch {
    appearance: none;
    -webkit-appearance: none;
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--radius);
    color: var(--text);
    font-family: inherit;
    font-size: 14.5px;
    font-weight: 600;
    letter-spacing: -.005em;
    padding: .3rem 1.5rem .3rem .45rem;
    margin-left: -.45rem;      /* optically flush with the wordmark above it */
    max-width: 26ch;
    text-overflow: ellipsis;
    cursor: pointer;
}
.tenant-switch:hover { background: var(--hover); border-color: var(--border); }
.tenant-switch:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }

/* Native selects inherit neither the page ground nor its ink in the popup on
   every platform, so the options are pinned explicitly. Without this the list
   is white-on-white in dark mode on Linux/Chromium. */
.tenant-switch option { background: var(--surface); color: var(--text); font-weight: 400; }

/* Both chrome bars stick as one unit.

   The obvious alternative — leave .masthead sticky at top:0 and give .subnav
   top:<masthead height> — needs a magic number that is wrong the moment the
   masthead wraps to two lines on a narrow window, and it silently slides the
   subnav under the masthead when it is. Wrapping them makes the browser do the
   measuring. The two inner bars are reset to static because sticky nested
   inside sticky is not reliable across engines. */
.chrome { position: sticky; top: 0; z-index: 40; }
.chrome > .masthead,
.chrome > .subnav { position: static; }

/* Second-level nav, shown only inside a client. It sits below the masthead
   rather than in it: two rows of links in one bar reads as one nav with a
   styling accident, and the whole point is that these six links are scoped to
   the client named above while the ones in the masthead are not. */
.subnav {
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    padding: 0 1.5rem;
    display: flex;
    gap: .25rem;
    flex-wrap: wrap;
}
.subnav a {
    padding: .5rem .7rem;
    color: var(--text-2);
    font-size: 13px;
    font-weight: 500;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    white-space: nowrap;
}
.subnav a:hover { color: var(--text); text-decoration: none; }
.subnav a.active { color: var(--accent); border-bottom-color: var(--accent); }
.subnav .nav-badge { margin-left: .3rem; }

/* On a narrow screen two stacked bars eat too much of a short viewport, so the
   chrome stops sticking altogether rather than pinning half the screen. */
@media (max-width: 720px) {
    .chrome { position: static; }
}

/* ---------- Client list -------------------------------------------------- */

.client-grid { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); }

/* A card, not a table row. A client is the one object in this app somebody
   arrives wanting to know the state of, and a row of eight cells does not let
   the health pill and the "what is unfinished" line both be the first thing
   seen. The whole card is the link; nothing else in it is focusable, so there
   is no nested-interactive problem. */
.client-card {
    display: flex;
    flex-direction: column;
    gap: .7rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-left: 3px solid var(--border-strong);
    border-radius: var(--radius-lg);
    padding: 1rem 1.1rem;
    color: var(--text);
}
.client-card:hover {
    text-decoration: none;
    border-color: var(--outline);
    background: var(--hover-strong);
}
.client-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The left rule carries the worst state, so a column of clients can be triaged
   without reading any of them. Same five states as the pills, same meanings. */
.client-card.s-ok     { border-left-color: var(--ok); }
.client-card.s-warn   { border-left-color: var(--warn); }
.client-card.s-err    { border-left-color: var(--err); }
.client-card.s-silent { border-left-color: var(--err); }
.client-card.s-idle   { border-left-color: var(--idle); }

.client-card__head { display: flex; align-items: flex-start; justify-content: space-between; gap: .6rem; }
.client-card__name { font-size: 16px; font-weight: 600; line-height: 1.3; }
.client-card__slug { font-family: var(--mono); font-size: 11px; color: var(--text-3); margin-top: .15rem; }

/* Counts. Inline rather than a grid of stat tiles: at four numbers per card and
   several cards, tiles turn the page into a wall of boxes and the client names
   stop being the structure. */
.client-card__counts { display: flex; flex-wrap: wrap; gap: .15rem .9rem; font-size: 13px; color: var(--text-2); }
.client-card__counts b { font-family: var(--mono); font-weight: 500; color: var(--text); }

.client-card__foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
    border-top: 1px solid var(--border);
    padding-top: .6rem;
    font-size: 12.5px;
    color: var(--text-2);
}
.client-card__go { color: var(--accent); font-weight: 500; white-space: nowrap; }

/* ---------- Setup checklist --------------------------------------------- */

/* An ordered list of what is still unfinished for a client, shown on the hub
   and summarised on the card. It is not styled as an error: a half-finished
   install is the expected state of a client added ten minutes ago, and
   colouring it red teaches operators to ignore red. */
.steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .5rem; }
.steps li {
    display: flex;
    align-items: flex-start;
    gap: .6rem;
    font-size: 13.5px;
    line-height: 1.45;
}
.steps .step-n {
    flex-shrink: 0;
    width: 1.35rem;
    height: 1.35rem;
    border-radius: 50%;
    border: 1px solid var(--outline);
    color: var(--text-2);
    font-family: var(--mono);
    font-size: 11px;
    display: grid;
    place-items: center;
    margin-top: .05rem;
}
.steps li.done .step-n { border-color: var(--ok); color: var(--ok); }
.steps li.done { color: var(--text-2); }
.steps .step-what { font-weight: 500; }
.steps .step-why { color: var(--text-2); font-size: 12.5px; }

/* ---------- Key/value block -------------------------------------------- */

/* Used on the client hub for facts that are neither a state nor a count. A
   two-column grid rather than a table: these are label/value pairs, and a
   <table> would promise a relationship between rows that does not exist. */
.kv { display: grid; grid-template-columns: minmax(7rem, auto) 1fr; gap: .45rem .9rem; font-size: 13.5px; align-items: baseline; }
.kv dt { color: var(--text-2); }
.kv dd { margin: 0; color: var(--text); min-width: 0; overflow-wrap: anywhere; }

/* =============================================================================
   Client portal
   =============================================================================
   Reuses the operator stylesheet on purpose — one visual system, and a client seeing a
   "silent" pill should see exactly what an operator sees. Only the pieces the portal adds
   live here.
   -------------------------------------------------------------------------- */

/* The impersonation banner.
   Deliberately loud and at the very top of the document, above the sticky chrome, on every
   page. An operator who forgets they are impersonating is the whole risk this guards
   against, so it does not tuck itself away, does not scroll off, and names the client. */
.impersonation-bar {
    background: var(--warn);
    color: var(--bg);
    padding: .6rem 1.5rem;
    font-size: 13.5px;
    display: flex;
    gap: .6rem;
    align-items: center;
    flex-wrap: wrap;
    position: relative;
    z-index: 60;
}
.impersonation-bar strong { font-weight: 700; }
.impersonation-bar a {
    color: var(--bg);
    text-decoration: underline;
    margin-left: auto;
    white-space: nowrap;
    font-weight: 600;
}

/* The client's own name in the portal masthead. Not a switcher — a client has exactly one
   client, so anything that looked like a control would be a lie. */
.client-name { font-size: 14.5px; font-weight: 600; letter-spacing: -.005em; }

/* A batch's totals, so a collapsed group of 200 tables still answers "is this healthy and how
   big is it". Same shape as .client-card__counts — a row of label/figure pairs — but a group
   header is not a client card, and naming it after one would mislead the next reader. */
.rollup { display: flex; flex-wrap: wrap; gap: .15rem .9rem; margin-top: .35rem;
          font-size: 13px; color: var(--text-2); }
.rollup b { font-family: var(--mono); font-weight: 500; color: var(--text); }
.rollup .tiny { margin-left: .25rem; }
