/* =============================================================================
   KTHX — application stylesheet
   Theme: utilitarian ops console. Light / dark / auto via [data-theme] on <html>
   plus CSS custom properties. Monospace key blocks are the signature element.
   ========================================================================== */

/* ---- Fonts: IBM Plex, self-hosted (workbench/license#136 Phase 1) --------
   The CSP is font-src 'self', so the mockups' Google Fonts link cannot ship;
   the files live in assets/fonts/ with the OFL text beside them.

   Latin1 / Latin2 / Pi subsets of the split build, each with IBM's own
   unicode-range, so a page only fetches the subset it draws: Latin1 is every
   page, Latin2 only once a name carries an accented letter, Pi (arrows, the
   check mark, and for Mono the box-drawing block the agents' output uses) only
   when one appears. Plex Sans 400 / 400 italic / 500 / 600 and Mono 400 / 500,
   ~270 KB on disk, ~21 KB for the common case of one weight.

   No local() source: a locally installed Plex can be an older cut with
   different metrics, and every user should see the same layout. swap, so a
   slow font never holds text back. */
@font-face { font-family: "IBM Plex Sans"; font-style: normal; font-weight: 400; font-display: swap;
    src: url("fonts/IBMPlexSans-Regular-Latin1.woff2") format("woff2");
    unicode-range: U+0000, U+000D, U+0020-007E, U+00A0-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2013-2014, U+2018-201A, U+201C-201E, U+2020-2022, U+2026, U+2030, U+2039-203A, U+2044, U+20AC, U+2122, U+2212, U+FB01-FB02; }
@font-face { font-family: "IBM Plex Sans"; font-style: normal; font-weight: 400; font-display: swap;
    src: url("fonts/IBMPlexSans-Regular-Latin2.woff2") format("woff2");
    unicode-range: U+0100-0101, U+0104-0130, U+0132-0151, U+0154-017F, U+018F, U+0192, U+01A0-01A1, U+01AF-01B0, U+01FA-01FF, U+0218-021B, U+0237, U+0259, U+1E80-1E85, U+1E9E, U+20A1, U+20A4, U+20A6, U+20A8-20AA, U+20AD-20AE, U+20B1-20B2, U+20B4-20B5, U+20B8-20BA, U+20BD, U+20BF; }
@font-face { font-family: "IBM Plex Sans"; font-style: normal; font-weight: 400; font-display: swap;
    src: url("fonts/IBMPlexSans-Regular-Pi.woff2") format("woff2");
    unicode-range: U+0E3F, U+2000-200D, U+2015, U+2028-2029, U+202F, U+2032-2033, U+2070, U+2074-2079, U+2080-2089, U+2113, U+2116, U+2126, U+212E, U+2150-2151, U+2153-215E, U+2190-2199, U+21A9-21AA, U+21B0-21B3, U+21B6-21B7, U+21BA-21BB, U+21C4, U+21C6, U+2202, U+2206, U+220F, U+2211, U+2215, U+221A, U+221E, U+222B, U+2248, U+2260, U+2264-2265, U+25CA, U+2713, U+274C, U+2B0E-2B11, U+FEFF, U+FFFD; }

@font-face { font-family: "IBM Plex Sans"; font-style: italic; font-weight: 400; font-display: swap;
    src: url("fonts/IBMPlexSans-Italic-Latin1.woff2") format("woff2");
    unicode-range: U+0000, U+000D, U+0020-007E, U+00A0-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2013-2014, U+2018-201A, U+201C-201E, U+2020-2022, U+2026, U+2030, U+2039-203A, U+2044, U+20AC, U+2122, U+2212, U+FB01-FB02; }
@font-face { font-family: "IBM Plex Sans"; font-style: italic; font-weight: 400; font-display: swap;
    src: url("fonts/IBMPlexSans-Italic-Latin2.woff2") format("woff2");
    unicode-range: U+0100-0101, U+0104-0130, U+0132-0151, U+0154-017F, U+018F, U+0192, U+01A0-01A1, U+01AF-01B0, U+01FA-01FF, U+0218-021B, U+0237, U+0259, U+1E80-1E85, U+1E9E, U+20A1, U+20A4, U+20A6, U+20A8-20AA, U+20AD-20AE, U+20B1-20B2, U+20B4-20B5, U+20B8-20BA, U+20BD, U+20BF; }
@font-face { font-family: "IBM Plex Sans"; font-style: italic; font-weight: 400; font-display: swap;
    src: url("fonts/IBMPlexSans-Italic-Pi.woff2") format("woff2");
    unicode-range: U+0E3F, U+2000-200D, U+2015, U+2028-2029, U+202F, U+2032-2033, U+2070, U+2074-2079, U+2080-2089, U+2113, U+2116, U+2126, U+212E, U+2150-2151, U+2153-215E, U+2190-2199, U+21A9-21AA, U+21B0-21B3, U+21B6-21B7, U+21BA-21BB, U+21C4, U+21C6, U+2202, U+2206, U+220F, U+2211, U+2215, U+221A, U+221E, U+222B, U+2248, U+2260, U+2264-2265, U+25CA, U+2713, U+274C, U+2B0E-2B11, U+FEFF, U+FFFD; }

@font-face { font-family: "IBM Plex Sans"; font-style: normal; font-weight: 500; font-display: swap;
    src: url("fonts/IBMPlexSans-Medium-Latin1.woff2") format("woff2");
    unicode-range: U+0000, U+000D, U+0020-007E, U+00A0-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2013-2014, U+2018-201A, U+201C-201E, U+2020-2022, U+2026, U+2030, U+2039-203A, U+2044, U+20AC, U+2122, U+2212, U+FB01-FB02; }
@font-face { font-family: "IBM Plex Sans"; font-style: normal; font-weight: 500; font-display: swap;
    src: url("fonts/IBMPlexSans-Medium-Latin2.woff2") format("woff2");
    unicode-range: U+0100-0101, U+0104-0130, U+0132-0151, U+0154-017F, U+018F, U+0192, U+01A0-01A1, U+01AF-01B0, U+01FA-01FF, U+0218-021B, U+0237, U+0259, U+1E80-1E85, U+1E9E, U+20A1, U+20A4, U+20A6, U+20A8-20AA, U+20AD-20AE, U+20B1-20B2, U+20B4-20B5, U+20B8-20BA, U+20BD, U+20BF; }
@font-face { font-family: "IBM Plex Sans"; font-style: normal; font-weight: 500; font-display: swap;
    src: url("fonts/IBMPlexSans-Medium-Pi.woff2") format("woff2");
    unicode-range: U+0E3F, U+2000-200D, U+2015, U+2028-2029, U+202F, U+2032-2033, U+2070, U+2074-2079, U+2080-2089, U+2113, U+2116, U+2126, U+212E, U+2150-2151, U+2153-215E, U+2190-2199, U+21A9-21AA, U+21B0-21B3, U+21B6-21B7, U+21BA-21BB, U+21C4, U+21C6, U+2202, U+2206, U+220F, U+2211, U+2215, U+221A, U+221E, U+222B, U+2248, U+2260, U+2264-2265, U+25CA, U+2713, U+274C, U+2B0E-2B11, U+FEFF, U+FFFD; }

/* 600 also serves the 650/700 weights the older rules still ask for: with no
   heavier face declared, the browser picks the nearest one at or above 600
   that exists, which is this, rather than synthesising a fake bold. */
@font-face { font-family: "IBM Plex Sans"; font-style: normal; font-weight: 600; font-display: swap;
    src: url("fonts/IBMPlexSans-SemiBold-Latin1.woff2") format("woff2");
    unicode-range: U+0000, U+000D, U+0020-007E, U+00A0-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2013-2014, U+2018-201A, U+201C-201E, U+2020-2022, U+2026, U+2030, U+2039-203A, U+2044, U+20AC, U+2122, U+2212, U+FB01-FB02; }
@font-face { font-family: "IBM Plex Sans"; font-style: normal; font-weight: 600; font-display: swap;
    src: url("fonts/IBMPlexSans-SemiBold-Latin2.woff2") format("woff2");
    unicode-range: U+0100-0101, U+0104-0130, U+0132-0151, U+0154-017F, U+018F, U+0192, U+01A0-01A1, U+01AF-01B0, U+01FA-01FF, U+0218-021B, U+0237, U+0259, U+1E80-1E85, U+1E9E, U+20A1, U+20A4, U+20A6, U+20A8-20AA, U+20AD-20AE, U+20B1-20B2, U+20B4-20B5, U+20B8-20BA, U+20BD, U+20BF; }
@font-face { font-family: "IBM Plex Sans"; font-style: normal; font-weight: 600; font-display: swap;
    src: url("fonts/IBMPlexSans-SemiBold-Pi.woff2") format("woff2");
    unicode-range: U+0E3F, U+2000-200D, U+2015, U+2028-2029, U+202F, U+2032-2033, U+2070, U+2074-2079, U+2080-2089, U+2113, U+2116, U+2126, U+212E, U+2150-2151, U+2153-215E, U+2190-2199, U+21A9-21AA, U+21B0-21B3, U+21B6-21B7, U+21BA-21BB, U+21C4, U+21C6, U+2202, U+2206, U+220F, U+2211, U+2215, U+221A, U+221E, U+222B, U+2248, U+2260, U+2264-2265, U+25CA, U+2713, U+274C, U+2B0E-2B11, U+FEFF, U+FFFD; }

@font-face { font-family: "IBM Plex Mono"; font-style: normal; font-weight: 400; font-display: swap;
    src: url("fonts/IBMPlexMono-Regular-Latin1.woff2") format("woff2");
    unicode-range: U+0020-007E, U+00A0-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2013-2014, U+2018-201A, U+201C-201E, U+2020-2022, U+2026, U+2030, U+2039-203A, U+2044, U+20AC, U+2122, U+2212, U+FB01-FB02; }
@font-face { font-family: "IBM Plex Mono"; font-style: normal; font-weight: 400; font-display: swap;
    src: url("fonts/IBMPlexMono-Regular-Latin2.woff2") format("woff2");
    unicode-range: U+0100-0101, U+0104-0130, U+0132-0151, U+0154-017F, U+018F, U+0192, U+01A0-01A1, U+01AF-01B0, U+01FA-01FF, U+0218-021B, U+0237, U+0259, U+1E80-1E85, U+1E9E, U+20A1, U+20A4, U+20A6, U+20A8-20AA, U+20AD-20AE, U+20B1-20B2, U+20B4-20B5, U+20B8-20BA, U+20BD, U+20BF; }
@font-face { font-family: "IBM Plex Mono"; font-style: normal; font-weight: 400; font-display: swap;
    src: url("fonts/IBMPlexMono-Regular-Pi.woff2") format("woff2");
    unicode-range: U+03C0, U+0E3F, U+2000-200D, U+2010-2012, U+2015, U+2028-2029, U+202F, U+2032-2033, U+203E, U+205F, U+2070, U+2074-2079, U+2080-2089, U+2113, U+2116, U+2126, U+212E, U+2150-2151, U+2153-215E, U+2190-2199, U+21A9-21AA, U+21B0-21B3, U+21B6-21B7, U+21BA-21BB, U+21C4, U+21C6, U+2202, U+2206, U+220F, U+2211, U+2215, U+2219-221A, U+221E, U+222B, U+2236, U+2248, U+2260, U+2264-2265, U+2400-2421, U+2500-259F, U+25CA, U+2713, U+274C, U+2B0E-2B11, U+3000, U+FEFF, U+FFFD; }
@font-face { font-family: "IBM Plex Mono"; font-style: normal; font-weight: 500; font-display: swap;
    src: url("fonts/IBMPlexMono-Medium-Latin1.woff2") format("woff2");
    unicode-range: U+0020-007E, U+00A0-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2013-2014, U+2018-201A, U+201C-201E, U+2020-2022, U+2026, U+2030, U+2039-203A, U+2044, U+20AC, U+2122, U+2212, U+FB01-FB02; }
@font-face { font-family: "IBM Plex Mono"; font-style: normal; font-weight: 500; font-display: swap;
    src: url("fonts/IBMPlexMono-Medium-Latin2.woff2") format("woff2");
    unicode-range: U+0100-0101, U+0104-0130, U+0132-0151, U+0154-017F, U+018F, U+0192, U+01A0-01A1, U+01AF-01B0, U+01FA-01FF, U+0218-021B, U+0237, U+0259, U+1E80-1E85, U+1E9E, U+20A1, U+20A4, U+20A6, U+20A8-20AA, U+20AD-20AE, U+20B1-20B2, U+20B4-20B5, U+20B8-20BA, U+20BD, U+20BF; }
@font-face { font-family: "IBM Plex Mono"; font-style: normal; font-weight: 500; font-display: swap;
    src: url("fonts/IBMPlexMono-Medium-Pi.woff2") format("woff2");
    unicode-range: U+03C0, U+0E3F, U+2000-200D, U+2010-2012, U+2015, U+2028-2029, U+202F, U+2032-2033, U+203E, U+205F, U+2070, U+2074-2079, U+2080-2089, U+2113, U+2116, U+2126, U+212E, U+2150-2151, U+2153-215E, U+2190-2199, U+21A9-21AA, U+21B0-21B3, U+21B6-21B7, U+21BA-21BB, U+21C4, U+21C6, U+2202, U+2206, U+220F, U+2211, U+2215, U+2219-221A, U+221E, U+222B, U+2236, U+2248, U+2260, U+2264-2265, U+2400-2421, U+2500-259F, U+25CA, U+2713, U+274C, U+2B0E-2B11, U+3000, U+FEFF, U+FFFD; }

/* ---- Theme tokens ------------------------------------------------------- */

