/* ==========================================================================
   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); }

/* ==========================================================================
   20. Choosing an account's owner
   --------------------------------------------------------------------------
   Every account has an owner, so the new-account form collects one. There are
   three answers — create them now, pick somebody who already signs in, or
   decide later — and only the fields belonging to the chosen answer are shown.

   Done with `:checked` and sibling selectors rather than script, because the
   panel's Content-Security-Policy has no `unsafe-inline` and a form is not
   worth a JavaScript dependency. It degrades the right way: with no CSS at all
   every field is visible and the server still validates the choice, so the
   worst case is a longer form rather than a form that cannot be completed.
   ========================================================================== */

.owner-choice {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: var(--space-3);
    margin: var(--space-4) 0;
}

.owner-choice legend {
    padding: 0 var(--space-2);
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
}

/* The radio and its label read as one control on one line. */
.owner-choice > input[type="radio"] {
    margin-right: var(--space-1);
}

.owner-choice > label {
    display: inline;
    margin-right: var(--space-3);
    font-weight: normal;
}

.owner-panel {
    margin-top: var(--space-3);
    padding-top: var(--space-3);
    border-top: 1px solid var(--border);
}

/* Hidden by default so that only the selected answer's fields are shown. The
   sibling combinator needs the radios to precede the panels, which is why the
   markup is ordered that way. */
.owner-choice #owner_mode_new:not(:checked) ~ .owner-new,
.owner-choice #owner_mode_existing:not(:checked) ~ .owner-existing {
    display: none;
}

/* Available to a screen reader, absent from the page. Used where a field's
   purpose is obvious in context but a label is still owed to somebody who
   cannot see the field above it. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ==========================================================================
   21. The MSP Reboot mark
   --------------------------------------------------------------------------
   The wordmark and the power mark are the ones the company's own site uses,
   cropped out of a 1024² PNG that was 98% empty canvas and 1.4 MB. Both are
   served from this origin: the Content-Security-Policy has no external image
   source, and a logo is not worth an exception.

   The artwork is transparent and carries its own colour — deep blue lettering
   with an orange power symbol — so it needs no treatment in either theme, and
   deliberately gets none. Tinting a brand to match a UI is how a brand stops
   being recognisable.
   ========================================================================== */

.brand-mark-img {
    width: 28px;
    height: 28px;
    flex: 0 0 28px;
    object-fit: contain;
}

.brand-wordmark {
    /* Sized by width alone; the height attribute on the element preserves the
       aspect ratio and stops the card jumping while the image loads. */
    width: 100%;
    max-width: 240px;
    height: auto;
    margin: 0 auto var(--space-2);
}

/* The mark sits on a dark rail in dark mode and a light one otherwise. The
   artwork's blue is dark enough to disappear against a very dark rail, so it
   gets the same subtle lift the rest of the chrome uses rather than a filter
   that would alter the brand's colours. */
@media (prefers-color-scheme: dark) {
    .brand-mark-img { filter: brightness(1.15); }
}

:root[data-theme="dark"] .brand-mark-img { filter: brightness(1.15); }
:root[data-theme="light"] .brand-mark-img { filter: none; }

/* The re-check control sits with the verdict it refreshes, not below the list
   of alerts: it answers "is this still true?", which is a question about the
   headline. Pushed to the end of the row so it never comes between the verdict
   and its reason. */
.decision-head .decision-action {
    margin-left: auto;
    margin-bottom: 0;
}

/* ==========================================================================
   22. The customer's dashboard
   --------------------------------------------------------------------------
   Follows §18 and adds no dialect of its own: the verdict is a `.decision`,
   the services are `.segment` tiles, the counts are `.kpi`s and the lists are
   `.widget`s, exactly as on the operations screens. Two rules were genuinely
   missing and are here rather than invented per-screen.
   ========================================================================== */

/* An allowance inside a `.kpi`.
   --------------------------------------------------------------------------
   §18.4 asks for a gauge wherever a figure is a proportion of a known whole,
   and a plan's allowance is exactly that. `.gauge` is `inline-flex` so that it
   can sit in a sentence; in a `.kpi`, which is a flex column, it needs to be a
   block-level row or the track collapses to its 62px minimum and the bar stops
   being comparable between tiles.

   Its own line, for the reason `.dense-table .gauge` has one: a hairline
   sharing a baseline with type reads as an underline. */
.kpi .gauge { display: flex; margin-top: 2px; }

/* A `.widget-row` whose left-hand side is a state and a name.
   --------------------------------------------------------------------------
   The row is `justify-content: space-between`, which spaces three children
   evenly and leaves the status glyph marooned in the middle of the row instead
   of attached to the name it describes. Grouping them makes the row two things
   again — what it is, and what is on it.

   `min-width: 0` because the name is a domain and domains are long; without it
   the flex item refuses to shrink and the page grows a horizontal scrollbar,
   which the layout rules say must never happen. */
