/* ==========================================================================
   Hosting control panel — design system
   --------------------------------------------------------------------------
   Hand-written, no build step. The panel must be deployable and patchable on
   a server with no Node toolchain, so there is no Tailwind, no PostCSS and no
   compiled asset: this file is what the browser gets.

   That constraint is why it is organised as tokens first, then primitives,
   then components. Anything that would otherwise be a utility class repeated
   across forty Blade files is a token here instead, so changing the shape of
   the product is one edit rather than forty.

   Contents
     1. Tokens          colour, type, space, elevation, motion
     2. Reset & base
     3. Layout          shell, sidebar, topbar, content
     4. Typography      headings, prose, code
     5. Cards & panels
     6. Tables
     7. Forms
     8. Buttons
     9. Badges & pills
    10. Meters & progress
    11. Alerts & banners
    12. Empty states
    13. Tabs, breadcrumbs, pagination
    14. Utilities
    15. Responsive
    16. Print & accessibility
   ========================================================================== */

/* ==========================================================================
   1. Tokens
   ========================================================================== */

:root {
    /* --- Palette ------------------------------------------------------
       Two ramps rather than named colours: a neutral for structure and a
       single accent for action. Semantic colours (success/warn/danger) are
       reserved for state and are never used decoratively, so that a red
       thing on screen always means something is wrong. */

    --grey-25:  #fbfcfd;
    --grey-50:  #f6f8fa;
    --grey-100: #eef1f5;
    --grey-200: #e1e6ed;
    --grey-300: #cbd3de;
    --grey-400: #9aa6b6;
    --grey-500: #6b7889;
    --grey-600: #4d5866;
    --grey-700: #38414d;
    --grey-800: #232b35;
    --grey-900: #161c24;
    --grey-950: #0e1319;

    --blue-300: #8cc2ff;
    --blue-400: #5aa6ff;
    --blue-500: #2f86f0;
    --blue-600: #1c68c7;
    --blue-700: #17529c;

    --green-400: #3fb950;
    --green-500: #2da44e;
    --amber-400: #d29922;
    --amber-500: #bf8700;
    --red-400:   #f85149;
    --red-500:   #cf222e;
    --violet-400: #a371f7;
    --violet-500: #8957e5;
    --violet-600: #6f42c1;

    /* --- Semantic surfaces (light is the default) --------------------- */

    --bg:          #f2f4f8;
    --surface:     #ffffff;
    --surface-2:   var(--grey-50);
    --surface-3:   var(--grey-100);
    --surface-inset: var(--grey-100);

    --border:        var(--grey-200);
    --border-strong: var(--grey-300);

    --text:        var(--grey-900);
    --text-2:      var(--grey-600);
    --muted:       var(--grey-500);
    --text-invert: #ffffff;

    --accent:        var(--violet-600);
    --accent-hover:  #5a34a0;
    --accent-subtle: rgba(111, 66, 193, .10);
    --accent-border: rgba(111, 66, 193, .32);
    --on-accent:     #ffffff;

    --success: var(--green-500);
    --warning: var(--amber-500);
    --danger:  var(--red-500);
    --info:    var(--blue-600);

    --success-subtle: rgba(45, 164, 78, .12);
    --warning-subtle: rgba(191, 135, 0, .13);
    --danger-subtle:  rgba(207, 34, 46, .10);
    --info-subtle:    rgba(47, 134, 240, .10);

    /* The same four states, at a weight that can be *read*.
       --------------------------------------------------------------------
       A green that is right for a 3px meter fill is not right for 11px type:
       on white, --success comes out at 3.2:1 and --warning at 3.1:1, both
       under the 4.5:1 that small text needs, and the panel says a status must
       be legible before it is decorative. Graphics keep the brighter colour
       (3:1 is the bar there); words take these.

       In the dark theme the bright ones already clear 5:1 on both surfaces, so
       the two sets converge and these are aliases. Only the light theme has
       the problem, and only the light theme is corrected. */
    --success-text: #1a7f37;
    --warning-text: #9a6700;
    --danger-text:  var(--danger);
    --info-text:    var(--info);

    /* --- Type ---------------------------------------------------------
       A 1.2 scale. Control panels are dense; anything larger wastes the
       screen a table needs. */

    --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
                 "Helvetica Neue", Arial, sans-serif;
    --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
                 "Liberation Mono", monospace;

    /* Tightened for an operations console.
       The previous scale was set for reading; this one is set for scanning —
       an operator watching a fleet wants rows on screen, not comfortable
       paragraphs. Everything below steps down roughly one notch, which fits
       about half again as much on a 1080p screen without shrinking the hit
       targets that matter. */
    --text-xs:   10.5px;
    --text-sm:   11.5px;
    --text-base: 12.5px;
    --text-md:   13.5px;
    --text-lg:   15px;
    --text-xl:   17px;
    --text-2xl:  20px;
    --text-3xl:  25px;

    --leading-tight: 1.25;
    --leading:       1.45;

    --weight-normal: 400;
    --weight-medium: 500;
    --weight-semi:   600;
    --weight-bold:   700;

    /* --- Space (4px base) --------------------------------------------- */

    --space-1: 4px;
    --space-2: 8px;
    --space-3: 12px;
    --space-4: 16px;
    --space-5: 20px;
    --space-6: 24px;
    --space-8: 32px;
    --space-10: 40px;
    --space-12: 48px;
    --space-16: 64px;

    /* --- Shape & depth ------------------------------------------------- */

    --radius-sm: 4px;
    --radius:    7px;
    --radius-md: 10px;
    --radius-lg: 14px;
    --radius-full: 999px;

    --shadow-xs: 0 1px 2px rgba(16, 24, 40, .05);
    --shadow-sm: 0 1px 3px rgba(16, 24, 40, .08), 0 1px 2px rgba(16, 24, 40, .04);
    --shadow-md: 0 4px 12px rgba(16, 24, 40, .08), 0 2px 4px rgba(16, 24, 40, .04);
    --shadow-lg: 0 12px 32px rgba(16, 24, 40, .12), 0 4px 8px rgba(16, 24, 40, .04);

    --ring: 0 0 0 3px var(--accent-subtle);

    /* --- Motion -------------------------------------------------------- */

    --ease: cubic-bezier(.4, 0, .2, 1);
    --fast: 120ms;
    --normal: 200ms;

    /* --- The navigation rail -------------------------------------------
       Dark in both themes, on purpose, and therefore declared once here
       rather than three times in the theme blocks.

       The rail is chrome, not content. Keeping it a fixed dark surface
       means the eye learns one edge of the screen as "where I am" and the
       rest as "what I am looking at" — and it stops a light-theme page
       being a single flat field of white from edge to edge, which is most
       of what made the old shell hard to read. */

    --rail:          #10151c;
    --rail-2:        #171e27;
    --rail-border:   #222c38;
    --rail-text:     #b9c5d3;
    --rail-text-dim: #7b8798;
    --rail-active:   #ffffff;
    --rail-accent:   var(--violet-400);

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

    --sidebar-w: 208px;
    --sidebar-w-collapsed: 52px;
    --topbar-h: 56px;
    --content-max: 1400px;
}

/* Dark theme.
   --------------------------------------------------------------------------
   Follows the system by default; `data-theme` on <html> overrides it in both
   directions, so an explicit user preference always wins over the OS.

   The palette is written out twice — once under the media query and once
   under the attribute — because CSS has no way to say "apply this block if
   either selector matches" without a preprocessor, and there is no
   preprocessor here. The two blocks must be kept identical. */

:root { color-scheme: light; }

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) { color-scheme: dark; }
}

:root[data-theme="dark"] { color-scheme: dark; }
:root[data-theme="light"] { color-scheme: light; }

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --bg:          var(--grey-950);
        --surface:     var(--grey-900);
        --surface-2:   var(--grey-800);
        --surface-3:   #2b333e;
        --surface-inset: #0a0e13;

        --border:        #29323d;
        --border-strong: #3b4655;

        --text:        #e6edf3;
        --text-2:      #b3c0ce;
        --muted:       #8b97a6;
        --text-invert: var(--grey-900);

        --accent:        var(--violet-400);
        --accent-hover:  #bd97ff;
        --accent-subtle: rgba(163, 113, 247, .16);
        --accent-border: rgba(163, 113, 247, .42);
        --on-accent:     #0d0716;

        --success: var(--green-400);
        --warning: var(--amber-400);
        --danger:  var(--red-400);
        --info:    var(--blue-400);

        --success-subtle: rgba(63, 185, 80, .14);
        --warning-subtle: rgba(210, 153, 34, .14);
        --danger-subtle:  rgba(248, 81, 73, .13);
        --info-subtle:    rgba(90, 166, 255, .13);

        /* Bright enough to read on a dark surface, so text and graphics agree.
           See the light-theme block for why the two sets exist at all. */
        --success-text: var(--success);
        --warning-text: var(--warning);
        --danger-text:  var(--danger);
        --info-text:    var(--info);

        --shadow-xs: 0 1px 2px rgba(0, 0, 0, .4);
        --shadow-sm: 0 1px 3px rgba(0, 0, 0, .45), 0 1px 2px rgba(0, 0, 0, .3);
        --shadow-md: 0 4px 12px rgba(0, 0, 0, .45), 0 2px 4px rgba(0, 0, 0, .3);
        --shadow-lg: 0 12px 32px rgba(0, 0, 0, .55), 0 4px 8px rgba(0, 0, 0, .35);
    }
}

:root[data-theme="dark"] {
    --bg:          var(--grey-950);
    --surface:     var(--grey-900);
    --surface-2:   var(--grey-800);
    --surface-3:   #2b333e;
    --surface-inset: #0a0e13;

    --border:        #29323d;
    --border-strong: #3b4655;

    --text:        #e6edf3;
    --text-2:      #b3c0ce;
    --muted:       #8b97a6;
    --text-invert: var(--grey-900);

    --accent:        var(--violet-400);
    --accent-hover:  #bd97ff;
    --accent-subtle: rgba(163, 113, 247, .16);
    --accent-border: rgba(163, 113, 247, .42);
    --on-accent:     #0d0716;

    --success: var(--green-400);
    --warning: var(--amber-400);
    --danger:  var(--red-400);
    --info:    var(--blue-400);

    --success-subtle: rgba(63, 185, 80, .14);
    --warning-subtle: rgba(210, 153, 34, .14);
    --danger-subtle:  rgba(248, 81, 73, .13);
    --info-subtle:    rgba(90, 166, 255, .13);

    /* Bright enough to read on a dark surface, so text and graphics agree.
       See the light-theme block for why the two sets exist at all. */
    --success-text: var(--success);
    --warning-text: var(--warning);
    --danger-text:  var(--danger);
    --info-text:    var(--info);

    --shadow-xs: 0 1px 2px rgba(0, 0, 0, .4);
    --shadow-sm: 0 1px 3px rgba(0, 0, 0, .45), 0 1px 2px rgba(0, 0, 0, .3);
    --shadow-md: 0 4px 12px rgba(0, 0, 0, .45), 0 2px 4px rgba(0, 0, 0, .3);
    --shadow-lg: 0 12px 32px rgba(0, 0, 0, .55), 0 4px 8px rgba(0, 0, 0, .35);
}

/* ==========================================================================
   2. Reset & base
   ========================================================================== */

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font-family: var(--font-sans);
    font-size: var(--text-base);
    line-height: var(--leading);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

h1, h2, h3, h4, h5, h6 { margin: 0; font-weight: var(--weight-semi); line-height: var(--leading-tight); }
p { margin: 0 0 var(--space-3); }
p:last-child { margin-bottom: 0; }

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

hr { border: 0; border-top: 1px solid var(--border); margin: var(--space-5) 0; }

img, svg { vertical-align: middle; max-width: 100%; }

/* One focus treatment everywhere, and only for keyboard users. A control
   panel is operated by people who live in it; losing the focus ring to a
   `outline: none` somewhere is a real accessibility regression. */
:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}
:focus:not(:focus-visible) { outline: none; }

::selection { background: var(--accent-subtle); }

/* ==========================================================================
   3. Layout
   ========================================================================== */

.shell { display: flex; min-height: 100vh; }

/* --- Navigation rail ---------------------------------------------------- */

.sidebar {
    width: var(--sidebar-w);
    flex: 0 0 var(--sidebar-w);
    background: var(--rail);
    color: var(--rail-text);
    border-right: 1px solid var(--rail-border);
    display: flex;
    flex-direction: column;
    position: sticky;
    top: 0;
    height: 100vh;
    overflow-y: auto;
    overscroll-behavior: contain;
    /* The rail scrolls independently and often has more items than fit; a
       visible scrollbar in the chrome is noise, so it is thinned rather than
       hidden — hidden would strand a keyboard user with no indication. */
    scrollbar-width: thin;
    scrollbar-color: var(--rail-border) transparent;
}

.brand {
    display: flex; align-items: center; gap: var(--space-3);
    padding: var(--space-5);
    border-bottom: 1px solid var(--rail-border);
    color: var(--rail-active);
}
.brand.centred { justify-content: center; border-bottom: 0; }