:root {
    /* Plex first, then the stacks this used before #136, which is what the
       page draws in until the woff2 arrives (font-display: swap) or if it
       never does. */
    --font-ui:   "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
                 Roboto, "Helvetica Neue", Arial, sans-serif;
    --font-mono: "IBM Plex Mono", ui-monospace, "SF Mono", "Cascadia Mono", Menlo,
                 Consolas, "Liberation Mono", monospace;

    /* Two radii (#136 visual system): 6px for controls -- buttons, inputs,
       chips, tabs -- and 8px for the panels that hold them. --radius keeps its
       name and its 6px, so every existing control is already right. */
    --radius:    6px;
    --radius-lg: 8px;

    /* Type scale (#136). Dense by default: body and tables are 13px, column
       labels 11px caps; page titles 20/600, section heads 15/600. Pixels on
       purpose -- these are the mockups' sizes, and a rem that happens to land
       on 13px would move the day someone changes the root size. */
    --text-xs:  11px;
    --text-sm:  12px;
    --text-md:  13px;
    --text-lg:  15px;
    --text-xl:  20px;

    --footer-h: 36px;

    /* Spacing scale (workbench/license#43 Phase 3). Block paddings and vertical
       gaps had drifted into one-off values; these six steps give the layout a
       consistent rhythm. Component-internal paddings tuned for optical size
       (inputs, buttons, table cells) stay off the scale on purpose. */
    --space-1: .25rem;
    --space-2: .5rem;
    --space-3: .75rem;
    --space-4: 1rem;
    --space-5: 1.5rem;
    --space-6: 2rem;

    /* Content column width (workbench/license#43). .main is a flex child with
       flex:1, so without a max-width it would run edge-to-edge; with one and no
       auto margin it filled from the left and abandoned the rest, which on a
       1440p/4K display left a large dead band down the right-hand side. .main
       centres itself within whatever this allows, so this is the single place
       to tune the column.

       One width for every page. There was briefly a second, wider token behind
       a $wide=true flag on page_header(): a page passing it filled the shell
       while an ordinary page centred here, so clicking from a wide list to a
       non-wide detail -- Firmware browse -> file detail, Software Updates ->
       item, Feed Sources -> source edit, Patching -> run, Duplicates -> file --
       shifted the content's left edge by ~228px at a 2560px viewport. That was
       a mistake; the flag and its token were removed in the Phase 3 component
       pass. One width removes that whole class of jump at once, and keeps the
       widest tables narrow enough to track a row across (Firmware browse
       otherwise leaves ~500px of air between filename and size). Edge-to-edge
       apps (Smart Licenses) opt out via $full, not a wider column. */
    --content-max: 1800px;

    /* Legacy names (workbench/license#136 Phase 1).
       The palette below is the mockups' token set, named the way the mockups
       name it. Every stylesheet in the app was written against the #43 names,
       so each of those is kept as an alias of its new equivalent rather than
       renamed across ~500 references in one go on a server with no staging.
       Screens move to the new names as they are rebuilt (Phases 2-6); once
       nothing reads an alias it can go.

       Declared once here, not per palette: a var() in a custom property is
       resolved on the element that declares it, and every palette block below
       is also :root, so each alias follows whichever palette is in force. */
    --bg-panel:    var(--panel);
    --bg-inset:    var(--sunk);
    --bg-hover:    var(--hover);
    --fg:          var(--text);
    --fg-muted:    var(--muted);
    --fg-dim:      var(--faint);
    --border:      var(--line);
    --border-soft: var(--line-soft);
    --accent-fg:   var(--accent-text);
    --danger:      var(--crit);
    --danger-soft: var(--crit-soft);
    --warn-fg:     var(--warn);
    /* Two undefined names monitoring.css reached for with a fallback; they now
       resolve to what they meant. */
    --bg-sunken:   var(--sunk);
}

/* Light palette (default)
   workbench/license#136: the mockups' graphite neutrals and one accent, teal
   #0A6C80 -- the cyan family #43 introduced, a step deeper. Ratios are WCAG 2.x,
   measured, against --panel (#fff) unless another ground is named. */
:root,
:root[data-theme="light"] {
    --bg:          #f3f4f6;
    --panel:       #ffffff;
    --raised:      #f8f9fa;   /* table header band, toolbars */
    --sunk:        #f6f7f9;   /* inset wells: code, notes, inputs' disabled ground */
    --hover:       #f1f3f5;
    --sel-row:     #eaf5f7;   /* a selected row; --text on it = 16.00:1 */
    --line:        #e4e7eb;
    --line-soft:   #eef0f3;
    --track:       #e9ecef;   /* empty part of a bar */
    --text:        #15181e;   /* 17.78:1; 16.15:1 on --bg */
    --muted:       #596170;   /* 6.23:1; 5.66:1 on --bg, 5.60:1 on --hover */
    /* --faint (--fg-dim) is NOT a text colour (workbench/license#77 §6).
       4.27:1 on --panel and 3.88:1 on --bg: under the 4.5:1 body-text floor.
       What it is for:
         - borders and backgrounds (status dots, the accordion chevron)
         - disabled controls, which WCAG 1.4.3 exempts
         - .mon-tile.is-zero's 2rem value, which is large text and clears the
           3:1 floor that applies to it
       Anything that is read at body or label size takes --muted. */
    --faint:       #737b88;
    --accent:      #0a6c80;   /* 6.05:1 as text; white on it 6.05:1; 5.23:1 on --accent-soft */
    --accent-soft: #e3f1f4;
    --accent-text: #ffffff;
    /* The five status levels (#136 "one status vocabulary"). Each pair is
       text-on-wash, which is how pills and chips draw them; each ink also
       holds 4.5:1 on --panel for a bare word or icon. */
    --crit:        #bf2a1e;   /* 5.07:1 on --crit-soft; 5.89:1 on --panel */
    --crit-soft:   #fceae8;
    --warn:        #a85207;   /* 4.86:1 on --warn-soft; 5.42:1 on --panel */
    --warn-soft:   #fdf1e1;
    --ok:          #16703f;   /* 5.37:1 on --ok-soft; 6.13:1 on --panel */
    --ok-soft:     #e6f3eb;
    --info:        #2a55b5;   /* 5.94:1 on --info-soft; 6.85:1 on --panel */
    --info-soft:   #e9effb;
    --off:         #5f6673;   /* 5.06:1 on --off-soft; 5.78:1 on --panel */
    --off-soft:    #eef0f2;
    /* The key block, the signature element: kept, dark in both themes. */
    --key-bg:      #0e1b26;
    --key-fg:      #79d3ea;   /* 10.24:1 */
    /* OS brand marks (workbench/license#77). Each project's own colour, except
       where its own colour cannot be read on this ground: an icon is a non-text UI
       component, so WCAG 1.4.11 asks 3:1, and every value below clears it against
       --bg-panel. Windows is the one light-mode change -- #00a4ef is 2.78:1 on
       white -- and is the brand hex untouched in dark, where it reaches 5.75:1. */
    --os-freebsd:  #ab2b28;   /* 6.76:1 on --bg-panel */
    --os-ubuntu:   #e95420;   /* 3.65:1 */
    --os-debian:   #a81d33;   /* 7.27:1 */
    --os-rhel:     #ee0000;   /* 4.53:1 */
    --os-windows:  #009ce3;   /* 3.06:1 -- darkened from the brand #00a4ef */
    --os-vmware:   #64a01c;   /* ESXi green, darkened from #76bc21 (2.34:1 on white); 3.19:1 */
    --os-vmware-accent: #bf860a;  /* ESXi amber, darkened from #efa80c (2.04:1); 3.17:1 */
    --os-unifi:    #0559c9;   /* 6.40:1 */
    /* Cisco blue darkened from the brand #00bceb, which is 2.24:1 on white --
       below the 3:1 that a non-text mark needs. Measured, not eyeballed. */
    --os-cisco:    #1189c4;   /* 3.89:1 on --bg-panel */
    /* A slate blue, deliberately NOT Cisco's cyan-blue: the two marks sit
       side by side in the OS filter row and must not read as one vendor. */
    --os-synology: #4a6fa5;   /* 5.11:1 on --bg-panel */
    --os-netscaler: #7a3ea8;  /* 6.81:1 on --bg-panel; purple, so it cannot be
                                 mistaken for the three blues above at chip size */
    --os-other:    #6b7480;   /* 4.74:1 */
    /* Elevation. The new system is flat -- panels are drawn by their line,
       not a shadow -- so level 1 is barely there; 2 and 3 are for things that
       genuinely float (the bulk bar, menus, the search results). */
    --shadow:      0 1px 2px rgba(16, 24, 40, .04);
    --shadow-1:    0 1px 2px rgba(16, 24, 40, .04);
    --shadow-2:    0 2px 6px rgba(16, 24, 40, .08);
    --shadow-3:    0 12px 32px rgba(16, 24, 40, .18);
    color-scheme: light;
}

/* Dark palette
   workbench/license#136. Ratios against --panel (#15181e) unless named. The
   ground-to-panel step is small (#0e1014 -> #15181e); panels are separated by
   --line, as in light, rather than by lightness alone. */
:root[data-theme="dark"] {
    --bg:          #0e1014;
    --panel:       #15181e;
    --raised:      #1a1e25;
    --sunk:        #12151a;
    --hover:       #1c2027;
    --sel-row:     #132a30;   /* --text on it = 12.33:1 */
    --line:        #262b34;
    --line-soft:   #1f232b;
    --track:       #262b34;
    --text:        #e7e9ed;   /* 14.62:1; 15.66:1 on --bg */
    --muted:       #a0a7b3;   /* 7.34:1; 6.75:1 on --hover */
    --faint:       #7c8391;   /* 4.67:1 -- still decoration only, see light */
    --accent:      #4fc1d6;   /* 8.41:1 as text; 6.59:1 on --accent-soft */
    --accent-soft: #123039;
    --accent-text: #06222a;   /* on --accent = 7.82:1 */
    --crit:        #f07a70;   /* 5.71:1 on --crit-soft; 6.54:1 on --panel */
    --crit-soft:   #3a1b1a;
    --warn:        #efa650;   /* 7.13:1 on --warn-soft */
    --warn-soft:   #35260f;
    --ok:          #56c58f;   /* 6.66:1 on --ok-soft */
    --ok-soft:     #12301f;
    --info:        #7fa2f2;   /* 6.29:1 on --info-soft */
    --info-soft:   #18223a;
    --off:         #9aa1ad;   /* 5.83:1 on --off-soft */
    --off-soft:    #22262e;
    --key-bg:      #0a1118;
    --key-fg:      #79d3ea;   /* 11.14:1 */
    /* Lightened where the brand hex cannot hold 3:1 on --bg-panel, left at the
       brand value where it can. Ratios were measured against the #43 panel
       (#1b222e); the #136 panel is darker, so every one only gains. */
    --os-freebsd:  #cf3531;   /* 3.20:1 */
    --os-ubuntu:   #e95420;   /* 4.38:1 -- brand */
    --os-debian:   #d0243f;   /* 3.05:1 */
    --os-rhel:     #ee0000;   /* 3.53:1 -- brand */
    --os-windows:  #00a4ef;   /* 5.75:1 -- brand */
    --os-vmware:   #76bc21;   /* ESXi green, brand value; 6.83:1 */
    --os-vmware-accent: #efa80c;  /* ESXi amber, brand value; 7.82:1 */
    --os-unifi:    #0666e7;   /* 3.09:1 */
    --os-cisco:    #00bceb;   /* Cisco blue, brand value; 7.14:1 */
    --os-synology: #7f9fd4;   /* 5.94:1 */
    --os-netscaler: #a678d8;  /* 4.79:1 */
    --os-other:    #8a94a2;   /* 5.20:1 */
    --shadow:      0 1px 2px rgba(0, 0, 0, .3);
    --shadow-1:    0 1px 2px rgba(0, 0, 0, .3);
    --shadow-2:    0 2px 8px rgba(0, 0, 0, .45);
    --shadow-3:    0 12px 32px rgba(0, 0, 0, .5);
    color-scheme: dark;
}

/* Auto: follow the OS when no explicit choice.
   Must stay a byte-for-byte mirror of the [data-theme="dark"] block above --
   any divergence means auto-users see a different dark theme than explicit-dark. */
@media (prefers-color-scheme: dark) {
    :root[data-theme="auto"] {
        --bg:          #0e1014;
        --panel:       #15181e;
        --raised:      #1a1e25;
        --sunk:        #12151a;
        --hover:       #1c2027;
        --sel-row:     #132a30;
        --line:        #262b34;
        --line-soft:   #1f232b;
        --track:       #262b34;
        --text:        #e7e9ed;
        --muted:       #a0a7b3;
        --faint:       #7c8391;
        --accent:      #4fc1d6;
        --accent-soft: #123039;
        --accent-text: #06222a;
        --crit:        #f07a70;
        --crit-soft:   #3a1b1a;
        --warn:        #efa650;
        --warn-soft:   #35260f;
        --ok:          #56c58f;
        --ok-soft:     #12301f;
        --info:        #7fa2f2;
        --info-soft:   #18223a;
        --off:         #9aa1ad;
        --off-soft:    #22262e;
        --key-bg:      #0a1118;
        --key-fg:      #79d3ea;
        --os-freebsd:  #cf3531;
        --os-ubuntu:   #e95420;
        --os-debian:   #d0243f;
        --os-rhel:     #ee0000;
        --os-windows:  #00a4ef;
        --os-vmware:   #76bc21;
        --os-vmware-accent: #efa80c;
        --os-unifi:    #0666e7;
        --os-cisco:    #00bceb;
        --os-synology: #7f9fd4;
        --os-netscaler: #a678d8;
        --os-other:    #8a94a2;
        --shadow:      0 1px 2px rgba(0, 0, 0, .3);
        --shadow-1:    0 1px 2px rgba(0, 0, 0, .3);
        --shadow-2:    0 2px 8px rgba(0, 0, 0, .45);
        --shadow-3:    0 12px 32px rgba(0, 0, 0, .5);
        color-scheme: dark;
    }
}

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

* { box-sizing: border-box; }

html { height: 100%; }

body {
    margin: 0;
    min-height: 100%;
    background: var(--bg);
    color: var(--fg);
    /* 13px, dense by default (#136). Was 15px; the mockups set body, tables
       and controls at 13 and only prose-heavy help text reads larger. */
    font: var(--text-md)/1.5 var(--font-ui);
    -webkit-font-smoothing: antialiased;
}

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

/* Page title 20/600, section 15/600 (#136 type scale). */
h1 { font-size: var(--text-xl); margin: 0 0 .25rem; font-weight: 600; letter-spacing: -.01em; line-height: 1.3; }
h2 { font-size: var(--text-lg); margin: 1.5rem 0 .5rem; font-weight: 600; }

/* Visually hidden, still read aloud (workbench/license#77 §13). The app had
   no such helper, which is why every icon-only control had to carry an
   aria-label and the live regions had nowhere to write. Clip rather than
   display:none or visibility:hidden -- both of those remove the element from
   the accessibility tree, which is the opposite of the point. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* The skip link is .sr-only until focused, then a real control at the top
   left. It must sit ABOVE the sticky topbar (z-index 20) or it lands behind
   it and a keyboard user sees nothing happen. */
.skip-link {
    position: absolute;
    left: -9999px;
    top: 0;
    z-index: 100;
    padding: .55rem .9rem;
    background: var(--accent);
    color: var(--accent-fg);
    border-radius: 0 0 var(--radius) 0;
    font: 600 .9rem var(--font-ui);
}
.skip-link:focus {
    left: 0;
    text-decoration: none;
}

/* The skip target takes focus programmatically via tabindex="-1"; it is not a
   control, so it must not draw a focus ring when it receives that focus. */
main:focus { outline: none; }

.mono  { font-family: var(--font-mono); font-size: .92em; }   /* Plex Mono runs large; 12px beside 13px body */
.muted { color: var(--fg-muted); }
.dim   { color: var(--fg-muted); font-size: var(--text-sm); }
.warn  { background: var(--warn-soft); color: var(--warn-fg);
         padding: .6rem .8rem; border-radius: var(--radius); }
.break { overflow-wrap: anywhere; }
.hint  { color: var(--fg-muted); font-size: var(--text-sm); margin: .25rem 0 0; }

fieldset { min-width: 0; }

/* ---- Topbar ------------------------------------------------------------- */

.topbar {
    display: flex;
    align-items: center;
    gap: .75rem;
    padding: 0 1rem;
    height: 52px;
    background: var(--bg-panel);
    border-bottom: 1px solid var(--border);
    position: sticky;
    top: 0;
    z-index: 30;
}

.brand {
    font-weight: 700;
    letter-spacing: .02em;
    color: var(--fg);
    font-size: 1.02rem;
}
.brand:hover { text-decoration: none; color: var(--accent); }

.topbar-right {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: .65rem;
}

/* Admin view-only impersonation banner (workbench/license#49). A static bar
   directly under the topbar -- deliberately not sticky, because the sidebar
   and drawers all hard-code the 52px topbar offset for their own sticky
   positioning and would slide underneath it. */
.impersonation-banner {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: .5rem .9rem;
    padding: .45rem 1rem;
    background: var(--warn-soft);
    color: var(--warn-fg);
    border-bottom: 1px solid var(--border);
    font-size: var(--text-md);
}