.widget-row .row-main {
    display: inline-flex; align-items: center; gap: var(--space-2);
    min-width: 0; overflow-wrap: anywhere;
}

/* The sentence behind the verdict.
   --------------------------------------------------------------------------
   The `.decision-head` is a single flex row — a verdict and a meta note pushed
   to its end — so an explanation of the verdict cannot go inside it without
   becoming a third column and squeezing both. It goes underneath, above the
   rows it introduces, and reads as elaboration rather than as another alert:
   muted, no marker, no level.

   Only rendered when the verdict is something other than plain good news. "We
   can see this as well, you do not need to report it" is worth a line; "nothing
   needs you right now" said twice is not. */
.decision-note {
    margin: 0;
    padding: var(--space-2) var(--space-3);
    border-top: 1px solid var(--border);
    font-size: var(--text-sm);
    color: var(--muted);
}

/* ==========================================================================
   23. The public site
   --------------------------------------------------------------------------
   hosting.mspreboot.com: the shop window, served by this application from the
   same document root and the same stylesheet as the panel.

   §18 governs operations screens and this is not one. An operator is working;
   a stranger is reading, on a phone, having arrived from a search result, and
   11.5px type in a 1200px column is the wrong answer for them however right it
   is for a server list. So `body.site` resets the base size and the page is
   built from the same tokens at a larger step — the spacing scale, the radii,
   the colours and the two themes are all the panel's, unchanged. Nothing here
   is a second palette or a second grid.

   No script anywhere on these pages, and no `style` attribute: the deployed
   Content-Security-Policy has no `unsafe-inline`, and the one page a stranger's
   browser loads is the last place worth an exception.
   ========================================================================== */

body.site {
    /* The one deliberate departure. Everything else on the page is the panel's
       own scale; this is the reading size a marketing page needs. */
    font-size: 15px;
    line-height: 1.6;
}

.site-main { max-width: 1080px; margin: 0 auto; padding: var(--space-10) var(--space-5) var(--space-16); }

/* --- Bar and footer ------------------------------------------------------- */

.site-bar {
    display: flex; align-items: center; justify-content: space-between;
    flex-wrap: wrap; gap: var(--space-4);
    max-width: 1080px; margin: 0 auto;
    padding: var(--space-5);
}

.site-brand { display: inline-flex; }

/* Sized by width alone; the height attribute on the element preserves the
   aspect ratio and stops the bar jumping while the image loads. The artwork
   carries its own colour and is deliberately not tinted to match the UI —
   see §21. */
.site-wordmark { width: 150px; height: auto; }

.site-nav { display: flex; align-items: center; gap: var(--space-5); flex-wrap: wrap; }
.site-nav a { color: var(--text); font-weight: var(--weight-medium); }
.site-nav a:hover { color: var(--accent); }
.site-nav .button { font-size: inherit; }
.site-nav .button:hover { color: var(--on-accent); }

.site-foot {
    max-width: 1080px; margin: 0 auto;
    padding: var(--space-6) var(--space-5) var(--space-10);
    border-top: 1px solid var(--border);
    color: var(--muted); font-size: var(--text-md);
    display: flex; justify-content: space-between; flex-wrap: wrap; gap: var(--space-3);
}
.site-foot p { margin: 0; }
.site-foot a { margin-right: var(--space-4); }
.site-foot a:last-child { margin-right: 0; }

/* --- Blocks --------------------------------------------------------------- */

.site-hero { padding: var(--space-10) 0 var(--space-8); max-width: 44rem; }
.site-hero-narrow { padding-bottom: var(--space-5); }
.site-hero h1 { font-size: 2.25rem; letter-spacing: -.02em; margin-bottom: var(--space-4); }

.site-lede { font-size: 1.125rem; color: var(--text-2); }

.site-cta { display: flex; flex-wrap: wrap; gap: var(--space-3); margin-top: var(--space-5); }
.site-cta .button { font-size: 1rem; padding: var(--space-3) var(--space-5); }

.site-section { padding: var(--space-10) 0; border-top: 1px solid var(--border); }
.site-section > h2 { font-size: 1.5rem; letter-spacing: -.01em; margin-bottom: var(--space-4); }

.site-closing { text-align: center; }
.site-closing .site-cta { justify-content: center; }

/* --- What you get --------------------------------------------------------- */

.site-features {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: var(--space-6); margin-top: var(--space-6);
}
.site-feature h3 { font-size: 1.0625rem; margin: var(--space-2) 0; }
.site-feature p { color: var(--text-2); margin: 0; }
.site-feature .icon { width: 22px; height: 22px; flex-basis: 22px; color: var(--accent); }

/* --- Plan cards ----------------------------------------------------------- */