.brand-mark {
    display: grid; place-items: center;
    width: 30px; height: 30px; flex: 0 0 30px;
    border-radius: var(--radius);
    background: linear-gradient(135deg, var(--violet-500), var(--violet-600));
    color: #fff;
    font-size: var(--text-sm);
    font-weight: var(--weight-bold);
    letter-spacing: -.02em;
}
.brand-name {
    font-weight: var(--weight-semi);
    font-size: var(--text-md);
    letter-spacing: -.01em;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.nav-section {
    padding: var(--space-5) var(--space-5) var(--space-2);
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: .08em;
    font-weight: var(--weight-semi);
    color: var(--rail-text-dim);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.sidebar nav { display: flex; flex-direction: column; padding: 0 var(--space-3); gap: 2px; }

.sidebar nav a {
    display: flex; align-items: center; gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius);
    color: var(--rail-text);
    font-size: var(--text-base);
    font-weight: var(--weight-medium);
    text-decoration: none;
    position: relative;
    transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.sidebar nav a:hover { background: var(--rail-2); color: var(--rail-active); text-decoration: none; }

.sidebar nav a.active {
    background: var(--rail-2);
    color: var(--rail-active);
}
/* A bar rather than a fill: the active item should be findable from the
   corner of the eye without the whole row shouting. */
.sidebar nav a.active::before {
    content: "";
    position: absolute;
    left: calc(var(--space-3) * -1 + 2px);
    top: 20%; bottom: 20%;
    width: 3px;
    border-radius: var(--radius-full);
    background: var(--rail-accent);
}

.sidebar nav a .icon { flex: 0 0 16px; opacity: .7; }
.sidebar nav a:hover .icon,
.sidebar nav a.active .icon { opacity: 1; }

.nav-count {
    margin-left: auto;
    min-width: 20px;
    padding: 0 6px;
    border-radius: var(--radius-full);
    background: var(--rail-2);
    color: var(--rail-text-dim);
    font-size: var(--text-xs);
    font-weight: var(--weight-semi);
    text-align: center;
    font-variant-numeric: tabular-nums;
}
.sidebar nav a.active .nav-count { background: var(--rail-accent); color: #150c22; }
.nav-count.attention { background: var(--red-500); color: #fff; }

/* Show whichever icon represents the theme you would switch *to*.
   These lived in a <style> block inside the toggle component; the CSP refuses
   inline styles, so both icons rendered at once. */
.theme-icon-dark { display: none; }
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .theme-icon-dark { display: inline; }
    :root:not([data-theme="light"]) .theme-icon-light { display: none; }
}
:root[data-theme="dark"] .theme-icon-dark { display: inline; }
:root[data-theme="dark"] .theme-icon-light { display: none; }
:root[data-theme="light"] .theme-icon-dark { display: none; }
:root[data-theme="light"] .theme-icon-light { display: inline; }

/* Role picker: a grid of checkboxes wide enough to read the services each one
   grants, because that is the part that decides the choice. */
.role-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: var(--space-3);
    margin: var(--space-4) 0;
}
.role-option {
    display: flex; align-items: flex-start; gap: var(--space-3);
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    cursor: pointer;
}
.role-option:hover { border-color: var(--border-strong); background: var(--surface-2); }
.role-option input { margin-top: 2px; }
.role-option .role-name { display: block; font-weight: var(--weight-medium); }

/* A row of actions, each its own form because each is a POST. */
.action-bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); }
.action-bar form { display: contents; }
/* Pushed to the far end, away from the routine actions it sits beside. */
.action-bar .action-bar-end button { margin-left: auto; }

/* Buttons inside a stat tile: the tile is a fixed-height card, so they sit on
   one row and wrap rather than stretching it. */
.stat-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: var(--space-2) 0 var(--space-1); }
.stat-actions form { display: contents; }

.sidebar-footer {
    margin-top: auto;
    padding: var(--space-4) var(--space-5);
    border-top: 1px solid var(--rail-border);
    display: flex; flex-direction: column; gap: var(--space-3);
}
.sidebar-footer a { color: var(--rail-text); }
.sidebar-footer a:hover { color: var(--rail-active); }
.sidebar-footer .muted { color: var(--rail-text-dim); }
.sidebar-footer .link-button { color: var(--rail-text-dim); }
.sidebar-footer .link-button:hover { color: var(--rail-active); }

/* The rail is dark in both themes, but a plain button inherits the *content
   area's* text colour — near-black in light mode, which put a black icon on a
   black rail and made the theme switch look like it was not there at all. The
   topbar already carried this override; the rail needs its own. */
.sidebar-footer .button-ghost,
.sidebar-footer .button-ghost:hover { color: var(--rail-text); border-color: transparent; }
.sidebar-footer .button-ghost:hover { background: var(--rail-2); color: var(--rail-active); }

.rail-version {
    font-size: var(--text-xs);
    color: var(--rail-text-dim);
    display: flex; align-items: center; justify-content: space-between; gap: var(--space-2);
}
.rail-version a { color: var(--rail-text-dim); }
.rail-version a:hover { color: var(--rail-active); text-decoration: none; }

.sidebar-user { display: flex; align-items: center; gap: var(--space-3); min-width: 0; }
.sidebar-user .stack { min-width: 0; }
.sidebar-user .name {
    font-weight: var(--weight-medium);
    color: var(--rail-active);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.avatar {
    display: grid; place-items: center;
    width: 30px; height: 30px; flex: 0 0 30px;
    border-radius: var(--radius-full);
    background: var(--accent-subtle);
    color: var(--accent);
    font-size: var(--text-xs);
    font-weight: var(--weight-semi);
    text-transform: uppercase;
}
.sidebar .avatar { background: var(--rail-2); color: var(--rail-text); }
.avatar-lg { width: 40px; height: 40px; flex-basis: 40px; font-size: var(--text-base); }

/* --- Content ------------------------------------------------------------ */

.content {
    flex: 1;
    min-width: 0;
    padding: var(--space-8) var(--space-8) var(--space-16);
    max-width: var(--content-max);
    width: 100%;
}

.page-header {
    display: flex; align-items: flex-start; justify-content: space-between;
    gap: var(--space-4);
    margin-bottom: var(--space-6);
    flex-wrap: wrap;
}
.page-header h1 {
    font-size: var(--text-2xl);
    letter-spacing: -.025em;
}
.page-header .subtitle { color: var(--muted); font-size: var(--text-md); margin-top: var(--space-1); }
.page-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; align-items: center; }

/* Mobile-only header bar that reveals the rail. */
.topbar { display: none; }

/* ==========================================================================
   4. Typography
   ========================================================================== */

.section-title {
    font-size: var(--text-lg); font-weight: var(--weight-semi);
    letter-spacing: -.01em; margin: 0 0 var(--space-4);
}

code, pre, .mono {
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    font-variant-ligatures: none;
}

code:not(pre code) {
    background: var(--surface-inset);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 1px 5px;
    color: var(--text-2);
}

.code-block {
    background: var(--surface-inset);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: var(--space-3) var(--space-4);
    overflow-x: auto;
    white-space: pre;
    margin: 0;
    line-height: 1.6;
    color: var(--text-2);
}

/* A value the user is meant to copy — a token, a fingerprint, a DNS record. */
.copyable {
    display: flex; align-items: center; gap: var(--space-2);
    background: var(--surface-inset);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: var(--space-2) var(--space-3);
    font-family: var(--font-mono); font-size: var(--text-sm);
    word-break: break-all;
}
.copyable > span { flex: 1; min-width: 0; }

/* ==========================================================================
   5. Cards & panels
   ========================================================================== */

.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    padding: var(--space-5);
    margin-bottom: var(--space-5);
    box-shadow: var(--shadow-xs);
}
.card:last-child { margin-bottom: 0; }

.card > h2 {
    font-size: var(--text-md);
    font-weight: var(--weight-semi);
    margin: 0 0 var(--space-4);
    display: flex; align-items: center; gap: var(--space-2);
}
.card > h2 + .hint { margin-top: calc(var(--space-4) * -1 + var(--space-1)); margin-bottom: var(--space-4); }

.card-header {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--space-3); margin: 0 0 var(--space-4); flex-wrap: wrap;
}
.card-header h2 { margin: 0; }

/* A card whose body is a table: the table should meet the card's edges. */
.card.flush { padding: 0; overflow: hidden; }
.card.flush .card-header { padding: var(--space-4) var(--space-5); margin: 0; border-bottom: 1px solid var(--border); }
.card.flush table { margin: 0; }
.card.flush th:first-child, .card.flush td:first-child { padding-left: var(--space-5); }
.card.flush th:last-child,  .card.flush td:last-child  { padding-right: var(--space-5); }
.card.flush .card-footer { padding: var(--space-3) var(--space-5); border-top: 1px solid var(--border); background: var(--surface-2); }

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

/* --- Stat tiles --------------------------------------------------------- */

.stat {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    padding: var(--space-4) var(--space-5);
    box-shadow: var(--shadow-xs);
    display: flex; flex-direction: column; gap: var(--space-1);
}
.stat .label {
    color: var(--muted); font-size: var(--text-xs);
    text-transform: uppercase; letter-spacing: .06em;
    font-weight: var(--weight-semi);
    display: flex; align-items: center; gap: var(--space-2);
}
.stat .value {
    font-size: var(--text-3xl); font-weight: var(--weight-semi);
    letter-spacing: -.03em; line-height: 1.1;
    font-variant-numeric: tabular-nums;
}
.stat .value small { font-size: var(--text-md); color: var(--muted); font-weight: var(--weight-normal); }
.stat .delta { font-size: var(--text-sm); color: var(--muted); }
.stat .delta.up { color: var(--success); }
.stat .delta.down { color: var(--danger); }

/* A stat that is itself a link to the thing it counts. */
a.stat { color: inherit; }
a.stat:hover { text-decoration: none; border-color: var(--border-strong); box-shadow: var(--shadow-sm); }

/* --- Definition lists (resource detail pages) --------------------------- */

.detail-list { display: grid; grid-template-columns: minmax(140px, 200px) 1fr; gap: var(--space-3) var(--space-4); margin: 0; }
.detail-list dt { color: var(--muted); font-size: var(--text-sm); }
.detail-list dd { margin: 0; min-width: 0; word-break: break-word; }

/* A postal address inside a definition list: one line per line, set tighter
   than body text so the block reads as an envelope rather than a paragraph
   that happens to have been broken up. */
.postal-address { line-height: 1.4; }

/* ==========================================================================
   6. Tables
   ========================================================================== */

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

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

th, td {
    text-align: left;
    padding: var(--space-3) var(--space-3);
    border-bottom: 1px solid var(--border);
    vertical-align: middle;
}

thead th {
    color: var(--muted);
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: .06em;
    font-weight: var(--weight-semi);
    background: var(--surface-2);
    white-space: nowrap;
    position: sticky; top: 0; z-index: 1;
}

tbody tr { transition: background var(--fast) var(--ease); }
tbody tr:hover { background: var(--surface-2); }
tbody tr:last-child td { border-bottom: 0; }

td.numeric, th.numeric { text-align: right; font-variant-numeric: tabular-nums; }
td.actions { text-align: right; white-space: nowrap; }

/* The first column of a list is the thing's name: give it weight. */
.table-primary { font-weight: var(--weight-medium); color: var(--text); }
.table-secondary { color: var(--muted); font-size: var(--text-sm); }

.table-empty { color: var(--muted); padding: var(--space-6) var(--space-3); text-align: center; }

/* An action column that has a sentence instead of a button: "no installer",
 * "add an SSH credential first". It is the answer to the same question the
 * button would have answered, so it sits on the row rather than dropping the
 * top margin a hint under a form wants — and it is bounded, so one long
 * explanation cannot squeeze the columns that carry the facts.
 */
td.right .hint { margin: 0; display: inline-block; max-width: 24ch; text-align: right; }

/* --- Fleet services -----------------------------------------------------
 *
 * One <tbody> per service, with the service named once in a cell spanning the
 * machines it was found on. The grouping is the whole point of that page — the
 * question is "is my nginx the same nginx everywhere?", and a flat table sorted
 * by server answers a different one — so the groups need to read as blocks
 * rather than as an undifferentiated run of rows.
 *
 * The generic rule above drops the border under the last row of every tbody,
 * which would erase the line between two groups. The separator is put back on
 * the *first* row of each subsequent group instead, so it belongs to the join
 * rather than to whichever row happens to fall last.
 */
.service-table tbody + tbody > tr:first-child > td { border-top: 1px solid var(--border-strong); }

.service-table td.service-cell {
    vertical-align: top;
    padding-top: var(--space-4);
    background: var(--surface-2);
    min-width: 15rem;
}

/* Attention is drawn down the left edge rather than by colouring the row.
 * Half of these groups are "fine" and the other half want looking at; tinting
 * the backgrounds would make the table look like an error report when most of
 * what it shows is an estate working as intended. */
.service-table tbody.needs-attention td.service-cell { box-shadow: inset 3px 0 0 var(--warning); }

.service-table .service-flags {
    display: flex; flex-wrap: wrap; align-items: center;
    gap: var(--space-1) var(--space-2);
    margin-top: var(--space-1);
}

/* The name cell must not take the row hover with it: it spans several rows, so
 * highlighting it would suggest the pointer is over all of them. */
.service-table tbody tr:hover td.service-cell { background: var(--surface-2); }

/* Running / stopped / absent / unknown, one value per service rather than per
 * server, so they span the group exactly as the name does.
 *
 * Narrow and fixed: they are single digits on most estates, and letting them
 * take their share of a nine-column table would squeeze the server names and
 * the "what to do" column that are the reason anybody scrolls this far. */
.service-table th.runtime-col,
.service-table td.runtime-count {
    width: 5.5rem;
    white-space: nowrap;
}

.service-table td.runtime-count {
    vertical-align: top;
    padding-top: var(--space-4);
    background: var(--surface-2);
    font-variant-numeric: tabular-nums;
    font-size: var(--text-lg);
    font-weight: var(--weight-medium);
}

.service-table tbody tr:hover td.runtime-count { background: var(--surface-2); }

/* Only stopped is coloured as a fault. Absent and unknown are answers rather
 * than failures — an appliance that was never containerised has no docker unit,
 * and a unit no role names has never been read — and painting either of them
 * red is how an operator learns that the red on this page means nothing. */
.service-table td.runtime-count.runtime-running { color: var(--success); }
.service-table td.runtime-count.runtime-stopped { color: var(--danger); }
.service-table td.runtime-count.runtime-absent  { color: var(--muted); }

/* Unknown is muted but not invisible, and carries a dotted underline so it
 * reads as a caveat rather than as a tidy zero. It is the one number on this
 * page that means the panel does not know. */