/* Collapsible folder groups on the Missing Files page (workbench/license#51). */
details.fw-group { margin: 0 0 .5rem; }
details.fw-group > summary {
    cursor: pointer;
    padding: .55rem .8rem;
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    font-weight: 600;
    overflow-wrap: anywhere;
}
details.fw-group > summary:hover { background: var(--bg-hover); }
details.fw-group[open] > summary { margin-bottom: .5rem; }


.theme-pick select {
    background: var(--bg-inset);
    color: var(--fg);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: .25rem .4rem;
    font-size: var(--text-sm);
}

.iconbtn {
    background: none;
    border: 1px solid transparent;
    color: var(--fg);
    font-size: 1.2rem;
    padding: .2rem .5rem;
    border-radius: var(--radius);
    cursor: pointer;
}
.iconbtn:hover { background: var(--bg-hover); }


/* ---- Footer ------------------------------------------------------------- */

.appfooter {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 1.25rem;
    height: var(--footer-h);
    background: var(--bg-panel);
    border-top: 1px solid var(--border);
    font-size: var(--text-sm);
    color: var(--fg-muted);
    position: sticky;
    bottom: 0;
    z-index: 20;
}

.footer-version {
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    color: var(--fg-muted);
}

/* ---- Shell: sidebar + main ----------------------------------------------
   The shell itself (.shell / .sidebar / .shell-main, the tab strip and the
   phone bottom bar) lives in console.css (#136). The .side-* rules below are
   the group tree, which is now the licence Group picker's body.
   -------------------------------------------------------------------------- */

.side-tree, .side-tree ul,
.side-list { list-style: none; margin: 0; padding: 0; }

.side-tree ul { padding-left: .85rem; border-left: 1px solid var(--border-soft); margin-left: .35rem; }

.side-tree a, .side-list a, .side-dim {
    display: block;
    padding: .28rem .5rem;
    border-radius: var(--radius);
    color: var(--fg);
    font-size: var(--text-md);
    overflow-wrap: anywhere;
}
.side-dim { color: var(--fg-muted); }
.side-tree a:hover, .side-list a:hover { background: var(--bg-hover); text-decoration: none; }
.side-tree a.active, .side-list a.active {
    background: var(--accent-soft);
    color: var(--accent);
    font-weight: 600;
}
.side-empty { color: var(--fg-muted); font-size: var(--text-md); padding: .25rem .5rem; }

.main {
    flex: 1;
    min-width: 0;
    padding: 1.5rem clamp(1rem, 3vw, 2.5rem) 3rem;
    max-width: var(--content-max);
    margin-inline: auto;
}

/* Edge-to-edge: no max-width, no padding. For apps that manage their own
   internal layout end-to-end (e.g. Smart Licenses) rather than sitting in
   the normal padded content column. */
.main-full {
    max-width: none;
    padding: 0;
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.main-bare {
    max-width: none;
    padding: 0 1rem;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: calc(100vh - var(--footer-h));
}

/* ---- Cards / page furniture --------------------------------------------- */

.card {
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow);
    padding: var(--space-4);
    margin: 0 0 var(--space-4);
}

.card-accent { border-left: 3px solid var(--accent); }

/* A card that opens with its own heading: the h2's 1.5rem top margin is for
   separating sections on the page, and inside a padded panel it only doubled
   the padding above the title (#135 §6, #136). */
.card > h2:first-child, .card > h3:first-child { margin-top: 0; }
.card-head > h2, .card-head > h3 { margin: 0; }

.card-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-4);
    margin-bottom: var(--space-2);
}

.page-head {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin-bottom: var(--space-4);
}
/* Slots for the page_head() helper (workbench/license#43 Phase 4). Title + meta
   sit left; actions push right via margin-left:auto. Right-alignment lives on
   the actions element, NOT on .page-head, so pages not yet converted to the
   helper render exactly as before. */
.page-head-main { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--space-3); min-width: 0; }
.page-head-meta { color: var(--fg-muted); font-size: var(--text-md); }
.page-head-actions { display: flex; align-items: center; gap: var(--space-2); margin-left: auto; flex-wrap: wrap; }

/* The small-caps label (workbench/license#77 §6).
   .72-.78rem / 700 / uppercase / .06-.07em letter-spacing is one component that
   grew five copies: .section-title, .table th, .publink-label,
   .mon-tile-label (monitoring.css) and .meter-label (metrics.css). All five
   were --fg-dim, which is 2.74:1 on --bg and 3.11:1 on --bg-panel in light --
   normal text at this size, so the bar is 4.5:1 and every one of them missed
   it. --fg-muted takes them to 5.28-5.98 light and 6.32-7.65 dark.

   This is the standard the project already set for itself: 1219737 rejected a
   tinted table header for putting --fg-dim at "~2.55:1 ... below the 3:1 floor
   for column labels". The background was rejected; the foreground it was to
   sit behind was left where it was.

   There turned out to be seven copies, not five -- .subnav-title in
   console.css and .se-table thead th in search_export.css are the same
   component again. All seven are --fg-muted. If an eighth appears, it is
   --fg-muted too; see the note on --fg-dim in the palette block above. */
.section-title {
    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--fg-muted);
    margin: var(--space-5) 0 var(--space-2);
}

.crumb { color: var(--fg-muted); font-size: var(--text-md); margin-bottom: .35rem; }
.crumb a { color: var(--fg-muted); }

.stack > * + * { margin-top: var(--space-3); }
.grow { flex: 1; }

/* Empty state (workbench/license#43). A shared placeholder for "no rows yet"
   -- a centred title, a line of guidance, and room for the action that fills
   the list -- rather than the ad-hoc left-aligned <p class="muted"> each module
   grew its own. Additive: the class ships now, modules adopt it as touched. */
.empty-state {
    text-align: center;
    color: var(--fg-muted);
    padding: var(--space-6) var(--space-4);
    font-size: var(--text-md);
}
/* --fg, not --fg-muted: the body around it moved to --fg-muted in the #77 §6
   contrast pass, and a title the same colour as its own body is not a title. */
.empty-state-title {
    display: block;
    color: var(--fg);
    font-weight: 600;
    font-size: var(--text-lg);
    margin-bottom: var(--space-1);
}
.empty-state .btn { margin-top: var(--space-3); }

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

/* #136 button rules: 32px high, 13/500, 6px radius. Secondary is the
   default -- a panel-coloured button drawn by its line -- because a view gets
   ONE primary, and everything else on it must read as quieter than that one.
   inline-flex so an icon and a label centre on the same line at any height. */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    min-height: 32px;
    background: var(--panel);
    color: var(--fg);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: .3rem .75rem;
    font: 500 var(--text-md)/1.25 var(--font-ui);
    cursor: pointer;
    text-decoration: none;
    vertical-align: middle;
}
.btn:hover { background: var(--bg-hover); text-decoration: none; }

.btn-primary {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-fg);
}
.btn-primary:hover { filter: brightness(1.08); background: var(--accent); }

/* Red is for the confirm step of something destructive, never a list row
   (#136, #135 §18). Until the confirm steps exist this is the only danger
   button there is, and most of its uses ARE list rows -- so at rest it is a
   secondary button with a red word, and only hover fills it. A column of
   Delete buttons then no longer outshouts the data beside it. */
.btn-danger {
    background: var(--panel);
    border-color: var(--border);
    color: var(--danger);
}
.btn-danger:hover { background: var(--danger-soft); border-color: var(--danger); }

/* Quiet: an accent word with no box, as the mockups draw it. */
.btn-quiet {
    background: transparent;
    border-color: transparent;
    color: var(--accent);
}
.btn-quiet:hover { background: var(--accent-soft); color: var(--accent); }

/* A button shown because the control matters, greyed because there is nothing
   for it to do right now -- it still has to say WHY, which is what its title
   is for, so it keeps the help cursor rather than the not-allowed one. */
.btn:disabled,
.btn[disabled] {
    opacity: .45;
    cursor: help;
    filter: none;
}
.btn:disabled:hover,
.btn[disabled]:hover { background: var(--panel); }
.btn-primary:disabled:hover,
.btn-primary[disabled]:hover { background: var(--accent); }

.btn-sm   { min-height: 26px; padding: .15rem .55rem; font-size: var(--text-sm); }
.btn-wide { width: 100%; text-align: center; }

.btn-row {
    display: flex;
    align-items: center;
    gap: .6rem;
    flex-wrap: wrap;
    margin: var(--space-4) 0;
}

.inline      { display: inline; }
.inline-form { display: inline-flex; gap: .4rem; align-items: center; }

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

label { display: block; font-weight: 500; font-size: var(--text-md); margin: .9rem 0 .3rem; }
label:first-child { margin-top: 0; }

.inline-label {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    font-weight: 500;
    font-size: var(--text-md);
    margin: 0;
}

input[type="text"], input[type="search"], input[type="password"],
input[type="email"], input[type="url"], input[type="date"], input[type="number"],
input:not([type]),
textarea, select {
    width: 100%;
    min-height: 32px;
    background: var(--panel);
    color: var(--fg);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: .35rem .6rem;
    font: inherit;
}
input:disabled, textarea:disabled, select:disabled { background: var(--sunk); color: var(--fg-muted); }
input::placeholder, textarea::placeholder { color: var(--fg-muted); }
.inline-label input, .inline-label select { width: auto; }

/* Same reasoning as .inline-label, for the label-less case: a field that sits
   on one line with its own submit sizes from content, not from the 100% above
   -- which is the width a stacked full-width field wants and the wrong one
   here. Without this the input claims the entire flex row and pushes the
   button onto a line of its own. See workbench/license#77.

   Direct children only, and deliberately so: Create group puts its fields
   inside <label> wrappers, which are the flex items. Those labels are stacked
   full-width fields that happen to sit in a row, and they still want 100% of
   the width their label gives them. Only a control that is itself a flex item
   is the case this rule is for. */
.inline-form > input, .inline-form > select { width: auto; }

input:focus, textarea:focus, select:focus {
    outline: 2px solid var(--accent);
    outline-offset: -1px;
}

textarea { min-height: 110px; resize: vertical; font-family: var(--font-mono); font-size: var(--text-sm); }

input[type="file"] { font-size: var(--text-md); }

.form-card { max-width: 640px; }

.check {
    display: flex;
    align-items: center;
    gap: .5rem;
    font-weight: 500;
    margin: .9rem 0 .25rem;
}
.check input { width: auto; }
/* .check sets display:flex, which would otherwise beat [hidden]. */
.check[hidden] { display: none; }

.upload-row {
    display: flex;
    align-items: center;
    gap: .75rem;
    flex-wrap: wrap;
}

/* ---- Key panel (signature element) --------------------------------------- */

.keypanel { margin: .5rem 0 1rem; }

.keyblock {
    display: block;
    background: var(--key-bg);
    color: var(--key-fg);
    font-family: var(--font-mono);
    font-size: 14px;
    letter-spacing: .04em;
    padding: .75rem 1rem;
    border-radius: var(--radius);
    border: 1px solid rgba(121, 211, 234, .2);
    margin: 0 0 .6rem;
    overflow-x: auto;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    user-select: text;
}
/* `display: block` above is an author rule, so it overrides the UA
   [hidden] rule -- without this a `hidden` keyblock stays visible. The
   revision-history (inline) panels hide their key by toggling `hidden`
   rather than re-masking, so they relied on this working. (#46) */
.keyblock[hidden] { display: none; }

.key-masked { user-select: none; color: rgba(121, 211, 234, .5); letter-spacing: .25em; }

.keypanel-inline { display: inline-flex; flex-direction: column; gap: .35rem; }
.keyblock-sm     { font-size: var(--text-md); padding: .5rem .7rem; }

/* ---- Search ------------------------------------------------------------- */

.searchwrap { position: relative; max-width: 640px; margin-bottom: 1.5rem; }

.search-input {
    font-size: 1.05rem;
    padding: .7rem .9rem;
    background: var(--bg-panel);
}

.results {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    right: 0;
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-3);
    max-height: 420px;
    overflow-y: auto;
    z-index: 20;
    display: none;
}
.results.open { display: block; }

.results a {
    display: block;
    padding: .55rem .8rem;
    border-bottom: 1px solid var(--border-soft);
    color: var(--fg);
}
.results a:last-child { border-bottom: 0; }
.results a:hover, .results a.sel { background: var(--bg-hover); text-decoration: none; }
.results .asset-name { font-weight: 600; }
.results .asset-path { color: var(--fg-muted); font-size: var(--text-sm); }
.results .none { padding: .55rem .8rem; color: var(--fg-muted); }

/* ---- Asset lists -------------------------------------------------------- */

.asset-list { display: flex; flex-direction: column; list-style: none; margin: 0; padding: 0; }

.asset-row {
    display: flex;
    align-items: baseline;
    gap: .75rem;
    padding: .55rem .25rem;
    border-bottom: 1px solid var(--border-soft);
    flex-wrap: wrap;
    color: var(--fg);
}
.asset-row:hover { text-decoration: none; background: var(--bg-hover); }
.asset-list li:last-child .asset-row { border-bottom: 0; }
.asset-row .asset-name { font-weight: 600; }
.asset-row .asset-path { color: var(--fg-muted); font-size: var(--text-sm); }
.asset-row .asset-when { margin-left: auto; color: var(--fg-muted); font-size: var(--text-sm); white-space: nowrap; }

/* ---- Bulk move bar -------------------------------------------------------- */

.bulk-bar {
    display: flex;
    align-items: center;
    gap: .75rem;
    flex-wrap: wrap;
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-2);
    padding: .5rem .75rem;
    margin-bottom: .5rem;
}
.bulk-bar select { width: auto; min-width: 200px; }
/* On a phone the label + select pair is wider than the bar (a 200px select
   beside its words); let the select shrink inside it rather than push the
   page sideways (#136, found rendering the new shell at 390px). */
@media (max-width: 480px) {
    .bulk-bar .inline-label { min-width: 0; max-width: 100%; }
    .bulk-bar select { min-width: 0; flex: 1 1 auto; }
}

.table .t-check { width: 1%; padding-right: 0; }
.table .t-check input[type="checkbox"] { width: auto; margin: 0; }

.asset-li { display: flex; align-items: center; gap: .6rem; }
.asset-li input[type="checkbox"] { width: auto; margin: 0 0 0 calc(.75rem + 1px); flex: 0 0 auto; }
.asset-li .asset-row { flex: 1; }

/* ---- Public download link row ------------------------------------------- */

.publink-row {
    background: var(--bg-inset);
    padding: .45rem .6rem !important;
    border-bottom: 1px solid var(--border-soft);
}

.publink-label {
    display: inline-block;
    font-size: var(--text-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--fg-muted);
    margin-right: .5rem;
    vertical-align: middle;
}

.publink-url {
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    color: var(--fg-muted);
    background: transparent;
    border: none;
    padding: 0;
    overflow-wrap: anywhere;
    word-break: break-all;
    user-select: all;
    vertical-align: middle;
}

.publink-hint {
    display: block;
    font-size: var(--text-xs);
    color: var(--fg-muted);
    margin-top: .2rem;
}

/* ---- Split (master-detail) view -----------------------------------------
   Uses CSS grid with two equal columns (1fr 1fr) so the list and detail
   pane always share the available width 50/50 regardless of the container
   width. No fixed pixels, no percentages that fight the content column.
   -------------------------------------------------------------------- */

.split-detail { display: none; }