.site-plans {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(268px, 1fr));
    gap: var(--space-4); margin-top: var(--space-6);
}

.site-plan {
    display: flex; flex-direction: column;
    padding: var(--space-6);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--surface);
}

.site-plan-kind {
    font-size: var(--text-sm); text-transform: uppercase; letter-spacing: .06em;
    color: var(--muted); font-weight: var(--weight-semi);
}
.site-plan h3 { font-size: 1.25rem; margin: var(--space-1) 0 var(--space-4); }

.site-plan-price { margin: 0; }
.site-plan-price strong { font-size: 1.875rem; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.site-plan-per { color: var(--muted); }

.site-plan-note { margin: var(--space-1) 0 0; color: var(--muted); font-size: var(--text-md); }
.site-plan-desc { margin: var(--space-4) 0 0; color: var(--text-2); }

.site-plan-features { list-style: none; margin: var(--space-4) 0 0; padding: 0; }
.site-plan-features li {
    display: flex; align-items: flex-start; gap: var(--space-2);
    padding: var(--space-1) 0; color: var(--text-2);
}
.site-plan-features .icon {
    width: 16px; height: 16px; flex-basis: 16px;
    color: var(--success); margin-top: 3px;
}

/* Pushed to the bottom edge so that cards of different heights still line their
   buttons up — a row where one call to action floats halfway up reads as though
   that plan has less to it. */
.site-plan-go { margin-top: auto; align-self: stretch; text-align: center; }

/* `margin-top: auto` puts the button on the bottom edge by absorbing the card's
   free space, which is exactly what lines the buttons up across a row — and on
   the tallest card in that row there is no free space left, so it would sit
   flush against whatever it follows. The gap therefore belongs to the element
   above it rather than to the button. `:last-of-type` because the last <p> is
   the price, a note or the description depending on the plan. */
.site-plan p:last-of-type,
.site-plan-features { margin-bottom: var(--space-5); }

/* --- The contact form ----------------------------------------------------- */

.site-form-section { max-width: 34rem; }
.site-form input[type="text"],
.site-form input[type="email"],
.site-form select,
.site-form textarea { font-size: inherit; }

/* A field a person never sees and a script fills in.
   --------------------------------------------------------------------------
   Hidden here rather than with a `hidden` attribute or a `style` attribute: the
   first is respected by the naive scrapers this is meant to catch, and the
   second is exactly what the Content-Security-Policy forbids. `absolute` and
   off-canvas rather than `display: none` for the same reason — a field that is
   `display: none` is one a form filler skips. */
.site-honeypot {
    position: absolute;
    left: -9999px;
    width: 1px; height: 1px;
    overflow: hidden;
}

@media (max-width: 640px) {
    .site-hero h1 { font-size: 1.75rem; }
    .site-main { padding-top: var(--space-6); }
    .site-foot { flex-direction: column; }
}


/* ==========================================================================
   23. Which countries may sign in
   --------------------------------------------------------------------------
   The Security tab's world map and the country list under it.

   Two things about this block are worth knowing before changing it.

   The map is not the control. Every country on it is an `<a href="#country-XX">`
   pointing at the checkbox that actually holds the value, so it works with the
   panel's JavaScript switched off and it is in the tab order because an anchor
   is, not because something here gave it a tabindex. Nothing below styles a
   `<path>` as though it were a button.

   And blocked is a hatch, not just a red. §18.3 — status is never a colour
   alone — is hardest to honour on a map, because there is no word beside each
   shape and about one operator in twelve cannot separate the red from the grey.
   The pattern carries the state, the colour reinforces it, and the checkbox
   list under the map spells it out in words for every country.
   ========================================================================== */

.country-map { margin: var(--space-3) 0; }

.country-map-canvas {
    display: block;
    width: 100%; height: auto;
    max-height: 58vh;
    /* The land is drawn as fills; this is the water behind it. */
    background: transparent;
}

.country-map-sea { fill: var(--surface-2); stroke: var(--border); stroke-width: 1; }

/* An anchor in SVG is still a link: it gets the pointer and the focus ring. */
.country-shape { cursor: pointer; outline: none; }
.country-shape path,
.country-shape circle {
    stroke: var(--border-strong);
    stroke-width: 0.4;
    stroke-linejoin: round;
    /* Fast enough to feel like a toggle rather than an animation. */
    transition: fill .12s ease-out, fill-opacity .12s ease-out;
}

.country-allowed path,
.country-allowed circle { fill: var(--surface-3); }

.country-blocked path { fill: url(#country-blocked-hatch); stroke: var(--danger); stroke-width: 0.6; }
/* A dot 3.4 units across cannot show a hatch, so the marks say it with weight
   instead: solid where a country is a pattern. */
.country-blocked circle { fill: var(--danger); stroke: var(--danger); }

.country-hatch-bg { fill: var(--danger-subtle); }
.country-hatch-line { stroke: var(--danger); }

/* Where this request is coming from. Not a state of the setting — a fact about
   the reader — so it takes the informational colour rather than a warning one,
   and the list below says in words that it cannot be blocked. */
.country-here path,
.country-here circle { fill: var(--info-subtle); stroke: var(--info); stroke-width: 0.9; }

.country-shape:hover path,
.country-shape:hover circle { fill-opacity: .75; }

/* The ring has to be drawn on the shape: an SVG anchor's own outline is a
   rectangle around the whole bounding box, which for Russia is most of the map. */
.country-shape:focus-visible path,
.country-shape:focus-visible circle {
    stroke: var(--accent);
    stroke-width: 2;
    paint-order: stroke;
}

.country-map-legend {
    display: flex; flex-wrap: wrap; align-items: center;
    gap: var(--space-2) var(--space-4);
    margin-top: var(--space-2);
    font-size: var(--text-sm); color: var(--text-2);
}

/* Each key carries its own swatch, and the blocked one carries the hatch, so
   the legend is readable at the same moment the map is. */
.country-key { display: inline-flex; align-items: center; gap: var(--space-1); }
.country-key::before {
    content: ""; width: 14px; height: 10px;
    border-radius: 2px; border: 1px solid var(--border-strong);
}
.country-key-allowed::before { background: var(--surface-3); }
.country-key-blocked::before {
    border-color: var(--danger);
    background: repeating-linear-gradient(
        45deg,
        var(--danger) 0 2px,
        var(--danger-subtle) 2px 5px
    );
}
.country-key-here::before { background: var(--info-subtle); border-color: var(--info); }

/* --- What is blocked, as text ---------------------------------------------
   The audit answer, which a map cannot be. Read before the form on purpose:
   it says what is saved, not what is ticked. */

.country-blocked-list { list-style: none; margin: var(--space-2) 0 var(--space-3); padding: 0; }
.country-blocked-list li {
    display: flex; flex-wrap: wrap; align-items: center;
    gap: var(--space-1) var(--space-3);
    padding: var(--space-1) 0;
    border-bottom: 1px solid var(--border);
}
.country-blocked-list li:last-child { border-bottom: 0; }
.country-blocked-name { font-size: var(--text-sm); }

/* Words take the readable weight, never the graphic one: --warning is 3.1:1 on
   white and must not carry type. */
.country-note-warn { color: var(--warning-text); }

/* --- The list that is actually the setting -------------------------------- */

.country-list { border: 0; padding: 0; margin: 0 0 var(--space-3); min-width: 0; }
.country-list legend {
    font-size: var(--text-xs); text-transform: uppercase; letter-spacing: .04em;
    color: var(--muted); padding: 0;
}

/* 250 rows is a page of its own if it is allowed to be. Scrolled inside the
   card instead, which also keeps the map and the Save button on screen
   together — the two things somebody moves between while deciding. */
.country-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
    gap: 0 var(--space-3);
    max-height: 420px;
    overflow-y: auto;
    margin-top: var(--space-2);
    padding: var(--space-1) var(--space-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface-2);
}

.country-option {
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: center;
    gap: var(--space-1) var(--space-2);
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-sm);
    font-size: var(--text-sm);
    cursor: pointer;
}
.country-option:hover { background: var(--surface-3); }
.country-option:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: -2px; }