.service-table td.runtime-count.runtime-unknown {
    color: var(--warning);
    text-decoration: underline dotted;
    text-underline-offset: 0.25em;
}

/* A genuine zero in any of the four is just arithmetic, so it recedes. */
.service-table td.runtime-count.is-zero {
    color: var(--muted);
    font-weight: var(--weight-normal);
    opacity: 0.45;
    text-decoration: none;
}

/* ==========================================================================
   7. Forms
   ========================================================================== */

.field { margin-bottom: var(--space-4); }
.field:last-child { margin-bottom: 0; }

label {
    display: block;
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    margin-bottom: var(--space-2);
    color: var(--text);
}

label .optional { color: var(--muted); font-weight: var(--weight-normal); }

input[type=text], input[type=email], input[type=password], input[type=number],
input[type=url], input[type=search], input[type=tel], input[type=date],
input[type=datetime-local], select, textarea {
    width: 100%;
    padding: var(--space-2) var(--space-3);
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    color: var(--text);
    font-size: var(--text-base);
    font-family: inherit;
    line-height: 1.5;
    transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease);
}

input::placeholder, textarea::placeholder { color: var(--grey-400); }

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

input:hover:not(:disabled), select:hover:not(:disabled), textarea:hover:not(:disabled) {
    border-color: var(--grey-400);
}

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

input:disabled, select:disabled, textarea:disabled {
    background: var(--surface-3);
    color: var(--muted);
    cursor: not-allowed;
}

input[aria-invalid="true"], .field.has-error input, .field.has-error select {
    border-color: var(--danger);
}

select {
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7889' d='M6 8.5 2.5 5h7z'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right var(--space-3) center;
    padding-right: var(--space-8);
}
select[multiple] { min-height: 180px; background-image: none; padding-right: var(--space-3); }

.field-row { display: flex; gap: var(--space-4); flex-wrap: wrap; }
.field-row > .field { flex: 1; min-width: 200px; }

/* An input with a fixed suffix, e.g. a mailbox local part before @domain. */
.input-group { display: flex; align-items: stretch; }
.input-group input { border-radius: var(--radius) 0 0 var(--radius); }
.input-group .addon {
    display: inline-flex; align-items: center;
    padding: 0 var(--space-3);
    background: var(--surface-3);
    border: 1px solid var(--border-strong);
    border-left: 0;
    border-radius: 0 var(--radius) var(--radius) 0;
    color: var(--muted);
    font-size: var(--text-sm);
    white-space: nowrap;
}

.checkbox { display: flex; align-items: flex-start; gap: var(--space-3); margin-bottom: var(--space-3); }
.checkbox input { width: auto; margin-top: 3px; accent-color: var(--accent); flex: 0 0 auto; }
.checkbox label { margin: 0; font-weight: var(--weight-normal); }
.checkbox .hint { margin-top: 2px; }

.hint { color: var(--muted); font-size: var(--text-sm); margin: var(--space-2) 0 0; }
.error-text { color: var(--danger); font-size: var(--text-sm); margin: var(--space-2) 0 0; }

fieldset { border: 0; padding: 0; margin: 0 0 var(--space-5); }
fieldset legend {
    padding: 0; margin-bottom: var(--space-3);
    font-size: var(--text-sm); font-weight: var(--weight-semi);
    color: var(--text);
}

.form-actions {
    display: flex; gap: var(--space-3); align-items: center;
    margin-top: var(--space-6); padding-top: var(--space-5);
    border-top: 1px solid var(--border);
}

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

/* A platform setting: the field, then what the installation falls back to.
 *
 * The fallback is shown for every setting rather than only the changed ones,
 * because "what happens if I clear this?" is the question that stops somebody
 * clearing it. Separated by a rule so a long group of settings reads as a list
 * of decisions rather than as one wall of inputs. */
.setting {
    padding-bottom: var(--space-4);
    margin-bottom: var(--space-4);
    border-bottom: 1px solid var(--border);
}
.setting:last-of-type { border-bottom: 0; padding-bottom: 0; }
.setting-toggle > .checkbox { margin-bottom: 0; }

.setting-default {
    color: var(--muted);
    font-size: var(--text-xs);
    margin: var(--space-2) 0 0;
}
.setting-default .mono { color: var(--muted); }

/* Sits on the label, so a scan down the page finds what has been changed away
   from the shipped value without reading any of the values. */
.setting-badge { margin-left: var(--space-2); vertical-align: middle; }

/* A setting that is a message rather than a value: the acknowledgement a
   customer reads when they open a ticket. Monospaced because the shipped
   wording aligns its labels into columns, and a proportional font turns that
   into ragged text the moment somebody edits a line. */
.setting-paragraph {
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    line-height: 1.6;
    min-height: 220px;
}

/* Filter bars above a table. */
.filters {
    display: flex; gap: var(--space-3); flex-wrap: wrap; align-items: flex-end;
    margin-bottom: var(--space-4);
}
.filters .field { margin: 0; min-width: 160px; }

/* ==========================================================================
   8. Buttons
   ========================================================================== */

button, .button {
    display: inline-flex; align-items: center; justify-content: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-4);
    border-radius: var(--radius);
    border: 1px solid var(--border-strong);
    background: var(--surface);
    color: var(--text);
    font-size: var(--text-base);
    font-family: inherit;
    font-weight: var(--weight-medium);
    line-height: 1.5;
    cursor: pointer;
    white-space: nowrap;
    transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease),
                box-shadow var(--fast) var(--ease), transform var(--fast) var(--ease);
    box-shadow: var(--shadow-xs);
}

button:hover, .button:hover {
    background: var(--surface-2);
    border-color: var(--grey-400);
    text-decoration: none;
    color: var(--text);
}
button:active, .button:active { transform: translateY(1px); box-shadow: none; }

button:disabled, .button.disabled {
    opacity: .55; cursor: not-allowed; transform: none; box-shadow: none;
}

.button-primary {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--on-accent);
    font-weight: var(--weight-semi);
}
.button-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); color: var(--on-accent); }

.button-danger {
    background: var(--danger);
    border-color: var(--danger);
    color: #fff;
    font-weight: var(--weight-semi);
}
.button-danger:hover { filter: brightness(1.08); color: #fff; }

/* A destructive action that is not the page's main call to action. */
.button-danger-quiet { color: var(--danger); border-color: var(--border-strong); background: var(--surface); }
.button-danger-quiet:hover { background: var(--danger-subtle); border-color: var(--danger); color: var(--danger); }

.button-ghost { background: transparent; border-color: transparent; box-shadow: none; }
.button-ghost:hover { background: var(--surface-2); border-color: transparent; }

.button-small { padding: var(--space-1) var(--space-3); font-size: var(--text-sm); }
.button-large { padding: var(--space-3) var(--space-5); font-size: var(--text-md); }
.button-block { width: 100%; }

.link-button {
    background: none; border: none; box-shadow: none;
    color: var(--accent); padding: 0; cursor: pointer;
    font-size: var(--text-sm); font-family: inherit;
}
.link-button:hover { background: none; border: none; text-decoration: underline; color: var(--accent-hover); }
.link-button.danger { color: var(--danger); }

.button-row { display: flex; gap: var(--space-2); flex-wrap: wrap; }

/* In a table's trailing cell, where the row's actions sit against the edge and
   each one is its own form — a form is a block, so the cell's text-align does
   nothing on its own. */
.button-row-right { justify-content: flex-end; align-items: center; }

/* ==========================================================================
   9. Badges & pills
   ========================================================================== */

.badge {
    display: inline-flex; align-items: center; gap: var(--space-1);
    padding: 2px var(--space-2);
    border-radius: var(--radius-full);
    font-size: var(--text-xs);
    font-weight: var(--weight-semi);
    border: 1px solid var(--border);
    background: var(--surface-3);
    color: var(--text-2);
    white-space: nowrap;
    line-height: 1.5;
}

.badge-success { color: var(--success); border-color: transparent; background: var(--success-subtle); }
.badge-warning { color: var(--warning); border-color: transparent; background: var(--warning-subtle); }
.badge-danger  { color: var(--danger);  border-color: transparent; background: var(--danger-subtle); }
.badge-info    { color: var(--info);    border-color: transparent; background: var(--info-subtle); }
.badge-neutral { color: var(--muted);   border-color: var(--border); background: var(--surface-3); }

/* A dot before the label, for status. Reads at a glance in a dense table. */
.badge .dot {
    width: 6px; height: 6px; border-radius: 50%;
    background: currentColor; flex: 0 0 6px;
}

/* Status colour is decided once, here, so "active" is the same green on
   every screen and nothing has to remember which modifier to use. */
.status-active,   .status-in_sync, .status-completed, .status-ok      { color: var(--success); background: var(--success-subtle); border-color: transparent; }
.status-pending,  .status-provisioning, .status-running, .status-draft { color: var(--info);   background: var(--info-subtle);   border-color: transparent; }
.status-suspended,.status-drifted, .status-maintenance, .status-warning { color: var(--warning); background: var(--warning-subtle); border-color: transparent; }
.status-failed,   .status-error,   .status-deleted, .status-danger    { color: var(--danger);  background: var(--danger-subtle);  border-color: transparent; }
.status-unknown,  .status-archived, .status-deleting                  { color: var(--muted);   background: var(--surface-3); }

/* ==========================================================================
   10. Meters & progress
   ========================================================================== */

.meter {
    height: 6px;
    background: var(--surface-3);
    border-radius: var(--radius-full);
    overflow: hidden;
    margin: var(--space-2) 0 0;
}
.meter > span {
    display: block; height: 100%;
    background: var(--accent);
    border-radius: inherit;
    transition: width var(--normal) var(--ease);
}
.meter.warn > span   { background: var(--warning); }
.meter.danger > span { background: var(--danger); }

.meter-row { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); font-size: var(--text-sm); }
.meter-row .used { font-variant-numeric: tabular-nums; }
.meter-row .limit { color: var(--muted); }

/* A quota summary: label, numbers, bar. Used on dashboards and plan pages. */
.quota { margin-bottom: var(--space-4); }
.quota:last-child { margin-bottom: 0; }
.quota .quota-label { font-size: var(--text-sm); font-weight: var(--weight-medium); }

/* --- Sparklines and bar charts ------------------------------------------
   Inline SVG and flexbox. There is no charting library and no build step, so
   a trend has to be something the browser can already draw.
   ------------------------------------------------------------------------ */

.sparkline { margin: 0; width: 100%; }
.sparkline svg {
    display: block;
    width: 100%;
    height: var(--sparkline-height, 48px);
    overflow: visible;
}
.sparkline-line {
    fill: none;
    stroke: var(--accent);
    stroke-width: 1.5;
    stroke-linejoin: round;
    stroke-linecap: round;
    /* Without this the same path draws a hairline in a wide card and a slab in
       a narrow one, because the viewBox is stretched on one axis. */
    vector-effect: non-scaling-stroke;
}
.sparkline-area { fill: var(--accent-subtle); stroke: none; }

.sparkline-success .sparkline-line { stroke: var(--success); }
.sparkline-success .sparkline-area { fill: var(--success-subtle); }
.sparkline-warning .sparkline-line { stroke: var(--warning); }
.sparkline-warning .sparkline-area { fill: var(--warning-subtle); }
.sparkline-danger  .sparkline-line { stroke: var(--danger); }
.sparkline-danger  .sparkline-area { fill: var(--danger-subtle); }

.sparkline-empty {
    height: var(--sparkline-height, 48px);
    display: flex; align-items: center; justify-content: center;
    border: 1px dashed var(--border);
    border-radius: var(--radius);
}
.sparkline-caption { margin-top: var(--space-2); }

.bar-chart {
    display: flex;
    align-items: flex-end;
    gap: 2px;
    height: 120px;
    padding-top: var(--space-2);
}
.bar-chart .bar {
    flex: 1;
    min-width: 2px;
    background: var(--accent-subtle);
    border-radius: 2px 2px 0 0;
    position: relative;
    /* A zero-value day must still occupy its slot: a gap in the row would read
       as "no data" when it means "nobody visited". */
    min-height: 2px;
    transition: background var(--fast) var(--ease);
}
.bar-chart .bar:hover { background: var(--accent); }
.bar-chart .bar.empty { background: var(--surface-3); }

.chart-axis {
    display: flex; justify-content: space-between;
    font-size: var(--text-xs); color: var(--muted);
    margin-top: var(--space-2);
    padding-top: var(--space-2);
    border-top: 1px solid var(--border);
}

/* The step between the panel and Matomo. Narrow on purpose: it holds one
   decision and one button, and a full-width card makes it look like a page
   with something to read. */
.handoff { max-width: 44rem; }
.handoff .button-row { margin-top: var(--space-4); }

/* Where the operator is actually being sent, and what to check if the browser
   refuses to send them. Set apart from the explanation above it because it is
   for whoever has to fix a broken hand-off, not for the person using one. */
.handoff-appliance {
    margin-top: var(--space-5);
    padding-top: var(--space-3);
    border-top: 1px solid var(--border);
    font-size: var(--text-xs);
}

/* ==========================================================================
   10b. Dashboard
   --------------------------------------------------------------------------
   A dashboard's job is to answer "is anything wrong, and what should I do?"
   before it answers "how many of things do I have". So the attention list
   comes first and counts come second, and the two look different enough that
   nobody mistakes one for the other.
   ========================================================================== */

.dash-hero {
    display: flex; align-items: flex-start; justify-content: space-between;
    gap: var(--space-5); flex-wrap: wrap;
    padding: var(--space-5) var(--space-6);
    margin-bottom: var(--space-6);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-xs);
}
.dash-hero h2 { font-size: var(--text-xl); margin: 0 0 var(--space-1); letter-spacing: -.02em; }
.dash-hero .hero-meta { color: var(--muted); font-size: var(--text-base); margin: 0; }
.dash-hero .hero-figures { display: flex; gap: var(--space-8); flex-wrap: wrap; }
.dash-hero .hero-figure .label {
    color: var(--muted); font-size: var(--text-xs);
    text-transform: uppercase; letter-spacing: .06em; font-weight: var(--weight-semi);
}
.dash-hero .hero-figure .value {
    font-size: var(--text-xl); font-weight: var(--weight-semi);
    font-variant-numeric: tabular-nums; letter-spacing: -.02em;
}