.split-detail-empty {
    color: var(--fg-muted);
    text-align: center;
    padding: 3rem 1rem;
    border: 1px dashed var(--border);
    border-radius: var(--radius);
}

.split-list .asset-row.active { background: var(--accent-soft); }

@media (min-width: 1100px) {
    .split-layout {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 2rem;
        align-items: start;
    }

    .split-list { min-width: 0; }

    .split-detail {
        display: block;
        min-width: 0;
        position: sticky;
        top: calc(52px + 1.5rem);
        max-height: calc(100vh - 52px - var(--footer-h) - 3rem);
        overflow-y: auto;
        padding-left: 2rem;
        border-left: 1px solid var(--border);
    }
    .split-detail .page-head { margin-top: 0; }

    /* Keep each row on one line; ellipsize long names. Date is hidden. */
    .split-list .asset-row { flex-wrap: nowrap; }
    .split-list .asset-row .asset-name {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        min-width: 0;
    }
    .split-list .asset-row .asset-path {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        min-width: 0;
        flex-shrink: 1;
    }
    .split-list .asset-row .asset-when { display: none; }
}

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

/* 13px rows with an 11px caps header on a --raised band (#136). The band is
   the mockups' header; the 2px rule it replaces was doing the same job less
   quietly. Header text stays --muted: 5.8:1 on --raised in light, 6.9:1 dark. */
.table { width: 100%; border-collapse: collapse; font-size: var(--text-md); }

.table th {
    text-align: left;
    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .05em;
    color: var(--fg-muted);
    background: var(--raised);
    padding: .5rem .65rem;
    border-bottom: 1px solid var(--border);
    white-space: nowrap;
}

/* Sortable column headers (Firmware browse table, workbench/license#18) --
   keep the same muted/uppercase look as plain <th> text rather than the
   default accent link color, since these are functional column controls,
   not content links. */
.table th a { color: inherit; text-decoration: none; }
.table th a:hover { color: var(--fg); text-decoration: underline; }
.table th .sort-hint { opacity: .45; }

.table td {
    padding: .55rem .65rem;
    border-bottom: 1px solid var(--border-soft);
    vertical-align: top;
}
.table tr:last-child td { border-bottom: 0; }

/* Row hover (workbench/license#43). Hover conventionally signals "clickable",
   and these rows aren't links -- a row's action is a Details/Edit/Dismiss
   control in its last cell. It earns its place here anyway as a *reading aid*:
   the widest tables (Firmware browse, Software Updates, Audit) leave several
   hundred px of air between the filename and the next column, and a row
   highlight is what keeps the eye on one record across that span.

   It also settles a genuine inconsistency. The .asset-row lists (My Licenses,
   group browse, the Home dashboard widgets) have always highlighted on hover,
   so two components that both present as rows of data behaved differently
   depending on which module happened to render them -- with no rule a user
   could infer. Scoped to tbody so header rows don't light up; an implicit
   <tbody> is created by the parser, so this matches whether or not a given
   table writes the tag. */
.table tbody tr:hover { background: var(--bg-hover); }

.t-actions { white-space: nowrap; text-align: right; }

/* Cells holding a value rather than prose -- a version, a count, a date, a
   status. Breaking those across lines to save width that is not scarce makes a
   row harder to follow, so they hold one line and the free-text column in the
   same table absorbs the slack. Paired with .table's width:100%, which is what
   makes the slack exist. */
.table td.nowrap, .table th.nowrap { white-space: nowrap; }
.notes-cell { max-width: 360px; }

.card .table { margin: -.25rem 0; }

/* ---- Chips / badges ----------------------------------------------------- */

/* A tag: 6px corners, not a pill (#136) -- the round shape is kept for
   .pill, the status token, so the two stop being mistaken for each other. */
.chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: .15rem .55rem;
    font-size: var(--text-sm);
    font-weight: 500;
    color: var(--fg-muted);
    white-space: nowrap;
}
.chip-sm { padding: .05rem .45rem; font-size: var(--text-xs); }
.chip-list { display: flex; flex-wrap: wrap; gap: .35rem; list-style: none; margin: 0; padding: 0; }
/* Verbatim vendor text, e.g. an SMU README (Golden ISO, #140). */
.pre-text { font-family: var(--font-mono); font-size: var(--text-sm); white-space: pre-wrap; overflow-wrap: anywhere; margin: 0; padding: .75rem 1rem; }
/* A log tail: fixed height, opens scrolled to the end (column-reverse, no script). */
.pre-tail { max-height: 28rem; overflow: auto; display: flex; flex-direction: column-reverse; }
a.chip:hover { background: var(--bg-hover); text-decoration: none; color: var(--fg); }
/* The selected chip in a row of link chips, e.g. the Expiring window (#135 §3).
   Same solid accent as the monitoring filter chips' .mon-filter.is-active, so
   the app has one "this one is on" look rather than a fourth. The :hover twin
   keeps a.chip:hover, which is more specific, from blanking it on mouse-over. */
.chip.active,
a.chip.active:hover { background: var(--accent); border-color: var(--accent); color: var(--accent-fg); }

/* Exclusion chips -- shown in the user list to indicate blocked groups */
.chip-exclude {
    background: var(--danger-soft);
    border-color: transparent;
    color: var(--danger);
}

.badge {
    display: inline-block;
    background: var(--accent-soft);
    color: var(--accent);
    border-radius: 999px;
    padding: .08rem .5rem;
    font-size: var(--text-xs);
    font-weight: 600;
}

/* ---- Hash copy badges (Firmware browse table) -----------------------------
   Small MD5/SHA512 labels shown between filename and size -- same visual
   language as the small rectangular id/group badges used elsewhere for
   compact metadata tags, rather than the fully-pill-shaped .chip. Hover
   shows the full hash via the native title tooltip (no JS needed); click
   copies it via the .js-copy-text handler in app.js. Deliberately
   tight/no-wrap so the column stays as narrow as possible next to the
   filename. */

.hash-cell { white-space: nowrap; width: 1%; text-align: right; }

.hash-icons, .link-badges { display: inline-flex; gap: .3rem; }

/* Shared small-badge look for both the hash-copy badges (MD5/SHA512) and
   the link-copy badges (HTTP/HTTPS, next to a filename) -- same visual
   language, different data being copied. */
.hash-icon, .link-badge {
    position: relative;
    display: inline-flex;
    align-items: center;
    border: 1px solid var(--border);
    border-radius: 4px;
    background: var(--bg-inset);
    color: var(--fg-muted);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-weight: 500;
    letter-spacing: .03em;
    text-transform: uppercase;
    line-height: 1.5;
    padding: .05rem .4rem;
    cursor: pointer;
    white-space: nowrap;
}
.hash-icon:hover, .link-badge:hover { background: var(--bg-hover); color: var(--fg); border-color: var(--fg-dim); }
/* Vendor checksum verdict (firmware_hash_verdicts): matches / differs. */
.hash-icon.is-ok  { background: var(--ok-soft);     color: var(--ok);     border-color: var(--ok); }
.hash-icon.is-bad { background: var(--danger-soft); color: var(--danger); border-color: var(--danger); }
.hash-icon.copied, .link-badge.copied { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }
.hash-icon.copy-failed, .link-badge.copy-failed { background: var(--danger-soft); color: var(--danger); border-color: var(--danger); }

/* Floating "Copied!" tooltip above a hash badge, added/removed by
   wireCopyText() in app.js. Positioned relative to its .hash-icon parent
   (position: relative is set above). */
.copy-tip {
    position: absolute;
    bottom: 100%;
    left: 50%;
    transform: translateX(-50%) translateY(2px);
    margin-bottom: 5px;
    background: var(--fg);
    color: var(--bg);
    font-family: var(--font-ui);
    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: none;
    letter-spacing: normal;
    padding: .2rem .5rem;
    border-radius: 4px;
    white-space: nowrap;
    pointer-events: none;
    opacity: 0;
    transition: opacity .15s ease, transform .15s ease;
    z-index: 5;
}
.copy-tip.show { opacity: 1; transform: translateX(-50%) translateY(0); }

/* ---- Content patterns: accordions, status dots, status pills ------------- */
/* Generic detail-view primitives (sit alongside .card/.table/.chip/.badge).
   Native <details> accordions need no JS and work inside the AJAX-loaded
   split-detail pane. */

details.acc {
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--bg-panel);
    box-shadow: var(--shadow);
    margin: 0 0 var(--space-4);
    overflow: hidden;
}
details.acc > summary {
    list-style: none;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: .6rem;
    padding: var(--space-3) var(--space-4);
    font-weight: 600;
    font-size: var(--text-lg);
    color: var(--fg);
}
details.acc > summary::-webkit-details-marker { display: none; }
details.acc > summary::after {
    content: "";
    margin-left: auto;
    width: 8px;
    height: 8px;
    border-right: 2px solid var(--fg-dim);
    border-bottom: 2px solid var(--fg-dim);
    transform: rotate(45deg);
    transition: transform .15s ease;
}
details.acc[open] > summary::after { transform: rotate(-135deg); }
details.acc > .acc-form { padding: 0 1rem .75rem; }   /* a form under the summary, e.g. Golden ISO platform label */
details.acc > summary:hover { background: var(--bg-hover); }
details.acc > summary .count { color: var(--fg-muted); font-weight: 400; font-size: var(--text-md); }
.acc-body { padding: var(--space-1) var(--space-4) var(--space-4); border-top: 1px solid var(--border-soft); }

/* ---- One status vocabulary (workbench/license#136) ------------------------
   Five levels, and every module's states map onto them:

     is-crit  Critical       down, failing, expired, name mismatch, overdue
     is-warn  Warning        late, expiring, disk >= 80%, rollout incomplete
     is-ok    Healthy        up, ok, verified, replicated, current
     is-info  Info           in progress, scheduled, pending routine work
     is-off   Unknown / off  never reported, disabled, not measured

   Each level has exactly one pill (a word on a wash, round), one dot (8px,
   the ink) and one chip (a word on a wash, 6px corners, room for an icon).
   The word is always there, so none of them relies on colour alone.

   The older class names are mapped here rather than rewritten in ~200 call
   sites, so every module renders in the vocabulary with no controller change:
     .dot-danger / .dot-warn / .dot-ok / .dot-muted       -> the dots
     .pill-danger / -warn / -ok / -info / -muted          -> the pills
     .pill.status-down / -late / -up / -never / -disabled -> the pills
     .chip.dot-danger / .chip.dot-ok (a chip that grew a
       dot's class to mean "a coloured chip")             -> the chips
   Two deliberate re-mappings: .dot-ok and .pill.status-up were drawn in the
   ACCENT, so "healthy" and "a link" were the same teal; they are --ok green
   now. And .pill-info was accent too; info is its own blue, so an "in
   progress" never looks like a control. .pill with no modifier stays accent.
   ------------------------------------------------------------------------ */

.dot { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.dot::before { content: ""; flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--fg-dim); }
.dot.is-crit::before, .dot-danger::before { background: var(--crit); }
.dot.is-warn::before, .dot-warn::before   { background: var(--warn); }
.dot.is-ok::before,   .dot-ok::before     { background: var(--ok); }
.dot.is-info::before                      { background: var(--info); }
.dot.is-off::before,  .dot-muted::before  { background: var(--off); }

/* status pills (a filled token; distinct from .badge / .chip) */
.pill {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    height: 20px;
    padding: 0 8px;
    border-radius: 10px;
    font-size: var(--text-xs);
    font-weight: 600;
    background: var(--accent-soft);
    color: var(--accent);
    /* Fixed height, so a pill whose label wrapped would spill its text out of
       its own rounded background rather than growing. Multi-word labels are
       ordinary here -- "EOL in 65 days", "3 need reboot", "reboot required". */
    white-space: nowrap;
}

/* Semantic pill colours. Named for what they mean rather than what they look
   like, so a palette change does not turn "danger" into a misleading word. */
.pill.is-crit, .pill-danger, .pill.status-down                   { background: var(--crit-soft); color: var(--crit); }
.pill.is-warn, .pill-warn,   .pill.status-late                   { background: var(--warn-soft); color: var(--warn); }
.pill.is-ok,   .pill-ok,     .pill.status-up                     { background: var(--ok-soft);   color: var(--ok); }
.pill.is-info, .pill-info                                        { background: var(--info-soft); color: var(--info); }
.pill.is-off,  .pill-muted,  .pill.status-never, .pill.status-disabled { background: var(--off-soft); color: var(--off); }

/* Status chips: the same five, as a 6px-cornered tag with room for an icon.
   The border goes transparent so the wash, not a line, draws the shape. */
.chip.is-crit, .chip.dot-danger { background: var(--crit-soft); color: var(--crit); border-color: transparent; }
.chip.is-warn, .chip.dot-warn   { background: var(--warn-soft); color: var(--warn); border-color: transparent; }
.chip.is-ok,   .chip.dot-ok     { background: var(--ok-soft);   color: var(--ok);   border-color: transparent; }
.chip.is-info                   { background: var(--info-soft); color: var(--info); border-color: transparent; }
.chip.is-off,  .chip.dot-muted  { background: var(--off-soft);  color: var(--off);  border-color: transparent; }
/* A chip is not a dot: the class it borrowed must not draw one. */
.chip.dot-danger::before, .chip.dot-warn::before,
.chip.dot-ok::before, .chip.dot-muted::before { content: none; }
.chip svg { width: 13px; height: 13px; flex: none; }

/* An icon inside a pill. Sized from the pill's own font-size so it tracks the
   label rather than needing a fixed pixel height, and given a little optical
   lead-in on the right. */
.pill-icon {
    display: inline-flex;
    align-items: center;
    margin-right: .3em;
}
.pill-icon svg { width: 1.15em; height: 1.15em; }

/* spacer helper for card heads */
.head-spacer { flex: 1; }

/* ---- Flash messages ----------------------------------------------------- */

/* #136: a wash with a 3px rule on the leading edge, in the status colour --
   the full coloured border made every banner as loud as an error. Body text
   is --fg, not the status ink, so a paragraph of guidance reads as text; the
   rule, the wash and a bold lead carry the level. Ratios are the status
   pairs' (all >= 4.86:1) for the lead, and --text on the washes (>= 13:1). */
.flash {
    border-radius: var(--radius);
    padding: .6rem .9rem;
    margin: 0 0 var(--space-4);
    font-size: var(--text-md);
    border: 1px solid transparent;
    border-left-width: 3px;
    color: var(--fg);
}
.flash strong, .flash b { color: inherit; }
.flash-info  { background: var(--info-soft); border-color: transparent; border-left-color: var(--info); }
.flash-ok    { background: var(--ok-soft);   border-color: transparent; border-left-color: var(--ok); }
.flash-error { background: var(--crit-soft); border-color: transparent; border-left-color: var(--crit); }
.flash-error strong, .flash-error b { color: var(--crit); }
.flash-warn strong, .flash-warn b { color: var(--warn); }
/* Amber, for "go and look" as distinct from "this has failed" (#68). The
   monitoring module had reached for .flash-warn and got an uncoloured box,
   because only info/ok/error existed -- the same shape as the seven banners in
   #73 that asked for `flash error` while the rule is `.flash-error`. Uses the
   --warn-soft / --warn-fg pair the pills and meters already use, so it carries
   its own light and dark values rather than a hardcoded amber. */
.flash-warn  { background: var(--warn-soft); border-color: transparent; border-left-color: var(--warn); }

/* ---- Auth screens ------------------------------------------------------- */