.country-option input { margin: 0; }
.country-option-name { min-width: 0; overflow-wrap: anywhere; }
.country-option-code { font-size: var(--text-xs); color: var(--muted); }

/* The state in words, on every row, for every country — not only the blocked
   ones. A row that says nothing is a row whose state has to be inferred from a
   tick box's shape, and that is the same mistake as inferring it from a colour. */
.country-option-state {
    grid-column: 2 / -1;
    font-size: var(--text-xs);
    color: var(--muted);
}

.country-option.is-blocked { background: var(--danger-subtle); }
.country-option.is-blocked .country-option-state { color: var(--danger-text); }
.country-option.is-here .country-option-state { color: var(--info-text); }
.country-option:has(input:disabled) { cursor: not-allowed; }

/* A link that jumps past the map, for somebody arriving by keyboard. Not a
   `.skip-link`: that one is the off-canvas rail skip and hides until focused,
   and this one should be visible to everybody, because scrolling past 241
   countries with a mouse is no better. */
.skip-inline { font-size: var(--text-sm); }

@media (max-width: 640px) {
    /* The map stops being readable long before this, but it stops being useful
       here: a 240-unit-wide Luxembourg is not a click target. The list below is
       the control and loses nothing. */
    .country-map-canvas { max-height: 40vh; }
    .country-grid { grid-template-columns: 1fr; max-height: 320px; }
}

/* A skip target that is only focusable programmatically must not then draw a
   focus ring around a 250-row list when it is clicked into. */