/* --- Attention list -----------------------------------------------------
   One row per thing a person has to decide about. Deliberately not an alert
   per item: fifteen stacked alert boxes is noise, and the first three stop
   being read. --------------------------------------------------------- */

.attention { display: flex; flex-direction: column; }
.attention-item {
    display: flex; align-items: flex-start; gap: var(--space-3);
    padding: var(--space-3) var(--space-5);
    border-bottom: 1px solid var(--border);
    color: inherit;
}
.attention-item:last-child { border-bottom: 0; }
a.attention-item:hover { background: var(--surface-2); text-decoration: none; }

.attention-item .marker {
    flex: 0 0 8px; width: 8px; height: 8px; margin-top: 6px;
    border-radius: var(--radius-full);
    background: var(--muted);
}
.attention-item.critical .marker { background: var(--danger); }
.attention-item.warning  .marker { background: var(--warning); }
.attention-item.info     .marker { background: var(--info); }

.attention-item .body { flex: 1; min-width: 0; }
.attention-item .headline { font-weight: var(--weight-medium); }
.attention-item .detail { color: var(--muted); font-size: var(--text-sm); margin-top: 2px; }
.attention-item .go { color: var(--muted); flex: 0 0 auto; align-self: center; }

/* Nothing wrong. Worth stating plainly rather than showing an empty box —
   "no items" and "not loaded" look identical otherwise. */
.attention-clear {
    display: flex; align-items: center; gap: var(--space-3);
    padding: var(--space-5);
    color: var(--success);
}
.attention-clear .icon { flex: 0 0 18px; width: 18px; height: 18px; }
.attention-clear span { color: var(--text-2); }

/* --- Activity feed -------------------------------------------------------
   A table of audit rows is unreadable at a glance; the timestamp column is
   the widest thing on it and the least interesting. ---------------------- */

.feed { display: flex; flex-direction: column; }
.feed-item {
    display: flex; gap: var(--space-3);
    padding: var(--space-3) var(--space-5);
    border-bottom: 1px solid var(--border);
    font-size: var(--text-base);
}
.feed-item:last-child { border-bottom: 0; }
.feed-item .feed-dot {
    flex: 0 0 8px; width: 8px; height: 8px; margin-top: 6px;
    border-radius: var(--radius-full); background: var(--border-strong);
}
.feed-item.ok      .feed-dot { background: var(--success); }
.feed-item.failure .feed-dot { background: var(--danger); }
.feed-item.denied  .feed-dot { background: var(--warning); }
.feed-item .feed-body { flex: 1; min-width: 0; }
.feed-item .feed-action { font-weight: var(--weight-medium); }
.feed-item .feed-meta { color: var(--muted); font-size: var(--text-sm); margin-top: 2px; }
.feed-item .feed-when { color: var(--muted); font-size: var(--text-sm); white-space: nowrap; }

/* --- Quick actions ------------------------------------------------------ */

.actions-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
    gap: var(--space-3);
}
.action-tile {
    display: flex; align-items: center; gap: var(--space-3);
    padding: var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
    color: inherit;
    transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease);
}
.action-tile:hover {
    text-decoration: none;
    border-color: var(--accent-border);
    box-shadow: var(--shadow-sm);
}
.action-tile .icon-wrap {
    flex: 0 0 32px; width: 32px; height: 32px;
    display: grid; place-items: center;
    border-radius: var(--radius);
    background: var(--accent-subtle); color: var(--accent);
}
.action-tile .stack { gap: 0; }
.action-tile .action-title { font-weight: var(--weight-medium); }
.action-tile .action-hint { color: var(--muted); font-size: var(--text-sm); }

/* --- Health strip -------------------------------------------------------
   One line per integration or service: name, state, and when it was last
   actually checked. The last of those is the point — a green dot that has not
   been re-tested since March is worse than no dot. --------------------- */

.health-list { display: flex; flex-direction: column; }
.health-row {
    display: flex; align-items: center; gap: var(--space-3);
    padding: var(--space-3) var(--space-5);
    border-bottom: 1px solid var(--border);
}
.health-row:last-child { border-bottom: 0; }
.health-row .health-name { font-weight: var(--weight-medium); flex: 1; min-width: 0; }
.health-row .health-when { color: var(--muted); font-size: var(--text-sm); white-space: nowrap; }

/* --- Metric tile with a trend behind it --------------------------------- */

.stat.with-trend { position: relative; overflow: hidden; padding-bottom: var(--space-2); }
.stat.with-trend .sparkline { margin-top: var(--space-2); }
.stat .stat-foot { color: var(--muted); font-size: var(--text-sm); margin-top: var(--space-1); }

/* A quota inside a stat tile: the number, then how much of the allowance it
   is. Two facts that are useless apart. */
.stat .quota { margin: var(--space-2) 0 0; }

/* --- Section heading between groups of cards ---------------------------- */

.dash-section {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: var(--space-3);
    margin: var(--space-8) 0 var(--space-4);
}
.dash-section:first-child { margin-top: 0; }
.dash-section h2 {
    font-size: var(--text-lg); font-weight: var(--weight-semi);
    letter-spacing: -.01em; margin: 0;
}
.dash-section .dash-section-link { font-size: var(--text-sm); }

/* --- Permission picker ---------------------------------------------------
   A flat list of 145 slugs is a list nobody reads: people tick "everything"
   because scanning costs more than the risk feels like it is worth, which is
   the opposite of what the list is for. Grouped and collapsed, choosing
   narrowly is the cheap option.
   ------------------------------------------------------------------------ */

.permission-all {
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface-2);
    margin-bottom: var(--space-3);
}

.permission-group {
    border-top: 1px solid var(--border);
    padding: var(--space-2) 0;
}
.permission-group > summary {
    cursor: pointer;
    padding: var(--space-2) 0;
    font-weight: var(--weight-medium);
    display: flex; align-items: center; gap: var(--space-2);
    list-style: none;
}
.permission-group > summary::-webkit-details-marker { display: none; }
.permission-group > summary::before {
    content: "›";
    display: inline-block;
    transition: transform var(--fast) var(--ease);
    color: var(--muted);
}
.permission-group[open] > summary::before { transform: rotate(90deg); }

.permission-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: var(--space-2) var(--space-5);
    padding: var(--space-2) 0 var(--space-3) var(--space-5);
}
.permission-list .checkbox { margin-bottom: 0; align-items: flex-start; }
.permission-list .checkbox > span { display: flex; flex-direction: column; gap: 1px; }
.permission-list .hint { margin: 0; }
.permission-list .badge { margin-left: var(--space-2); }

/* A boxed group of related fields, so a long form reads as three decisions
   rather than one column of inputs. Same treatment the permission picker
   already had; named separately because it is now used for the limits too. */
.fieldgroup {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: var(--space-4);
    margin: 0 0 var(--space-4);
}
.fieldgroup legend {
    padding: 0 var(--space-2);
    font-size: var(--text-sm);
    font-weight: var(--weight-semi);
    color: var(--muted);
    text-transform: uppercase;
    letter-spacing: .06em;
    margin-bottom: 0;
}
.fieldgroup > .field-row { margin-top: var(--space-2); }
.fieldgroup .field:last-child { margin-bottom: 0; }

/* --- What a key is for ---------------------------------------------------
   Radios and their labels are siblings rather than nested, for two reasons.
   The grid then lines every control up under the one above it whatever the
   description below it runs to; and a sibling combinator can reach past the
   list to the picker, which is what reveals the hand-picked path without a
   line of script — `script-src 'self'` refuses inline handlers outright.
   ------------------------------------------------------------------------ */

.purpose-choices {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--space-3) var(--space-3);
    align-items: start;
}
.purpose-choices > input[type="radio"] {
    width: auto; margin: .3em 0 0; accent-color: var(--accent); flex: 0 0 auto;
}
.purpose-choices > label {
    display: flex; flex-direction: column; gap: 2px;
    margin: 0; cursor: pointer; font-weight: var(--weight-normal);
}
.purpose-choices > label .hint { margin: 0; }
.purpose-title { font-weight: var(--weight-medium); color: var(--text); }

.purpose-detail { grid-column: 1 / -1; min-width: 0; }

/* Only when a radio group exists to gate it — the platform screen has no
   scopes, draws no radios, and shows the picker on its own. */
.purpose-custom:not(:checked) ~ .purpose-detail { display: none; }

/* The one sentence that keeps the screen honest: everything above states what
   the key may ask for, and that is not the same as what it will get. */
.effective-access {
    border-left: 2px solid var(--border-strong);
    padding-left: var(--space-3);
    margin-top: var(--space-4);
}

/* --- Existing keys -------------------------------------------------------
   A card each rather than a wider table: what a key can reach is a list, and
   the question that brings anybody to this screen is exactly that.
   ------------------------------------------------------------------------ */

.api-key {
    padding: var(--space-4) var(--space-5);
    border-bottom: 1px solid var(--border);
}
.api-key:last-child { border-bottom: 0; }

/* Kept legible rather than greyed out. A revoked key is still something an
   operator reads — usually to find out when it stopped and who stopped it. */
.api-key.is-inactive { background: var(--surface-2); }

/* Same rule for the support integrations table, which is rows rather than
   cards because the useful facts there do fit a row. */
tr.is-inactive > td { background: var(--surface-2); }

.api-key-head {
    display: flex; gap: var(--space-4);
    align-items: flex-start; justify-content: space-between;
}
.api-key-head .stack { min-width: 0; }
.api-key-actions { display: flex; gap: var(--space-3); align-items: center; flex: 0 0 auto; }

.api-key-facts {
    display: flex; flex-wrap: wrap;
    gap: var(--space-2) var(--space-6);
    margin-top: var(--space-3);
    font-size: var(--text-sm);
}
.api-key-facts > div { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.api-key-facts .fact-label {
    color: var(--muted);
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: .06em;
}
.api-key-facts code { font-size: var(--text-xs); }

.api-key-reach { margin-top: var(--space-3); }
.api-key-reach .detail-list { margin-top: var(--space-2); font-size: var(--text-sm); }
.api-key-reach > p { font-size: var(--text-sm); margin: var(--space-2) 0 0; }

/* ==========================================================================
   11. Alerts & banners
   ========================================================================== */

.alert {
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius);
    margin-bottom: var(--space-5);
    border: 1px solid;
    font-size: var(--text-base);
    display: flex; gap: var(--space-3); align-items: flex-start;
}
.alert .icon { flex: 0 0 16px; margin-top: 2px; }
.alert > div { flex: 1; min-width: 0; }
.alert ul { margin: var(--space-2) 0 0; padding-left: var(--space-5); }
.alert strong { font-weight: var(--weight-semi); }

.alert-success { border-color: transparent; background: var(--success-subtle); color: var(--success); }
.alert-warning { border-color: transparent; background: var(--warning-subtle); color: var(--warning); }
.alert-error   { border-color: transparent; background: var(--danger-subtle);  color: var(--danger); }
.alert-info    { border-color: transparent; background: var(--info-subtle);    color: var(--info); }

/* Keep body text readable rather than tinting a whole paragraph. */
.alert-success div, .alert-warning div, .alert-error div, .alert-info div { color: var(--text); }
.alert strong:first-child { color: inherit; }

.alert .token {
    display: block;
    margin-top: var(--space-3);
    padding: var(--space-3);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    word-break: break-all;
    color: var(--text);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
}

.impersonation-banner {
    background: var(--warning-subtle);
    border-bottom: 1px solid var(--warning);
    color: var(--warning);
    padding: var(--space-2) var(--space-5);
    font-size: var(--text-sm);
    text-align: center;
    position: sticky; top: 0; z-index: 30;
}
.impersonation-banner strong { color: inherit; }

/* ==========================================================================
   12. Empty states
   ========================================================================== */

.empty-state {
    text-align: center;
    padding: var(--space-12) var(--space-5);
    color: var(--muted);
}
.empty-state .empty-icon {
    width: 40px; height: 40px;
    margin: 0 auto var(--space-4);
    color: var(--grey-400);
    opacity: .8;
}
.empty-state h3 {
    font-size: var(--text-md); color: var(--text);
    margin: 0 0 var(--space-2); font-weight: var(--weight-semi);
}
.empty-state p { max-width: 46ch; margin: 0 auto var(--space-5); }
.empty-state .button { margin: 0 auto; }

/* ==========================================================================
   12b. The domain selector
   ==========================================================================

   "Which domain am I working on?" — deliberately not styled as a card. It is
   chrome: a control you reach past on the way to the page, not a panel of the
   page's own. Hence the tinted strip rather than the raised surface every card
   uses, and the accent border on the left, which is the only thing on a screen
   full of white boxes that reads as "start here".

   The `compact` variant is for the rail, where the label sits above a
   full-width select and there is no room for anything beside it.
   ========================================================================== */

.domain-switcher {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-4);
    margin-bottom: var(--space-5);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
    border-radius: var(--radius);
}
.domain-switcher-title {
    font-size: var(--text-sm);
    font-weight: var(--weight-semi);
    color: var(--text-2);
    text-transform: uppercase;
    letter-spacing: .04em;
}
.domain-switcher-form {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
}
.domain-switcher-form select { min-width: 16rem; max-width: 100%; }
.domain-switcher .hint { margin: 0; }

.domain-switcher.compact {
    padding: var(--space-3);
    margin-bottom: var(--space-4);
    border-radius: var(--radius-sm);
}
.domain-switcher.compact .domain-switcher-form { flex-wrap: nowrap; }
.domain-switcher.compact .domain-switcher-form select { min-width: 0; flex: 1 1 auto; }