.authbox {
    width: 100%;
    max-width: 380px;
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow);
    padding: 1.75rem 1.6rem 1.6rem;
    margin: 2rem 0;
}
.authbox-wide { max-width: 560px; }

/* The brand tile over every auth screen (#136), as the sidebar draws it: the
   tile spells the name, so it sits beside a descriptor, not a wordmark. */
.auth-brand {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 0 0 1.25rem;
    color: var(--fg-muted);
}
.auth-mark { width: 40px; height: 40px; display: block; flex: 0 0 auto; }

/* The tile is key-block navy in both themes, so on a dark panel it has no
   edge; dark mode gives it the same 1px rim favicon.svg draws (#2c4a5e). */
:root { --mark-edge: transparent; }
:root[data-theme="dark"] { --mark-edge: #2c4a5e; }
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) { --mark-edge: #2c4a5e; }
}
.auth-mark, .sb-mark, .topbar-mark, .sh-mark {
    border-radius: 23%;
    box-shadow: 0 0 0 1px var(--mark-edge);
}
.auth-brand-text { font-size: var(--text-sm); font-weight: 500; line-height: 1.2; }

.auth-title {
    font-size: 1.15rem;
    font-weight: 700;
    margin: 0 0 1rem;
    letter-spacing: .01em;
}

.totp-grid {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 1.25rem;
    align-items: start;
    margin: 1rem 0;
}
.qr { background: #fff; padding: 10px; border-radius: var(--radius); width: 200px; height: 200px; }
.qr img, .qr canvas, .qr svg { display: block; }

/* ---- Breadcrumb (Firmware browse + file detail) ---------------------------
   Was previously unstyled (the div used class="breadcrumb" but only
   .crumb had rules) -- styled the same way as .crumb rather than merging
   the two, since .crumb is used elsewhere with different surrounding
   markup and this keeps that untouched. */

.breadcrumb { color: var(--fg-muted); font-size: var(--text-md); margin-bottom: .75rem; }
.breadcrumb a { color: var(--fg-muted); }

/* ---- File format icons (Firmware browse table + file detail title) -------
   Small inline SVG pictograms to the left of a filename, generated by
   firmware_file_icon() based on extension (disc image, VM export, zip,
   tar-family archive, raw binary, or a generic fallback page glyph). */

.file-icon {
    display: inline-flex;
    vertical-align: -3px;
    color: var(--fg-muted);
}
.file-icon svg { display: block; }

/* ---- KV / detail panels -------------------------------------------------- */

.kv { display: grid; grid-template-columns: 140px 1fr; gap: .3rem .9rem; font-size: var(--text-md); margin: 0; }
.kv dt, .kv-label { color: var(--fg-muted); }
.kv dd { margin: 0; }
/* The same key/value shape as a table (<th>key</th><td>value</td>), as the
   Certificates facts use it: a fixed key column so the values line up. Was an
   inline width the CSP discarded (#135 §5). */
.table-kv th { width: 16rem; }

.notes {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    background: var(--bg-inset);
    border-radius: var(--radius);
    padding: .7rem .9rem;
    font-size: var(--text-md);
}

.history { margin-top: .75rem; }
.history summary { cursor: pointer; color: var(--fg-muted); font-size: var(--text-md); }

/* ---- Admin group tree ---------------------------------------------------- */

.tree-admin .tree-row {
    display: flex;
    align-items: center;
    gap: .6rem;
    padding: .35rem .25rem;
    border-bottom: 1px solid var(--border-soft);
    flex-wrap: wrap;
}
.tree-admin .tree-row input[name="name"] { width: 220px; }
.tree-admin .tree-row select { width: auto; }
.tree-admin .tree-row input[name="description"] { flex: 1; min-width: 140px; }
.tree-del { margin: 0 0 .5rem; }

/* Depth indentation via data-depth attribute (avoids inline style CSP violation) */
.tree-row[data-depth="0"], .tree-del[data-depth="0"] { padding-left: 0; }
.tree-row[data-depth="1"], .tree-del[data-depth="1"] { padding-left: 1.4rem; }
.tree-row[data-depth="2"], .tree-del[data-depth="2"] { padding-left: 2.8rem; }
.tree-row[data-depth="3"], .tree-del[data-depth="3"] { padding-left: 4.2rem; }
.tree-row[data-depth="4"], .tree-del[data-depth="4"] { padding-left: 5.6rem; }
.tree-row[data-depth="5"], .tree-del[data-depth="5"] { padding-left: 7.0rem; }

/* ---- User membership editor ---------------------------------------------- */

.member-row {
    display: flex;
    gap: .5rem;
    align-items: center;
    margin-bottom: .45rem;
    flex-wrap: wrap;
}
.member-row select { width: auto; flex: 1; min-width: 0; }
.memberships { margin: .25rem 0 .75rem; border: 1px solid var(--border-soft); border-radius: var(--radius); padding: .75rem 1rem 1rem; }
.memberships legend { font-weight: 600; font-size: var(--text-md); padding: 0 .35rem; }

/* ---- Patch run progress (workbench/license#28) ----------------------------
   Live per-step progress card on the patch run status page while a run is
   queued/running (see patch_render_progress_card() in patching.php and
   wirePatchProgress() in app.js). The bar is indeterminate rather than a
   computed percentage -- the step count varies by format/target/container
   count, so there's no fixed total to divide by -- it slides while running,
   then app.js swaps in .done/.failed for a final beat before the page
   reloads to its ordinary server-rendered state (download link / error log
   / files table). Steps appear as the runner reaches them rather than being
   pre-listed and greyed out, for the same reason: there's no fixed template
   to pre-render against. */

.patch-progress-bar {
    position: relative;
    height: 6px;
    border-radius: 999px;
    background: var(--track);
    overflow: hidden;
    margin: 0 0 1rem;
}
.patch-progress-fill {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 40%;
    border-radius: 999px;
    background: var(--accent);
    transition: width .2s ease;
}
/* Chunked upload progress (#116). */
.patch-upload-progress { margin: .75rem 0 0; }
.patch-upload-status { margin-top: .4rem; font-size: var(--text-md); }
.patch-progress-fill.indeterminate {
    animation: patch-progress-slide 1.4s ease-in-out infinite;
}
.patch-progress-fill.done   { width: 100%; background: var(--accent); animation: none; }
.patch-progress-fill.failed { width: 100%; background: var(--danger); animation: none; }

@keyframes patch-progress-slide {
    0%   { left: -40%; width: 40%; }
    50%  { left: 30%;  width: 55%; }
    100% { left: 100%; width: 40%; }
}

.patch-steps {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
}

.patch-step {
    display: flex;
    align-items: flex-start;
    gap: .65rem;
    padding: .5rem .1rem;
    border-bottom: 1px solid var(--border-soft);
}
.patch-step:last-child { border-bottom: 0; }

.patch-step-icon {
    flex: 0 0 auto;
    width: 18px;
    height: 18px;
    margin-top: .1rem;
    border-radius: 50%;
    position: relative;
    display: inline-block;
}

/* running: spinning ring, accent-colored */
.patch-step-icon.patch-step-running {
    border: 2px solid var(--border);
    border-top-color: var(--accent);
    animation: patch-step-spin .8s linear infinite;
}
@keyframes patch-step-spin { to { transform: rotate(360deg); } }

/* ok: filled accent circle with a checkmark */
.patch-step-icon.patch-step-ok {
    background: var(--accent);
    border: 2px solid var(--accent);
}
.patch-step-icon.patch-step-ok::after {
    content: "";
    position: absolute;
    left: 4px;
    top: 2px;
    width: 6px;
    height: 10px;
    border-right: 2px solid var(--accent-fg);
    border-bottom: 2px solid var(--accent-fg);
    transform: rotate(45deg);
}

/* error: filled danger circle with a cross */
.patch-step-icon.patch-step-error {
    background: var(--danger);
    border: 2px solid var(--danger);
}
.patch-step-icon.patch-step-error::before,
.patch-step-icon.patch-step-error::after {
    content: "";
    position: absolute;
    left: 3px;
    top: 8px;
    width: 10px;
    height: 2px;
    background: var(--danger-soft);
}
.patch-step-icon.patch-step-error::before { transform: rotate(45deg); }
.patch-step-icon.patch-step-error::after  { transform: rotate(-45deg); }

.patch-step-body { min-width: 0; flex: 1; }
.patch-step-label { font-size: var(--text-md); color: var(--fg); }
.patch-step-row-running .patch-step-label { color: var(--fg-muted); }
.patch-step-row-error .patch-step-label { color: var(--danger); font-weight: 600; }

.patch-step-detail {
    color: var(--fg-muted);
    font-size: var(--text-sm);
    margin-top: .15rem;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

/* ---- Icon buttons and the "⋯" menu (#136) --------------------------------
   An action drawn as its icon alone: 30px square, no box until hover, the
   word in aria-label and title. Used where a row or a header carries several
   small actions and words would crowd the thing they act on. Position is
   relative so the "Copied!" tip (showCopyTip in app.js) can sit above one. */
.btn > svg { width: 15px; height: 15px; flex: none; }

.icon-btn {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 30px;
    height: 30px;
    padding: 0;
    border: 0;
    border-radius: var(--radius);
    background: transparent;
    color: var(--fg-muted);
    cursor: pointer;
    list-style: none;                       /* when it is a <summary> */
}
.icon-btn::-webkit-details-marker { display: none; }
.icon-btn::marker { content: ""; }
.icon-btn svg { width: 16px; height: 16px; }
.icon-btn:hover { background: var(--bg-hover); color: var(--fg); text-decoration: none; }
.icon-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.icon-btn:disabled { opacity: .4; cursor: help; }
.icon-btn:disabled:hover { background: transparent; color: var(--fg-muted); }
.icon-btn.copied { color: var(--ok); }
.icon-btn.copy-failed { color: var(--crit); }

/* The overflow menu: a native <details>, so it opens with scripts off; app.js
   only closes it on an outside click or Escape. Destructive items live here
   rather than as a red button beside Edit (#135 §18). */
.more-menu { position: relative; display: inline-block; }
.more-menu[open] > summary { background: var(--bg-hover); color: var(--fg); }
.more-menu-list {
    position: absolute;
    right: 0;
    top: calc(100% + 4px);
    z-index: 40;
    min-width: 180px;
    padding: 4px;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-3);
}
.more-menu-list form { margin: 0; }
.more-menu-item {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    padding: 6px 10px;
    border: 0;
    border-radius: var(--radius);
    background: transparent;
    color: var(--fg);
    font: 400 var(--text-md)/1.4 var(--font-ui);
    text-align: left;
    white-space: nowrap;
    cursor: pointer;
}
.more-menu-item:hover { background: var(--bg-hover); text-decoration: none; }
.more-menu-item.is-danger { color: var(--crit); }
.more-menu-item.is-danger:hover { background: var(--crit-soft); }

/* ---- Licences: list + drawer (#136 board "Licences", #135 §15) ------------
   A grid list rather than a <table> so a row can be one link (the name's
   ::after stretches over the row) and still hold a checkbox above it. The
   list sizes its columns from its own width (a container query), because the
   same list is full width until the drawer opens and ~60% after. */
.lic-main { min-width: 0; container-type: inline-size; }

.lic-table {
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    overflow: hidden;
    font-size: var(--text-md);
}
.lic-tr {
    position: relative;
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 15rem) 9.5rem 3.5rem;
    align-items: center;
    gap: 2px 12px;
    min-height: 40px;
    padding: 6px 14px;
    border-bottom: 1px solid var(--line-soft);
}
.lic-tr:last-child { border-bottom: 0; }
.has-pick .lic-tr  { grid-template-columns: 1.5rem minmax(0, 1fr) minmax(0, 15rem) 9.5rem 3.5rem; }
.no-group .lic-tr  { grid-template-columns: minmax(0, 1fr) 9.5rem 3.5rem; }
.has-pick.no-group .lic-tr { grid-template-columns: 1.5rem minmax(0, 1fr) 9.5rem 3.5rem; }

.lic-thead {
    min-height: 34px;
    background: var(--raised);
    border-bottom-color: var(--line);
    color: var(--fg-muted);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: .05em;
    text-transform: uppercase;
}
.lic-tr:not(.lic-thead):hover { background: var(--hover); }
.lic-tr.is-selected,
.lic-tr.is-selected:hover { background: var(--sel-row); box-shadow: inset 3px 0 0 var(--accent); }

.lic-c-name { min-width: 0; overflow-wrap: anywhere; }
.lic-link { color: var(--fg); font-weight: 500; }
.lic-link::after { content: ""; position: absolute; inset: 0; }
.lic-link:hover { text-decoration: none; }
.lic-link:focus-visible { outline: none; }
.lic-link:focus-visible::after { outline: 2px solid var(--accent); outline-offset: -2px; }
.lic-c-pick input { position: relative; z-index: 1; width: auto; margin: 0; }
.lic-c-group { min-width: 0; color: var(--fg-muted); font-size: var(--text-sm); overflow-wrap: anywhere; }

/* The word carries the level as well as the dot: expired and the last 30 days
   are the rows to act on, so they are the ones in colour and weight. */
.lic-exp.is-crit { color: var(--crit); font-weight: 600; }
.lic-exp.is-warn { color: var(--warn); font-weight: 600; }

.lic-keychip {
    display: inline-block;
    padding: 1px 6px;
    border-radius: 4px;
    background: var(--off-soft);
    color: var(--fg-muted);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    line-height: 1.4;
}
.lic-keychip.is-none { background: transparent; }

/* Narrower than the four columns want (the drawer is open, or a tablet): the
   group path goes first, since the drawer names it anyway. */
@container (max-width: 760px) {
    .lic-tr, .no-group .lic-tr { grid-template-columns: minmax(0, 1fr) 9.5rem 3.5rem; }
    .has-pick .lic-tr, .has-pick.no-group .lic-tr { grid-template-columns: 1.5rem minmax(0, 1fr) 9.5rem 3.5rem; }
    .lic-c-group { display: none; }
}
/* A phone: name on its own line, expiry and key under it. */
@container (max-width: 480px) {
    .lic-thead { display: none; }
    .lic-tr, .no-group .lic-tr { grid-template-columns: minmax(0, 1fr) auto; }
    .has-pick .lic-tr, .has-pick.no-group .lic-tr { grid-template-columns: 1.5rem minmax(0, 1fr) auto; }
    .lic-c-name { grid-column: 1 / -1; }
    .has-pick .lic-c-name { grid-column: 2 / -1; }
    .has-pick .lic-c-exp { grid-column: 2; }
}

/* The drawer. Hidden until app.js opens it, so with scripts off the list is
   the whole page and every name is a plain link to the full detail. */
.lic-layout.is-open {
    display: grid;
    grid-template-columns: minmax(0, 1fr) clamp(400px, 36%, 620px);
    gap: var(--space-4);
    align-items: start;
}
.lic-drawer[hidden] { display: none; }
.lic-drawer {
    position: sticky;
    top: calc(var(--topbar-h, 52px) + var(--space-4));
    max-height: calc(100vh - var(--topbar-h, 52px) - 2 * var(--space-4));
    overflow-y: auto;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-2);
}
.lic-drawer-bar {
    position: sticky;
    top: 0;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    padding: 6px 8px 6px 12px;
    background: var(--panel);
    border-bottom: 1px solid var(--line-soft);
}
.lic-drawer-body { padding: var(--space-4) 20px; }
.lic-drawer .kv { grid-template-columns: 110px 1fr; }