.country-list:focus { outline: none; }
.country-list:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ==========================================================================
   24. The public site, dark
   --------------------------------------------------------------------------
   §23 built the site from the panel's tokens, which already carry a dark
   value each, so `data-theme="dark"` on the document is most of the work. What
   is left is the difference between a UI that happens to be dark and a page
   that was designed that way.

   Two things change on a dark page and only on a dark page. Large text at full
   contrast glares, so headings step *down* from pure foreground rather than
   up. And a border is a poor separator against a dark ground — the eye reads
   a lighter surface instead — so panels lift by background, not by outline.

   Nothing here is a second palette: every value is an existing token or a
   transparency over one, so the panel's own dark mode and this page cannot
   drift apart.
   ========================================================================== */

:root[data-theme="dark"] body.site {
    /* A shade under the panel's own, so the cards sitting on it can lift by
       background alone rather than needing a border to be found. */
    background: var(--surface-sunken, var(--bg));
}

/* Headings at slightly less than full contrast. On a dark ground the largest
   type is what glares, and it is also the type that needs the least help. */
:root[data-theme="dark"] .site-main h1,
:root[data-theme="dark"] .site-main h2 {
    color: color-mix(in srgb, var(--text) 92%, transparent);
    letter-spacing: -0.015em;
}

/* Body copy is the opposite case: muted grey that reads on white is thin on
   black, so it comes up rather than down. */
:root[data-theme="dark"] .site-main p,
:root[data-theme="dark"] .site-foot {
    color: color-mix(in srgb, var(--text) 74%, transparent);
}

/* Panels lift, rather than outline. */
:root[data-theme="dark"] .site-main .card,
:root[data-theme="dark"] .site-main .plan-card {
    background: color-mix(in srgb, var(--text) 4%, var(--surface));
    border-color: color-mix(in srgb, var(--text) 10%, transparent);
}

:root[data-theme="dark"] .site-bar,
:root[data-theme="dark"] .site-foot {
    border-color: color-mix(in srgb, var(--text) 10%, transparent);
}

/* The wordmark carries deep blue lettering that disappears against a dark
   ground. Lifted the same way the rail's mark is in §21 — brightness, not a
   hue shift, so the brand's own colours are not restated. */
:root[data-theme="dark"] .site-wordmark { filter: brightness(1.25); }

/* A form on a dark page needs its fields to read as recessed rather than as
   more cards. */
:root[data-theme="dark"] .site-main input[type="text"],
:root[data-theme="dark"] .site-main input[type="email"],
:root[data-theme="dark"] .site-main select,
:root[data-theme="dark"] .site-main textarea {
    background: color-mix(in srgb, var(--text) 6%, transparent);
    border-color: color-mix(in srgb, var(--text) 16%, transparent);
    color: var(--text);
}

:root[data-theme="dark"] .site-main input:focus,
:root[data-theme="dark"] .site-main select:focus,
:root[data-theme="dark"] .site-main textarea:focus {
    border-color: var(--accent);
}

/* The hidden honeypot must stay hidden in this theme too — it is positioned,
   not coloured, so nothing above can reveal it, but it is worth stating. */
:root[data-theme="dark"] .site-honeypot { display: none; }


/* ==========================================================================
   25. Platform settings, condensed
   --------------------------------------------------------------------------
   Follows §18. Forty-nine settings on one tab, each of which used a full page
   width for a value that is usually a port number, and put four lines and two
   lots of `--space-4` around it. The page was several screens tall and almost
   all of it was air.

   Everything here is DENSITY ONLY. No field is hidden, no group is dropped, and
   the group summaries carry their own settings' names as text so a shut group
   still answers "which card is that on?" — including to find-in-page, which is
   the part that would otherwise quietly break when a group is collapsed.

   Scoped under `.settings-dense` throughout, and that is deliberate. `.setting`,
   `.setting-toggle` and `.checkbox` are the shared settings vocabulary from §7
   and the Support tab renders `.setting setting-toggle` with them; retuning
   those rules globally would have re-spaced a screen this change never looked
   at. The wrapper is one class in one template and costs a specificity point.
   ========================================================================== */

/* The note about what is deliberately not on this screen. Worth reading once,
   worth finding again, not worth a paragraph above the fold every visit. */
.settings-note { margin-bottom: var(--space-3); }
.settings-note summary { display: flex; align-items: center; gap: var(--space-1); }

.settings-dense > .settings-group { margin-bottom: var(--space-3); }

/* The group heading IS the summary, so it is styled as a heading and not as the
   accent-coloured "show more" link of `.disclosure`. The marker stays: `display`
   is left at the summary default of `list-item`, and the two children are set
   to block instead, because a flexed summary drops its own disclosure triangle
   and there is then nothing on the row that says it opens. */