/* Drawn but not applying: servers, accounting, plans, jobs, settings. The
   accent border is what says "this narrows the page", so it is the thing to
   take away — a greyed select alone reads as a control that has broken rather
   than one that does not apply here. */
.domain-switcher.inactive {
    border-left-color: var(--border);
    background: transparent;
}
.domain-switcher.inactive .domain-switcher-title { color: var(--muted); }

/* In the rail the control sits on the dark chrome, which the card colours were
   not chosen for. */
.sidebar .domain-switcher {
    background: var(--rail-2);
    border-color: var(--rail-border);
    margin: 0 var(--space-3) var(--space-4);
}
.sidebar .domain-switcher .domain-switcher-title,
.sidebar .domain-switcher .hint { color: var(--rail-text-dim); }
:root[data-rail="collapsed"] .sidebar .domain-switcher { display: none; }

/* The top bar is a single row, and only exists below the rail's breakpoint. */
.topbar .domain-switcher {
    flex-direction: row;
    align-items: center;
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    min-width: 0;
}
.topbar .domain-switcher .domain-switcher-title,
.topbar .domain-switcher .hint { display: none; }
.topbar .domain-switcher select { max-width: 10rem; }

/* What the page is showing, said beside its heading. */
.domain-scope {
    display: flex; align-items: baseline; gap: var(--space-2);
    flex-wrap: wrap;
    margin-top: var(--space-2);
    font-size: var(--text-sm);
    color: var(--text-2);
}
.domain-scope-label { color: var(--muted); }
.domain-scope form { display: inline; }

/* ==========================================================================
   13. Tabs, breadcrumbs, pagination
   ========================================================================== */

.tabs {
    display: flex; gap: var(--space-1);
    border-bottom: 1px solid var(--border);
    margin-bottom: var(--space-5);
    overflow-x: auto;
}
.tabs a {
    padding: var(--space-3) var(--space-4);
    color: var(--text-2);
    font-size: var(--text-base);
    font-weight: var(--weight-medium);
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    white-space: nowrap;
}
.tabs a:hover { color: var(--text); text-decoration: none; }
.tabs a.active { color: var(--accent); border-bottom-color: var(--accent); }

.breadcrumbs {
    display: flex; align-items: center; gap: var(--space-2);
    font-size: var(--text-sm); color: var(--muted);
    margin-bottom: var(--space-3); flex-wrap: wrap;
}
.breadcrumbs a { color: var(--muted); }
.breadcrumbs a:hover { color: var(--text); }
.breadcrumbs .sep { opacity: .5; }

.pagination {
    display: flex; gap: var(--space-1); margin-top: var(--space-5);
    flex-wrap: wrap; align-items: center; font-size: var(--text-sm);
}
.pagination a, .pagination span {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 32px; height: 32px; padding: 0 var(--space-2);
    border-radius: var(--radius);
    border: 1px solid transparent;
    color: var(--text-2);
}
.pagination a:hover { background: var(--surface-2); text-decoration: none; color: var(--text); }
.pagination [aria-current="page"] span,
.pagination .active span { background: var(--accent); color: var(--on-accent); font-weight: var(--weight-semi); }
.pagination svg { width: 14px; height: 14px; }

/* Per-protocol mailbox switches. Laid out in columns rather than as a stacked
   list because they are read as a set — "what may this mailbox do" is one
   question with six answers, and a single column makes it look like six. */
.access-toggles {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 0 var(--space-4);
    padding: var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface-2);
}
.access-toggles legend, .access-toggles .hint { grid-column: 1 / -1; }
.access-toggles .checkbox { margin-bottom: var(--space-2); }

/* The second form inside a mailbox editor. Separated by a rule because it is a
   different act from the fields above it: it replaces them wholesale. */
.profile-apply {
    margin-top: var(--space-5);
    padding-top: var(--space-5);
    border-top: 1px solid var(--border);
}

/* ==========================================================================
   14. Utilities
   ========================================================================== */