/* ---- Licence detail (the drawer and ?p=asset share it) ------------------- */
.lic-detail { max-width: 880px; }
.lic-head { margin-bottom: var(--space-2); }
.lic-head-top { display: flex; align-items: flex-start; gap: var(--space-2); }
.lic-head-group { flex: 1; min-width: 0; padding-top: 6px; color: var(--fg-muted); font-size: var(--text-sm); }
.lic-head-group a { color: inherit; }
.lic-head-actions { display: flex; align-items: center; gap: 2px; }
.lic-head-name { margin: 2px 0 0; font-size: 17px; font-weight: 600; line-height: 1.3; overflow-wrap: anywhere; }

.lic-pills { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 var(--space-4); }
.lic-restore { margin: 0 0 var(--space-4); flex-wrap: wrap; }
.lic-restore select { width: auto; max-width: 100%; }

.lic-sec { margin: 0 0 var(--space-5); }
.lic-sec > .section-title { margin: 0 0 var(--space-2); }
.lic-facts { margin: 0 0 var(--space-5); }

/* The key block, the signature element: the key and its two actions on one
   dark ground, as the mockup draws it. The <pre> inside keeps .keyblock (and
   so the print rule that never prints it); only its own box is dropped. */
.lic-key {
    display: flex;
    align-items: center;
    gap: 6px;
    margin: 0;
    padding: 10px 10px 10px 14px;
    background: var(--key-bg);
    border: 1px solid rgba(121, 211, 234, .2);
    border-radius: var(--radius-lg);
}
.lic-key .keyblock { flex: 1; min-width: 0; margin: 0; padding: 0; border: 0; background: transparent; font-size: var(--text-md); }
.lic-key .icon-btn { background: rgba(121, 211, 234, .1); color: var(--key-fg); }
.lic-key .icon-btn:hover,
.lic-key .icon-btn.is-on { background: rgba(121, 211, 234, .24); color: #d9f4fb; }
.lic-key .icon-btn:disabled:hover { background: rgba(121, 211, 234, .1); color: var(--key-fg); }

.lic-files { display: flex; flex-direction: column; gap: 6px; margin: 0; padding: 0; list-style: none; }
.lic-file {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 6px 6px 6px 10px;
    border: 1px solid var(--line-soft);
    border-radius: var(--radius);
}
.lic-file > svg { width: 16px; height: 16px; flex: none; color: var(--fg-muted); }
.lic-file-main { display: flex; flex: 1; flex-direction: column; min-width: 0; }
.lic-file-actions { display: flex; align-items: center; gap: 2px; flex: none; }
.lic-file.is-old a { color: var(--fg-muted); }
.lic-older { margin-top: var(--space-2); }
.lic-older > summary { cursor: pointer; color: var(--fg-muted); font-size: var(--text-sm); }
.lic-older > .lic-files { margin-top: 6px; }
.lic-upload { margin-top: var(--space-3); }
.lic-upload input[type="file"] { min-width: 0; max-width: 100%; }
.lic-upload select { flex: 1 1 12rem; width: auto; min-width: 0; }

.lic-history { display: flex; flex-direction: column; gap: 10px; margin: 0; padding: 0; list-style: none; }
.lic-history > li { position: relative; display: flex; flex-direction: column; align-items: flex-start; gap: 2px; padding-left: 17px; }
.lic-history > li::before {
    content: "";
    position: absolute;
    left: 0;
    top: 6px;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--faint);
}
.lic-history > li.is-current::before { background: var(--accent); }
.lic-history-what { overflow-wrap: anywhere; }
.lic-history .keypanel-inline { margin: 4px 0 0; }
.lic-history .notes { margin-top: 4px; align-self: stretch; }

/* ---- Certificates: severity view (#136 board "Certificates", #135 §10) ----
   One summary line instead of a tile per state, then the endpoints grouped
   Critical / Warning / Healthy with a lifetime bar per row. The bars are
   inline SVG (positions are data; see cert_lifetime_bar()), coloured here. */
.cert-sum {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-4);
    margin: 0 0 var(--space-4);
    padding: 12px 16px;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
}
.cert-sum-bar { display: block; width: min(320px, 100%); height: 10px; border-radius: 5px; background: var(--track); }
.cert-sum-bar .is-crit { fill: var(--crit); }
.cert-sum-bar .is-warn { fill: var(--warn); }
.cert-sum-bar .is-ok   { fill: var(--ok); }
.cert-sum-text { flex: 1 1 auto; }
.cert-sum-crit { color: var(--crit); font-weight: 600; }
.cert-sum-warn { color: var(--warn); font-weight: 600; }
.cert-sum-key { display: inline-flex; align-items: center; gap: 14px; color: var(--fg-muted); font-size: var(--text-sm); }
.cert-key-today, .cert-key-due { display: inline-flex; align-items: center; gap: 6px; }
.cert-key-today::before { content: ""; width: 2px; height: 12px; background: var(--text); }
.cert-key-due::before {
    content: "";
    width: 8px;
    height: 8px;
    transform: rotate(45deg);
    border: 2px solid var(--warn);
    background: var(--panel);
}

.cert-list {
    container-type: inline-size;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    overflow: hidden;
}
.cert-tr {
    display: grid;
    grid-template-columns: minmax(0, 1.2fr) 8rem minmax(12rem, 1fr) 7rem minmax(0, 1.3fr);
    align-items: center;
    gap: 2px 16px;
    min-height: 48px;
    padding: 8px 16px;
    border-bottom: 1px solid var(--line-soft);
}
.cert-thead {
    min-height: 34px;
    background: var(--raised);
    border-bottom-color: var(--line);
    color: var(--fg-muted);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: .05em;
    text-transform: uppercase;
}
.cert-group-head {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 16px;
    background: var(--raised);
    border-bottom: 1px solid var(--line-soft);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: .06em;
    text-transform: uppercase;
}
.cert-group-head.is-crit { color: var(--crit); }
.cert-group-head.is-warn { color: var(--warn); }
.cert-group:last-child > .cert-tr:last-child { border-bottom: 0; }

/* Healthy: a <details> whose summary is the "N healthy endpoints · show" row. */
.cert-healthy > summary {
    min-height: 42px;
    background: var(--panel);
    color: var(--fg-muted);
    font-size: var(--text-md);
    font-weight: 400;
    letter-spacing: 0;
    text-transform: none;
    cursor: pointer;
    list-style: none;
}
.cert-healthy > summary::-webkit-details-marker { display: none; }
.cert-healthy > summary svg { width: 14px; height: 14px; transition: transform .15s ease; }
.cert-healthy[open] > summary svg { transform: rotate(90deg); }
.cert-healthy[open] > summary { border-bottom-color: var(--line-soft); }
.cert-healthy:not([open]) > summary { border-bottom: 0; }
.cert-show { color: var(--accent); }
.cert-healthy[open] .cert-show { display: none; }