.settings-dense .settings-group-summary {
    cursor: pointer;
    padding: var(--space-1) 0;
}

.settings-dense .settings-group-name {
    display: block;
    margin: 0;
    font-size: var(--text-base);
}

/* Set in the figure's weight rather than a badge's: it is how many settings the
   group holds, which is the one number that says how much is behind the arrow. */
.settings-dense .settings-group-count {
    color: var(--muted);
    font-weight: var(--weight-normal);
    font-variant-numeric: tabular-nums;
    margin-left: var(--space-2);
}

/* Every setting in the group, by name, on one wrapped line. This is what makes
   collapsing honest — the operator who cannot find a setting is the operator who
   changes the wrong one, so the names never go behind the fold even when the
   controls do. */
.settings-dense .settings-group-fields {
    display: block;
    margin-top: var(--space-1);
    color: var(--muted);
    font-size: var(--text-xs);
    line-height: 1.5;
}

/* Open, the summary needs a line under it: without one the first field's label
   sits directly beneath the group's own list of field names and reads as one
   more of them. */
.settings-dense .settings-group[open] > .settings-group-summary {
    margin-bottom: var(--space-3);
    padding-bottom: var(--space-2);
    border-bottom: 1px solid var(--border);
}

/* --- The fields --------------------------------------------------------- */

/* `auto-fill` with a 19rem floor: two columns on a laptop, three on a wide
   monitor, one on a phone, and no breakpoint to keep in step with the rail.
   The column gap is wider than the row gap because a neighbouring column is
   what a value could be misread as belonging to; the setting above it is not. */
.settings-dense .settings-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr));
    gap: var(--space-3) var(--space-5);
    align-items: start;
}

/* The rule between settings goes with the grid: a horizontal line drawn across
   cells that are side by side joins two unrelated settings into a row that
   looks like one record. The gap separates them, and the columns do the work
   the rule was doing. */
.settings-dense .setting {
    margin: 0;
    padding: 0;
    border-bottom: 0;
}

.settings-dense .setting label {
    margin-bottom: var(--space-1);
    font-size: var(--text-sm);
}

/* §18's list-row density. `--text-base` in a field that holds "8080" was set for
   a two-column form and is a third taller than it needs to be here. */
.settings-dense .setting input[type="text"],
.settings-dense .setting input[type="email"],
.settings-dense .setting input[type="number"],
.settings-dense .setting select,
.settings-dense .setting textarea {
    padding: var(--space-1) var(--space-3);
    font-size: var(--text-sm);
}

.settings-dense .setting select { padding-right: var(--space-8); }

/* What it does, then where it comes from, on one line. Two paragraphs said the
   same two things and cost a line each, forty-nine times. */
.settings-dense .setting-meta {
    margin: var(--space-1) 0 0;
    color: var(--muted);
    font-size: var(--text-xs);
    line-height: 1.5;
}

/* The config key and the shipped value are reference rather than reading: same
   line, one shade back, so the sentence in front of them is what the eye takes
   on a scan down the column. */
.settings-dense .setting-origin { display: block; opacity: 0.85; }
.settings-dense .setting-origin .mono { font-size: var(--text-xs); }

.settings-dense .setting-toggle > .checkbox { margin: 0; gap: var(--space-2); }
.settings-dense .setting-toggle .setting-meta { margin-top: 2px; }

/* Prose a person reads gets the whole row whatever the grid is doing. */
.settings-dense .setting-wide { grid-column: 1 / -1; }
.settings-dense .setting-paragraph { min-height: 0; }

/* The save button belongs to the group above it, not to the page. `--space-6`
   plus `--space-5` of rule was a card's worth of gap under every one of nine
   forms. */
.settings-dense .form-actions {
    margin-top: var(--space-3);
    padding-top: var(--space-3);
}

/* -----------------------------------------------------------------------
   The popular plan on the public pricing page.

   A pricing page with three tiers and nothing distinguishing them makes the
   reader compare from scratch. The flag is a word as well as a colour: a card
   that is only "the blue one" says nothing to somebody who cannot see the
   difference (see section 18).
   ----------------------------------------------------------------------- */