.muted { color: var(--muted); }
.small { font-size: var(--text-sm); }
.tiny  { font-size: var(--text-xs); }
.strong { font-weight: var(--weight-semi); }
.right { text-align: right; }
.center { text-align: center; }
.nowrap { white-space: nowrap; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tabular { font-variant-numeric: tabular-nums; }

.stack { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.stack-3 { display: flex; flex-direction: column; gap: var(--space-3); }
.row { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.row-between { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
.spacer { height: var(--space-3); }
.grow { flex: 1; }

/* --- Spacing, width and layout utilities --------------------------------
   These exist because the Content-Security-Policy is `style-src 'self'` with
   no `unsafe-inline`: a `style=""` attribute is refused by the browser, not
   merely discouraged. Every one of them in the views was silently doing
   nothing in production.

   Named on the space scale rather than in pixels, so a spacing change is one
   token edit rather than a search for "14px". */

.m-0  { margin: 0; }
.mt-0 { margin-top: 0; }
.mt-1 { margin-top: var(--space-1); }
.mt-2 { margin-top: var(--space-2); }
.mt-3 { margin-top: var(--space-3); }
.mt-4 { margin-top: var(--space-4); }
.mt-5 { margin-top: var(--space-5); }
.mt-6 { margin-top: var(--space-6); }
.mb-0 { margin-bottom: 0; }
.mb-1 { margin-bottom: var(--space-1); }
.mb-2 { margin-bottom: var(--space-2); }
.mb-3 { margin-bottom: var(--space-3); }
.mb-4 { margin-bottom: var(--space-4); }
.mb-5 { margin-bottom: var(--space-5); }
.my-3 { margin-top: var(--space-3); margin-bottom: var(--space-3); }
.mr-4 { margin-right: var(--space-4); }

/* Measures, for prose and forms that should not run the width of a monitor. */
.mw-200 { max-width: 200px; }
.mw-220 { max-width: 220px; }
.mw-320 { max-width: 320px; }
.mw-360 { max-width: 360px; }
.mw-420 { max-width: 420px; }
.mw-520 { max-width: 520px; }
.mw-560 { max-width: 560px; }
.mw-620 { max-width: 620px; }
.mw-640 { max-width: 640px; }
.mw-680 { max-width: 680px; }
.mw-720 { max-width: 720px; }
.mw-760 { max-width: 760px; }
.minw-140 { min-width: 140px; }
.minw-160 { min-width: 160px; }
.minw-280 { min-width: 280px; }
.minh-70 { min-height: 70px; }

.break-all { word-break: break-all; }
.pre-line { white-space: pre-line; }
.left { text-align: left; }
.inline { display: inline; }
.row-end { display: flex; justify-content: flex-end; gap: var(--space-2); }
.row-top { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--space-4); }
.row-bottom { display: flex; align-items: flex-end; gap: var(--space-2); }
.row-middle { display: flex; align-items: center; gap: var(--space-2); }
.gap-2 { gap: var(--space-2); }
.justify-end { justify-content: flex-end; }

/* Two-column splits whose ratio is the point, so they are named rather than
   written out at each call site. */
.split-3-2 { grid-template-columns: minmax(0, 3fr) minmax(280px, 2fr); }
.split-2-1 { grid-template-columns: minmax(0, 2fr) minmax(260px, 1fr); }

.text-warn { color: var(--warning); }

/* --- DNS import preview -------------------------------------------------
   The review table for a domain being moved in. A skipped row is dimmed
   rather than hidden: "this record exists and is not coming across, and here
   is why" is the most important thing on the page, and a list that quietly
   omitted them would read as complete when it is not. */

.import-tick { width: 4.5rem; text-align: center; }
.import-skipped td { opacity: .62; }
.import-skipped td:last-child { opacity: 1; }

/* --- Current beside desired ---------------------------------------------
   The comparison a domain being transferred in lives inside until it moves.
   A row that changes at the cutover is marked on its leading edge rather than
   tinted across its width: the table is read left to right for what is
   published now, and colouring the whole row makes the live value look like a
   problem when it is the one that currently works. */

.plan-will-be-replaced td:first-child { box-shadow: inset 3px 0 0 var(--warning); }
.plan-ours-missing td:first-child { box-shadow: inset 3px 0 0 var(--border-strong); }
.plan-ours-missing td { opacity: .72; }

.plan-legend { margin: 0 0 var(--space-3); }
.plan-legend dt { font-weight: var(--weight-semi); font-size: var(--text-sm); margin-top: var(--space-2); }
.plan-legend dd { margin: 0; max-width: 70ch; }


/* --- Support ticket thread ---------------------------------------------
   Lived in a <style> block inside the component, which the CSP refuses — so a
   staff reply and a customer message rendered identically. */
.ticket-message { margin-bottom: 0; border-left: 3px solid var(--border); }
.ticket-message.is-staff { border-left-color: var(--accent); }
.ticket-message.is-note {
    border-left-color: var(--warning);
    background: var(--warning-subtle);
}
.ticket-body { line-height: 1.65; overflow-wrap: anywhere; }

/* Sparkline heights.
   A named set rather than a custom property in a style attribute, which the
   CSP refuses. These are the sizes the components actually ask for; anything
   else falls through to the 48px default in the rules above. */
.sparkline-h-24 { --sparkline-height: 24px; }
.sparkline-h-32 { --sparkline-height: 32px; }
.sparkline-h-34 { --sparkline-height: 34px; }
.sparkline-h-40 { --sparkline-height: 40px; }
.sparkline-h-48 { --sparkline-height: 48px; }
.sparkline-h-64 { --sparkline-height: 64px; }

/* --- Quantised proportions ---------------------------------------------
   For meters and bars, whose size is data rather than an authoring choice.
   A style attribute would be the obvious way to express this and is exactly
   what the CSP refuses, so the value is rounded to a whole percent and named.
   Widths and heights are separate: an element sized by one is never sized by
   the other, and sharing a class would let a bug swap them silently. */

.w-pct-0 { width: 0%; }
.w-pct-1 { width: 1%; }
.w-pct-2 { width: 2%; }
.w-pct-3 { width: 3%; }
.w-pct-4 { width: 4%; }
.w-pct-5 { width: 5%; }
.w-pct-6 { width: 6%; }
.w-pct-7 { width: 7%; }
.w-pct-8 { width: 8%; }
.w-pct-9 { width: 9%; }
.w-pct-10 { width: 10%; }
.w-pct-11 { width: 11%; }
.w-pct-12 { width: 12%; }
.w-pct-13 { width: 13%; }
.w-pct-14 { width: 14%; }
.w-pct-15 { width: 15%; }
.w-pct-16 { width: 16%; }
.w-pct-17 { width: 17%; }
.w-pct-18 { width: 18%; }
.w-pct-19 { width: 19%; }
.w-pct-20 { width: 20%; }
.w-pct-21 { width: 21%; }
.w-pct-22 { width: 22%; }
.w-pct-23 { width: 23%; }
.w-pct-24 { width: 24%; }
.w-pct-25 { width: 25%; }
.w-pct-26 { width: 26%; }
.w-pct-27 { width: 27%; }
.w-pct-28 { width: 28%; }
.w-pct-29 { width: 29%; }
.w-pct-30 { width: 30%; }
.w-pct-31 { width: 31%; }
.w-pct-32 { width: 32%; }
.w-pct-33 { width: 33%; }
.w-pct-34 { width: 34%; }
.w-pct-35 { width: 35%; }
.w-pct-36 { width: 36%; }
.w-pct-37 { width: 37%; }
.w-pct-38 { width: 38%; }
.w-pct-39 { width: 39%; }
.w-pct-40 { width: 40%; }
.w-pct-41 { width: 41%; }
.w-pct-42 { width: 42%; }
.w-pct-43 { width: 43%; }
.w-pct-44 { width: 44%; }
.w-pct-45 { width: 45%; }
.w-pct-46 { width: 46%; }
.w-pct-47 { width: 47%; }
.w-pct-48 { width: 48%; }
.w-pct-49 { width: 49%; }
.w-pct-50 { width: 50%; }
.w-pct-51 { width: 51%; }
.w-pct-52 { width: 52%; }
.w-pct-53 { width: 53%; }
.w-pct-54 { width: 54%; }
.w-pct-55 { width: 55%; }
.w-pct-56 { width: 56%; }
.w-pct-57 { width: 57%; }
.w-pct-58 { width: 58%; }
.w-pct-59 { width: 59%; }
.w-pct-60 { width: 60%; }
.w-pct-61 { width: 61%; }
.w-pct-62 { width: 62%; }
.w-pct-63 { width: 63%; }
.w-pct-64 { width: 64%; }
.w-pct-65 { width: 65%; }
.w-pct-66 { width: 66%; }
.w-pct-67 { width: 67%; }
.w-pct-68 { width: 68%; }
.w-pct-69 { width: 69%; }
.w-pct-70 { width: 70%; }
.w-pct-71 { width: 71%; }
.w-pct-72 { width: 72%; }
.w-pct-73 { width: 73%; }
.w-pct-74 { width: 74%; }
.w-pct-75 { width: 75%; }
.w-pct-76 { width: 76%; }
.w-pct-77 { width: 77%; }
.w-pct-78 { width: 78%; }
.w-pct-79 { width: 79%; }
.w-pct-80 { width: 80%; }
.w-pct-81 { width: 81%; }
.w-pct-82 { width: 82%; }
.w-pct-83 { width: 83%; }
.w-pct-84 { width: 84%; }
.w-pct-85 { width: 85%; }
.w-pct-86 { width: 86%; }
.w-pct-87 { width: 87%; }
.w-pct-88 { width: 88%; }
.w-pct-89 { width: 89%; }
.w-pct-90 { width: 90%; }
.w-pct-91 { width: 91%; }
.w-pct-92 { width: 92%; }
.w-pct-93 { width: 93%; }
.w-pct-94 { width: 94%; }
.w-pct-95 { width: 95%; }
.w-pct-96 { width: 96%; }
.w-pct-97 { width: 97%; }
.w-pct-98 { width: 98%; }
.w-pct-99 { width: 99%; }
.w-pct-100 { width: 100%; }

.h-pct-0 { height: 0%; }
.h-pct-1 { height: 1%; }
.h-pct-2 { height: 2%; }
.h-pct-3 { height: 3%; }
.h-pct-4 { height: 4%; }
.h-pct-5 { height: 5%; }
.h-pct-6 { height: 6%; }
.h-pct-7 { height: 7%; }
.h-pct-8 { height: 8%; }
.h-pct-9 { height: 9%; }
.h-pct-10 { height: 10%; }
.h-pct-11 { height: 11%; }
.h-pct-12 { height: 12%; }
.h-pct-13 { height: 13%; }
.h-pct-14 { height: 14%; }
.h-pct-15 { height: 15%; }
.h-pct-16 { height: 16%; }
.h-pct-17 { height: 17%; }
.h-pct-18 { height: 18%; }
.h-pct-19 { height: 19%; }
.h-pct-20 { height: 20%; }
.h-pct-21 { height: 21%; }
.h-pct-22 { height: 22%; }
.h-pct-23 { height: 23%; }
.h-pct-24 { height: 24%; }
.h-pct-25 { height: 25%; }
.h-pct-26 { height: 26%; }
.h-pct-27 { height: 27%; }
.h-pct-28 { height: 28%; }
.h-pct-29 { height: 29%; }
.h-pct-30 { height: 30%; }
.h-pct-31 { height: 31%; }
.h-pct-32 { height: 32%; }
.h-pct-33 { height: 33%; }
.h-pct-34 { height: 34%; }
.h-pct-35 { height: 35%; }
.h-pct-36 { height: 36%; }
.h-pct-37 { height: 37%; }
.h-pct-38 { height: 38%; }
.h-pct-39 { height: 39%; }
.h-pct-40 { height: 40%; }
.h-pct-41 { height: 41%; }
.h-pct-42 { height: 42%; }
.h-pct-43 { height: 43%; }
.h-pct-44 { height: 44%; }
.h-pct-45 { height: 45%; }
.h-pct-46 { height: 46%; }
.h-pct-47 { height: 47%; }
.h-pct-48 { height: 48%; }
.h-pct-49 { height: 49%; }
.h-pct-50 { height: 50%; }
.h-pct-51 { height: 51%; }
.h-pct-52 { height: 52%; }
.h-pct-53 { height: 53%; }
.h-pct-54 { height: 54%; }
.h-pct-55 { height: 55%; }
.h-pct-56 { height: 56%; }
.h-pct-57 { height: 57%; }
.h-pct-58 { height: 58%; }
.h-pct-59 { height: 59%; }
.h-pct-60 { height: 60%; }
.h-pct-61 { height: 61%; }
.h-pct-62 { height: 62%; }
.h-pct-63 { height: 63%; }
.h-pct-64 { height: 64%; }
.h-pct-65 { height: 65%; }
.h-pct-66 { height: 66%; }
.h-pct-67 { height: 67%; }
.h-pct-68 { height: 68%; }
.h-pct-69 { height: 69%; }
.h-pct-70 { height: 70%; }
.h-pct-71 { height: 71%; }
.h-pct-72 { height: 72%; }
.h-pct-73 { height: 73%; }
.h-pct-74 { height: 74%; }
.h-pct-75 { height: 75%; }
.h-pct-76 { height: 76%; }
.h-pct-77 { height: 77%; }
.h-pct-78 { height: 78%; }
.h-pct-79 { height: 79%; }
.h-pct-80 { height: 80%; }
.h-pct-81 { height: 81%; }
.h-pct-82 { height: 82%; }
.h-pct-83 { height: 83%; }
.h-pct-84 { height: 84%; }
.h-pct-85 { height: 85%; }
.h-pct-86 { height: 86%; }
.h-pct-87 { height: 87%; }
.h-pct-88 { height: 88%; }
.h-pct-89 { height: 89%; }
.h-pct-90 { height: 90%; }
.h-pct-91 { height: 91%; }
.h-pct-92 { height: 92%; }
.h-pct-93 { height: 93%; }
.h-pct-94 { height: 94%; }
.h-pct-95 { height: 95%; }
.h-pct-96 { height: 96%; }
.h-pct-97 { height: 97%; }
.h-pct-98 { height: 98%; }
.h-pct-99 { height: 99%; }
.h-pct-100 { height: 100%; }

.text-success { color: var(--success); }
.text-warning { color: var(--warning); }
.text-danger  { color: var(--danger); }

.icon { width: 16px; height: 16px; flex: 0 0 16px; }
.icon-sm { width: 14px; height: 14px; flex-basis: 14px; }
.icon-lg { width: 20px; height: 20px; flex-basis: 20px; }

details.disclosure summary {
    cursor: pointer; color: var(--accent); font-size: var(--text-sm);
    padding: var(--space-2) 0; font-weight: var(--weight-medium);
}
details.disclosure summary:hover { color: var(--accent-hover); }
/* Jumped to from the page header. Without this the anchor scrolls to a
   summary that is still shut, which reads as the link having done nothing. */
details.disclosure summary:target { color: var(--accent-hover); font-weight: 600; }
details.disclosure[open] summary { margin-bottom: var(--space-3); }

/* A table row that holds an editor or a confirmation for the row above it,
 * rather than a record of its own.
 *
 * The class has been on those rows in the DNS and website pages since they were
 * written and never had a rule, so a "Delete this database" confirmation read
 * as another database — one more line in a list of things you own, in the same
 * weight as the real ones. Recessed and without the hover highlight, so it
 * reads as attached to the row it belongs to and not as an entry beside it.
 */
tbody tr.row-editor,
tbody tr.row-editor:hover { background: var(--surface-2); }
tbody tr.row-editor > td { padding-top: var(--space-2); }

/* Two or more disclosures offered side by side on a row-editor row — Reply and
 * Edit on the support queue.
 *
 * Closed, they read as a pair of links under the ticket and cost one line.
 * Opened, the one in use takes the whole row: a reply typed into half a table
 * cell is the reason a queue reply gets abandoned and the ticket opened
 * instead, which is exactly what these are here to avoid. `flex-start` so the
 * closed one does not stretch to the height of the open one and turn its
 * summary into a stray link floating in the middle of the row.
 */
.row-disclosures {
    display: flex; flex-wrap: wrap; align-items: flex-start;
    gap: var(--space-1) var(--space-5);
}
.row-disclosures > details { min-width: 0; }
.row-disclosures > details[open] { flex: 1 1 100%; }

.qr-code svg { background: #fff; border-radius: var(--radius); padding: var(--space-3); }

/* Guest pages: login, password reset, MFA challenge. */
body.guest {
    display: flex; align-items: center; justify-content: center;
    min-height: 100vh; padding: var(--space-6);
    background:
        radial-gradient(1200px 600px at 50% -10%, var(--accent-subtle), transparent 70%),
        var(--bg);
}
.guest-card {
    width: 100%; max-width: 420px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: var(--space-8);
    box-shadow: var(--shadow-lg);
}
.guest-card h1 { font-size: var(--text-xl); margin: 0 0 var(--space-2); letter-spacing: -.02em; }
.guest-card .subtitle { color: var(--muted); font-size: var(--text-base); margin-bottom: var(--space-6); }
.guest-footer { text-align: center; margin-top: var(--space-5); font-size: var(--text-sm); }

/* ==========================================================================
   15. Responsive
   ========================================================================== */

@media (max-width: 1024px) {
    .content { padding: var(--space-5) var(--space-5) var(--space-12); }
}

@media (max-width: 860px) {
    .shell { flex-direction: column; }

    /* The sidebar becomes a disclosure panel under a bar. No JavaScript:
       a checkbox toggle keeps the panel usable on a phone without adding a
       script dependency to a page that otherwise needs none. */
    .topbar {
        display: flex; align-items: center; gap: var(--space-3);
        padding: var(--space-3) var(--space-4);
        /* Rail colours, not surface colours: on a phone this bar *is* the
           rail, and a white strip above a dark panel reads as two products. */
        background: var(--rail);
        color: var(--rail-active);
        border-bottom: 1px solid var(--rail-border);
        position: sticky; top: 0; z-index: 20;
        min-height: var(--topbar-h);
    }
    .topbar .brand { border: 0; padding: 0; min-height: 0; flex: 1; }
    .topbar .button-ghost { color: var(--rail-text); }
    .topbar .button-ghost:hover { background: var(--rail-2); color: var(--rail-active); }

    .sidebar { border-bottom: 1px solid var(--rail-border); }

    .nav-toggle { display: inline-flex; }

    .sidebar {
        position: static; height: auto; width: 100%; flex-basis: auto;
        border-right: 0; border-bottom: 1px solid var(--border);
        display: none;
    }
    .sidebar .brand { display: none; }

    /* Revealed by the hidden checkbox in the layout. */
    #nav-toggle:checked ~ .shell .sidebar { display: flex; }

    .content { padding: var(--space-4) var(--space-4) var(--space-12); max-width: none; }
    .page-header h1 { font-size: var(--text-xl); }

    .detail-list { grid-template-columns: 1fr; gap: var(--space-1) 0; }
    .detail-list dd { margin-bottom: var(--space-3); }

    .form-actions { flex-direction: column; align-items: stretch; }
    .form-actions .button, .form-actions button { width: 100%; }
}

@media (min-width: 861px) {
    .nav-toggle { display: none; }
}

/* ==========================================================================
   16. Accessibility & print
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: .01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .01ms !important;
        scroll-behavior: auto !important;
    }
}

/* Higher-contrast borders for users who ask for them. */
@media (prefers-contrast: more) {
    :root { --border: var(--grey-400); --border-strong: var(--grey-600); --muted: var(--grey-600); }
}

.visually-hidden {
    position: absolute; width: 1px; height: 1px;
    padding: 0; margin: -1px; overflow: hidden;
    clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Skip link: the sidebar is long, and keyboard users should not have to tab
   through every nav item on every page. */
.skip-link {
    position: absolute; left: var(--space-3); top: var(--space-3);
    transform: translateY(-200%);
    background: var(--accent); color: var(--on-accent);
    padding: var(--space-2) var(--space-4); border-radius: var(--radius);
    z-index: 100; font-weight: var(--weight-semi);
}
.skip-link:focus { transform: translateY(0); text-decoration: none; }

/* ---------------------------------------------------------------------------
   Jobs
   ---------------------------------------------------------------------------
   Work that now happens on a worker: provisioning jobs and the server
   operations underneath them.
*/

/* A row the operator is meant to notice — something past its own timeout with
 * no result written against it. A bar down the left edge rather than a tinted
 * row: the status badge in the row is already carrying colour, and two colour
 * signals competing in one line is how neither gets read. Follows the
 * needs-attention marker on the services table so the two mean the same thing.
 */
tbody tr.row-attention > td:first-child { box-shadow: inset 3px 0 0 var(--warning); }

/* Stored output, verbatim. An agent's stderr arrives with its own line breaks
 * and they are most of the meaning, so it wraps rather than being squeezed onto
 * one line, and scrolls rather than stretching the page when it does not. */
.job-output {
    background: var(--surface-inset);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: var(--space-2) var(--space-3);
    margin: var(--space-2) 0 0;
    max-height: 22em;
    overflow: auto;
    white-space: pre-wrap;
    word-break: break-word;
    color: var(--text-2);
}

/* An error inside a table cell. Truncated in the markup already; this keeps a
 * long single-token message (a path, a URL) from widening the column past the
 * rest of the table. */
.job-error { max-width: 44ch; word-break: break-word; }

.job-step { border-top: 1px solid var(--border); }
.job-step:last-of-type { border-bottom: 1px solid var(--border); }

.job-step > summary {
    cursor: pointer;
    display: flex; align-items: center; gap: var(--space-3);
    padding: var(--space-3) 0;
    list-style: none;
}
.job-step > summary::-webkit-details-marker { display: none; }
.job-step > summary::before {
    content: "›";
    color: var(--muted);
    transition: transform var(--fast) var(--ease);
}
.job-step[open] > summary::before { transform: rotate(90deg); }
.job-step > summary:hover { background: var(--surface-2); }

/* The sequence number, fixed width so the step names line up whatever the
   count reaches. */
.job-step-seq {
    color: var(--muted);
    font-size: var(--text-sm);
    min-width: 2ch;
    text-align: right;
}
.job-step-name { flex: 1; font-weight: var(--weight-medium); }

.job-step-body { padding: 0 0 var(--space-4) var(--space-6); }
.job-step-body > table { margin-top: var(--space-2); }

/* --- File manager --------------------------------------------------------
 *
 * The editor. Monospace, tab-sized to four, and explicitly *not* wrapped: a
 * config file whose long lines are soft-wrapped reads as though it has line
 * breaks that are not there, and the person editing it acts on what they see.
 * Horizontal scroll is the honest rendering.
 *
 * `resize: vertical` rather than `both`: the field is already the width of its
 * column, and letting it grow sideways only ever pushes the page into a
 * horizontal scroll of its own.
 */
.code-editor {
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    line-height: 1.55;
    tab-size: 4;
    white-space: pre;
    overflow-wrap: normal;
    overflow-x: auto;
    resize: vertical;
    min-height: 12rem;
}

/* A listing row's own name column carries a note under it for a symlink, and
 * the note must not inherit the weight the first column gives the name. */
.table-primary .hint { font-weight: var(--weight-regular); max-width: 52ch; }

@media print {
    .sidebar, .topbar, .page-actions, .pagination, .form-actions { display: none !important; }
    body { background: #fff; color: #000; }
    .card { border: 1px solid #ccc; box-shadow: none; break-inside: avoid; }
    .content { padding: 0; max-width: none; }
}

/* Scheduled jobs
   ---------------------------------------------------------------------------
   A cron command is a shell line: long, unbroken, and full of slashes. Left to
   itself it either forces the whole table sideways or gets clipped at the cell
   edge, and a clipped command is worse than no command because it looks
   complete. Broken anywhere and given a ceiling instead, so the row stays a row
   and the schedule beside it keeps its place. */
.cron-command { display: block; word-break: break-all; max-width: 62ch; }

/* The two halves of a drift report — what is on the server, what is not. Lines
   are shown verbatim, one per line, because the point of the comparison is that
   the operator can read what is actually in the file. */
.cron-lines { list-style: none; margin: var(--space-2) 0 0; padding: 0; }
.cron-lines li { padding: var(--space-1) 0; word-break: break-all; }

/* ==========================================================================
   17. Operations dashboard
   --------------------------------------------------------------------------
   Built for a screen somebody leaves open all day. The governing idea is that
   vertical space is the scarce resource: every element here is sized so that
   the health of the whole platform fits above the fold on a 1080p display, and
   detail is reached by scanning right rather than scrolling down.

   Four state colours run through the whole section — ok, warn, critical, idle.
   `idle` is deliberately not a fault: a platform with no websites yet is not
   unhealthy, and colouring it red would cry wolf on the first day.
   ========================================================================== */

.ops { display: flex; flex-direction: column; gap: var(--space-3); }

/* --- The decision block ---------------------------------------------------
   The verdict and the things to do about it, in one block at the top of the
   page.

   They used to be two panels with a strip of six segment tiles between them,
   which put the only two items that needed a person — an account with no TOTP
   factor, a host key waiting for approval — underneath a row of figures that
   were merely true. Whatever is worst on the platform now lives in the first
   hundred pixels. */

.decision {
    border: 1px solid var(--border);
    border-left: 3px solid var(--border-strong);
    border-radius: var(--radius-md);
    background: var(--surface);
    overflow: hidden;
}
.decision.ok       { border-left-color: var(--success); }
.decision.warn     { border-left-color: var(--warning); }
.decision.critical { border-left-color: var(--danger); }

.decision-head {
    display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-1) var(--space-4);
    padding: var(--space-2) var(--space-4);
}
/* Given a list beneath it, the head becomes that list's heading. */
.decision.has-alerts .decision-head { background: var(--surface-2); }

/* The verdict earns the only emphatic type on the page: it is the one thing
   that should be readable from across a room. */
.decision-verdict { font-size: var(--text-md); font-weight: var(--weight-semi); }
.decision-verdict .state-icon { width: 15px; height: 15px; flex-basis: 15px; }

.decision-meta { margin-left: auto; font-size: var(--text-sm); color: var(--muted); }

.alert-row {
    display: flex; align-items: flex-start; gap: var(--space-3);
    padding: var(--space-2) var(--space-4);
    border-top: 1px solid var(--border);
    font-size: var(--text-sm);
    text-decoration: none; color: inherit;
}
/* Tinted, not merely coloured. A critical row has to survive being the fourth
   thing in a list, and a tint is the only difference visible in peripheral
   vision — the icon and the word do the work once the eye arrives. */
.alert-row.critical { background: var(--danger-subtle); }
.alert-row:hover { background: var(--surface-2); text-decoration: none; }
.alert-row.critical:hover { background: var(--danger-subtle); }

/* One column, so the levels line up and the headlines start at the same x. */
.alert-level {
    flex: 0 0 auto; min-width: 5.6rem;
    font-size: var(--text-xs); font-weight: var(--weight-semi);
    text-transform: uppercase; letter-spacing: .05em;
}

.alert-row .what { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-2); }
.alert-row .headline { font-weight: var(--weight-medium); }
.alert-row .what .muted { min-width: 0; overflow-wrap: anywhere; }

.alert-go {
    flex: 0 0 13px; width: 13px; height: 13px;
    align-self: center;
    color: var(--muted);
    transition: transform var(--fast) var(--ease);
}
a.alert-row:hover .alert-go { transform: translateX(2px); color: var(--text); }

/* The rows run edge to edge inside a block that clips to its own corners, so
   the default focus ring — drawn 2px *outside* the element — would be cut off
   on three sides. Drawn inside instead. Never removed: a keyboard operator
   tabbing through the alert list has to be able to see where they are. */
.alert-row:focus-visible { outline-offset: -2px; }

/* --- Segment tiles --------------------------------------------------------
   The parts behind the verdict. Two lines each — a label, then the figure and
   what the figure means side by side — so six of them fit on one row of a
   laptop screen instead of the three lines each they used to take. */

.segments { display: grid; grid-template-columns: repeat(auto-fit, minmax(158px, 1fr)); gap: var(--space-2); }

.segment {
    display: flex; flex-direction: column; gap: 2px;
    min-width: 0;
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--border);
    border-left: 3px solid var(--border-strong);
    border-radius: var(--radius);
    background: var(--surface);
    text-decoration: none; color: inherit;
    transition: background var(--fast) var(--ease);
}
a.segment:hover { background: var(--surface-2); text-decoration: none; }
.segment.ok       { border-left-color: var(--success); }
.segment.warn     { border-left-color: var(--warning); }
.segment.critical { border-left-color: var(--danger); }
.segment.idle     { border-left-color: var(--border-strong); }

.segment-label {
    display: flex; align-items: center; gap: var(--space-1);
    font-size: var(--text-xs); color: var(--muted);
    text-transform: uppercase; letter-spacing: .05em;
}
.segment-label .icon { width: 12px; height: 12px; flex-basis: 12px; }

.segment-body { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2); min-width: 0; }
.segment-value { font-size: var(--text-lg); font-weight: var(--weight-semi); line-height: 1.15; font-variant-numeric: tabular-nums; }
.segment-detail { font-size: var(--text-xs); min-width: 0; }

/* --- KPI strip -----------------------------------------------------------
   Money and workload. Every tile ends in a sentence rather than a second
   number, because "£58.00" is not news and "3 subscriptions billing" is. */

.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(168px, 1fr)); gap: var(--space-2); }
.kpi {
    display: flex; flex-direction: column; gap: 2px;
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
    text-decoration: none; color: inherit;
    transition: border-color var(--fast) var(--ease), background var(--fast) var(--ease);
}
a.kpi:hover { border-color: var(--border-strong); background: var(--surface-2); text-decoration: none; }
.kpi .label {
    display: flex; align-items: center; gap: var(--space-1);
    font-size: var(--text-xs); color: var(--muted);
    text-transform: uppercase; letter-spacing: .05em;
}
.kpi .label .icon { width: 12px; height: 12px; flex-basis: 12px; }
.kpi .value { font-size: var(--text-xl); font-weight: var(--weight-semi); line-height: 1.15; font-variant-numeric: tabular-nums; }
.kpi .foot { font-size: var(--text-xs); }

/* --- Infrastructure table ------------------------------------------------ */

.infra { border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface); overflow: hidden; }
.infra table { margin: 0; }
.infra thead th {
    font-size: var(--text-xs); text-transform: uppercase; letter-spacing: .05em;
    padding: var(--space-2) var(--space-3);
    background: var(--surface-2);
}
.infra tbody td { padding: var(--space-2) var(--space-3); font-size: var(--text-sm); vertical-align: middle; }
.infra tbody tr:hover { background: var(--surface-2); }
.infra .host { font-weight: var(--weight-medium); }
.infra .host a { text-decoration: none; }
.infra .host a:hover { text-decoration: underline; }
/* Address and agent version live under the name rather than in columns of
   their own: they are things an operator looks up about one machine, not
   things anybody scans down a list. */