.cert-c-ep, .cert-c-state, .cert-c-life { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.cert-c-ep a { font-weight: 500; overflow-wrap: anywhere; }
.cert-c-issuer { color: var(--fg-muted); overflow-wrap: anywhere; }
.cert-c-exp { font-family: var(--font-mono); font-size: var(--text-sm); }
.cert-c-exp.is-soon { color: var(--crit); font-weight: 500; }
.cert-c-state .pill { align-self: flex-start; }
.cert-sub { color: var(--fg-muted); font-size: var(--text-sm); overflow-wrap: anywhere; }

.cert-bar { display: block; width: 100%; height: 16px; overflow: visible; }
.cert-bar-track { fill: var(--track); }
.cert-bar-used { fill: var(--accent); }
.cert-bar-used.is-crit { fill: var(--crit); }
.cert-bar-used.is-warn { fill: var(--warn); }
.cert-bar-used.is-off  { fill: var(--off); }
.cert-bar-today { fill: var(--text); }
.cert-bar-due { fill: var(--panel); stroke: var(--warn); stroke-width: 2; }

/* Narrow: issuer goes, and the lifetime bar takes a line of its own. */
@container (max-width: 900px) {
    .cert-tr { grid-template-columns: minmax(0, 1fr) 6.5rem minmax(0, 1fr); }
    .cert-thead .cert-c-life { display: none; }
    .cert-c-issuer { display: none; }
    .cert-c-life { grid-column: 1 / -1; order: 5; }
}
@container (max-width: 520px) {
    .cert-thead { display: none; }
    .cert-tr { grid-template-columns: minmax(0, 1fr) auto; }
    .cert-c-state { grid-column: 1 / -1; }
}

/* ---- Software Library folders (#135 §17) ----------------------------------
   Folders as a grid of tiles with their file counts, where there was a card
   holding a one-line tree. */
.fw-folders {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: var(--space-2);
    margin: 0 0 var(--space-5);
    padding: 0;
    list-style: none;
}
.fw-folder {
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: 48px;
    padding: 10px 14px;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    color: var(--fg);
}
.fw-folder:hover { background: var(--hover); border-color: var(--fg-dim); text-decoration: none; }
.fw-folder .file-icon { color: var(--accent); }
.fw-folder-name { flex: 1; min-width: 0; font-weight: 500; overflow-wrap: anywhere; }
.fw-folder-count { color: var(--fg-muted); font-size: var(--text-sm); white-space: nowrap; }

/* A shared-folder URL with its Copy link button (#135 §19). */
.share-link { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 4px var(--space-2); min-width: 0; }

/* ---- Software Updates (#135 §16) -----------------------------------------
   Unseen releases in bold, the way a mail list shows unread; the bulk dismiss
   button sits in the table's header cell, which is caps -- the button isn't. */
.su-new td:nth-child(2) a { font-weight: 600; }
.su-table th .btn { text-transform: none; letter-spacing: normal; }
.pager { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: var(--space-2); margin: var(--space-4) 0; }

/* ---- Backups → Veeam detail (#138 Phase 2) --------------------------------
   Veeam's messages are long and unbroken (VSS writer GUIDs, CRLF-separated
   lines): they wrap anywhere and keep their line breaks, so a warning's
   reason reads in full and never pushes the page sideways at 390px. */
.veeam-msg { white-space: pre-line; overflow-wrap: anywhere; min-width: 0; }
td .veeam-msg { margin-top: 4px; font-size: var(--text-sm); }
.veeam-vms td:nth-child(2) { min-width: 16rem; }
.veeam-strip { display: flex; flex-wrap: wrap; align-items: baseline; gap: .25rem .5rem; margin: .5rem 0; }
.veeam-repos { margin: .75rem 0; }
.veeam-sub { margin: 1.25rem 0 .5rem; font-size: var(--text-md); font-weight: 600; }
.veeam-run + .veeam-run { margin-top: .75rem; }
.veeam-run p { margin: .25rem 0; }
.veeam-vm-muted td, .veeam-vm-muted td strong { color: var(--fg-muted); font-weight: 400; }
.veeam-log { list-style: none; margin: 0; padding: 0; font-size: var(--text-sm); }
.veeam-log li { padding: 4px 8px; border-radius: 6px; }
.veeam-log li.is-warn { background: var(--warn-soft); }
.veeam-log li.is-crit { background: var(--crit-soft); }
.veeam-log-time { color: var(--fg-muted); font-family: var(--font-mono); white-space: nowrap; }
.veeam-more { margin-top: 4px; font-size: var(--text-sm); }
.veeam-more summary { cursor: pointer; color: var(--fg-muted); }
.veeam-squares { display: flex; flex-wrap: wrap; gap: 3px; margin: 0 0 1rem; }
.veeam-sq { width: 14px; height: 14px; border-radius: 3px; background: var(--off-soft); }
.veeam-sq.is-ok   { background: var(--ok); }
.veeam-sq.is-warn { background: var(--warn); }
.veeam-sq.is-crit { background: var(--crit); }
.veeam-sq.is-info { background: var(--info); }
.veeam-sq.is-off  { background: var(--off); }
@media (max-width: 699px) {
    .veeam-vms td:nth-child(2) { min-width: 0; }
}

/* ---- Responsive ---------------------------------------------------------- */

@media (max-width: 860px) {
    .main { padding: 1rem .9rem 3rem; }

    .kv { grid-template-columns: 110px 1fr; }
    .totp-grid { grid-template-columns: 1fr; }

    .table { display: block; overflow-x: auto; }

    .asset-row .asset-when { margin-left: 0; width: 100%; }
}

@media (max-width: 480px) {
    .keyblock { font-size: var(--text-md); }
}

/* ---- Print: never print key material ------------------------------------- */
@media print {
    .keyblock { display: none; }
}

/* Databases module: per-database backup calendar (workbench/license#126) */
.cal-nav { display: flex; align-items: center; gap: var(--space-4); margin-bottom: var(--space-3); }
.cal-nav h3 { margin: 0; }
.cal-nav .cal-spacer { flex: 1; }
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 6px; }
.cal-dow { text-align: center; font-size: var(--text-xs); text-transform: uppercase; letter-spacing: .06em; color: var(--fg-muted); padding: .25rem 0; }
.cal-cell { min-height: 68px; border: 1px solid var(--border-soft); border-radius: var(--radius); padding: .35rem; background: var(--bg-panel); }
.cal-cell.cal-out { background: transparent; border-color: transparent; }
.cal-cell.cal-has { border-color: var(--ok); background: var(--ok-soft); }
.cal-num { font-size: var(--text-xs); color: var(--fg-muted); }
.cal-bk { display: block; font-size: var(--text-xs); margin-top: .15rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* Backups → Veeam: restore point calendar per VM (#138 Phase 3). A day with
   points is a link (?day=), so selecting one needs no script. At phone width
   the seven columns must fit 390px: tighter gaps and cells, the words go,
   and "Full" becomes a dot. */
.veeam-cal a.cal-cell { display: block; color: var(--fg); text-decoration: none; }
.veeam-cal a.cal-cell:hover { border-color: var(--accent); }
.veeam-cal .cal-cell.cal-sel { outline: 2px solid var(--accent); outline-offset: -1px; }
.veeam-cal .cal-num { display: block; }
.veeam-cal .cal-count { display: block; font-weight: 600; font-variant-numeric: tabular-nums; }
.veeam-cal .cal-word { font-weight: 400; color: var(--fg-muted); font-size: var(--text-xs); }
.veeam-cal .cal-full { display: inline-block; margin-top: .15rem; padding: 0 .35rem; border-radius: 999px;
    font-size: var(--text-xs); background: var(--accent); color: var(--accent-text); }
.veeam-cal-end { opacity: .35; pointer-events: none; }
.veeam-held { display: flex; flex-wrap: wrap; gap: .35rem 1.25rem; margin: 0 0 var(--space-4); }
.veeam-stale { color: var(--warn-fg); font-weight: 600; }
@media (max-width: 699px) {
    .veeam-cal { gap: 3px; }
    .veeam-cal .cal-dow { letter-spacing: 0; font-size: 11px; }
    .veeam-cal .cal-cell { min-height: 46px; padding: 2px 4px; min-width: 0; }
    .veeam-cal .cal-word { display: none; }
    .veeam-cal .cal-full { display: block; width: 7px; height: 7px; padding: 0; margin-top: 2px; font-size: 0; }
}

/* =============================================================================
   Overview: the cross-module "needs attention" inbox (#136 Phase 6, #135 §15)
   src/controllers/home.php. One summary strip, the inbox, and a right column
   of "Coming up" and "Recent activity". Severity colours are the status
   vocabulary's tokens; nothing here introduces a sixth level.
   ========================================================================== */

/* The summary strip: one panel, five figures divided by hairlines, so it reads
   as one statement about the estate rather than five cards competing. */
.ov-kpis {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    margin-bottom: var(--space-4);
    overflow: hidden;
}
.ov-kpi {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 14px 16px;
    color: inherit;
    border-left: 1px solid var(--line-soft);
    margin-left: -1px;               /* the first column's line sits under the border */
    min-width: 0;
}
.ov-kpi:hover { background: var(--hover); text-decoration: none; }
.ov-kpi-label {
    color: var(--fg-muted);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: .05em;
    text-transform: uppercase;
}
.ov-kpi-figure { display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; }
.ov-kpi-value { font-family: var(--font-mono); font-size: 24px; font-weight: 600; line-height: 1.2; color: var(--fg); }
.ov-kpi-of { color: var(--fg-muted); }
.ov-kpi-sub { font-size: var(--text-sm); color: var(--fg-muted); overflow-wrap: anywhere; }
.ov-kpi-sub.is-crit { color: var(--crit); }
.ov-kpi-sub.is-warn { color: var(--warn); }
.ov-kpi-sub.is-ok   { color: var(--ok); }

.ov-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 380px;
    gap: var(--space-4);
    align-items: start;
}
.ov-grid > .card, .ov-side > .card { margin: 0; }
.ov-side { display: flex; flex-direction: column; gap: var(--space-4); min-width: 0; }

/* The inbox itself: a card with no padding, so the severity bands and row
   dividers run edge to edge as in the mockup. */
.ov-inbox { padding: 0; overflow: hidden; }
.ov-inbox-head {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
    padding: 12px 16px;
    border-bottom: 1px solid var(--border);
}
.ov-inbox-head h2, .ov-panel-head h2 { margin: 0; font-size: 14px; }
.ov-count { color: var(--fg-muted); font-family: var(--font-mono); font-size: var(--text-sm); }
.ov-filter { display: flex; flex-wrap: wrap; gap: 4px; margin-left: auto; }
.ov-filter-btn {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    min-height: 28px;
    padding: 0 10px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--panel);
    color: var(--fg-muted);
    font-size: var(--text-sm);
}
.ov-filter-btn:hover { background: var(--hover); color: var(--fg); text-decoration: none; }
.ov-filter-btn.is-on { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.ov-filter-n { font-family: var(--font-mono); font-size: var(--text-xs); }

.ov-sev {
    margin: 0;
    padding: 8px 16px;
    background: var(--raised);
    border-bottom: 1px solid var(--line-soft);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: .06em;
    text-transform: uppercase;
}
.ov-sev.is-crit { color: var(--crit); }
.ov-sev.is-warn { color: var(--warn); }
.ov-sev span { font-weight: 600; }

.ov-list, .ov-dated { list-style: none; margin: 0; padding: 0; }
.ov-item {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 11px 16px;
    border-bottom: 1px solid var(--line-soft);
}
.ov-list:last-child .ov-item:last-child { border-bottom: 0; }
.ov-icon {
    flex: none;
    width: 28px;
    height: 28px;
    border-radius: 7px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.ov-icon svg { width: 14px; height: 14px; }
.ov-icon.is-crit { color: var(--crit); background: var(--crit-soft); }
.ov-icon.is-warn { color: var(--warn); background: var(--warn-soft); }
.ov-body { display: flex; flex-direction: column; gap: 2px; flex: 1 1 auto; min-width: 0; }
.ov-title { font-weight: 500; overflow-wrap: anywhere; }
.ov-sub { color: var(--fg-muted); font-size: var(--text-sm); overflow-wrap: anywhere; }
.ov-sub.is-crit { color: var(--crit); }
.ov-meta { display: flex; align-items: center; gap: 12px; flex: none; }
.ov-age {
    width: 3.2em;
    text-align: right;
    color: var(--fg-muted);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
}
.ov-action { white-space: nowrap; }

.ov-clear {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: var(--space-5) 16px;
    color: var(--fg-muted);
}
.ov-clear svg { width: 18px; height: 18px; color: var(--ok); flex: none; }
.ov-clear strong { color: var(--fg); }

/* The right column's two panels. */
.ov-panel { padding: 14px 16px; }
.ov-panel-head { display: flex; align-items: baseline; gap: var(--space-2); margin-bottom: 10px; }
.ov-panel-head h2 { flex: 1 1 auto; }
.ov-none { margin: 0; }
.ov-dated { display: flex; flex-direction: column; gap: 10px; }
.ov-dated > li { display: flex; gap: 10px; align-items: flex-start; min-width: 0; }
.ov-dated .ov-title { color: var(--fg); }
.ov-date {
    flex: none;
    width: 7.5em;
    padding-top: 1px;
    color: var(--fg-muted);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
}
.ov-date.is-soon { color: var(--warn); font-weight: 600; }
.ov-avatar {
    flex: none;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 10px;
    font-weight: 600;
    background: var(--accent-soft);
    color: var(--accent);
}
.ov-avatar.is-crit { background: var(--crit-soft); color: var(--crit); }

@media (max-width: 1280px) {
    .ov-grid { grid-template-columns: minmax(0, 1fr); }
}
/* A phone: the figures go two-up, and each row puts its chip, age and action
   on a second line under the text rather than squeezing the title to a word. */
@media (max-width: 640px) {
    .ov-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .ov-kpi { border-top: 1px solid var(--line-soft); margin-top: -1px; }
    .ov-item { flex-wrap: wrap; align-items: flex-start; }
    .ov-body { flex-basis: calc(100% - 40px); }
    .ov-meta { width: 100%; padding-left: 40px; }
    .ov-age { width: auto; text-align: left; }
    .ov-action { margin-left: auto; }
    .ov-filter { margin-left: 0; }
}

/* =============================================================================
   The ⌘K command palette (#136; public/assets/palette.js). A modal over a
   dimmed page: input, grouped results, a key legend. Built by the script, so
   nothing here renders with scripts off.
   ========================================================================== */

/* The sidebar trigger, when it is a button (a user without licence search):
   the <a> version is styled in console.css; this only evens out the element. */
button.sb-search { width: calc(100% - 24px); font: inherit; text-align: left; cursor: pointer; }
/* .sb-search sets display:flex, which would otherwise beat [hidden]. */
.sb-search[hidden] { display: none; }
/* palette.js writes "Ctrl K" off a Mac, which wrapped onto two lines. */
.sb-search .sb-kbd { white-space: nowrap; }

.pal-open, .pal-open body { overflow: hidden; }
.pal-backdrop {
    position: fixed;
    inset: 0;
    z-index: 1000;
    background: rgba(15, 17, 21, .38);
    display: flex;
    justify-content: center;
    align-items: flex-start;
    padding: 12vh 16px 16px;
}
.pal-backdrop[hidden] { display: none; }
.pal {
    width: 680px;
    max-width: 100%;
    max-height: 76vh;
    display: flex;
    flex-direction: column;
    background: var(--panel);
    color: var(--fg);
    border: 1px solid var(--border);
    border-radius: 12px;
    box-shadow: 0 24px 64px rgba(16, 24, 40, .32);
    overflow: hidden;
}
.pal-head {
    display: flex;
    align-items: center;
    gap: 12px;
    height: 56px;
    flex: none;
    padding: 0 18px;
    border-bottom: 1px solid var(--border);
    color: var(--fg-muted);
}
.pal-head > svg { width: 18px; height: 18px; flex: none; }
.pal .pal-input,
.pal .pal-input:focus {
    flex: 1 1 auto;
    min-width: 0;
    width: auto;
    border: 0;
    outline: none;
    box-shadow: none;
    background: transparent;
    padding: 0;
    font: inherit;
    font-size: 16px;
    color: var(--fg);
}
.pal-kbd, .pal-foot kbd {
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    padding: 1px 6px;
    border: 1px solid var(--border);
    border-radius: 4px;
    color: var(--fg-muted);
    background: var(--panel);
}
.pal-list { overflow-y: auto; padding: 8px; display: flex; flex-direction: column; gap: 2px; }
.pal-group {
    padding: 8px 10px 4px;
    color: var(--fg-muted);
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: .06em;
    text-transform: uppercase;
}
.pal-opt {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 8px 10px;
    border-radius: var(--radius-lg);
    color: var(--fg);
}
.pal-opt:hover { text-decoration: none; }
.pal-opt.is-sel { background: var(--sel-row); }
.pal-icon {
    flex: none;
    width: 30px;
    height: 30px;
    border-radius: 7px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--off-soft);
    color: var(--fg-muted);
}
.pal-icon svg { width: 15px; height: 15px; }
.pal-icon.is-action { background: var(--accent-soft); color: var(--accent); }
.pal-text { display: flex; flex-direction: column; gap: 1px; flex: 1 1 auto; min-width: 0; }
.pal-title { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pal-sub { color: var(--fg-muted); font-size: var(--text-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pal-hit { background: var(--warn-soft); color: inherit; border-radius: 3px; }
.pal-enter { visibility: hidden; font-family: var(--font-mono); font-size: var(--text-xs); color: var(--fg-muted); }
.pal-opt.is-sel .pal-enter { visibility: visible; }
.pal-empty { padding: 18px 10px; color: var(--fg-muted); text-align: center; }
.pal-foot {
    display: flex;
    flex-wrap: wrap;
    gap: 6px 16px;
    flex: none;
    padding: 10px 18px;
    border-top: 1px solid var(--border);
    background: var(--raised);
    color: var(--fg-muted);
    font-size: var(--text-sm);
}
.pal-foot-end { margin-left: auto; }

@media (max-width: 640px) {
    .pal-backdrop { padding: 8px; }
    .pal { max-height: calc(100vh - 16px); }
    .pal-foot { display: none; }   /* no arrow keys on a phone */
}

/* =============================================================================
   Phones (#136 mobile)

   ONE breakpoint for the phone layout: (max-width: 699px). Every phone rule in
   app.css and console.css sits under that exact query, and app.js tests the
   same string (PHONE_MQ). Above it -- tablets, laptops, the 1440p and 4K
   desktops the app is tuned for -- nothing in these sections applies, so a
   wide screen renders exactly as it did before them. The older 860px query
   (the drawer and bottom bar) is the tablet layout and is left as it was.

   The phone renders the app like an app (Nathan, #136): no page scrolls
   sideways, a table is a stack of cards, a menu is a bottom sheet, every
   control is a 44px target, and a field is 16px text (iOS zooms the page on
   focusing anything smaller). The app bar is in console.css; the components
   and the sheets are here.
   ========================================================================== */

/* The sheets' scrim. Declared on :root rather than inside the query so it
   follows the theme the same way as every other token; nothing reads it
   above 699px. */
:root { --scrim: rgba(15, 17, 21, .35); }
:root[data-theme="dark"] { --scrim: rgba(0, 0, 0, .5); }
@media (prefers-color-scheme: dark) {
    :root[data-theme="auto"] { --scrim: rgba(0, 0, 0, .5); }
}

/* The sheet only ever exists on a phone (app.js builds it there), so its
   hidden state needs no query. */
.m-sheet[hidden], .m-scrim[hidden] { display: none; }

@media (max-width: 699px) {
    /* ---- Touch and platform ----------------------------------------------
       A tap flashes a faint accent wash, not the platform's grey slab. */
    html {
        -webkit-tap-highlight-color: rgba(10, 108, 128, .14);
        -webkit-text-size-adjust: 100%;
        text-size-adjust: 100%;
    }

    /* 16px or iOS zooms in on focus and leaves the page zoomed. 44px high. */
    input[type="text"], input[type="search"], input[type="password"],
    input[type="email"], input[type="url"], input[type="date"], input[type="number"],
    input[type="time"], input[type="datetime-local"], input[type="tel"],
    input:not([type]), textarea, select {
        font-size: 16px;
        min-height: 44px;
    }
    input[type="checkbox"], input[type="radio"] { width: 20px; height: 20px; }
    input[type="file"] { max-width: 100%; font-size: 14px; }

    .btn, .btn-sm {
        min-height: 44px;
        padding: .5rem 1rem;
        font-size: 15px;
    }
    .btn > svg { width: 18px; height: 18px; }
    .icon-btn { width: 44px; height: 44px; }
    .icon-btn svg { width: 20px; height: 20px; }
    .iconbtn { min-width: 44px; min-height: 44px; }

    /* ---- Forms: full width and stacked ------------------------------------
       The filter bars' label+control pairs stack, and a field that shared a
       line with its button takes the line and puts the button under it. */
    label { font-size: 14px; }
    .form-card { max-width: none; }
    .inline-label {
        display: flex;
        flex-direction: column;
        align-items: stretch;
        gap: 4px;
        width: 100%;
        margin: 0 0 var(--space-3);
    }
    .inline-label input, .inline-label select { width: 100%; min-width: 0; }
    .inline-form { display: flex; flex-wrap: wrap; width: 100%; gap: var(--space-2); }
    .inline-form > input, .inline-form > select, .inline-form > textarea { flex: 1 1 100%; width: 100%; min-width: 0; }
    .inline-form > label { flex: 1 1 100%; min-width: 0; margin: 0; }
    .inline-form > label :is(input, select) { width: 100%; }
    .inline-form > .btn, .inline-form > button { flex: 1 1 auto; }
    .upload-row > * { max-width: 100%; }
    .upload-row > .btn { flex: 1 1 auto; }
    .bulk-bar select { min-width: 0; }
    .tree-admin .tree-row input[name="name"], .tree-admin .tree-row select,
    .tree-admin .tree-row input[name="description"] { width: 100%; flex: 1 1 100%; min-width: 0; }

    /* One or two buttons in a row become full-width buttons, one per line --
       the shape a phone's own confirm and cancel take. Three or more stay a
       wrapping row. */
    .btn-row > .btn:only-child,
    .btn-row > .btn:first-child:nth-last-child(2),
    .btn-row > .btn:first-child:nth-last-child(2) ~ .btn { flex: 1 1 100%; }
    .form-card form > .btn:last-child { width: 100%; }

    /* ---- Long unbroken text: wrap, never widen the page ------------------
       Hashes, tokens, URLs and FQDNs have no spaces to break at. */
    .main pre { white-space: pre-wrap; overflow-wrap: anywhere; max-width: 100%; }
    .main code, .main .mono { overflow-wrap: anywhere; }
    .main a, .main td, .main dd, .main li, .main p { overflow-wrap: anywhere; }
    .kv { grid-template-columns: minmax(84px, 34%) minmax(0, 1fr); }
    .kv dd { overflow-wrap: anywhere; min-width: 0; }
    .keyblock { font-size: 14px; }

    /* ---- Tables become cards (every .table; the Nodes fleet is its own
       grid, .fl-*, and is not touched) ------------------------------------
       Each row is a card: its first column is the title, every other cell a
       label / value line, and a cell of buttons goes to the card's foot as
       44px targets. The labels are the column headers, copied onto each cell
       as data-label by app.js (labelTables) on load and after every live
       refresh; with scripts off the cards still stack, unlabelled. A table
       that must stay a grid opts out with .keep-table. */
    .table:not(.keep-table) {
        display: block;
        overflow: visible;
        background: transparent;
        border: 0;
        box-shadow: none;
        padding: 0;
        margin: 0;
        font-size: 14px;
    }
    /* The header row is only kept for what can be clicked in it -- a sort
       link, the Software Updates bulk button, a select-all box -- as a
       wrapping toolbar over the cards. The words are on every card. */
    .table:not(.keep-table) > thead { display: block; }
    .table:not(.keep-table) > thead th { display: none; }
    .table:not(.keep-table) > thead th:has(a, button, input, select) {
        display: flex;
        align-items: center;
        gap: 8px;
        min-height: 36px;
        padding: 0;
        background: transparent;
        border: 0;
    }
    /* Sort links read as a row of chips under a "Sort" label. */
    .table:not(.keep-table) > thead > tr:has(th a)::before {
        content: "Sort";
        font-size: 13px;
        color: var(--fg-muted);
    }
    .table:not(.keep-table) > thead th a {
        display: inline-flex;
        align-items: center;
        min-height: 36px;
        padding: 0 12px;
        border: 1px solid var(--line);
        border-radius: 18px;
        background: var(--panel);
        color: var(--fg);
        font-size: 13px;
        font-weight: 500;
        text-transform: none;
        letter-spacing: 0;
    }
    .table:not(.keep-table) > tbody,
    .table:not(.keep-table) > tfoot { display: block; }
    .table:not(.keep-table) tr {
        position: relative;
        display: flex;
        flex-direction: column;
        gap: 6px;
        margin: 0 0 10px;
        padding: 12px 14px;
        background: var(--panel);
        border: 1px solid var(--line);
        border-radius: 12px;
    }
    .table:not(.keep-table) > thead > tr {
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        gap: 6px 14px;
        margin: 0 0 var(--space-2);
        padding: 0;
        background: transparent;
        border: 0;
        border-radius: 0;
    }
    .table:not(.keep-table) tbody tr:hover { background: var(--panel); }
    .table:not(.keep-table) td,
    .table:not(.keep-table) tbody th {
        display: block;
        min-width: 0;
        max-width: none;
        padding: 0;
        border: 0;
        background: transparent;
        text-align: left;
        white-space: normal;
        overflow-wrap: anywhere;
    }
    .table:not(.keep-table) td:empty { display: none; }
    /* A key/value table (th + td per row): the key is the line's label. */
    .table:not(.keep-table) tbody th {
        width: auto;
        font-size: var(--text-sm);
        font-weight: 500;
        text-transform: none;
        letter-spacing: 0;
        color: var(--fg-muted);
    }
    /* ...and the table is one panel of lines, not a card per fact. */
    .table:not(.keep-table):has(> tbody > tr > th:first-child) {
        background: var(--panel);
        border: 1px solid var(--line);
        border-radius: 12px;
        padding: 0 14px;
        margin: 0 0 var(--space-4);
    }
    .table:not(.keep-table) > tbody > tr:has(> th:first-child) {
        gap: 2px;
        margin: 0;
        padding: 10px 0;
        background: transparent;
        border: 0;
        border-top: 1px solid var(--line-soft);
        border-radius: 0;
    }
    .table:not(.keep-table) > tbody > tr:has(> th:first-child):first-child { border-top: 0; }
    .table:not(.keep-table) > tbody > tr > th:first-child + td { font-size: 15px; font-weight: 400; }
    /* The title: the first cell, or the one after the select box. */
    .table:not(.keep-table) tr > td:first-child:not(.t-check),
    .table:not(.keep-table) tr > td.t-check + td {
        font-size: 16px;
        font-weight: 600;
        line-height: 1.3;
        color: var(--fg);
    }
    /* ...but what the title cell says under the name (a description, a
       display name) stays secondary. */
    .table:not(.keep-table) tr > td:first-child :is(.dim, .muted, .hint, small),
    .table:not(.keep-table) tr > td.t-check + td :is(.dim, .muted, .hint, small) {
        font-size: 13px;
        font-weight: 400;
    }
    .table:not(.keep-table) tr > td.t-check + td { padding-right: 36px; }
    .table:not(.keep-table) td.t-check {
        position: absolute;
        top: 4px;
        right: 4px;
        width: 44px;
        height: 44px;
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 0;
    }
    /* A labelled line: the header on the left, the value beside it. The label
       is positioned rather than a grid column, so a value made of several
       inline pieces (a pill, a version, a date) stays one flowing value. */
    .table:not(.keep-table) td[data-label] {
        position: relative;
        padding-left: calc(min(38%, 136px) + 10px);
        min-height: 20px;
        font-size: 14px;
        font-weight: 400;
    }
    .table:not(.keep-table) td[data-label]::before {
        content: attr(data-label);
        position: absolute;
        left: 0;
        top: 1px;
        width: min(38%, 136px);
        color: var(--fg-muted);
        font-family: var(--font-ui);
        font-size: 13px;
        font-weight: 400;
        line-height: 1.5;
        overflow-wrap: anywhere;
    }
    /* A long value (an error, a note) goes under its label, full width,
       rather than down a narrow column beside it. app.js marks it. */
    .table:not(.keep-table) td[data-label].is-long { padding-left: 0; padding-top: 20px; }
    .table:not(.keep-table) td[data-label].is-long::before { width: auto; }
    /* The actions: the foot of the card, full-size buttons. */
    .table:not(.keep-table) td.t-actions,
    .table:not(.keep-table) td.is-actions {
        order: 99;
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: 8px;
        margin-top: 4px;
        padding: 10px 0 0;
        border-top: 1px solid var(--line-soft);
        font-size: 14px;
        font-weight: 400;
        text-align: left;
        white-space: normal;
    }
    .table:not(.keep-table) td.t-actions::before,
    .table:not(.keep-table) td.is-actions::before { content: none; }
    .table:not(.keep-table) td.t-actions > .btn,
    .table:not(.keep-table) td.is-actions > .btn,
    .table:not(.keep-table) td.t-actions > form,
    .table:not(.keep-table) td.is-actions > form { flex: 1 1 auto; margin: 0; }
    .table:not(.keep-table) td.t-actions > form > .btn,
    .table:not(.keep-table) td.is-actions > form > .btn { width: 100%; }
    .table:not(.keep-table) td.t-actions > .more-menu,
    .table:not(.keep-table) td.is-actions > .more-menu { margin-left: auto; }
    /* A bare card of rows needs no second box around it. */
    .card:has(> .table:not(.keep-table)),
    .card:has(> .table-scroll > .table:not(.keep-table)) {
        background: transparent;
        border: 0;
        box-shadow: none;
        padding: 0;
    }
    .table-scroll { overflow: visible; margin: 0; padding: 0; }
    .notes-cell { max-width: none; }

    /* ---- The "⋯" menus, with scripts off -----------------------------------
       app.js opens every .more-menu and .tab-menu as a bottom sheet on a
       phone, so the <details> never opens there. Without it, the menu opens
       in place, in the flow, rather than as a popover that can hang off the
       edge of the screen (Nathan's Manage menu at 390px). */
    .more-menu[open], .tab-menu[open] { display: block; width: 100%; }
    .more-menu[open] > .more-menu-list,
    .tab-menu[open] > .tab-menu-list {
        position: static;
        width: 100%;
        min-width: 0;
        margin-top: 4px;
        box-shadow: none;
    }
    .more-menu-item, .tab-menu-item {
        display: flex;
        align-items: center;
        min-height: 48px;
        font-size: 16px;
        white-space: normal;
    }

    /* ---- Sheets (built by app.js) -----------------------------------------
       Two shapes. The action sheet (the app bar's ⋯, MAppActions): grouped
       rows on a panel inset 8px from the edges, and Cancel as its own panel
       under it. The bottom sheet (a row's ⋯, Manage, Group): edge to edge,
       rounded top, a handle, a title, the rows, and Cancel. Both 50px rows. */
    .m-open, .m-open body { overflow: hidden; }
    .m-scrim {
        position: fixed;
        inset: 0;
        z-index: 900;
        background: var(--scrim);
    }
    .m-sheet {
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 901;
        display: flex;
        flex-direction: column;
        max-height: calc(100% - 24px - env(safe-area-inset-top, 0px));
        background: var(--panel);
        color: var(--fg);
        border-radius: 18px 18px 0 0;
        box-shadow: 0 -8px 30px rgba(0, 0, 0, .2);
        padding-bottom: env(safe-area-inset-bottom, 0px);
        font-size: 16px;
    }
    .m-sheet:focus { outline: none; }
    .m-sheet-handle {
        flex: none;
        width: 40px;
        height: 5px;
        margin: 8px auto 2px;
        border-radius: 3px;
        background: var(--line);
    }
    .m-sheet-title {
        flex: none;
        padding: 6px 16px 10px;
        font-size: 18px;
        font-weight: 600;
        line-height: 1.3;
        overflow-wrap: anywhere;
    }
    .m-sheet-sub { display: block; font-size: 13px; font-weight: 400; color: var(--fg-muted); }
    .m-sheet-body {
        flex: 1 1 auto;
        min-height: 0;
        overflow-y: auto;
        overscroll-behavior: contain;
        border-top: 1px solid var(--line-soft);
    }
    .m-sheet-group-title {
        padding: 12px 16px 4px;
        font-size: var(--text-sm);
        font-weight: 600;
        letter-spacing: .06em;
        text-transform: uppercase;
        color: var(--fg-muted);
    }
    .m-sheet-cancel {
        flex: none;
        height: 54px;
        margin: 8px 12px 12px;
        border: 0;
        border-radius: 14px;
        background: var(--sunk);
        color: var(--accent);
        font: 600 17px/1 var(--font-ui);
        cursor: pointer;
    }

    /* The action sheet: a floating stack. */
    .m-sheet.is-action {
        left: 8px;
        right: 8px;
        bottom: calc(12px + env(safe-area-inset-bottom, 0px));
        padding-bottom: 0;
        gap: 8px;
        background: transparent;
        border-radius: 0;
        box-shadow: none;
    }
    .m-sheet.is-action .m-sheet-body {
        background: var(--panel);
        border-top: 0;
        border-radius: 14px;
        box-shadow: 0 8px 30px rgba(0, 0, 0, .18);
    }
    .m-sheet.is-action .m-sheet-cancel {
        margin: 0;
        background: var(--panel);
        box-shadow: 0 8px 30px rgba(0, 0, 0, .18);
    }

    /* A row. The sheet moves the page's own controls in (a link, a button, a
       form's submit), so each keeps its behaviour -- CSRF, confirm, its
       script -- and only its look is a row's here. */
    .m-row > a, .m-row > button, .m-row > form > button,
    .m-row > form > .btn, .m-row > .btn, .m-row > .icon-btn,
    .m-sheet .more-menu-item, .m-sheet .tab-menu-item, .m-sheet .m-link {
        display: flex;
        align-items: center;
        justify-content: flex-start;
        gap: 12px;
        width: 100%;
        height: auto;
        min-height: 50px;
        margin: 0;
        padding: 10px 16px;
        border: 0;
        border-top: 1px solid var(--line-soft);
        border-radius: 0;
        background: transparent;
        color: var(--fg);
        font: 400 16px/1.3 var(--font-ui);
        text-align: left;
        text-decoration: none;
        white-space: normal;
        filter: none;
        cursor: pointer;
    }
    /* Module icon buttons carry their own two-class sizing (.mon-iconbtn.is-
       bordered is 32px square in monitoring.css), which outranks the row rule
       above and squeezed "Check all nodes for updates" into a 32px column. */
    .m-sheet .m-row > form > .mon-iconbtn, .m-sheet .m-row > .mon-iconbtn {
        width: 100%;
        height: auto;
        min-height: 50px;
        border: 0;
        border-top: 1px solid var(--line-soft);
        background: transparent;
    }
    .m-sheet .m-row .mon-iconbtn svg { width: 20px; height: 20px; }
    .m-sheet .m-row form { display: block; margin: 0; }
    .m-sheet-group-title + .m-row > *,
    .m-sheet-group-title + .m-link,
    .m-sheet-body > .m-row:first-child > *,
    .m-sheet-body > .m-link:first-child { border-top: 0; }
    .m-row > :active, .m-sheet .m-link:active,
    .m-sheet .more-menu-item:active, .m-sheet .tab-menu-item:active { background: var(--hover); }
    .m-row svg, .m-sheet .m-link svg { width: 20px; height: 20px; flex: none; color: var(--fg-muted); }
    .m-row .btn-primary, .m-row .btn-primary svg { color: var(--accent); font-weight: 600; }
    .m-row .btn-danger, .m-row .is-danger, .m-sheet .more-menu-item.is-danger { color: var(--crit); }
    .m-row .btn-danger svg { color: var(--crit); }
    .m-row :disabled, .m-row [disabled] { opacity: .45; }
    .m-link-text { flex: 1 1 auto; min-width: 0; }
    .m-sheet .m-link.is-current { color: var(--accent); font-weight: 600; }
    .m-sheet .m-link.is-current svg { color: var(--accent); }
    /* A list moved in whole (a row's menu, Manage) loses its popover box. */
    .m-sheet .more-menu-list, .m-sheet .tab-menu-list {
        position: static;
        min-width: 0;
        width: auto;
        max-height: none;
        margin: 0;
        padding: 0;
        list-style: none;
        background: transparent;
        border: 0;
        border-radius: 0;
        box-shadow: none;
    }
    .m-sheet .more-menu-list form { display: block; margin: 0; }
    .m-sheet .more-menu-list > :first-child .more-menu-item,
    .m-sheet .more-menu-list > .more-menu-item:first-child,
    .m-sheet .tab-menu-list > li:first-child > .tab-menu-item { border-top: 0; }
    .m-sheet .tab-menu-item.is-current { background: transparent; color: var(--accent); font-weight: 600; }
    /* The licence Group picker's tree. */
    .m-sheet .tab-menu-tree { padding: 0 8px 8px; }
    .m-sheet .side-tree a, .m-sheet .side-dim {
        display: flex;
        align-items: center;
        min-height: 44px;
        padding: 0 10px;
        font-size: 16px;
    }

    /* ---- The ⌘K palette: a full-screen sheet, input at the top ------------ */
    .pal-backdrop { padding: 0; align-items: stretch; }
    .pal {
        width: 100%;
        max-width: none;
        height: 100%;
        max-height: none;
        border: 0;
        border-radius: 0;
        box-shadow: none;
        padding-top: env(safe-area-inset-top, 0px);
        padding-bottom: env(safe-area-inset-bottom, 0px);
    }
    .pal-head { min-height: 56px; }
    .pal .pal-input, .pal .pal-input:focus { font-size: 16px; min-height: 44px; }
    .pal-kbd { display: none; }
    .pal-cancel {
        flex: none;
        min-height: 44px;
        padding: 0 4px 0 8px;
        border: 0;
        background: transparent;
        color: var(--accent);
        font: 600 16px/1 var(--font-ui);
        cursor: pointer;
    }
    .pal-opt { min-height: 52px; }
    .pal-enter { display: none; }
    .pal-foot { display: none; }   /* no arrow keys on a phone */

    /* ---- Module lists that were grids already ------------------------------
       The licence list and the certificate list reflow on their own; on a
       phone their rows get a touch-sized height. */
    .lic-tr:not(.lic-thead) { min-height: 56px; padding-top: 10px; padding-bottom: 10px; }
}

@media (max-width: 699px) and (prefers-reduced-motion: no-preference) {
    .m-scrim { animation: m-fade .18s ease; }
    .m-sheet { animation: m-rise .2s ease; }
}
@keyframes m-rise { from { transform: translateY(24px); opacity: .6; } to { transform: none; opacity: 1; } }
@keyframes m-fade { from { opacity: 0; } to { opacity: 1; } }

/* Not a phone: the Cancel the palette grows for one stays out of the way. */
@media (min-width: 700px) {
    .pal-cancel { display: none; }
}