.site-plan-featured {
    border-color: var(--site-accent, #2563eb);
    box-shadow: 0 0 0 1px var(--site-accent, #2563eb),
                0 18px 40px -22px rgba(15, 23, 42, .45);
    position: relative;
}

.site-plan-flag {
    position: absolute;
    top: -.75rem;
    left: 50%;
    transform: translateX(-50%);
    background: var(--site-accent, #2563eb);
    color: #fff;
    font-size: .6875rem;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    padding: .2rem .6rem;
    border-radius: 999px;
    white-space: nowrap;
}


/* ==========================================================================
   26. Browsing a site's files
   --------------------------------------------------------------------------
   Follows §18, and exists because the file manager was the one screen that had
   invented its own shape. Every entry carried a second, full-width table row
   holding a `<details>` reading "Rename, move or delete index.php" — so a
   folder of forty was eighty rows, half of them a disclosure nobody had opened,
   and reading a directory listing meant skipping every other line.

   The fix is not CSS. One entry is picked with a link, its actions are drawn
   once at the top in the `.decision` block, and the table goes back to one row
   per file. What is left here is the little that could not be said with the
   classes §18 already provides.
   ========================================================================== */

/* The picked entry's two forms, side by side while there is room for them.
   Move and delete rather than a stack, because the pair is the whole point of
   having picked something: a rename that turns out to be a delete is the one
   mistake this screen has to make hard to reach by accident, and they read as
   two decisions when they sit apart. */
.file-actions {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: var(--space-3) var(--space-5);
    padding: var(--space-3) var(--space-4);
}
.file-actions > form { min-width: 0; }
.file-actions .field { margin-bottom: var(--space-2); }
/* The destination is a path, and a path read in the body face is a path whose
   trailing space nobody notices. */
.file-actions input[type="text"] { font-family: var(--font-mono); font-size: var(--text-sm); }

/* The row the picked entry is on. Marked by a rule down its leading edge
   rather than a fill: the actions are already at the top of the page under the
   file's own name, so this only has to answer "which row was that", and a
   filled row in a listing reads as a state the file is in. */
.dense-table .row-selected > td { background: var(--surface-2); }
.dense-table .row-selected > td:first-child { box-shadow: inset 2px 0 0 var(--accent); }

/* "Up to the site root". Recessed, because it is navigation sitting in a list
   of things, and every listing below the root has one — at full weight it is a
   row the eye stops on once per folder for no reason. */
.dense-table .row-up > td { background: var(--surface-2); font-size: var(--text-sm); }
.dense-table .row-up .icon { transform: rotate(-90deg); color: var(--muted); }

/* Adding something: three ways, one disclosure. The new folder and the upload
   share a row because each is one control and a label; the new text file has an
   editor and takes the width underneath. */
.file-add {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: var(--space-3) var(--space-5);
}
.file-add > form { min-width: 0; }

/* A `<details>` that is itself the card, so its summary has to carry the card's
   own padding rather than sit flush against the border. */
details.card.disclosure > summary { display: flex; align-items: center; gap: var(--space-2); }
details.card.disclosure > summary .icon { width: 14px; height: 14px; flex: 0 0 14px; }

/* The sentence about line endings and ownership, beside the Save button rather
   than under the editor.

   Its own class rather than `.button-row .hint`: that selector would have
   reached every button row in the panel that happens to carry a hint, and a
   rule written for one screen that silently restyles nine others is how the
   stylesheet grows a second dialect. `align-self` because the row does not
   centre its items, and a hint aligned to the row's top sits half a line above
   the words on the button beside it. */
.button-row .field-note { align-self: center; margin: 0; }

/* One firewall rule, on the fleet list.
   --------------------------------------------------------------------------
   Port, protocol and what it is for, on one line and in that order — the port
   is what somebody is scanning the column for, and the protocol matters
   because DNS needs both and the row that forwards only UDP is the one that
   works until a zone grows a DKIM key.

   A row rather than a chip: several of these stack in a cell, and chips at
   this density read as one run-on string. */
.port-rule { display: flex; align-items: baseline; gap: var(--space-2); }
.port-rule .mono { min-width: 4.5rem; font-variant-numeric: tabular-nums; }

/* The domain selector where it is operable but not applied.
   --------------------------------------------------------------------------
   `.inactive` greys the whole control, which is right when it cannot be
   touched. A platform operator can still set it on those pages — choosing where
   you are going before you go there is ordinary — so the control itself reads
   as live while the surrounding note stays muted to say it is not filtering
   *this* page. Without this it looked disabled and worked, which is worse than
   either. */
.domain-switcher.settable select { opacity: 1; cursor: pointer; }

/* ==========================================================================
   27. Documentation
   --------------------------------------------------------------------------
   Follows §18. Prose is the exception it earns: everywhere else in the panel a
   line of text answers a question about one row, and here a paragraph is the
   product. So the measure is narrowed and the leading opened, which is the
   opposite of what the dense tables want and the reason these rules are scoped
   rather than added to the base.
   ========================================================================== */

.docs-search input[type="search"] { width: 100%; max-width: 32rem; }
.docs-search .hint { margin-top: var(--space-2); }

.docs-list { list-style: none; margin: 0; padding: 0; }
.docs-list li {
    display: grid; gap: var(--space-1);
    padding: var(--space-2) 0; border-bottom: 1px solid var(--border);
}
.docs-list li:last-child { border-bottom: 0; }
.docs-list .hint { margin: 0; }

.docs-hits { list-style: none; margin: 0; padding: 0; }
.docs-hits li {
    display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2);
    padding: var(--space-2) 0; border-bottom: 1px solid var(--border);
}
.docs-hits li:last-child { border-bottom: 0; }
.docs-hits .hint { flex: 1 1 100%; margin: 0; }

/* The screenshot, and the numbers sitting on it.
   -------------------------------------------------------------------------
   `position: relative` on the frame rather than the figure: the caption must
   not be inside the positioning context, or a pin placed at 90% would sit over
   the words explaining it. */
.docs-shot { margin: 0; }
.docs-shot-frame {
    position: relative; display: block;
    border: 1px solid var(--border); border-radius: var(--radius);
    overflow: hidden; background: var(--surface-2);
}
.docs-shot-frame img { display: block; width: 100%; height: auto; }

/* A pin is centred on its coordinate, not hung below and right of it — the
   number marks a spot, and half of marking it is being on it. */
.docs-pin {
    position: absolute; transform: translate(-50%, -50%);
    display: inline-flex; align-items: center; justify-content: center;
    width: 1.55rem; height: 1.55rem; border-radius: 50%;
    background: var(--accent); color: #fff;
    font-size: var(--text-xs); font-weight: 700;
    box-shadow: 0 0 0 3px rgba(255, 255, 255, .9), 0 2px 6px rgba(15, 23, 42, .35);
}

.docs-callouts { list-style: none; margin: var(--space-3) 0 0; padding: 0; }
.docs-callouts li {
    display: flex; gap: var(--space-2); align-items: baseline;
    padding: var(--space-1) 0; font-size: var(--text-sm); color: var(--muted);
}
.docs-callouts .n {
    flex: 0 0 1.35rem; height: 1.35rem; border-radius: 50%;
    background: var(--accent); color: #fff;
    font-size: var(--text-xs); font-weight: 700;
    display: inline-flex; align-items: center; justify-content: center;
}

/* Prose measure. Around 70 characters: past that the eye loses the start of
   the next line, which is why every book ever printed is narrower than this
   panel's tables. */
.docs-prose p { max-width: 68ch; line-height: 1.7; }
.docs-prose h2 { margin-top: var(--space-5); }
.docs-prose h2:first-child { margin-top: 0; }

/* Callout positions, snapped to 5%. See resources/views/docs/show.blade.php
   for why these are classes and not inline styles. */
.docs-pin.pin-x-0 { left: 0%; }
.docs-pin.pin-x-5 { left: 5%; }
.docs-pin.pin-x-10 { left: 10%; }
.docs-pin.pin-x-15 { left: 15%; }
.docs-pin.pin-x-20 { left: 20%; }
.docs-pin.pin-x-25 { left: 25%; }
.docs-pin.pin-x-30 { left: 30%; }
.docs-pin.pin-x-35 { left: 35%; }
.docs-pin.pin-x-40 { left: 40%; }
.docs-pin.pin-x-45 { left: 45%; }
.docs-pin.pin-x-50 { left: 50%; }
.docs-pin.pin-x-55 { left: 55%; }
.docs-pin.pin-x-60 { left: 60%; }
.docs-pin.pin-x-65 { left: 65%; }
.docs-pin.pin-x-70 { left: 70%; }
.docs-pin.pin-x-75 { left: 75%; }
.docs-pin.pin-x-80 { left: 80%; }
.docs-pin.pin-x-85 { left: 85%; }
.docs-pin.pin-x-90 { left: 90%; }
.docs-pin.pin-x-95 { left: 95%; }
.docs-pin.pin-x-100 { left: 100%; }
.docs-pin.pin-y-0 { top: 0%; }
.docs-pin.pin-y-5 { top: 5%; }
.docs-pin.pin-y-10 { top: 10%; }
.docs-pin.pin-y-15 { top: 15%; }
.docs-pin.pin-y-20 { top: 20%; }
.docs-pin.pin-y-25 { top: 25%; }
.docs-pin.pin-y-30 { top: 30%; }
.docs-pin.pin-y-35 { top: 35%; }
.docs-pin.pin-y-40 { top: 40%; }
.docs-pin.pin-y-45 { top: 45%; }
.docs-pin.pin-y-50 { top: 50%; }
.docs-pin.pin-y-55 { top: 55%; }
.docs-pin.pin-y-60 { top: 60%; }
.docs-pin.pin-y-65 { top: 65%; }
.docs-pin.pin-y-70 { top: 70%; }
.docs-pin.pin-y-75 { top: 75%; }
.docs-pin.pin-y-80 { top: 80%; }
.docs-pin.pin-y-85 { top: 85%; }
.docs-pin.pin-y-90 { top: 90%; }
.docs-pin.pin-y-95 { top: 95%; }
.docs-pin.pin-y-100 { top: 100%; }