.infra .host-sub { display: block; }

/* The state column is a glyph wide and nothing more. */
.infra .cell-state { width: 1%; padding-right: 0; }

/* --- Widget grid --------------------------------------------------------- */

.widgets { display: grid; grid-template-columns: repeat(auto-fit, minmax(232px, 1fr)); gap: var(--space-3); align-items: start; }

.widget {
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
    overflow: hidden;
}
.widget-head {
    display: flex; align-items: center; justify-content: space-between; gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--border);
    background: var(--surface-2);
    font-size: var(--text-xs); text-transform: uppercase; letter-spacing: .05em;
    font-weight: var(--weight-semi); color: var(--text-2);
}
.widget-head > span { display: inline-flex; align-items: center; gap: var(--space-1); min-width: 0; }
.widget-head .icon { width: 12px; height: 12px; flex-basis: 12px; }
.widget-head a {
    display: inline-flex; align-items: center; gap: 1px;
    color: var(--muted); text-transform: none; letter-spacing: 0; font-size: var(--text-xs);
    white-space: nowrap;
}
.widget-head a .icon { width: 11px; height: 11px; flex-basis: 11px; }

/* A picture at the head of a widget, above the rows it summarises. */
.widget-chart { padding: var(--space-2) var(--space-3) var(--space-1); border-bottom: 1px solid var(--border); }
.widget-chart .sparkline-caption { margin-top: 2px; }

.widget-row {
    display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
    padding: var(--space-1) var(--space-3);
    border-bottom: 1px solid var(--border);
    font-size: var(--text-sm);
}
.widget-row:last-child { border-bottom: 0; }
.widget-row .n { font-variant-numeric: tabular-nums; font-weight: var(--weight-medium); }
.widget-row .n.zero { color: var(--muted); font-weight: var(--weight-normal); }
.widget-row .n.warn { color: var(--warning-text); }
.widget-row .n.ok { color: var(--success-text); }
.widget-empty { padding: var(--space-3); font-size: var(--text-sm); color: var(--muted); text-align: center; }

/* --- Quick actions ------------------------------------------------------- */

.quick { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.quick a {
    display: inline-flex; align-items: center; gap: 5px;
    padding: 3px var(--space-3);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    background: var(--surface);
    font-size: var(--text-sm);
    text-decoration: none; color: var(--text);
    white-space: nowrap;
}
.quick a:hover { background: var(--surface-2); border-color: var(--accent-border); color: var(--accent); text-decoration: none; }
.quick .icon { width: 13px; height: 13px; flex-basis: 13px; }

/* --- Density elsewhere ---------------------------------------------------- */

.card { padding: var(--space-4); }
.page-header { margin-bottom: var(--space-3); }
.page-header h1 { font-size: var(--text-xl); }

/* --- Collapsible rail ----------------------------------------------------- */

.sidebar { transition: width var(--fast) var(--ease); }
.sidebar nav a { padding: 5px var(--space-3); }
.nav-section { padding: var(--space-3) var(--space-4) var(--space-1); }
.brand { padding: var(--space-3) var(--space-4); }
.sidebar-footer { padding: var(--space-3) var(--space-4); }

:root[data-rail="collapsed"] .sidebar {
    width: var(--sidebar-w-collapsed);
    flex-basis: var(--sidebar-w-collapsed);
}
/* Everything but the icons is hidden rather than removed, so the rail can be
   restored without a reflow of the page behind it. */
:root[data-rail="collapsed"] .sidebar .brand-name,
:root[data-rail="collapsed"] .sidebar nav a span,
:root[data-rail="collapsed"] .sidebar .nav-section,
:root[data-rail="collapsed"] .sidebar .sidebar-user .stack,
:root[data-rail="collapsed"] .sidebar .rail-version,
:root[data-rail="collapsed"] .sidebar .link-button { display: none; }
:root[data-rail="collapsed"] .sidebar nav a { justify-content: center; }
:root[data-rail="collapsed"] .sidebar .brand { justify-content: center; padding: var(--space-3) 0; }
:root[data-rail="collapsed"] .sidebar-footer { align-items: center; }

.rail-toggle {
    margin-top: auto;
    display: flex; align-items: center; gap: var(--space-2);
    padding: var(--space-2) var(--space-4);
    background: none; border: 0; box-shadow: none;
    color: var(--rail-text-dim);
    font-size: var(--text-xs);
    cursor: pointer;
    width: 100%;
}
.rail-toggle:hover { background: var(--rail-2); color: var(--rail-active); }
:root[data-rail="collapsed"] .rail-toggle span { display: none; }
:root[data-rail="collapsed"] .rail-toggle { justify-content: center; }

@media (max-width: 900px) {
    /* The columns that answer "which of these is it" survive; the ones that
       answer "tell me more about this one" go, because that answer is a click
       away on the server's own page. */
    .infra .optional { display: none; }

    /* Stacked, the level column no longer has neighbours to line up with. */
    .alert-level { min-width: 0; }
    .decision-meta { margin-left: 0; }
}

/* --- Rail sections -------------------------------------------------------
   Collapsed by default so the rail shows structure rather than a routing
   table. A section is a summary row at the same height as a nav item, so the
   rail reads as one column and not as two competing rhythms. */

.nav-group { border: 0; }
.nav-group > summary {
    display: flex; align-items: center; gap: var(--space-2);
    padding: var(--space-2) var(--space-4);
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: .07em;
    font-weight: var(--weight-semi);
    color: var(--rail-text-dim);
    cursor: pointer;
    list-style: none;
    user-select: none;
}
.nav-group > summary::-webkit-details-marker { display: none; }
.nav-group > summary::after {
    content: "";
    margin-left: auto;
    width: 5px; height: 5px;
    border-right: 1.5px solid currentColor;
    border-bottom: 1.5px solid currentColor;
    transform: rotate(-45deg);
    transition: transform var(--fast) var(--ease);
}
.nav-group[open] > summary::after { transform: rotate(45deg); }
.nav-group > summary:hover { color: var(--rail-active); background: var(--rail-2); }
.nav-group > summary:focus-visible { outline: 2px solid var(--rail-accent); outline-offset: -2px; }

/* The way back out of an account. Entering one is otherwise a one-way door:
   the platform rail is gone by design, so something has to offer to leave. */
.scope-out {
    display: flex; align-items: center; gap: var(--space-2);
    padding: var(--space-2) var(--space-4);
    color: var(--rail-text-dim);
    font-size: var(--text-xs);
    text-decoration: none;
    border-bottom: 1px solid var(--rail-border);
}
.scope-out:hover { color: var(--rail-active); background: var(--rail-2); text-decoration: none; }

.nav-scope { padding: var(--space-3) var(--space-4) var(--space-2); }
.nav-scope-label {
    display: block;
    font-size: var(--text-xs); text-transform: uppercase; letter-spacing: .07em;
    color: var(--rail-text-dim);
}
.nav-scope-name {
    display: block;
    font-size: var(--text-md); font-weight: var(--weight-semi);
    color: var(--rail-active);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

:root[data-rail="collapsed"] .nav-group > summary,
:root[data-rail="collapsed"] .scope-out span,
:root[data-rail="collapsed"] .nav-scope { display: none; }
/* Collapsed to icons, a closed section would hide its items with no way to
   open them, so every section is forced open and only the icons remain. */
:root[data-rail="collapsed"] .nav-group > nav { display: flex; }

/* Platform tooling inside an account, for somebody who is both. Set apart, so
   it never reads as part of the customer's own estate. */
.nav-group-admin { margin-top: var(--space-3); border-top: 1px solid var(--rail-border); }
.nav-group-admin > summary { color: var(--rail-accent); }

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

   A server-rendered SVG rather than a charting library, for the same reason the
   sparkline is one: the panel is patched on servers with no Node toolchain, and
   the Content-Security-Policy here is `script-src 'self'` with no unsafe-inline,
   so a CDN chart is not merely heavy — it is blocked outright and draws nothing.

   Geometry is computed in Blade and carried on SVG presentation attributes
   (x, width), which are not the `style` attribute the policy refuses. Colour is
   here, so the same chart is correct in both themes without a second asset.
--------------------------------------------------------------------------- */
.geo-chart { margin: 0; width: 100%; }
.geo-chart svg { display: block; width: 100%; height: auto; overflow: visible; }

.geo-track   { fill: var(--surface-3); }
.geo-success { fill: var(--success); }
.geo-failed  { fill: var(--danger); }

/* Places with no country — private, loopback, reserved, not in the database.
   Deliberately not a country colour: these are not places. */
.geo-unplaceable .geo-track { fill: var(--surface-3); }
.geo-unplaceable .geo-success { fill: var(--muted); }
.geo-unplaceable .geo-failed { fill: var(--warning); }

.geo-label {
    fill: var(--text);
    font-size: 13px;
    font-family: inherit;
}
.geo-sub {
    fill: var(--muted);
    font-size: 11px;
    font-family: inherit;
}
.geo-count {
    fill: var(--muted);
    font-size: 12px;
    font-family: inherit;
    font-variant-numeric: tabular-nums;
}

.geo-legend {
    display: flex; flex-wrap: wrap; gap: var(--space-4);
    margin-top: var(--space-3);
    font-size: var(--text-xs); color: var(--muted);
}
.geo-legend span { display: inline-flex; align-items: center; gap: var(--space-2); }
.geo-key { width: 10px; height: 10px; border-radius: 2px; display: inline-block; }
.geo-key-success { background: var(--success); }
.geo-key-failed  { background: var(--danger); }
.geo-key-nowhere { background: var(--muted); }

/* ---------------------------------------------------------------------------
   Address translation (server Network tab)

   One block per recorded mapping: what it is, where it goes, and the DNS
   records that point at it. The head is a row that wraps rather than a grid,
   because the endpoints are of wildly different lengths — an IPv6 address with
   a port is three times the width of an IPv4 one — and a fixed grid would
   leave a hole beside every short one.
--------------------------------------------------------------------------- */

.nat-mapping-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-2) var(--space-4);
    margin-bottom: var(--space-3);
}
.nat-mapping-head > div { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2); }

.nat-endpoint { font-weight: var(--weight-medium); }

.nat-records-heading {
    font-size: var(--text-sm);
    color: var(--muted);
    font-weight: var(--weight-medium);
    text-transform: none;
    margin: var(--space-5) 0 var(--space-2);
}

/* Last time anybody asked the address itself what was there. Its own row
   under the mapping: "never checked" has to sit at the same weight as a
   result, not be the absence of one. */
.nat-verification {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-2) var(--space-3);
    margin-top: var(--space-4);
    padding-top: var(--space-3);
    border-top: 1px solid var(--border);
}
.nat-verification .muted { flex: 1 1 20rem; min-width: 0; }

/*
 * Long unbreakable values in a key/value table.
 *
 * A UUID, an SSH fingerprint or "IPv4 / IPv6" with both present is fifty-odd
 * characters with nowhere to break. In a two-column table inside a card, that
 * sets the table's minimum width, the card cannot shrink below it, and the
 * page grows a horizontal scrollbar — the one thing the layout rules here say
 * must never happen.
 *
 * `.detail-list dd` already solved this for definition lists; table cells were
 * simply never given the same treatment. anywhere rather than break-all so a
 * value only breaks when it genuinely does not fit, instead of wrapping mid-
 * token whenever the column is narrow.
 */
td.mono,
td .mono {
    overflow-wrap: anywhere;
}

/* ==========================================================================
   18. The screen pattern
   --------------------------------------------------------------------------
   THE HOUSE STYLE FOR EVERY OPERATIONS SCREEN. Follow it; do not invent a
   second dialect on the server, support or DNS pages.

   1. DENSITY. Tiles and table cells are padded `var(--space-2) var(--space-3)`;
      list rows `var(--space-1) var(--space-3)`; an icon sits `var(--space-1)`
      from its word. Blocks are `var(--space-3)` apart, tiles within a grid
      `var(--space-2)`. Label `--text-xs` uppercase, figure `--text-lg` (or
      `--text-xl` for the four that matter most), everything else `--text-sm`.
      Never hardcode a pixel: if the scale has no step for it, the design is
      wrong, not the scale.
   2. LEAD WITH THE DECISION. Whatever needs a person goes in a `.decision`
      block at the top, worst first. Breakdowns follow as `.segment` tiles,
      context as `.kpi` tiles, detail as a table, the rest in `.widget`s. If a
      screen has nothing to decide, its `.decision` block says so in one line
      and takes one line to do it.
   3. STATUS IS NEVER A COLOUR ALONE. Use `<x-status>`: shape, colour and word
      together, in that order of importance. Five states only — ok, warn,
      critical, info, idle. `icon-only` for a status *column*, which keeps the
      word for screen readers and the tooltip; nowhere else. Words take
      `--success-text`/`--warning-text`, graphics take `--success`/`--warning`;
      the bright pair is under 3.2:1 on white and must never carry type. The
      component renders `.state-*`; `.status-*` is the old badge vocabulary and
      is not the same thing. Do not reintroduce a bare coloured dot — that is
      what this replaced.
   4. FIGURE, BAR OR LINE. A plain figure when one number is the whole answer.
      A `.gauge` when the number is a proportion of a known whole and the
      outlier matters more than the value (disk, memory, an allowance). An
      `<x-sparkline>` only for a series the database actually stores, one
      point per period including the empty periods. Never a shape invented
      from a single reading, and never a trend nothing was recorded for.
   5. SAY WHAT THE NUMBER MEANS. Every figure is followed by the sentence an
      operator would say about it — "all 18 running", not "18"; "nothing
      overdue", not a second number. Where a state exists, that sentence is an
      `<x-status>`.
   6. NOTHING RECORDED IS NOT ZERO. A reading nobody took is `.blank` — an em
      dash, muted, with a `title` saying why — and its `.gauge` draws no bar
      at all, because an empty track is how a bar draws 0%. A list nobody has
      filled gets a sentence in `.widget-empty`/`.table-empty`. A real count of
      zero is the word "none" in a list row, and stays a digit in a headline
      figure where the sentence under it says what none of them means.
      `0`, `0%` and a flat sparkline belong only to things that genuinely
      happened zero times.
   ========================================================================== */

/* `<x-status>` renders `.state`, NOT `.status`.
   --------------------------------------------------------------------------
   `.status-ok`, `.status-warning` and friends were taken years ago by the badge
   modifiers in section 9, and `.meter` by the progress bars in section 10. Both
   sit earlier in this file, so a component reusing those names would have
   quietly inherited a pill background it never asked for. The vocabulary here
   is `.state-*` and `.gauge`; leave the older names to the older components.

   Two things to know before using it:

   The glyph is centred on the text rather than sat on its baseline — at 13px
   beside 11.5px type, a baselined icon reads as though it has fallen off the
   line. `vertical-align` keeps that honest when a state sits inline in a
   sentence instead of being a flex item.

   And because a state is an inline-flex box whose first child is an SVG, a flex
   row aligned by `baseline` has no text baseline to take and falls back to the
   icon's bottom edge, which drops the state's own words below the line they
   belong on. Every row here that can hold one aligns by `center` or
   `flex-start`. Remember that before putting `align-items: baseline` on a row
   on the server, support or DNS screens. */
.state {
    display: inline-flex; align-items: center; gap: var(--space-1);
    min-width: 0; vertical-align: middle;
}
.state-icon { width: 13px; height: 13px; flex: 0 0 13px; }
.state-text { min-width: 0; overflow-wrap: anywhere; }

/* The reading weights, not the graphic ones: a state is a word first. */
.state-ok       { color: var(--success-text); }
.state-warn     { color: var(--warning-text); }
.state-critical { color: var(--danger-text); }
.state-info     { color: var(--info-text); }
.state-idle     { color: var(--muted); }

/* A proportion of a known whole. The figure and the bar say the same thing, so
   the bar can be a hairline: it is there to make the outlier findable without
   reading every number in the column. Thresholds are a convention, so anything
   past one must also be said in words somewhere on the row. */
.gauge { display: inline-flex; align-items: center; gap: var(--space-2); min-width: 62px; }
.gauge .figure { font-variant-numeric: tabular-nums; min-width: 30px; text-align: right; }
.gauge .track { flex: 1; height: 3px; border-radius: var(--radius-full); background: var(--surface-3); overflow: hidden; }
.gauge .fill { display: block; height: 100%; background: var(--success); }
.gauge.warn .fill { background: var(--warning); }
.gauge.critical .fill { background: var(--danger); }
/* Nobody took this reading. Not a full bar, not an empty one — no bar at all,
   because an empty track is exactly how a bar draws 0%. Hidden rather than
   removed so the column still lines up with the ones that did report. */
.gauge.unknown .figure { color: var(--muted); }
.gauge.unknown .track { visibility: hidden; }

/* The absence of a value, as opposed to a value of nothing. */
.blank { color: var(--muted); }

/* One name, checked.
   --------------------------------------------------------------------------
   A server answers to several names and each gets its own verdict, its own
   caveat and sometimes its own table of records, stacked in one panel. Without
   a separator the second name's heading reads as a continuation of the first
   one's evidence — which is precisely the confusion the per-name split exists
   to remove.

   A rule rather than `.stack-3` or a run of `.mt-3`: the blocks need a line
   between them, not only space, and the last one must not leave a rule
   floating above the panel's own edge. */
.fqdn-check { padding: var(--space-3) 0; border-bottom: 1px solid var(--border); }
.fqdn-check:first-of-type { padding-top: 0; }
.fqdn-check:last-of-type { padding-bottom: 0; border-bottom: 0; }
.fqdn-check .hint { margin-top: var(--space-1); }

/* --- A table that follows the pattern -------------------------------------
   Rule 1 puts table cells at `var(--space-2) var(--space-3)` and their text at
   `--text-sm`, which the panel's generic `th, td` does not do: it is set for a
   settings form's two-column table, and at 25 support tickets it costs a third
   of the screen.

   Density only. The border, the radius and the clipped corners already come
   from `.card.flush`, so this is a modifier on that rather than a second panel
   component — which is also why the dashboard's `.infra` is not reused here.
   `.infra` is the same contract under the overview page's own name, and a
   support queue carrying `class="infra"` would be a name that lies about what
   the table holds. */

.dense-table thead th { padding: var(--space-2) var(--space-3); }
.dense-table tbody td { padding: var(--space-2) var(--space-3); font-size: var(--text-sm); }

/* A status column is a glyph wide and nothing more. */
.dense-table .cell-state { width: 1%; padding-right: 0; }

/* What a row says about itself once you have found it — its reference, whose
   account it is, when it last moved. Under the name rather than in columns of
   their own, for the reason the dashboard puts an agent version under a
   hostname: nobody scans a list by them. */
.dense-table .row-sub { display: block; }

/* Beside the sentence, never instead of it: the words say how long is left and
   the bar is there so the row closest to its deadline can be found without
   reading every row. Its own line, because a hairline sharing a baseline with
   type reads as an underline. */
.dense-table .gauge { display: flex; margin-top: var(--space-1); }

/* The blocks on a pattern screen are spaced by the flex gap on `.ops`. A card
   inside one would add its own bottom margin to that and open a gap half again
   as wide as every other gap on the page. */
.ops > .card { margin-bottom: 0; }

@media (max-width: 900px) {
    /* The columns that answer "which of these is it" survive; the ones that
       answer "tell me more about this one" go, because that answer is a click
       away on the ticket's own page. */
    .dense-table .optional { display: none; }
}


/* ==========================================================================
   19. Choosing backup storage
   --------------------------------------------------------------------------
   Follows §18. Nothing here is a new dialect: the screen's verdict is a
   `.decision`, its states are `<x-status>`, and its rows are the ordinary
   table. The only thing that needed its own rules is the type chooser, because
   picking storage is a choice between five things that each need a sentence —
   too much for a `<select>`, and a radio list would put the sentence in a
   place nobody reads before deciding.
   ========================================================================== */

.storage-types {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: var(--space-2);
    margin-bottom: var(--space-3);
}

.storage-type {
    display: flex; flex-direction: column; gap: var(--space-1);
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
    text-decoration: none; color: inherit;
}
.storage-type:hover { background: var(--surface-2); text-decoration: none; }

/* The chosen one is marked by a border rather than a fill: the form below it
   is already the strongest signal that a choice has been made, and a filled
   tile competing with it reads as two selections. */
.storage-type.chosen { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }

.storage-type-name { font-weight: var(--weight-medium); font-size: var(--text-sm); }
.storage-type-note { font-size: var(--text-xs); color: var(--muted); }
