/* ── JetBrains Mono, self-hosted ────────────────────────────────────────────────────────
   Vendored rather than linked, for two reasons that are both about this app rather than
   about taste:

   * the Content-Security-Policy has no `font-src`, so `default-src 'self'` BLOCKS
     fonts.googleapis.com and fonts.gstatic.com. A Google Fonts link would be refused by the
     browser and the UI would fall back silently -- looking almost right, which is the worst
     kind of wrong;
   * this is a self-hosted MDM on a school network. A console that needs the public internet
     to render correctly is a console that renders wrong on the day the internet is the thing
     you are trying to fix.

   One VARIABLE file per subset covers 400-700, which is why there are two files and not six:
   Google serves the same woff2 for every weight of this family. 42 KB for both subsets.

   `font-display: swap` on purpose. The fallback is SF Mono or Menlo -- a real monospace at
   the same metrics -- so swapping in is a small shift, and blocking paint on a webfont in an
   ops console is a worse trade.

   Licence: SIL Open Font Licence 1.1, in OFL.txt beside these files. Redistribution is
   permitted; the licence file travelling with them is the condition. */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/static/fonts/jetbrains-mono-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/static/fonts/jetbrains-mono-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

  :root {
    /* Ops console: near-black ground, one green accent, everything set in mono, and borders
       instead of elevation. Implemented from design_handoff_ops_console_redesign, whose
       values are exact rather than approximate -- tests/test_ops_theme.py holds them to it.

       DARK IS THE DEFAULT, and that is a deliberate inversion of what this file used to do.
       The console's identity is the terminal look; light is a toggle for people who want it,
       not the system's choice made for them. A visitor who has never touched the toggle gets
       this block whatever their OS prefers.

       Three things about the palette that are load-bearing rather than taste:
       * `--good` IS `--brand`. One positive colour, plus amber and red for problems. A
         second green (or a blue) would make "fine" ambiguous at a glance, which is the one
         thing a status console cannot afford.
       * `--shadow` is `none`. Every card in this file already asks for `box-shadow:
         var(--shadow)`, so flattening the token flattens the whole app in one place, and the
         separation comes from `--line-soft` borders instead.
       * `--ink-faint` and `--ink-faint-solid` are now the SAME value. They differed only
         because one carried alpha, which is exactly how the two drifted apart before.
*/
    --ground: #0b0e11;
    --surface: #0d1117;
    --surface-2: #161b22;
    --ink: #e6edf3;
    --ink-soft: #8b949e;
    /* The handoff's #6e7681 is 4.12:1 on --surface, under AA -- and this token is not
       decoration: it colours every label, eyebrow, table header and section title in the
       console, all of it small text people read to learn what a value MEANS. Lifted to the
       first value on the same hue that clears 4.5 on all three surfaces -- including
       --surface-2, which is the lightest and so the hardest, and which is where the
       first attempt at this still failed; it keeps 1.34:1 of
       separation from --ink-soft, so the three tiers of grey survive. */
    --ink-faint: #7a838e;
    --ink-faint-solid: #7a838e;
    --line: #30363d;
    --line-soft: #1f262d;
    --brand: #39ff9d;
    --brand-strong: #6bffb8;
    --brand-soft: rgba(57,255,157,.08);
    --brand-line: rgba(57,255,157,.28);
    /* What text becomes when it sits ON the accent. Named, because the answer is not "white"
       and the pages that assumed it was ended up with 1.3:1 buttons: #39ff9d is a LIGHT
       colour. Five components had their own copy of `color: #fff` on a brand background --
       the primary button, filter chips, the bulk bar, and two in the Baseline builder. One
       token means the next filled-accent thing gets it right without anybody remembering.
       `--on-brand-veil` is the same idea for a translucent layer drawn on the accent. */
    --on-brand: #0b0e11;
    --on-brand-veil: rgba(0,0,0,.18);
    /* No fourth hue. `--warm` predates this theme and is still referenced by the sidebar
       glow and a few chips; it points at the amber so it cannot reintroduce a colour the
       palette does not have. */
    --warm: #ffc107;
    --warm-soft: rgba(255,193,7,.12);
    --warn: #ffc107;
    --warn-soft: rgba(255,193,7,.12);
    --crit: #ff5c5c;
    --crit-soft: rgba(255,92,92,.12);
    /* "good" IS the brand here -- one positive colour, not two. */
    --good: #39ff9d;
    --good-soft: rgba(57,255,157,.12);
    --shadow: none;
    --shadow-lift: none;

    --font-mono: 'JetBrains Mono', ui-monospace, "SF Mono", Menlo, monospace;
    --font-display: var(--font-mono);
    --font-body: var(--font-mono);

    --r-card: 6px;
    --r-tile: 6px;
    --r-btn: 4px;
  }
  /* Explicit dark, for the toggle. Same values as :root; kept as its own block so a reader
     looking for what `data-theme="dark"` does finds it rather than inferring it. */
  :root[data-theme="dark"] {
    --ground: #0b0e11;
    --surface: #0d1117;
    --surface-2: #161b22;
    --ink: #e6edf3;
    --ink-soft: #8b949e;
    --ink-faint: #7a838e;
    --ink-faint-solid: #7a838e;
    --line: #30363d;
    --line-soft: #1f262d;
    --brand: #39ff9d;
    --brand-strong: #6bffb8;
    --brand-soft: rgba(57,255,157,.08);
    --brand-line: rgba(57,255,157,.28);
    /* What text becomes when it sits ON the accent. Named, because the answer is not "white"
       and the pages that assumed it was ended up with 1.3:1 buttons: #39ff9d is a LIGHT
       colour. Five components had their own copy of `color: #fff` on a brand background --
       the primary button, filter chips, the bulk bar, and two in the Baseline builder. One
       token means the next filled-accent thing gets it right without anybody remembering.
       `--on-brand-veil` is the same idea for a translucent layer drawn on the accent. */
    --on-brand: #0b0e11;
    --on-brand-veil: rgba(0,0,0,.18);
    /* No fourth hue. `--warm` predates this theme and is still referenced by the sidebar
       glow and a few chips; it points at the amber so it cannot reintroduce a colour the
       palette does not have. */
    --warm: #ffc107;
    --warm-soft: rgba(255,193,7,.12);
    --warn: #ffc107;
    --warn-soft: rgba(255,193,7,.12);
    --crit: #ff5c5c;
    --crit-soft: rgba(255,92,92,.12);
    /* "good" IS the brand here -- one positive colour, not two. */
    --good: #39ff9d;
    --good-soft: rgba(57,255,157,.12);
    --shadow: none;
    --shadow-lift: none;
  }
  /* Light is opt-in. There is no `prefers-color-scheme` block on purpose: the redesign makes
     dark the console's identity, so a system-light visitor still gets the terminal look until
     they ask for something else. */
  :root[data-theme="light"] {
    --ground: #f6f7f8;
    --surface: #ffffff;
    --surface-2: #eef0f2;
    --ink: #14181c;
    --ink-soft: #57606a;
    --ink-faint: #666e78;
    --ink-faint-solid: #666e78;
    --line: #c7ccd1;
    --line-soft: #dde1e6;
    /* Deliberately a muted forest green, not the dark theme's neon #39ff9d: that green at
       full saturation on white reads as garish, and it fails every contrast floor besides
       (1.32:1). Same hue, legible value. Was #0f8a52 until the accent palette landed, which
       is 4.40:1 as plain text -- under AA by a hair. #0f8851 is 4.51:1 and 0.4% darker, a
       difference nobody can see side by side. All eight accents are checked by
       tests/test_accent_palette.py. */
    --brand: #0f8851;
    --brand-strong: #0c6f42;
    --brand-soft: rgba(15,136,81,.08);
    --brand-line: rgba(15,136,81,.28);
    /* The light theme's accent is a dark forest green, so here it really is white. */
    --on-brand: #ffffff;
    --on-brand-veil: rgba(255,255,255,.20);
    --warm: #9a6700;
    --warm-soft: rgba(154,103,0,.12);
    --warn: #9a6700;
    --warn-soft: rgba(154,103,0,.12);
    --crit: #cf222e;
    --crit-soft: rgba(207,34,46,.10);
    --good: #0f8851;
    --good-soft: rgba(15,136,81,.12);
    --shadow: none;
    --shadow-lift: none;
  }

  /* ---------- Accent palettes ----------------------------------------------------------
     Set server-side as `data-accent` on <html> (see base.html). Green is the default and has
     NO block here: it is what the theme blocks above already define, so picking green is a
     genuine no-op rather than a re-statement that could drift from it.

     Specificity is the whole trick. `:root[data-accent="x"]` is (0,2,0) -- the same weight as
     `:root[data-theme="light"]` above it, and later in the file, so on its own it would apply
     the DARK accent to the light theme. `:root[data-theme="light"][data-accent="x"]` is
     (0,3,0) and wins. Both are needed; neither works alone.

     --on-brand is deliberately absent: every dark accent is light enough for near-black text
     and every light accent dark enough for white, so the values the theme blocks set are
     already right for all eight. See app/accent.py for the measured ratios. */
  :root[data-accent="teal"] {
    --brand: #0bb1b1;
    --brand-strong: #0fefef;
    --brand-soft: rgba(11,177,177,.08);
    --brand-line: rgba(11,177,177,.28);
  }
  :root[data-theme="light"][data-accent="teal"] {
    --brand: #088181;
    --brand-strong: #065f5f;
    --brand-soft: rgba(8,129,129,.08);
    --brand-line: rgba(8,129,129,.28);
  }
  :root[data-accent="sky"] {
    --brand: #23a5f6;
    --brand-strong: #63bff9;
    --brand-soft: rgba(35,165,246,.08);
    --brand-line: rgba(35,165,246,.28);
  }
  :root[data-theme="light"][data-accent="sky"] {
    --brand: #0879bf;
    --brand-strong: #07639d;
    --brand-soft: rgba(8,121,191,.08);
    --brand-line: rgba(8,121,191,.28);
  }
  :root[data-accent="indigo"] {
    --brand: #9e90f9;
    --brand-strong: #d5cffc;
    --brand-soft: rgba(158,144,249,.08);
    --brand-line: rgba(158,144,249,.28);
  }
  :root[data-theme="light"][data-accent="indigo"] {
    --brand: #6f5af6;
    --brand-strong: #5138f4;
    --brand-soft: rgba(111,90,246,.08);
    --brand-line: rgba(111,90,246,.28);
  }
  :root[data-accent="violet"] {
    --brand: #d07bf4;
    --brand-strong: #e6b8f9;
    --brand-soft: rgba(208,123,244,.08);
    --brand-line: rgba(208,123,244,.28);
  }
  :root[data-theme="light"][data-accent="violet"] {
    --brand: #b52fee;
    --brand-strong: #a713e6;
    --brand-soft: rgba(181,47,238,.08);
    --brand-line: rgba(181,47,238,.28);
  }
  :root[data-accent="magenta"] {
    --brand: #f76eb9;
    --brand-strong: #faadd7;
    --brand-soft: rgba(247,110,185,.08);
    --brand-line: rgba(247,110,185,.28);
  }
  :root[data-theme="light"][data-accent="magenta"] {
    --brand: #df0c80;
    --brand-strong: #bd0a6d;
    --brand-soft: rgba(223,12,128,.08);
    --brand-line: rgba(223,12,128,.28);
  }
  :root[data-accent="amber"] {
    --brand: #cc9900;
    --brand-strong: #ffc30f;
    --brand-soft: rgba(204,153,0,.08);
    --brand-line: rgba(204,153,0,.28);
  }
  :root[data-theme="light"][data-accent="amber"] {
    --brand: #946f00;
    --brand-strong: #705400;
    --brand-soft: rgba(148,111,0,.08);
    --brand-line: rgba(148,111,0,.28);
  }
  :root[data-accent="orange"] {
    --brand: #fa792e;
    --brand-strong: #fca36f;
    --brand-soft: rgba(250,121,46,.08);
    --brand-line: rgba(250,121,46,.28);
  }
  :root[data-theme="light"][data-accent="orange"] {
    --brand: #cc4e05;
    --brand-strong: #a94104;
    --brand-soft: rgba(204,78,5,.08);
    --brand-line: rgba(204,78,5,.28);
  }

  /* ---------- Status green intensity ---------------------------------------------------
     `--good` is the ambient colour of a healthy fleet: three signal cells, a presence dot and
     an on-campus label per device. At full strength a good day is a wall of neon, which is
     what this turns down. Set server-side as `data-green` on <html>.

     Same specificity shape as the accent blocks above, and needed for the same reason: the
     bare `:root[data-green=x]` ties with `:root[data-theme="light"]` and would otherwise put
     the DARK value on the light theme.

     "full" has no block -- it is what the theme blocks already define, so it is a true no-op.
     --warn and --crit are deliberately absent: they are rare by design, and dimming an alarm
     is the one thing this must not do. */
  :root[data-green="soft"] {
    --good: #20e985;
    --good-soft: rgba(32,233,133,.12);
  }
  :root[data-theme="light"][data-green="soft"] {
    --good: #23724f;
    --good-soft: rgba(35,114,79,.12);
  }
  :root[data-green="muted"] {
    --good: #28b870;
    --good-soft: rgba(40,184,112,.12);
  }
  :root[data-theme="light"][data-green="muted"] {
    --good: #2e644c;
    --good-soft: rgba(46,100,76,.12);
  }
  :root[data-green="faint"] {
    --good: #328f61;
    --good-soft: rgba(50,143,97,.12);
  }
  :root[data-theme="light"][data-green="faint"] {
    --good: #365949;
    --good-soft: rgba(54,89,73,.12);
  }

  /* ---------- Indicator lights, per accent -----------------------------------------------
     `--good` follows the accent, because the palette at the top of this file says the thing
     it has always been built on: --good IS --brand, one positive colour plus amber and red
     for problems. That held while green was the only accent; the moment a picker shipped,
     choosing amber gave amber text beside green lights -- two positive colours, the exact
     ambiguity the rule exists to prevent.

     --warn and --crit are NOT here and never will be. Recolouring an alarm is the one thing
     this must not do, which is also why amber and orange do not get their literal accent as
     an indicator: amber is dE 19.7 from --warn, so healthy and needs-attention would be the
     same colour. Those two get the nearest colour to the accent that is still in the positive
     band and still clears both alarms. See app/accent.py::INDICATOR.

     Generated. Values, contrast floors and the reasoning all live in app/accent.py. */
/* >>> generated by app/accent.py::indicator_css -- do not hand-edit */
  :root[data-accent="teal"] { --good: #0bb1b1; --good-soft: rgba(11,177,177,.12); }
  :root[data-accent="teal"][data-green="soft"] { --good: #26a8a8; --good-soft: rgba(38,168,168,.12); }
  :root[data-accent="teal"][data-green="muted"] { --good: #3d9695; --good-soft: rgba(61,150,149,.12); }
  :root[data-accent="teal"][data-green="faint"] { --good: #488686; --good-soft: rgba(72,134,134,.12); }
  :root[data-theme="light"][data-accent="teal"] { --good: #088181; --good-soft: rgba(8,129,129,.12); }
  :root[data-theme="light"][data-accent="teal"][data-green="soft"] { --good: #1f7878; --good-soft: rgba(31,120,120,.12); }
  :root[data-theme="light"][data-accent="teal"][data-green="muted"] { --good: #316565; --good-soft: rgba(49,101,101,.12); }
  :root[data-theme="light"][data-accent="teal"][data-green="faint"] { --good: #385555; --good-soft: rgba(56,85,85,.12); }
  :root[data-accent="sky"] { --good: #23a5f6; --good-soft: rgba(35,165,246,.12); }
  :root[data-accent="sky"][data-green="soft"] { --good: #319ee7; --good-soft: rgba(49,158,231,.12); }
  :root[data-accent="sky"][data-green="muted"] { --good: #428fc7; --good-soft: rgba(66,143,199,.12); }
  :root[data-accent="sky"][data-green="faint"] { --good: #4b83ad; --good-soft: rgba(75,131,173,.12); }
  :root[data-theme="light"][data-accent="sky"] { --good: #0879bf; --good-soft: rgba(8,121,191,.12); }
  :root[data-theme="light"][data-accent="sky"][data-green="soft"] { --good: #1e71ad; --good-soft: rgba(30,113,173,.12); }
  :root[data-theme="light"][data-accent="sky"][data-green="muted"] { --good: #31628a; --good-soft: rgba(49,98,138,.12); }
  :root[data-theme="light"][data-accent="sky"][data-green="faint"] { --good: #38546d; --good-soft: rgba(56,84,109,.12); }
  :root[data-accent="indigo"] { --good: #9e90f9; --good-soft: rgba(158,144,249,.12); }
  :root[data-accent="indigo"][data-green="soft"] { --good: #978be9; --good-soft: rgba(151,139,233,.12); }
  :root[data-accent="indigo"][data-green="muted"] { --good: #8981ca; --good-soft: rgba(137,129,202,.12); }
  :root[data-accent="indigo"][data-green="faint"] { --good: #7e78b0; --good-soft: rgba(126,120,176,.12); }
  :root[data-theme="light"][data-accent="indigo"] { --good: #6f5af6; --good-soft: rgba(111,90,246,.12); }
  :root[data-theme="light"][data-accent="indigo"][data-green="soft"] { --good: #6859dd; --good-soft: rgba(104,89,221,.12); }
  :root[data-theme="light"][data-accent="indigo"][data-green="muted"] { --good: #5a55ac; --good-soft: rgba(90,85,172,.12); }
  :root[data-theme="light"][data-accent="indigo"][data-green="faint"] { --good: #504f83; --good-soft: rgba(80,79,131,.12); }
  :root[data-accent="violet"] { --good: #d07bf4; --good-soft: rgba(208,123,244,.12); }
  :root[data-accent="violet"][data-green="soft"] { --good: #c579e5; --good-soft: rgba(197,121,229,.12); }
  :root[data-accent="violet"][data-green="muted"] { --good: #ae74c7; --good-soft: rgba(174,116,199,.12); }
  :root[data-accent="violet"][data-green="faint"] { --good: #9b6fae; --good-soft: rgba(155,111,174,.12); }
  :root[data-theme="light"][data-accent="violet"] { --good: #b52fee; --good-soft: rgba(181,47,238,.12); }
  :root[data-theme="light"][data-accent="violet"][data-green="soft"] { --good: #a63ad7; --good-soft: rgba(166,58,215,.12); }
  :root[data-theme="light"][data-accent="violet"][data-green="muted"] { --good: #8745a9; --good-soft: rgba(135,69,169,.12); }
  :root[data-theme="light"][data-accent="violet"][data-green="faint"] { --good: #6e4883; --good-soft: rgba(110,72,131,.12); }
  :root[data-accent="magenta"] { --good: #f76eb9; --good-soft: rgba(247,110,185,.12); }
  :root[data-accent="magenta"][data-green="soft"] { --good: #e86db0; --good-soft: rgba(232,109,176,.12); }
  :root[data-accent="magenta"][data-green="muted"] { --good: #ca6b9d; --good-soft: rgba(202,107,157,.12); }
  :root[data-accent="magenta"][data-green="faint"] { --good: #b1688d; --good-soft: rgba(177,104,141,.12); }
  :root[data-theme="light"][data-accent="magenta"] { --good: #df0c80; --good-soft: rgba(223,12,128,.12); }
  :root[data-theme="light"][data-accent="magenta"][data-green="soft"] { --good: #cb2778; --good-soft: rgba(203,39,120,.12); }
  :root[data-theme="light"][data-accent="magenta"][data-green="muted"] { --good: #a23b67; --good-soft: rgba(162,59,103,.12); }
  :root[data-theme="light"][data-accent="magenta"][data-green="faint"] { --good: #7f4259; --good-soft: rgba(127,66,89,.12); }
  :root[data-accent="amber"] { --good: #d9e080; --good-soft: rgba(217,224,128,.12); }
  :root[data-accent="amber"][data-green="soft"] { --good: #c5cc76; --good-soft: rgba(197,204,118,.12); }
  :root[data-accent="amber"][data-green="muted"] { --good: #9ea363; --good-soft: rgba(158,163,99,.12); }
  :root[data-accent="amber"][data-green="faint"] { --good: #7e8253; --good-soft: rgba(126,130,83,.12); }
  :root[data-theme="light"][data-accent="amber"] { --good: #797600; --good-soft: rgba(121,118,0,.12); }
  :root[data-theme="light"][data-accent="amber"][data-green="soft"] { --good: #716e19; --good-soft: rgba(113,110,25,.12); }
  :root[data-theme="light"][data-accent="amber"][data-green="muted"] { --good: #605f2c; --good-soft: rgba(96,95,44,.12); }
  :root[data-theme="light"][data-accent="amber"][data-green="faint"] { --good: #525235; --good-soft: rgba(82,82,53,.12); }
  :root[data-accent="orange"] { --good: #c4eb09; --good-soft: rgba(196,235,9,.12); }
  :root[data-accent="orange"][data-green="soft"] { --good: #b2d51c; --good-soft: rgba(178,213,28,.12); }
  :root[data-accent="orange"][data-green="muted"] { --good: #8faa2a; --good-soft: rgba(143,170,42,.12); }
  :root[data-accent="orange"][data-green="faint"] { --good: #72862d; --good-soft: rgba(114,134,45,.12); }
  :root[data-theme="light"][data-accent="orange"] { --good: #647d0e; --good-soft: rgba(100,125,14,.12); }
  :root[data-theme="light"][data-accent="orange"][data-green="soft"] { --good: #5f741e; --good-soft: rgba(95,116,30,.12); }
  :root[data-theme="light"][data-accent="orange"][data-green="muted"] { --good: #54632f; --good-soft: rgba(84,99,47,.12); }
  :root[data-theme="light"][data-accent="orange"][data-green="faint"] { --good: #4b5436; --good-soft: rgba(75,84,54,.12); }
/* <<< end generated indicator palettes */



  * { box-sizing: border-box; }
  body { margin: 0; }
  /* Base link color: any unstyled <a> uses the brand color (never browser blue/
     purple-visited). Components that set their own color still win. */
  a, a:link, a:visited { color: var(--brand-strong); }
  .app {
    font-family: var(--font-body);
    color: var(--ink);
    background: var(--ground);
    min-height: 100vh;
    display: grid;
    grid-template-columns: 220px 1fr;
    -webkit-font-smoothing: antialiased;
    font-size: 14px;
    line-height: 1.5;
  }
  .app::before { /* soft warm glow behind sidebar area */
    content: ""; position: fixed; inset: 0 auto 0 0; width: 220px;
    background: radial-gradient(120% 40% at 0% 0%, var(--warm-soft), transparent 70%);
    pointer-events: none; z-index: 0;
  }

  /* ---------- Sidebar ---------- */
  .side {
    position: sticky; top: 0; align-self: start; height: 100vh;
    display: flex; flex-direction: column; gap: 4px;
    padding: 16px 12px; border-right: 1px solid var(--line-soft);
    background: var(--surface); z-index: 1;
  }
  .brand { display: flex; align-items: center; gap: 10px; padding: 4px 6px 14px; }
  .mark {
    /* Flat: a tinted square with the mark in the accent. The gradient-and-glow chip belonged
       to the previous theme's elevation language, which this one no longer has. */
    width: 26px; height: 26px; border-radius: 5px; flex: none;
    background: var(--brand-soft); border: 1px solid var(--brand-line);
    display: grid; place-items: center; color: var(--brand); position: relative;
  }
  .mark::after { /* marigold presence pin */
    content: ""; position: absolute; right: -3px; bottom: -3px; width: 14px; height: 14px;
    border-radius: 50% 50% 50% 2px; background: var(--warm); border: 2px solid var(--surface);
  }
  .mark svg { width: 20px; height: 20px; }
  .mark-img { width: 100%; height: 100%; object-fit: contain; border-radius: inherit; }
  .mark-emoji { font-size: 22px; line-height: 1; }
  .mark.mark-custom { background: var(--surface-2); box-shadow: none; }
  .mark.mark-custom::after { display: none; }
  .brand b { font-family: var(--font-display); font-size: 15px; font-weight: 700; letter-spacing: -0.01em; display: block; }
  /* The rail lost 30px in the retheme and a two-word subtitle started wrapping onto a second
     line, pushing the whole nav down. Clipped rather than wrapped: a brand subtitle is
     decoration, and decoration must not move the navigation. */
  .brand span { font-size: 11px; color: var(--ink-faint-solid); display: block;
                white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .brand b { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .brand .nav-t { min-width: 0; }
  .nav { display: flex; flex-direction: column; gap: 2px; margin-top: 4px; flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
  .nav-label { font-size: 10.5px; text-transform: lowercase; letter-spacing: 0.09em; color: var(--ink-faint-solid); padding: 12px 10px 5px; font-weight: 700; }
  .nav a {
    display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: var(--r-btn);
    color: var(--ink-soft); text-decoration: none; font-weight: 500; font-size: 12.5px; cursor: pointer;
    transition: background .15s, color .15s;
  }
  .nav a svg { width: 16px; height: 16px; opacity: .85; flex: none; }
  .nav a .count { margin-left: auto; font-size: 11px; background: var(--surface-2); border: 1px solid var(--line); color: var(--ink-soft); padding: 1px 7px; border-radius: var(--r-btn); font-variant-numeric: tabular-nums; }
  .nav a:hover { background: var(--surface-2); color: var(--ink); }
  /* A background tint and the accent colour -- no left-border accent bar. */
  .nav a.active { background: var(--brand-soft); color: var(--brand); }
  .nav a.active svg { opacity: 1; }
  .nav a.active .count { background: var(--surface); border-color: var(--brand-line); color: var(--brand-strong); }
  .nav-tree-h { display:flex; align-items:center; gap:9px; width:100%; background:none; border:0; cursor:pointer; font:inherit; color:var(--ink-soft); font-weight:550; font-size:13px; padding:7px 10px; border-radius:var(--r-btn); }
  .nav-tree-h:hover { background:var(--surface-2); color:var(--ink); }
  .nav-tree-h svg { width:18px; height:18px; opacity:.85; flex:none; }
  .nav-tree-h .caret { margin-left:auto; display:flex; transition:transform .12s; color:var(--ink-soft); }
  .nav-tree-h .caret svg { width:16px; height:16px; opacity:1; }
  .nav-tree-h:hover .caret { color:var(--ink); }
  .nav-tree-h[aria-expanded="false"] .caret { transform:rotate(-90deg); }
  .nav-sub { display:flex; flex-direction:column; gap:2px; padding-left:16px; }
  .nav-sub a { padding-left:14px; }
  .side-foot { margin-top: auto; border-top: 1px solid var(--line); padding-top: 12px; display: flex; align-items: center; gap: 10px; }
  .avatar { width: 34px; height: 34px; border-radius: var(--r-btn); background: var(--warm-soft); color: var(--warm); display: grid; place-items: center; font-weight: 800; font-family: var(--font-display); flex: none; }
  .side-foot .who { min-width: 0; }
  .side-foot .who b { font-size: 13px; display: block; }
  .side-foot .who span { font-size: 11px; color: var(--ink-soft); }
  .icon-btn { -webkit-appearance: none; appearance: none; margin-left: auto; background: none; border: 1px solid transparent; color: var(--ink-faint-solid); border-radius: var(--r-btn); padding: 6px; cursor: pointer; display: grid; place-items: center; }
  .icon-btn:hover { background: var(--surface-2); color: var(--ink); }
  .icon-btn svg { width: 17px; height: 17px; }

  /* ---------- Main ---------- */
  .main { position: relative; z-index: 1; min-width: 0; }
  .topbar {
    position: sticky; top: 0; z-index: 5; display: flex; align-items: center; gap: 16px;
    padding: 16px 30px; background: color-mix(in srgb, var(--ground) 82%, transparent);
    backdrop-filter: blur(10px); border-bottom: 1px solid var(--line);
    /* Wraps rather than crushing: below the breakpoint the search drops to its own line. */
    flex-wrap: wrap;
  }
  /* A page title is a device NAME here, and a truncated device name is a different device.
     min-width: 0 lets it shrink inside the flex row without forcing the search out. */
  .topbar h1 { min-width: 0; }
  .topbar h1 { font-family: var(--font-display); font-size: 22px; font-weight: 700; letter-spacing: -0.01em; margin: 0; }
  .topbar h1 small { display: block; font-family: var(--font-body); font-size: 12.5px; font-weight: 400; color: var(--ink-faint-solid); letter-spacing: 0; margin-top: 2px; }
  /* The search gives up width BEFORE the title breaks. It was `flex: 0 0 210px` in effect,
     so a long device name wrapped to two lines beside a search box that had lost nothing. */
  .search { margin-left: auto; position: relative; flex: 0 1 210px; min-width: 132px; }
  .search svg { position: absolute; left: 11px; top: 50%; transform: translateY(-50%); width: 16px; height: 16px; color: var(--ink-faint-solid); }
  .search input { font-family: inherit; font-size: 12.5px; color: var(--ink); background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-btn); padding: 8px 12px 8px 34px; width: 100%; outline: none; }
  .search input::placeholder { color: var(--ink-faint-solid); }
  .search input:focus { border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand-soft); }
  /* `line-height` is stated, and it is not cosmetic. A <button class="btn"> takes the browser's
     `normal`; an <a class="btn"> inherits the body's 1.5, which at 12.5px is 18.75px. Same
     class, same padding, same border -- and 35px against 37px. They sit side by side all over
     this console (the device header stacks "refresh" as a button and "delivery_trace" as a
     link), so one row of buttons was quietly two heights. Stating it makes the element type
     stop mattering. */
  .btn { font-family: inherit; font-size: 12.5px; font-weight: 600; line-height: 17px; border-radius: var(--r-btn); padding: 8px 13px; cursor: pointer; border: 1px solid var(--line); background: transparent; color: var(--ink); display: inline-flex; align-items: center; gap: 7px; transition: background .15s, border-color .15s, color .15s; }
  .btn svg { width: 16px; height: 16px; }
  .btn:hover { background: var(--surface-2); }
  /* The dark theme's surface-2 is lighter than the ground, so it reads as a hover on its own.
     On light it is barely a step, so hover goes one shade DOWN instead of up. */
  :root[data-theme="light"] .btn:hover { background: #e2e5e9; }
  /* Dark text ON the bright green, not white. #39ff9d is a light colour -- white on it is
     about 1.3:1, which is not a contrast problem so much as an unreadable button. */
  .btn.primary { background: var(--brand); border-color: var(--brand); color: var(--on-brand); }
  .btn.primary:hover { background: var(--brand-strong); }
  /* Light theme's brand is dark, so the text flips back to white -- and the FILL steps to
     --brand-strong. That step was originally a workaround: --brand was 4.40:1 against white,
     missing AA for text this size by a hair, while --brand-strong was 6.24:1. --brand now
     clears AA on its own, but the step stays: --brand-strong is the deeper of the two in
     every palette, so a filled button keeps the larger margin, and a button that reads as
     *pressable* wants to be a shade heavier than body text regardless. */
  :root[data-theme="light"] .btn.primary {
    color: #fff; background: var(--brand-strong); border-color: var(--brand-strong); }
  /* One step darker again than --brand-strong, which this button already fills with.
     Derived rather than a fifth palette value: it is a hover state, not a contrast-critical
     text colour, and deriving it means every accent gets a correct one for free instead of
     eight more hand-checked hex values. It was hardcoded #0a5c37 -- the only brand colour
     outside the token system, and so the one thing that would have stayed green no matter
     which accent you picked. */
  :root[data-theme="light"] .btn.primary:hover {
    background: color-mix(in srgb, var(--brand-strong) 86%, #000);
    border-color: color-mix(in srgb, var(--brand-strong) 86%, #000); }
  .btn.ghost { border-color: transparent; background: transparent; }
  .btn.danger { color: var(--crit); border-color: var(--crit-soft); background: var(--crit-soft); }

  .view { padding: 26px 30px 60px; max-width: 1120px; display: none; }
  .view.active { display: block; animation: fade .35s ease; }
  @keyframes fade { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

  /* ---------- Overview ---------- */
  .hello { font-family: var(--font-display); font-size: 15px; color: var(--ink-soft); margin: 0 0 18px; }
  .hello b { color: var(--ink); }
  .tiles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin-bottom: 22px; }
  .tile { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-tile); padding: 16px 16px 14px; box-shadow: var(--shadow); position: relative; overflow: hidden; }
  .tile .lab { font-size: 12px; color: var(--ink-soft); font-weight: 550; display: flex; align-items: center; gap: 7px; }
  .tile .num { font-family: var(--font-display); font-size: 30px; font-weight: 800; letter-spacing: -0.02em; margin-top: 8px; font-variant-numeric: tabular-nums; }
  .tile .sub { font-size: 11.5px; color: var(--ink-faint-solid); margin-top: 2px; }
  /* `.dot` lived here: an 8px circle used only by the overview's six tiles, in a console
     where status is a 9x15 rounded cell (.sigcell) everywhere else. The tiles now use that
     cell, so the shape means one thing in one place. Removed rather than left unused, because
     its colour modifier `.dot.brand` matched `.brand` -- the sidebar logo block -- and
     inherited its display:flex and padding, which border-box clamped to 12x18. Four tiles drew
     an ellipse and two drew a circle from identical markup. See _macros.html::status_pip. */
  .tile.accent-good { color: var(--good); } .tile.accent-warn .num { color: var(--warn); }
  .spark { position: absolute; right: 12px; bottom: 10px; opacity: .9; }

  .grid2 { display: grid; grid-template-columns: 1.3fr 1fr; gap: 16px; }
  .grid3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
  .credstrip { margin-bottom: 22px; }
  .credstrip .card-h .pill { margin-left: auto; font-size: 11.5px; font-weight: 700; padding: 2px 10px; border-radius: var(--r-btn); }
  .credstrip .pill.good { background: var(--good-soft); color: var(--good); }
  .credstrip .pill.warn { background: var(--warn-soft); color: var(--warn); }
  .credstrip .pill.crit { background: var(--crit-soft); color: var(--crit); }
  .credrow { display: flex; align-items: center; gap: 10px; padding: 11px 18px; border-bottom: 1px solid var(--line-soft); font-size: 13px; }
  .credrow:last-child { border-bottom: none; }
  .credrow .cname { font-weight: 600; }
  .credrow .cstate { margin-left: auto; font-size: 12px; color: var(--ink-soft); }
  .credrow.crit .cstate { color: var(--crit); font-weight: 600; }
  .credrow.warn .cstate { color: var(--warn); font-weight: 600; }
  .card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-card); box-shadow: var(--shadow); overflow: hidden; }
  .card-h { display: flex; align-items: center; gap: 10px; padding: 15px 18px; border-bottom: 1px solid var(--line-soft); }
  .card-h h3 { font-family: var(--font-display); font-size: 15px; font-weight: 700; margin: 0; }
  .card-h .link { margin-left: auto; font-size: 12.5px; color: var(--brand-strong); text-decoration: none; font-weight: 600; cursor: pointer; }
  .card-h .link:hover { text-decoration: underline; }

  /* A .card deliberately has NO padding of its own. That is what lets a header rule, a row
     divider and a table edge run the full width of the card instead of stopping 18px short.
     The cost is that every child has to remember the 18px, and the ones that forget put text
     hard against the border -- which is how a churn explanation and two delivery rows ended
     up touching the card edge on /health.

     This is the floor for the ones that forget. `:where()` makes it specificity ZERO, so any
     component that states its own padding -- .attn-row, .crow, .callout, an inline style --
     still wins outright. It only fills in where nothing else has an opinion.

     Only text-bearing elements are listed. A <div> in a card is as likely to be a full-bleed
     wrapper (.attn holds .attn-rows that supply their own inset) as it is to be prose, and
     insetting a wrapper double-insets its children and shortens their dividers. A <p> or a
     list is never a wrapper.

     `.pad` opts a card OUT. A card that pads its own body does not need the floor, and gets
     hurt by it: the two paddings ADD, so its prose lands 34px in while its headings and its
     wrapper <div>s stay at 16px. That is not a subtle difference -- on the content_caching
     card it made a section heading look like the footer of the block above it, because the
     heading was a <div> and everything around it was a <p>. `.scard` used to be in this list
     and is now gone for the same reason: it carries `padding: 16px` unconditionally, so the
     floor could only ever double it. */
  :where(.card:not(.pad), .attn) > :where(p, h4, h5, ul, ol, dl, pre, blockquote) {
    padding-left: 18px; padding-right: 18px;
  }

  /* A row of facts inside a card: text inset by the card's gutter, divider full-bleed. The
     border sits on the padded box, so it still spans edge to edge -- that is the whole reason
     the padding goes here rather than on the card. */
  /* A GRID, not a flex row, and that is the whole fix. As flex, .cname was content-sized, so
     "Library iPad 3" and "Workshop Mac mini" pushed the description to a different x on every
     row -- the same defect the Groups list had, where each row is its own formatting context
     and cannot agree with the row above it. Named columns agree by construction. */
  .crow { display: grid; grid-template-columns: minmax(150px, 210px) minmax(0, 1fr) auto;
          align-items: baseline; gap: 10px; padding: 9px 18px;
          border-top: 1px solid var(--line-soft); font-size: 12px; }
  .crow.mid { align-items: center; }
  .crow > .cname { font-weight: 600; font-size: 13px; color: var(--ink); text-decoration: none;
                   min-width: 0; overflow-wrap: anywhere; }
  .crow > .cwhat { color: var(--ink-soft); min-width: 0; }

  /* The closing note under a card's rows -- what the numbers above do and do not mean. */
  .cnote { padding: 9px 18px 14px; border-top: 1px solid var(--line-soft);
           font-size: 11.5px; line-height: 1.6; color: var(--ink-soft); margin: 0; }
  .cnote > b { color: var(--ink); }

  /* Opt in to a padded card body, for a card that holds a form or a block of prose rather
     than full-bleed rows. branding.html asked for `card pad` before this existed, so its
     whole form sat against the border -- a class that is used but never defined fails
     silently, which is why test_component_classes.py checks every one. */
  .pad { padding: 16px 18px; }
  .card-h + .pad { padding-top: 14px; }

  .attn { display: flex; flex-direction: column; }
  .attn-row { display: flex; align-items: center; gap: 12px; padding: 13px 18px; border-bottom: 1px solid var(--line-soft); cursor: pointer; }
  .attn-row:last-child { border-bottom: none; }
  .attn-row:hover { background: var(--surface-2); }
  .badge-ic { width: 30px; height: 30px; border-radius: var(--r-btn); display: grid; place-items: center; flex: none; }
  .badge-ic.crit { background: var(--crit-soft); color: var(--crit); } .badge-ic.warn { background: var(--warn-soft); color: var(--warn); } .badge-ic.info { background: var(--brand-soft); color: var(--brand-strong); }
  .badge-ic svg { width: 16px; height: 16px; }
  .attn-row .t { min-width: 0; }
  .attn-row .t b { font-size: 13.5px; font-weight: 600; display: block; color: var(--ink); }
  .attn-row .t span { font-size: 12px; color: var(--ink-soft); }
  .attn-row .go { margin-left: auto; color: var(--ink-faint-solid); }

  /* ---------- Components that were living in page <style> blocks -----------------------
     Every one of these was defined in two to SEVEN templates' own <style> blocks, and six of
     them had drifted apart: .ag-h was a 13.5px heading on three pages and 15px on two, .pchip
     was 10px/700 on the device page and 11px/600 on the groups page, .pk-card had a shadow on
     five pages and none on two. Same component, different appearance depending which page you
     were looking at.

     That is the .nh-note failure one step further along: a component copied per page is one
     that is missing from some of them, and one that is copied and then edited is worse -- it
     looks deliberate. Consolidated to the majority definition, which is also the one the most
     pages already showed. */
  .page-head { margin-bottom: 20px; }
  .page-head h2 { margin: 0 0 6px; font-size: 24px; font-weight: 700; }
  .page-head .sub { margin: 0; font-size: 13px; color: var(--ink-soft); }
  .pk-card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-card); box-shadow: var(--shadow); padding: 16px 18px; margin-bottom: 18px; }
  .pk-row { display: grid; grid-template-columns: 1fr auto auto; gap: 10px; align-items: center; border-top: 1px solid var(--line-soft); padding: 10px 0; }
  .pk-name { font-weight: 600; font-size: 13px; }
  .pk-meta { font-size: 12px; color: var(--ink-soft); }
  .pk-url { font-family: var(--font-mono); font-size: 11.5px; color: var(--ink-soft); overflow-wrap: anywhere; }
  .ag-h { font-weight: 700; font-size: 13.5px; margin: 0 0 10px; }
  .ag-hint { font-size: 12px; color: var(--ink-soft); margin: 8px 0 0; }
  .ag-form { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
  .ag-radios { display: flex; gap: 12px; font-size: 12.5px; }
  .ag-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
  .ag-out { font-family: var(--font-mono); font-size: 11px; white-space: pre-wrap; overflow-wrap: anywhere; background: var(--surface-2); border-radius:var(--r-btn); padding: 8px 10px; margin: 6px 0 0; }
  .ag-out-lab { font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em; color: var(--ink-faint-solid); font-weight: 700; margin-top: 8px; }
  .field { font: inherit; font-size: 12.5px; padding: 8px 10px; border-radius: var(--r-btn); border: 1px solid var(--line); background: var(--surface); color: var(--ink); width: 100%; box-sizing: border-box; }
  .pchip { display: inline-block; font-size: 10px; font-weight: 700; letter-spacing: .02em; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--line); color: var(--ink-soft); }

  /* campus snapshot */
  .zones { display: flex; flex-direction: column; gap: 2px; padding: 8px 8px 12px; }
  .zone { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: var(--r-card); cursor: pointer; }
  .zone:hover { background: var(--surface-2); }
  .zone .pin { width: 30px; height: 30px; border-radius: var(--r-btn); background: var(--brand-soft); color: var(--brand-strong); display: grid; place-items: center; flex: none; }
  .zone .pin svg { width: 15px; height: 15px; }
  .zone .zn { font-size: 13.5px; font-weight: 600; }
  .zone .zn small { display: block; font-weight: 500; font-size: 11.5px; color: var(--ink-soft); }
  .bar { margin-left: auto; display: flex; align-items: center; gap: 9px; }
  .bar .track { width: 80px; height: 6px; border-radius: 4px; background: var(--line); overflow: hidden; }
  .bar .fill { height: 100%; background: linear-gradient(90deg, var(--brand), var(--brand-strong)); border-radius: 4px; }
  .bar .n { font-size: 12.5px; font-weight: 700; font-variant-numeric: tabular-nums; width: 22px; text-align: right; }

  /* ---------- Devices ---------- */
  .filters { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
  .chip { font-family: inherit; font-size: 12.5px; font-weight: 600; padding: 7px 13px; border-radius: var(--r-btn); border: 1px solid var(--line); background: var(--surface); color: var(--ink-soft); cursor: pointer; }
  .chip:hover { color: var(--ink); }
  .chip.on { background: var(--brand); border-color: var(--brand); color: var(--on-brand); }
  /* No `overflow: hidden` here. It was clipping the rounded corners for free, but it also
     clipped the signal-strip tooltip on the LAST row -- the one place a long list ends, and so
     the row whose explanation a reader is most likely to want. The corners are now clipped by
     the first and last rows themselves, which is the only thing that ever painted over them. */
  .dlist { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-card); box-shadow: var(--shadow); }
  .dlist > :first-child { border-radius: var(--r-card) var(--r-card) 0 0; }
  .dlist > :last-child { border-radius: 0 0 var(--r-card) var(--r-card); }

  /* `.card` clips so a rounded corner never shows a square child through it. A card that holds a
     signal strip has to opt out: the tooltip is drawn below the cell and would be cut off at the
     card's padding, which is the whole explanation gone. Safe here only because these cards paint
     nothing at their own edge -- do not hang this on a card with a full-bleed child. */
  .card.sigcard { overflow: visible; }
  .drow { display: grid; grid-template-columns: auto 2.2fr 1.6fr 1fr auto; gap: 14px; align-items: center; padding: 13px 18px; border-bottom: 1px solid var(--line-soft); cursor: pointer; transition: background .13s; }
  .drow:last-child { border-bottom: none; }
  .drow:hover { background: var(--surface-2); }
  .drow.sel { background: var(--brand-soft); }
  .selcell { display: flex; align-items: center; justify-content: center; }
  .selcell input { cursor: pointer; width: 15px; height: 15px; accent-color: var(--brand); }
  .dlist-head { display: grid; grid-template-columns: auto 1fr; gap: 14px; align-items: center; padding: 6px 18px 8px; }
  .filters .fcount { margin-left: auto; align-self: center; font-size: 12px; color: var(--ink-soft); font-weight: 600; }
  .bulkbar { position: fixed; left: 50%; transform: translateX(-50%); bottom: 22px; z-index: 40; display: none; align-items: center; gap: 9px; padding: 9px 12px 9px 15px; background: var(--brand-strong); color: var(--on-brand); border-radius: var(--r-btn); box-shadow: var(--shadow-lift); max-width: calc(100% - 32px); flex-wrap: wrap; }
  .bulkbar.show { display: flex; }
  .bulkbar .bcount { font-weight: 700; font-size: 13px; }
  .bulkbar .bbtn { font: inherit; font-size: 12.5px; font-weight: 600; color: var(--on-brand); background: var(--on-brand-veil); border: 1px solid rgba(255,255,255,.22); border-radius: var(--r-btn); padding: 6px 11px; cursor: pointer; }
  .bulkbar .bbtn:hover { background: var(--on-brand-veil); }
  .bulkbar .bbtn.danger:hover { background: var(--crit); border-color: var(--crit); }
  .bulkbar select { font: inherit; font-size: 12px; border-radius: var(--r-btn); padding: 5px 8px; border: none; background: var(--surface); color: var(--ink); cursor: pointer; }
  .bulkbar .bclose { font-family: inherit; background: none; border: none; color: var(--on-brand); cursor: pointer; font-size: 17px; line-height: 1; padding: 0 2px; }
  .bulkbar .bclose:hover { color: var(--on-brand); }
  .dev, .dev:visited, .dev:link { display: flex; align-items: center; gap: 12px; min-width: 0; color: var(--ink); text-decoration: none; }
  .dev:hover .nm b { color: var(--brand-strong); }
  /* Device detail: compact facts strip + responsive info grid */
  .backlink, .backlink:visited { display: inline-block; color: var(--brand-strong); text-decoration: none; font-weight: 600; font-size: 12.5px; }
  /* ── Device header ──────────────────────────────────────────────────────────
     Three fixed zones -- icon, body, actions -- as a grid rather than one wrapping
     flex row. The old card put ten facts of wildly unequal width through
     `flex-wrap`: "Type: iPad" at 60px sat beside a Software update line at 600px,
     so the number of items per row, and WHICH items were neighbours, changed at
     every window width. That is what read as haphazard, and it is why the card
     reshuffled while you resized.

     The deeper fix is that two kinds of content had been wearing the same clothes.
     Short scalar identifiers (type, platform, OS, owner) want a uniform grid; status
     SENTENCES with chips and timestamps (presence, software update, declarations)
     want full-width rows with a shared label gutter. Split, each behaves. */
  .devhero { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 8px 18px;
             align-items: start; background: var(--surface); border: 1px solid var(--line);
             border-radius: var(--r-card); box-shadow: var(--shadow); padding: 16px 18px;
             margin: 12px 0 4px; }
  .devhero .dev-ic { width: 46px; height: 46px; }
  .devhero .dev-ic svg { width: 25px; height: 25px; }
  .devbody { min-width: 0; display: grid; gap: 12px; }

  /* The device NAME as the page's heading. The topbar carries it too, as its small subtitle,
     because the topbar is sticky and is what answers "which device am I on" once this has
     scrolled away -- but the name belongs in the page, at the size of a page title.

     The dot is the device's health, and the 24px subtitle indent is the dot plus the gap, so
     the model line starts under the first letter of the name rather than under the dot. */
  .devtitle { font-family: var(--font-display); font-size: 26px; font-weight: 700;
              letter-spacing: -0.01em; margin: 18px 0 3px; min-width: 0; overflow-wrap: anywhere; }
  /* The dot is INSIDE the heading rather than a flex sibling of it, so it sits in the first
     line box. A long device name wraps to two lines at narrow widths, and a flex row centres
     the dot between them -- beside neither line. Inline, it stays on line one at any size,
     and `vertical-align` optically centres it against the caps rather than the baseline. */
  /* Three cells, one per row in the status block below, in place of a single dot.
     Borrowed from a capability map in another project, and the borrowed idea is NOT the shape:
     it is that AN ABSENT ANSWER IS DRAWN RATHER THAN OMITTED. A dot has one colour, so "we do
     not know" has to be a quiet grey that reads like a pass. An outlined cell says "no answer"
     and can be counted across a page.

     Taller than wide on purpose -- it reads as a cell rather than a bullet, which is what stops
     three of them looking like a loading spinner.

     No opacity ramp, unlike the original: on this near-black ground its faintest step (.22)
     lands far under the 3:1 floor for non-text, so a "weak" cell would be indistinguishable
     from an empty slot. Fill versus outline carries the same information and every state stays
     legible. */
  .sigstrip { display: inline-flex; gap: 3px; margin-right: 14px; vertical-align: .12em; }
  /* The device row's signal cell. The strip is 15px of solid colour and the presence chip is
     a line of 12.5px text; left to baseline alignment they settled 5.2px apart, which reads
     as the strip floating above the row. `vertical-align` cannot fix that -- it aligns the
     inline box, and the two boxes have different heights AND different internal baselines, so
     any value that centres one leaves the other wrong. Making the cell a flex row centres
     both against each other regardless of what either contains, and keeps doing so when a
     tag is added. The strip's own margin-right goes: `gap` owns the spacing here. */
  .dsig { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }
  .dsig .sigstrip { margin-right: 0; }
  /* The group row's member strip. This class was used in the template and never defined --
     it rendered as a plain block, which looked fine because a lone strip in a block LOOKS
     centred until you measure it (it sat 3.6px high, the same vertical-align artefact as the
     device row). Same fix, same reason: centre by layout, not by baseline. */
  .gmembers { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }
  .gmembers .sigstrip { margin-right: 0; }

  /* The save/queue confirmation banner. It lived in SIX templates' own <style> blocks as
     copy-paste, and the two pages that used it without copying it -- k12_restrictions and
     login_fx -- rendered a bare unstyled <div>. A component duplicated per page is a
     component that is missing from some of them. */
  /* The note under a network heading. It lived in network.html's own <style> block while
     unmanaged.html used it too, so one of the two pages rendered it as a bare <p> at browser
     default size. Same lesson as .sc-banner below. */
  .nh-note { color: var(--ink-soft); font-size: 12.5px; margin: 0; }
  .sc-banner { background: var(--brand-soft); color: var(--brand-strong); border: 1px solid var(--brand-line); border-radius: var(--r-card); padding: 12px 16px; font-size: 12.5px; margin-bottom: 18px; }
  .sc-banner.ok { background: var(--good-soft); color: var(--good); border-color: var(--good); }
  .sc-banner.warn { background: var(--warn-soft); color: var(--warn); border-color: var(--warn); }
  /* An error is not a warning. ladmin_password.html was asking for `banner danger`, which was
     defined nowhere at all, so a failed password change rendered as plain text. */
  .sc-banner.crit { background: var(--crit-soft); color: var(--crit); border-color: var(--crit); }

  /* Page and section headings outside the layout's own scopes. Every other heading in the
     console gets its type from a SCOPED rule (.topbar h1, .man-body h2, .sheet-h h2), and
     the stylesheet resets no bare h1/h2 -- so an <h1 class="page-h"> was falling all the way
     back to the browser default: 2em, default margins, in whatever font it inherited. It did
     not look broken so much as look like a different product. */
  .page-h { font-family: var(--font-display); font-size: 24px; font-weight: 700; letter-spacing: -0.01em; margin: 0 0 6px; }
  .sec-h { font-family: var(--font-display); font-size: 18px; font-weight: 800; margin: 0 0 12px; }
  .sigcell { width: 9px; height: 15px; border-radius: 2px; border: 1px solid transparent;
             background: var(--ink-faint-solid); }
  .sigcell.is-ok { background: var(--good); }
  .sigcell.is-warn { background: var(--warn); }
  /* Never reported. Transparent with a visible edge -- the hole is the point. */
  .sigcell.is-unknown { background: transparent; border-color: var(--ink-faint-solid); }
  @media (max-width: 620px) { .sigstrip { gap: 2px; margin-right: 10px; } }

  /* The cell's own tooltip, because the browser's cannot be made bigger.
     `title` is unstylable by design -- size, font and position all belong to the browser -- so
     "make the hover text larger" is not a tuning job, it is a replacement job. Same conclusion
     the k12brain report reached for the same reason.

     Below the strip rather than above or over it: a tooltip that covers what it describes makes
     you move the mouse to check what you were pointing at. */
  .sigcell { position: relative; }
  /* The attribute is part of the selector, not just the content, AND it has to be non-empty.
     `content: attr(data-tip)` resolves to "" both when the attribute is missing and when it is
     present but blank -- and an empty string still draws the box, so the cell grows a 24x20
     empty dark rectangle on hover. The first version of this guard only tested for the
     attribute's presence, which fixed the missing case and left the blank one: the branding
     preview passed `why: ""` and hit it immediately. Both are closed now. */
  .sigcell[data-tip]:not([data-tip=""]):hover::after,
  .sigcell[data-tip]:not([data-tip=""]):focus-visible::after {
    content: attr(data-tip);
    position: absolute; top: calc(100% + 9px); left: -2px; z-index: 30;
    width: max-content; max-width: 300px; white-space: normal; text-align: left;
    /* Every one of these resets undoes something inherited from the <h2> the strip lives in:
       700 weight, lowercase, tight tracking and 26px display type. Without them the tooltip
       renders as a shouty fragment of the heading. */
    font-family: var(--font-body); font-size: 13px; font-weight: 400; line-height: 1.5;
    letter-spacing: 0; text-transform: none; color: var(--ink);
    padding: 9px 11px; border-radius: var(--r-btn);
    background: var(--surface-2); border: 1px solid var(--line);
  }
  .sigcell:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
  .devsub { font-size: 13px; color: var(--ink-faint-solid); margin: 0 0 16px; padding-left: 24px; }
  @media (max-width: 620px) { .devtitle { font-size: 21px; } }

  /* Sized to CONTENT and packed left. Equal `1fr` columns were the mistake, and measurement
     is what showed it: at a 620px card every cell was 279px wide to hold "Mac", "macOS",
     "26.6.2" -- values of three to six characters -- so seven facts took four rows and most
     of the card was stretched emptiness.
     A column count, fixed or responsive, cannot fix that: `1fr` means "share the row equally"
     regardless of what is in it. Flex items that size themselves do, and they also pack, so a
     row is full before the next one starts. `min-width` keeps short values from collapsing
     into a ragged left edge; `max-width` stops a long owner from forcing a horizontal
     scrollbar. */
  .devfacts { display: flex; flex-wrap: wrap; gap: 12px 30px; min-width: 0; }
  .devfacts > div { display: flex; flex-direction: column; gap: 1px;
                    flex: 0 0 auto; min-width: 88px; max-width: 100%; }
  .devfacts > div > span { font-size: 10px; text-transform: lowercase; letter-spacing: .07em; color: var(--ink-faint-solid); font-weight: 700; }
  .devfacts > div > b { font-size: 13px; font-weight: 650; color: var(--ink); min-width: 0; overflow-wrap: anywhere; }
  .devfacts b.oncampus { color: var(--good); }
  /* Owner is the one long value among short ones -- a full name and an email address. Under
     content sizing it simply takes the width it needs and the short facts stop paying for
     it, which is what a fixed column count could never manage. */

  /* The status block, divided from the facts by a hairline rather than by whitespace alone.
     The rule sits inside .devbody, which is its own grid column, so it stops where the facts
     stop and never runs under the action buttons -- the two halves it separates are both in
     that column and the buttons are not.

     The health colour used to live here as a left edge rule. It moved to the dot beside the
     page title: one signal, at the top, where you look first. See .devtitle. */
  .devstatus { display: grid; gap: 7px; padding-top: 12px;
               border-top: 1px solid var(--line-soft); }
  .devstatus > div { display: grid; grid-template-columns: 118px minmax(0, 1fr); gap: 3px 14px;
                     align-items: baseline; }
  .devstatus > div > span { font-size: 10px; text-transform: lowercase; letter-spacing: .07em;
                            color: var(--ink-faint-solid); font-weight: 700; padding-top: 2px; }
  .devstatus > div > b { font-size: 13px; font-weight: 650; color: var(--ink); min-width: 0;
                         line-height: 1.5; }
  .devstatus b.oncampus { color: var(--good); }
  /* "· reported 6 min ago" wraps as one phrase. Left to itself it breaks after "6 min" and
     strands "ago" on a line of its own, which is the sort of small raggedness that makes a
     card read as unfinished. */
  .devstatus .whenever { white-space: nowrap; }

  /* Own column, so expanding "Custom range" grows downward instead of moving the facts. */
  .devactions { display: flex; flex-direction: column; align-items: stretch; gap: 6px;
                min-width: 168px; }
  .devactions form { margin: 0; }
  /* An <a class="btn"> was picking up the global link colour and underline while the real
     <button> next to it did not, so three controls that do the same kind of thing looked like
     two kinds of thing. */
  .devactions .btn { width: 100%; padding: 8px 12px; font-size: 12px; text-align: center;
                     color: var(--ink); text-decoration: none; }
  .devactions details summary { font-size: 11.5px; color: var(--ink-soft); cursor: pointer;
                                padding: 2px 0; }
  .devnotes { grid-column: 1 / -1; }

  @media (min-width: 1420px) { .devfacts { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
  @media (max-width: 1040px) {
    /* Actions below the body rather than squeezed beside it: three buttons in a 168px column
       is the point where they start truncating. Given a rule above them so they read as an
       action row rather than as a footer the card grew. */
    .devhero { grid-template-columns: auto minmax(0, 1fr); }
    .devactions { grid-column: 2; flex-direction: row; flex-wrap: wrap; align-items: center;
                  padding-top: 12px; border-top: 1px solid var(--line-soft); }
    .devactions .btn { width: auto; }
    /* The icon is decoration, and this is where the space it takes stops being free. */
    .devhero .dev-ic { display: none; }
    .devhero { grid-template-columns: minmax(0, 1fr); }
    .devactions, .devbody, .devnotes { grid-column: 1; }
    /* Once the search has wrapped to its own line, right-aligning it leaves a band of empty
       topbar that reads as a mistake. Full width instead, which reads as a decision. */
    .search { flex: 1 1 100%; margin-left: 0; }
  }
  @media (max-width: 620px) {
    .devhero { grid-template-columns: 1fr; }
    .devhero .dev-ic { display: none; }
    .devactions { grid-column: 1; }
    /* The status rows give up their label gutter here: at this width a 118px column leaves
       too little for the value beside it. */
    .devstatus > div { grid-template-columns: 1fr; }
    .devstatus > div > span { padding-top: 0; }
  }
  .dgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 0 30px; align-items: start; }
  .dev-ic { width: 38px; height: 38px; border-radius: var(--r-card); background: var(--surface-2); border: 1px solid var(--line); display: grid; place-items: center; flex: none; color: var(--ink-soft); }
  .dev-ic svg { width: 20px; height: 20px; }
  /* `.nm` is a name-over-detail pair, and its typography was scoped `.dev .nm` -- but 11 of
     the 18 uses in this app are NOT wrapped in a .dev, so most of them rendered with no
     hierarchy at all: the <b> and the <span> both inline, both 14px, both --ink, so a profile's
     identifier was exactly as loud as its name. These base rules give the component its shape
     wherever it is used. Every scoped variant (.dev, .ms-row, .um-row, .ade-*) is (0,2,1) and
     still outranks this (0,1,1), so nothing that was already styled can change.

     Deliberately NO nowrap/ellipsis here: truncation belongs to the constrained column in the
     devices list, not to a detail panel where the row can afford to wrap. */
  .nm b { display: block; font-size: 13.5px; font-weight: 650; line-height: 1.35; }
  .nm span { display: block; font-size: 12px; color: var(--ink-soft); line-height: 1.4; }
  .dev .nm { min-width: 0; }
  .dev .nm b { font-size: 13.5px; font-weight: 650; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .dev .nm span { font-size: 12px; color: var(--ink-soft); display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .presence { display: inline-flex; align-items: center; gap: 8px; font-size: 12.5px; font-weight: 550; color: var(--ink-soft); }
  .live { position: relative; width: 8px; height: 8px; border-radius: 50%; background: var(--good); flex: none; }
  .live::after { content: ""; position: absolute; inset: -4px; border-radius: 50%; border: 2px solid var(--good); opacity: .5; animation: ping 2s ease-out infinite; }
  @keyframes ping { 0% { transform: scale(.6); opacity: .6; } 100% { transform: scale(1.5); opacity: 0; } }
  .presence.off { color: var(--ink-faint-solid); }
  .presence.off .live { background: var(--ink-faint-solid); } .presence.off .live::after { display: none; }
  .presence .loc { color: var(--ink); font-weight: 600; }
  .vpill { font-size: 11px; font-weight: 650; padding: 2px 8px; border-radius: var(--r-btn); background: var(--brand-soft); color: var(--brand-strong); }
  .vpill.staff { background: var(--warm-soft); color: var(--warm); }
  .vpill.iot { background: var(--surface-2); color: var(--ink-soft); border: 1px solid var(--line); }
  .stat-cell { display: flex; align-items: center; gap: 8px; font-size: 12.5px; }
  .checkin { font-size: 12px; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
  .rowacts { display: flex; gap: 6px; justify-self: end; }
  .mini { font-family: inherit; border: 1px solid var(--line); background: var(--surface); color: var(--ink-soft); border-radius: var(--r-btn); padding: 6px 9px; font-size: 12px; font-weight: 600; cursor: pointer; display: inline-flex; gap: 5px; align-items: center; }
  .mini:hover { background: var(--surface-2); color: var(--ink); border-color: var(--brand-line); }
  /* A button label must never break mid-word ("Unlin / k"). Both classes are used as flex
     items beside variable-length values -- a MAC address, a device name -- and a flex item
     shrinks below its content by default, so the label wrapped rather than the row. Shrinking
     is what needs to stop; the container is what should wrap. */
  .btn, .mini { white-space: nowrap; flex-shrink: 0; }
  .mini svg { width: 14px; height: 14px; }

  /* ---------- Network view ---------- */
  .net-intro { display: flex; align-items: center; gap: 12px; background: var(--brand-soft); border: 1px solid var(--brand-line); border-radius: var(--r-card); padding: 14px 18px; margin-bottom: 18px; }
  .net-intro svg { width: 20px; height: 20px; color: var(--brand-strong); flex: none; }
  .net-intro p { margin: 0; font-size: 13px; color: var(--ink); }
  .net-intro b { color: var(--brand-strong); }
  .netgrid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
  .apcard { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-card); box-shadow: var(--shadow); overflow: hidden; }
  .apcard .aph { display: flex; align-items: center; gap: 11px; padding: 14px 16px; border-bottom: 1px solid var(--line-soft); }
  .apcard .aph .pin { width: 32px; height: 32px; border-radius: var(--r-btn); background: var(--brand-soft); color: var(--brand-strong); display: grid; place-items: center; flex: none; }
  .apcard .aph .pin svg { width: 16px; height: 16px; }
  .apcard .aph b { font-family: var(--font-display); font-size: 14.5px; font-weight: 700; }
  .apcard .aph small { display: block; font-size: 11.5px; color: var(--ink-soft); font-family: var(--font-mono); }
  .apcard .aph .cnt { margin-left: auto; font-size: 12px; font-weight: 700; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
  .apchips { display: flex; flex-wrap: wrap; gap: 7px; padding: 13px 16px; }
  .dchip, .dchip:link, .dchip:visited { display: inline-flex; align-items: center; gap: 7px; font-size: 12px; font-weight: 600; padding: 6px 10px 6px 8px; border-radius: var(--r-btn); background: var(--surface-2); border: 1px solid var(--line); cursor: pointer; color: var(--ink); text-decoration: none; }
  .dchip:hover { border-color: var(--brand-line); color: var(--brand-strong); }
  .dchip .live { width: 6px; height: 6px; } .dchip .live::after { display: none; }

  /* ---------- placeholder views ---------- */
  .placeholder { text-align: center; padding: 60px 20px; }
  .placeholder .pic { width: 64px; height: 64px; margin: 0 auto 16px; border-radius: var(--r-card); background: var(--brand-soft); color: var(--brand-strong); display: grid; place-items: center; }
  .placeholder .pic svg { width: 30px; height: 30px; }
  .placeholder h2 { font-family: var(--font-display); font-size: 20px; margin: 0 0 6px; }
  .placeholder p { color: var(--ink-soft); max-width: 420px; margin: 0 auto 18px; }
  .setup-cards { display: grid; grid-template-columns: repeat(2,1fr); gap: 14px; text-align: left; margin-top: 8px; }
  /* Card grids on pages that fill the content width. At .view's 1120px a 2-up grid gives each
     card ~550px for a tag, a title and one line of prose; 3-up keeps cards the size they were.
     The collapses are restated here because a media query adds NO specificity: the global
     860px `.setup-cards` rule below is (0,1,0) and loses to this class's (0,2,0), so without
     these two lines every wide page would stay three-up on a phone. */
  .setup-cards.wide { grid-template-columns: repeat(3,1fr); }
  @media (max-width: 1000px) { .setup-cards.wide { grid-template-columns: repeat(2,1fr); } }
  @media (max-width: 860px) { .setup-cards.wide { grid-template-columns: 1fr; } }
  /* Cards that must not share a grid row. Grid equalises row heights, so a prose card or a
     single-control form placed beside an 8-field form gets stretched to that form's height --
     which is how this page ended up with 500px of empty card under one file input. */
  .setup-cards .span-row { grid-column: 1 / -1; }
  /* The upload form is one control and a button; a stacked .field column would give it a whole
     card's height for no benefit. */
  .upload-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
  .scard { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-tile); padding: 16px; box-shadow: var(--shadow); }
  .scard .st { display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 700; }
  .scard h4 { font-family: var(--font-display); margin: 10px 0 4px; font-size: 15px; }
  .scard p { font-size: 12.5px; line-height: 1.6; color: var(--ink-soft); margin: 0; }
  /* margin: 0 above suits a one-paragraph card. Multi-paragraph cards closed into one
     unbroken block at column width -- three paragraphs of Mac prerequisites read as nine
     undifferentiated lines. Adjacent-sibling only, so single-paragraph cards are untouched. */
  .setup-cards .scard p + p { margin-top: 9px; }
  .scard a, .scard a:link, .scard a:visited { color: var(--brand-strong); text-decoration: none; font-weight: 600; }
  .scard a:hover { text-decoration: underline; }
  .tag { font-size: 10.5px; font-weight: 600; text-transform: lowercase; letter-spacing: .02em; padding: 3px 8px; border-radius: 3px; }
  /* Four states, and each has to LOOK different from the others or the distinction the code
     is drawing is invisible on the page.

     `.no` was amber, identical to `.warn`, so a device that is offline and a device that is
     merely due something read the same. It is red now, which is what the design handoff
     specifies and what its uses ask for -- offline, missing, error, authentication_failed.

     `.crit` and `.muted` were USED (11 times between them, on the audit and the compliance
     pages) and never defined, so they rendered as bare padded text with no chip at all: a
     critical status that looked like a caption. `.crit` is the same red as `.no`; a separate
     name for the same meaning is worth keeping rather than rewriting eleven call sites.

     `.muted` is deliberately the quietest of the four. It means "not reported yet", which is
     neither good nor bad, and the whole system depends on that third state not being read as
     either. */
  .tag.ok { background: var(--good-soft); color: var(--good); }
  .tag.no, .tag.crit { background: var(--crit-soft); color: var(--crit); }
  .tag.warn { background: var(--warn-soft); color: var(--warn); }
  .tag.soon { background: var(--surface-2); color: var(--ink-soft); border: 1px solid var(--line); }
  .tag.muted { background: transparent; color: var(--ink-faint-solid); border: 1px solid var(--line-soft); }
  .muted { color: var(--ink-soft); font-size: 12.5px; }

  /* ---------- Device detail slide-over ---------- */
  .scrim { position: fixed; inset: 0; background: rgba(20,18,15,0.4); backdrop-filter: blur(2px); opacity: 0; pointer-events: none; transition: opacity .25s; z-index: 40; }
  .scrim.open { opacity: 1; pointer-events: auto; }
  .sheet { position: fixed; top: 0; right: 0; height: 100vh; width: 440px; max-width: 92vw; background: var(--surface); border-left: 1px solid var(--line); box-shadow: var(--shadow-lift); transform: translateX(100%); transition: transform .3s cubic-bezier(.4,0,.2,1); z-index: 41; display: flex; flex-direction: column; }
  .sheet.open { transform: none; }
  .sheet-h { padding: 20px 22px 16px; border-bottom: 1px solid var(--line-soft); }
  .sheet-h .top { display: flex; align-items: flex-start; gap: 13px; }
  .sheet-h .dev-ic { width: 46px; height: 46px; border-radius: var(--r-card); }
  .sheet-h .dev-ic svg { width: 24px; height: 24px; }
  .sheet-h h2 { font-family: var(--font-display); font-size: 18px; margin: 0; font-weight: 800; }
  .sheet-h .meta { font-size: 12.5px; color: var(--ink-soft); margin-top: 2px; }
  .sheet-h .close { margin-left: auto; }
  .sheet-body { overflow-y: auto; padding: 6px 22px 22px; flex: 1; }
  .sect { margin-top: 20px; }
  /* A section heading, separated by a RULE ABOVE rather than by colour or weight.
     
     Colour and weight cannot do this job here, and trying them is how this got fixed twice
     and stayed wrong. At --ink-faint-solid the heading was DIMMER than its own contents.
     Promoting it to --ink removed the inversion and, with it, the only thing distinguishing
     the two -- because much of what these headings introduce is ALSO bold --ink: a cache
     group's name, a settings card's title. An 11px bold white heading over 12.5px bold white
     content reads as just another row, and measurably so.

     A rule above works whatever the content below happens to look like. ABOVE, never below:
     a rule UNDER a heading closes the block above it instead of opening the one below, which
     is the exact fault that made `cache_contents` read as a footer. The spacing does the
     other half -- ~34px above, 9px below -- so proximity binds the label to what it heads.

     .invcard .lab keeps its rule BELOW instead: it heads a <dl> with no internal rules, so a
     header band works there and there is no block above it to close. */
  .lab, .subhead {
    /* 13px, and that is not a taste call. A heading has to OUTRANK the first thing it heads,
       and at 11px it was SMALLER than the 12.5px bold labels underneath it -- so the rule above
       marked where the section began while the eye still went to the content first. Bigger,
       bolder, and tracked; the content below it drops to 600 so the ranking runs one way.

       Scoped to `.lab`, NOT `.sect .lab`. The narrower selector was arbitrary and it showed:
       ade.html uses `.lab` outside any .sect, so its step headings stayed 11px and kept the
       exact fault this rule exists to fix. `.tile .lab` and `.invcard .lab` are more specific
       and still win, which is what they should do -- they are different components. */
    font-size: 13px; text-transform: lowercase; letter-spacing: .08em;
    color: var(--ink); font-weight: 700;
    border-top: 1px solid var(--line-soft);
    margin: 20px 0 9px; padding-top: 14px;
  }
  /* The exemption is about the PARENT HAVING ITS OWN EDGE, not about being first child.
     `:first-child` was the first attempt and it was wrong in every real case: these headings
     start a plain wrapper (.sect, .ccwhat, .cctraffic) that has content above it, so stripping
     the rule left exactly the headings that needed separating with none. A card is different --
     it already draws a border there, and a second line 1px below it reads as a crack. */
  .card > .lab:first-child, .card > .subhead:first-child,
  .scard > .lab:first-child, .scard > .subhead:first-child,
  .pk-card > .lab:first-child, .card-h + .lab, .card-h + .subhead {
    border-top: 0; margin-top: 0; padding-top: 0;
  }
  .kv { display: grid; grid-template-columns: 128px 1fr; gap: 7px 12px; font-size: 13px; align-items: start; }
  .kv dt { color: var(--ink-soft); min-width: 0; overflow-wrap: anywhere; }
  .kv dd { margin: 0; font-weight: 550; font-family: var(--font-mono); font-size: 12.5px; min-width: 0; overflow-wrap: anywhere; }
  .presence-box { background: var(--brand-soft); border: 1px solid var(--brand-line); border-radius: var(--r-card); padding: 14px; }
  .presence-box .now { display: flex; align-items: center; gap: 9px; font-weight: 700; font-family: var(--font-display); font-size: 14px; color: var(--brand-strong); }
  .presence-box .det { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 12px; margin-top: 11px; font-size: 12.5px; }
  .presence-box .det span { color: var(--ink-soft); display: block; font-size: 11px; }
  .presence-box .det b { font-family: var(--font-mono); font-size: 12.5px; }
  .presence-box.off { background: var(--surface-2); border-color: var(--line); }
  .presence-box.off .now { color: var(--ink-soft); }
  .presence-box.off .live { background: var(--ink-faint-solid); }
  .presence-box.off .live::after { display: none; }
  .actions-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
  .actions-grid .btn { justify-content: center; }
  .timeline { display: flex; flex-direction: column; gap: 0; }
  .tl { display: flex; gap: 11px; padding-bottom: 14px; position: relative; }
  .tl:not(:last-child)::before { content: ""; position: absolute; left: 8px; top: 20px; bottom: 0; width: 2px; background: var(--line); }
  .tl .tic { width: 18px; height: 18px; border-radius: 50%; background: var(--good-soft); color: var(--good); display: grid; place-items: center; flex: none; z-index: 1; }
  .tl .tic svg { width: 11px; height: 11px; }
  .tl .tc b { font-size: 12.5px; font-weight: 600; display: block; }
  .tl .tc span { font-size: 11.5px; color: var(--ink-soft); }

  /* ADE profile builder — focused create/edit page */
  .pform { padding: 22px 24px; max-width: 780px; }
  .pform-form { display: flex; flex-direction: column; gap: 22px; }
  .pform-sec { display: flex; flex-direction: column; gap: 10px; }
  .pform-h { font-size: 11px; text-transform: lowercase; letter-spacing: .07em; color: var(--ink-faint-solid); font-weight: 700; margin: 0; }
  .pform-row { display: flex; gap: 12px; flex-wrap: wrap; }
  .pform-field { display: flex; flex-direction: column; gap: 5px; font-size: 12px; color: var(--ink-soft); }
  .pform-field > span { font-weight: 600; }
  .pform-field input, .pform-field select, .pform .fld {
    font: inherit; font-size: 13px; padding: 9px 12px; border-radius: var(--r-btn);
    border: 1px solid var(--line); background: var(--surface); color: var(--ink); width: 100%;
  }
  .pform-row .fld { flex: 1 1 180px; min-width: 160px; }
  .pform-checks { display: flex; gap: 9px 18px; flex-wrap: wrap; font-size: 13px; }
  .pform-checks label, .skip-group label, .mac-accounts label { display: inline-flex; align-items: center; gap: 7px; cursor: pointer; }
  .skip-group { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 8px 16px; font-size: 12.5px; border: 1px solid var(--line); border-radius: var(--r-btn); padding: 14px 16px; background: var(--surface-sunk, transparent); }
  .mac-accounts { flex-direction: column; gap: 10px; border: 1px solid var(--line); border-radius: var(--r-btn); padding: 12px 14px 15px; font-size: 12.5px; margin: 0; }
  .mac-accounts legend { padding: 0 6px; }

  @media (max-width: 860px) {
    .app { grid-template-columns: 1fr; }
    .side { position: fixed; left: -260px; transition: left .25s; z-index: 60; }
    .tiles { grid-template-columns: repeat(2,1fr); }
    .grid2, .grid3, .netgrid, .setup-cards { grid-template-columns: 1fr; }
    .drow { grid-template-columns: auto 1fr auto; }
    .drow .stat-cell, .drow .checkin { display: none; }
    .topbar { padding: 14px 18px; } .view { padding: 20px 18px 50px; }
  }
  @media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
  a, button { -webkit-tap-highlight-color: transparent; }
  /* Focus, in two halves, and the order matters.
     The app styles `:focus-visible` and never suppressed the user agent's own ring on plain
     `:focus`, so clicking a button with a MOUSE left the browser's default halo sitting on it
     -- a bright square that reads as an error rather than as "this has focus". Most visible on
     the sidebar toggle, because that button changes the layout and then keeps the ring.

     Replaced, never simply removed: the `:focus-visible` rule below has the same specificity
     and comes second, so anyone arriving by keyboard still gets a ring, and a stronger one
     than the browser drew. Deleting the first line brings the click halo back; deleting the
     second takes focus indication away from keyboard users entirely, which is worse than the
     thing being fixed. tests/test_focus_styles.py holds both. */
  :focus { outline: none; }
  :focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 4px; }

/* Profile payload disclosure tree */
.pf-tree { margin-top: 6px; }
.pf-leaf { display: flex; gap: 12px; padding: 3px 0; font-size: 12.5px; }
.pf-leaf .pf-k { color: var(--ink-soft); min-width: 200px; flex: none; }
.pf-leaf .pf-v { color: var(--ink); word-break: break-word; }
.pf-node { margin: 2px 0; }
.pf-node > summary { cursor: pointer; font-size: 12.5px; color: var(--ink); list-style: revert; }
.pf-node > summary::-webkit-details-marker { display: revert; }
.pf-node > summary .pf-count { color: var(--ink-soft); font-size: 11.5px; margin-left: 4px; }
.pf-node-body { margin-left: 16px; padding-left: 10px; border-left: 1px solid var(--line); }

/* Shared form + list components (thin-page consistency).
   Kept to single-class specificity with no !important so a page's own
   inline <style> (which renders later in the body) still overrides. */
.field:where(div) { display: flex; flex-direction: column; gap: 5px; margin-bottom: 12px; font-size: 12.5px; color: var(--ink-soft); }
.field label { font-weight: 600; }
.field input:not([type=checkbox]):not([type=radio]):not([type=file]),
.field select,
.field textarea {
  font: inherit; font-size: 13px; color: var(--ink);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-btn); padding: 9px 12px; width: 100%;
}
.field input:not([type=checkbox]):not([type=radio]):not([type=file]):focus,
.field select:focus, .field textarea:focus {
  border-color: var(--brand-line); box-shadow: 0 0 0 3px var(--brand-soft); outline: none;
}
.data-list { border: 1px solid var(--line); border-radius: var(--r-card); overflow: hidden; background: var(--surface); }
.data-row { display: flex; align-items: center; gap: 12px; padding: 10px 14px; border-bottom: 1px solid var(--line-soft); font-size: 13px; }
.data-row:last-child { border-bottom: none; }
.data-row .sub { color: var(--ink-soft); font-size: 12px; }
.data-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.data-table th { text-align: left; font-size: 11px; text-transform: lowercase; letter-spacing: .05em; color: var(--ink-faint-solid); font-weight: 700; padding: 8px 12px; border-bottom: 1px solid var(--line); }
.data-table td { padding: 9px 12px; border-bottom: 1px solid var(--line-soft); }
.data-table tr:last-child td { border-bottom: none; }
/* ---- clickable panels: whole surface navigates (data-href helper in base.html) ---- */
.clickable { cursor: pointer; transition: background .13s, border-color .13s; }
.clickable:hover { background: var(--surface-2); border-color: var(--brand-line); }

/* ---- disclosure chevrons: one visible marker for every <details> ---- */
details > summary { list-style: none; cursor: pointer; }
details > summary::-webkit-details-marker { display: none; }
details > summary::before {
  content: ""; display: inline-block; flex: none; width: 13px; height: 13px;
  margin-right: 5px; vertical-align: -2px; background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 6 6 6-6 6'/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 6 6 6-6 6'/%3E%3C/svg%3E") center / contain no-repeat;
  transition: transform .12s;
}
details[open] > summary::before { transform: rotate(90deg); }

/* ---- section tabs (device detail, directory) ---- */
/* Scrolls rather than wrapping. A tab bar that wraps puts one tab on a line of its own and
   stops looking like a tab bar; a row that scrolls still reads as one control. The scrollbar
   is hidden because the overflow is horizontal and short -- the tabs themselves are the
   affordance. */
.dtabs { display: flex; gap: 2px; flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
         border-bottom: 1px solid var(--line-soft); margin: 16px 0 10px; }
.dtabs::-webkit-scrollbar { display: none; }
.dtab { flex: none; }
.dtab { font: inherit; font-size: 12.5px; font-weight: 600; color: var(--ink-soft); background: none; border: 0; border-bottom: 2px solid transparent; padding: 8px 13px; margin-bottom: -1px; cursor: pointer; border-radius: 0; }
.dtab:hover { color: var(--ink); }
/* The bottom border carries the active state. No fill, no rounded top: a tab that is also a
   pill is two metaphors for one control. */
.dtab.on { color: var(--brand); border-bottom-color: var(--brand); background: none; }

/* ---- directory sync strip ---- */
.dirstrip { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-card); box-shadow: var(--shadow); padding: 14px 18px; display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.dirstrip .dirsync { font-size: 12.5px; color: var(--ink-soft); }
.dirstrip .dirstrip-a { margin-left: auto; display: flex; gap: 8px; }

/* ---- admin manual ---- */
.man-wrap { display: flex; gap: 26px; align-items: flex-start; }
/* The chapter column is sticky, and once it is TALLER than the viewport a sticky element
   stops being a convenience and becomes two scroll regions fighting: it pins, its own lower
   half is then unreachable, and it only "catches up" when the article finally runs out --
   which reads as the page pausing mid-scroll and then moving the left side on its own.
   That is exactly what happened at 24 chapters; at 16 it fitted and nobody noticed.
   Bounding it to the viewport and giving it its own scroll makes the whole list reachable
   while pinned, so the column simply stays put and only the article moves. */
.man-nav { flex: 0 0 200px; position: sticky; top: 20px; display: flex; flex-direction: column; gap: 1px;
  max-height: calc(100vh - 40px); overflow-y: auto; scrollbar-width: thin;
  /* Room for the scrollbar so it never sits on top of a chapter name. */
  padding-right: 4px; }
.man-nav .man-group { font-size: 10.5px; text-transform: uppercase; letter-spacing: .08em; color: var(--ink-faint-solid); font-weight: 700; margin: 14px 0 4px; }
.man-nav .man-group:first-child { margin-top: 0; }
.man-nav a { font-size: 12.5px; color: var(--ink-soft); text-decoration: none; padding: 5px 9px; border-radius: var(--r-btn); }
.man-nav a:hover { background: var(--surface-2); color: var(--ink); }
.man-nav a.on { background: var(--brand-soft); color: var(--brand-strong); font-weight: 600; }
.man-body { flex: 1 1 auto; min-width: 0; max-width: 760px; background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-card); box-shadow: var(--shadow); padding: 24px 28px 20px; }
.man-body h2 { font-family: var(--font-display); font-size: 20px; margin: 0 0 12px; }
.man-body h3 { font-family: var(--font-display); font-size: 15px; margin: 22px 0 8px; }
.man-body p, .man-body li { font-size: 13px; line-height: 1.65; color: var(--ink); }
.man-body p { margin: 0 0 10px; }
.man-plats { display: inline-flex; gap: 5px; margin: 2px 0 8px; }
.man-steps { padding-left: 22px; margin: 8px 0 12px; }
.man-steps li { margin-bottom: 6px; }
.man-list { padding-left: 22px; margin: 8px 0 12px; }
.man-list li { margin-bottom: 6px; }
.man-call { border: 1px solid var(--line); border-left: 3px solid var(--brand-strong); border-radius: var(--r-btn); padding: 10px 13px; margin: 12px 0; font-size: 12.5px; color: var(--ink-soft); }
.man-call b { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; margin-bottom: 3px; color: var(--brand-strong); }
.man-call.man-gotcha { border-left-color: var(--warn); }
.man-call.man-gotcha b { color: var(--warn); }
  /* The manual's third callout. The macro asked for it and nothing defined it, so "why this
     matters" rendered identically to a plain callout -- three kinds of aside, two appearances.
     Quieter than both on purpose: a gotcha interrupts, a rationale explains. */
  .man-call.man-why { border-left-color: var(--ink-faint-solid); }
  .man-call.man-why b { color: var(--ink-soft); }
.man-pager { display: flex; justify-content: space-between; margin-top: 22px; padding-top: 14px; border-top: 1px solid var(--line-soft); font-size: 12.5px; }
/* Narrow: the column becomes a wrapped row above the article, so it is no longer sticky and
   must NOT keep a viewport height -- that would put a second scrollbar in the middle of the
   page, which is the very thing this is here to prevent. */
@media (max-width: 900px) { .man-wrap { flex-direction: column; } .man-nav { position: static; flex-basis: auto; flex-direction: row; flex-wrap: wrap; gap: 4px; max-height: none; overflow: visible; padding-right: 0; } .man-nav .man-group { width: 100%; margin: 8px 0 2px; } }

/* ---- first-run wizard ---- */
.wiz { max-width: 620px; margin: 0 auto; }
.wiz-prog { display: flex; align-items: center; gap: 7px; margin: 4px 0 16px; }
.wiz-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--line); }
.wiz-dot.on { background: var(--brand-strong); }
.wiz-dot.past { background: var(--good); }
.wiz-count { margin-left: 6px; font-size: 12px; color: var(--ink-soft); font-weight: 600; }
.wiz-card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-card); box-shadow: var(--shadow); padding: 26px 30px; }
.wiz-card h2 { font-family: var(--font-display); font-size: 20px; margin: 0 0 14px; }
.wiz-card p { font-size: 13.5px; line-height: 1.6; color: var(--ink); margin: 0 0 12px; }
.wiz-card .wiz-sub { color: var(--ink-soft); font-size: 12.5px; }
.wiz-err { color: var(--crit); font-size: 12.5px; font-weight: 600; margin: 0 0 12px; }
/* Back out of a wizard step. Deliberately quiet -- it sits above the heading and must not
   compete with the step's own primary action, which is the way forward. */
.wiz-back { display: inline-block; font-size: 12.5px; font-weight: 600; margin-bottom: 10px; }
.wiz-field { margin-bottom: 14px; }
.wiz-field label { display: block; font-size: 12px; font-weight: 600; color: var(--ink-soft); margin-bottom: 6px; }
.wiz-field input[type="file"] { box-sizing: border-box; width: 100%; font: inherit; font-size: 13px; color: var(--ink); background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-btn); padding: 9px 12px; }
.wiz-actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-top: 18px; }
.wiz-back { margin-left: auto; font-size: 12.5px; color: var(--ink-soft); text-decoration: none; }
.wiz-back:hover { color: var(--ink); text-decoration: underline; }
/* The Shared iPad scenario card's Conditional Access limitation is the usually-decisive fact
   (SI§1), not one more grey aside -- without this it inherits the same styling as the
   .sso-produces line under it and reads as trivia. Scoped under .wiz so it can't leak onto any
   other page; this stylesheet has a history of unscoped rules colliding with baseline.html. */
/* `.k12-choice .k12-txt small` is (0,2,1) and sets font-size, colour AND weight. `.wiz
   .sso-limit-lead` is (0,2,0), so it LOST -- the decisive fact this rule exists to highlight
   has been rendering in the same grey as the aside beneath it for as long as both existed.
   Nothing failed; the rule was simply outvoted. Matching the .k12-txt depth wins it back. */
.wiz .k12-txt .sso-limit-lead { color: var(--warn); font-weight: 600; }
/* The card's trailing "Produces: ..." line -- the grey aside the comment above is measured
   against. It had no rule of its own, so it inherited <small> at the card's own body colour and
   read as part of the blurb rather than as a footnote about the outcome. */
.wiz .k12-txt .sso-produces { display: block; margin-top: 5px; font-size: 11.5px; color: var(--ink-faint-solid); }

/* The "already have a profile" escape hatch under the wizard's primary action. It had NO rule
   at all: its summary sat ZERO pixels below the continue button, close enough to mis-click, and
   at full body weight it competed with the action it was an alternative to. The hairline says
   these are two different paths rather than one block of controls. */
.sso-upload { margin-top: 22px; padding-top: 15px; border-top: 1px solid var(--line-soft); }
.sso-upload > summary { font-size: 12.5px; color: var(--ink-soft); }
.sso-upload > summary:hover { color: var(--brand-strong); }
.sso-upload[open] > summary { color: var(--ink); }

/* ---- K-12 wizard polish (choice rows, EDU|IT split context, symbology) ---- */
.k12-lead { color: var(--ink-soft); font-size: 13px; line-height: 1.6; margin: 0 0 20px; }
.k12-q { font-size: 11.5px; font-weight: 700; letter-spacing: .05em; text-transform: lowercase; color: var(--ink-faint-solid); margin: 0 0 10px; }
.k12-group { margin: 0 0 22px; }
.k12-choices { display: flex; flex-direction: column; gap: 10px; }
.k12-choice { display: flex; align-items: center; gap: 13px; padding: 13px 15px; border: 1px solid var(--line); border-radius: var(--r-tile); background: var(--surface-2); cursor: pointer; transition: border-color .12s, background .12s; }
.k12-choice:hover { border-color: var(--brand-line); }
.k12-choice:has(input:checked) { border-color: var(--brand-strong); background: var(--brand-soft); }
.k12-choice input { accent-color: var(--brand-strong); width: 17px; height: 17px; margin: 0 0 0 auto; flex: 0 0 auto; }
.k12-choice .k12-ic { flex: 0 0 auto; width: 30px; height: 30px; display: grid; place-items: center; border-radius: var(--r-btn); background: var(--brand-soft); color: var(--brand-strong); }
.k12-choice .k12-ic svg { width: 18px; height: 18px; }
.k12-choice .k12-txt { font-size: 14px; color: var(--ink); line-height: 1.3; }
.k12-choice .k12-txt small { display: block; font-size: 12px; color: var(--ink-soft); margin-top: 2px; font-weight: 400; }

.k12-split { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin: 6px 0 20px; }
.k12-ctx { border: 1px solid var(--line-soft); border-radius: var(--r-tile); padding: 13px 15px; background: var(--surface-2); }
.k12-ctx-h { display: flex; align-items: center; gap: 7px; font-size: 11px; font-weight: 700; letter-spacing: .04em; text-transform: lowercase; margin: 0 0 7px; }
.k12-ctx-h svg { width: 15px; height: 15px; }
.k12-ctx.edu .k12-ctx-h { color: var(--good); }
.k12-ctx.it .k12-ctx-h { color: var(--brand-strong); }
.k12-ctx p { font-size: 12.5px; line-height: 1.55; color: var(--ink-soft); margin: 0; }
.k12-ctx .k12-tally { font-family: var(--font-display); font-size: 22px; font-weight: 700; color: var(--ink); }

.k12-list { list-style: none; margin: 0 0 4px; padding: 0; }
.k12-list li { display: flex; align-items: flex-start; gap: 9px; font-size: 12.5px; line-height: 1.5; color: var(--ink-soft); padding: 4px 0; }
.k12-list li svg { flex: 0 0 auto; width: 15px; height: 15px; margin-top: 2px; color: var(--good); }
.k12-summary { max-height: 210px; overflow: auto; padding-right: 4px; }
.k12-summary li svg { color: var(--brand-strong); }
.k12-field { margin: 0 0 12px; }
.k12-field label { display: block; font-size: 11.5px; font-weight: 600; color: var(--ink-soft); margin: 0 0 5px; }
.k12-field input[type="text"] { box-sizing: border-box; width: 100%; max-width: 380px; font: inherit; font-size: 13.5px; color: var(--ink); background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-btn); padding: 9px 12px; }
.k12-actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-top: 22px; }
.k12-done-badge { display: grid; place-items: center; width: 56px; height: 56px; border-radius: 50%; background: var(--good-soft); color: var(--good); margin: 0 0 14px; }
.k12-done-badge svg { width: 30px; height: 30px; }
@media (max-width: 560px) { .k12-split { grid-template-columns: 1fr; } }
.btn.is-clean { opacity: .5; }
.k12-status { font-size: 12.5px; margin: 0 0 8px; }
.k12-saved { color: var(--ink-soft); margin-left: 6px; }
.k12-dirty { color: var(--warn); margin-left: 6px; }

/* ---- SSO wizard settings review (Task 8) ---- */
.wiz .sso-setting-row { margin: 0 0 16px; padding-bottom: 14px; border-bottom: 1px solid var(--line-soft); }
.wiz .sso-setting-row:last-child { border-bottom: none; padding-bottom: 0; margin-bottom: 0; }
.wiz .sso-setting-label { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; color: var(--ink); margin: 0 0 6px; }
/* The wipe warning has to read as more than one more grey caption -- it is why a risky setting
   defaults off in the first place, and it renders ABOVE the control in the template, not below. */
.wiz .sso-setting-warn { color: var(--warn); font-weight: 600; font-size: 12.5px; margin: 0 0 8px; }
.wiz .sso-setting-why { color: var(--ink-soft); font-size: 12px; line-height: 1.5; margin: 6px 0 0; }
.wiz .sso-setting-hint { display: block; color: var(--ink-soft); font-size: 11.5px; margin-top: 4px; }
.wiz .sso-setting-row input[type="text"],
.wiz .sso-setting-row input[type="number"],
.wiz .sso-setting-row select {
  box-sizing: border-box; width: 100%; max-width: 420px; font: inherit; font-size: 13px;
  color: var(--ink); background: var(--surface-2); border: 1px solid var(--line);
  border-radius: var(--r-btn); padding: 8px 11px;
}
.wiz .sso-setting-row input[type="checkbox"] { width: 17px; height: 17px; accent-color: var(--brand-strong); }

/* ---- Content caching card -------------------------------------------------
   The settings form used to borrow .kv, which is a label-to-VALUE grid: it put
   each field's control under its own label and dropped the next, unrelated
   field into the second column, so two settings read as one row. Forms use
   .field (label above control) like everywhere else in the console; the only
   thing needed here is a grid that tiles whole fields, never splitting one. */
.ccform .fieldgrid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 0 18px; margin-top: 10px;
}
.ccform .field .hint {
  font-weight: 400; font-size: 11.5px; line-height: 1.45;
  color: var(--ink-faint-solid); margin-top: -1px;
}
.ccform .field .hint:empty { display: none; }
.ccmanage {
  display: flex; align-items: center; gap: 9px; font-size: 13px; font-weight: 600;
  padding: 11px 13px; border: 1px solid var(--line); border-radius: var(--r-btn);
  background: var(--surface-2); cursor: pointer;
}
.ccmanage input { width: 16px; height: 16px; accent-color: var(--brand-strong); }
.cchint {
  margin: 9px 0 0; padding: 9px 12px; font-size: 12.5px; line-height: 1.5;
  color: var(--ink-soft); background: var(--warn-soft);
  border-left: 2px solid var(--warn); border-radius: 0 var(--r-btn) var(--r-btn) 0;
}
.ccgroup { margin-top: 12px; border-top: 1px solid var(--line-soft); padding-top: 10px; }
.ccgroup > summary {
  cursor: pointer; font-size: 11px; font-weight: 700; text-transform: lowercase;
  letter-spacing: .08em; color: var(--ink-faint-solid);
  display: flex; align-items: center; gap: 8px;
}
.ccgroup > summary::marker { color: var(--ink-faint-solid); }
.ccgroup > summary:hover { color: var(--ink-soft); }
.ccgroup > summary .tag { text-transform: none; letter-spacing: 0; font-weight: 600; }
.ccform > .btn { margin-top: 16px; }

/* ---- Content caching card: Traffic (Task 6) --------------------------------
   Two inline-SVG charts, no library (strict CSP). Every fill below is an
   EXISTING token -- brand/warm/crit is the one ordering of this app's accent
   colors that clears the dataviz-skill palette validator's CVD-separation
   check for a 3-way pie (--pairs all: any two slices can end up adjacent).
   A 4th distinct hue doesn't exist in this file without inventing one, so a
   4th+ category folds into the grey "Other" slot (ccslice-3) instead. Reused
   for the bandwidth bar too, so the whole section reads as one small theme:
     ccseg-cache / ccslice-0 -> brand   ccseg-peers / ccslice-1 -> warm
     ccseg-origin / ccslice-2 -> crit   ccslice-3 (Other)       -> ink-faint-solid
   Status colors are NOT reused for "series 4": good/warn stay reserved for
   device/compliance state elsewhere in the console. */
.ccbar-wrap { border-radius: var(--r-card); overflow: hidden; height: 16px; background: var(--surface-2); }
.ccbar { display: block; width: 100%; height: 100%; }
.ccbar rect, .ccpie .ccslice { cursor: default; }
.ccbar rect:focus-visible, .ccpie .ccslice:focus-visible { outline: 2px solid var(--brand-line); outline-offset: 1px; }
.ccbar rect:hover, .ccpie .ccslice:hover, .ccbar rect:focus-visible, .ccpie .ccslice:focus-visible {
  filter: brightness(1.1);
}
.ccgap { fill: var(--surface-2); }
/* Bandwidth bar keeps the original three, which carry MEANING there (cache/peers/
   origin) rather than mere identity. */
.ccseg-cache { fill: var(--brand); background: var(--brand); }
.ccseg-peers { fill: var(--warm); background: var(--warm); }
.ccseg-origin { fill: var(--crit); background: var(--crit); }

/* Pie slots: Okabe-Ito, the published colour-blind-safe qualitative palette, adapted for
   this app (slot 0 is the brand teal, within a hair of Okabe-Ito's bluish-green).
   Eight slots because the cache breakdown must show every category Apple reports -- folding
   a 4th into grey hid "iOS Software" on an iPad fleet, the single category anyone is looking
   for. Beyond eight, slot 7 grey absorbs the tail. Chosen over inventing hues by eye:
   Okabe-Ito is designed and validated for deuteranopia/protanopia/tritanopia, and adjacent
   slots also differ in lightness, so the ordering survives where hue separation does not.

   ⚠️ THEME SELECTORS, and they are the reason this block was rewritten. These rules used to
   switch on `@media (prefers-color-scheme: dark)` ALONE, while every other themed thing in
   this file keys off `[data-theme]` -- the explicit toggle in the header, remembered in
   localStorage. So a person whose SYSTEM is dark and who switched the APP to light kept the
   dark palette on a white surface: five pastel wedges at 1.8-2.9:1 contrast. Three states,
   the same shape used for the colour tokens at the top of this file:
     - bare rules              -> light, the default
     - media query, guarded    -> follow the system, unless the app was explicitly set light
     - [data-theme="dark"]     -> the toggle wins in both directions

   Three light values are darkened from raw Okabe-Ito, because they missed the 3:1 floor a
   non-text UI element needs against the card surface. Note the surface is #F8F4EC, not white:
   warming the theme LOWERED these ratios rather than raising them (the slices are mid-dark,
   so a darker background moves them closer), which is the opposite of the intuition and the
   reason the test computes the ratio from --surface instead of assuming. Darkened in place,
   hue kept, so CVD separation is preserved -- the palette's tightest pair under Machado
   simulation is vermillion/olive at dE 7.7, untouched and unchanged by any of this. */
/* DARK IS THE BASE, because the theme's default is dark. This block was the other way round
   until the ops-console retheme and the inversion was missed: the base rules carried the LIGHT
   slices, and the dark ones only arrived through `prefers-color-scheme: dark`. So a visitor on
   a system-light machine who had never touched the toggle got the dark console with mid-dark
   slices on a near-black card -- readable-ish, wrong, and invisible to anyone whose own machine
   is set to dark. Structure follows the theme now: base is dark, light is the explicit opt-in.

   Hue ORDER is identical between the two, so a slice keeps its identity across a theme switch
   mid-session. The hues are Okabe-Ito rather than the brand palette on purpose -- a categorical
   chart needs colours that stay distinguishable under colour-vision deficiency, which is a
   different job from a status colour, and the tightest pair under Machado simulation is
   vermillion/olive at dE 7.7. */
.ccslice-0 { fill: #2BC3B8; background: #2BC3B8; }
.ccslice-1 { fill: #F0B24A; background: #F0B24A; }
.ccslice-2 { fill: #7FC9F0; background: #7FC9F0; }
.ccslice-3 { fill: #DB94BC; background: #DB94BC; }
.ccslice-4 { fill: #4D9FD6; background: #4D9FD6; }
.ccslice-5 { fill: #E8794E; background: #E8794E; }
.ccslice-6 { fill: #C4A03C; background: #C4A03C; }
.ccslice-7 { fill: var(--ink-faint-solid); background: var(--ink-faint-solid); }  /* tail */

/* Light: the same eight hues brought down so they hold contrast against a white surface.
   tests/test_chart_palette.py computes every ratio from --surface rather than assuming one. */
:root[data-theme="light"] .ccslice-0 { fill: #0E9E96; background: #0E9E96; }   /* brand teal */
:root[data-theme="light"] .ccslice-1 { fill: #BA8000; background: #BA8000; }   /* orange, darkened */
:root[data-theme="light"] .ccslice-2 { fill: #1C94D8; background: #1C94D8; }   /* sky, darkened */
:root[data-theme="light"] .ccslice-3 { fill: #C86FA0; background: #C86FA0; }   /* reddish purple */
:root[data-theme="light"] .ccslice-4 { fill: #0072B2; background: #0072B2; }   /* blue */
:root[data-theme="light"] .ccslice-5 { fill: #D55E00; background: #D55E00; }   /* vermillion */
:root[data-theme="light"] .ccslice-6 { fill: #8C6D1F; background: #8C6D1F; }   /* olive */

/* Collapsed sidebar: icons only.
   The width is a CSS variable so the main column follows without a second source of truth --
   two places holding the same number is how a layout ends up half-collapsed. Icons keep the
   same left offset expanded and collapsed, so nothing jumps horizontally on toggle; only the
   labels go. */
/* `appearance: none` first: a <button> on macOS renders with the native push-button bezel --
   a light grey rounded rectangle with its own border -- and WebKit does not always drop it
   just because a background and border are set. That bezel is what a "weird looking" button
   in this app has twice turned out to be. */
.sidetoggle { -webkit-appearance: none; appearance: none;
  margin-left: auto; flex: none; display: grid; place-items: center;
  width: 28px; height: 28px; border-radius: var(--r-btn); border: 1px solid transparent;
  background: transparent; color: var(--ink-soft); cursor: pointer; }
.sidetoggle:hover { background: var(--surface-2); color: var(--ink); }
/* Solid brand, not the 32%-alpha line token: composited on the dark sidebar that washed out
   to roughly rgb(38,87,89), which is a focus ring nobody can see. A focus indicator that is
   present in the stylesheet and invisible on screen is the same as not having one. */
.sidetoggle:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
/* No filled "pressed" state here, deliberately. One was added on 2026-08-20 and made the
   button look worse, not better: a filled rounded box in the brand row reads as a control
   that is stuck rather than a sidebar that is collapsed -- and the collapsed sidebar is
   already unmistakable, because it is 62px wide with no labels in it. The icon itself is the
   indicator; it flips with the state, and so does the title. */
.sidetoggle svg { width: 16px; height: 16px; }

@media (min-width: 861px) {

/* Applied by the pre-paint script so the collapsed width is right on first render; the
   real state moves onto .app as soon as the toggle script runs. */
html.pre-sidemini .app { grid-template-columns: 56px 1fr; }
html.pre-sidemini .app .nav-t { visibility: hidden; }
.app.sidemini { grid-template-columns: 56px 1fr; }
.app.sidemini::before { width: 56px; }
.app.sidemini .nav-t { display: none; }
.app.sidemini .caret { display: none; }
/* Sub-items hang off a label that is no longer readable, so keeping them would be a list of
   anonymous icons under another anonymous icon. */
.app.sidemini .nav-sub { display: none; }
.app.sidemini .nav a,
.app.sidemini .nav .nav-tree-h { justify-content: center; padding-left: 0; padding-right: 0;
  gap: 0; }
.app.sidemini .side { padding-left: 8px; padding-right: 8px; }
/* Stacked, not side by side: a 38px mark next to a 28px button does not fit a 62px rail, and
   flex let them overflow the aside rather than wrap. */
.app.sidemini .brand { flex-direction: column; align-items: center; gap: 8px;
  padding: 6px 0 12px; }
.app.sidemini .sidetoggle { margin-left: 0; }
.app.sidemini .mark { width: 26px; height: 26px; border-radius: 5px; }

/* Collapsed, a platform's sub-items open beside the rail instead of expanding the whole
   sidebar. Expanding it threw away the collapse the moment someone used the nav for what it
   is for -- which is why it read as the sidebar "popping open again".
   position: fixed, placed by JS from the button's rect, because .nav scrolls and an absolutely
   positioned panel would be clipped by that overflow. */
.app.sidemini .nav-sub.navfly { position: fixed; z-index: 40; min-width: 190px; padding: 6px;
  max-height: calc(100vh - 24px); overflow-y: auto;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-btn);
  box-shadow: 0 8px 24px -12px rgba(0,0,0,0.7); display: block; }
/* Specificity, deliberately: `.app.sidemini .nav a` centres and de-gaps every rail item at
   (0,3,0), and it applies inside the flyout too -- which rendered the panel's rows centred and
   cramped. This is (0,4,0) so the panel reads like a normal menu again. */
.app.sidemini .nav .nav-sub.navfly a { display: flex; align-items: center; gap: 9px;
  justify-content: flex-start; padding: 7px 9px; border-radius: var(--r-btn); font-size: 13px; }
/* The heading this panel belongs to is an unlabelled icon, so the panel names itself. */
.app.sidemini .nav-sub.navfly::before { content: attr(data-fly-title); display: block;
  font-size: 10.5px; text-transform: lowercase; letter-spacing: 0.09em; font-weight: 700;
  color: var(--ink-faint-solid); padding: 4px 9px 7px; }
.app.sidemini .nav-sub.navfly .nav-t { display: inline; }
/* Same overflow as the brand row, at the other end of the rail: a 34px avatar plus two icon
   buttons plus their gaps is ~124px of content in a 46px-wide space, and `.icon-btn`'s
   `margin-left: auto` shoved the row hard left until the avatar hung off the edge. Stack. */
.app.sidemini .side-foot { flex-direction: column; justify-content: center; align-items: center;
  gap: 8px; }
.app.sidemini .side-foot .who { display: none; }
.app.sidemini .side-foot .icon-btn { margin-left: 0; }
.app.sidemini .avatar { width: 30px; height: 30px; border-radius: var(--r-btn); font-size: 13px; }
/* The logout button is wrapped in a form, which is the flex item that actually gets laid out. */
.app.sidemini .side-foot form { display: grid; place-items: center; }
/* The section heading survives as a rule: it still separates Deploy from the rest without
   claiming a word nobody can read. */
.app.sidemini .nav-label { display: block; height: 1px; padding: 0; margin: 10px 6px;
  background: var(--ink-faint); overflow: hidden; text-indent: -999em; }

/* The panel has to clear .main, and z-index alone cannot do it: `.side` is position:sticky with
   z-index:1, which makes it a STACKING CONTEXT, so the panel's own z-index only ranks it
   against its siblings inside the sidebar. The sidebar as a whole was tied with .main at level
   1 and lost on document order. Lifting the sidebar is what actually moves the panel; 30 keeps
   it under the slide-over scrim (40) and sheet (41), which should still cover the nav. */
.app.sidemini .side { z-index: 30; }

}  /* end @media (min-width: 861px) -- collapsed rail */

/* Two command SYSTEMS on the Advanced tab, not two features of one.
   DDM = state the device holds and reports on. MDM = a one-shot instruction that is
   acknowledged and finished. Conflating them is how someone waits for an acknowledgement
   that never comes, or hunts for a declaration in the command log -- so each gets its own
   card, its own badge, and its own accent edge. Teal is the app's brand and reads as the
   steady/persistent one; amber reads as the momentary action. */
.syscompare { display: grid; gap: 8px; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  margin: 0 0 14px; font-size: 12.5px; color: var(--ink-soft); }
.syscompare > div { padding: 10px 12px; background: var(--surface-2); border-radius: var(--r-btn); }
.syscompare em { font-style: normal; font-weight: 650; color: var(--ink); }

.syscard { border: 1px solid var(--ink-faint); border-radius: var(--r-card); padding: 14px 16px;
  margin-bottom: 18px; border-left-width: 4px; }
.syscard-ddm { border-left-color: var(--brand); }
.syscard-mdm { border-left-color: var(--warm); }
.syshead { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
.syshead > b { font-family: var(--font-display); font-size: 15px; }
.sysbadge { font-size: 10.5px; font-weight: 800; letter-spacing: 0.07em; padding: 3px 7px;
  border-radius: 3px; color: var(--on-brand); }
.syscard-ddm .sysbadge { background: var(--brand); }
.syscard-mdm .sysbadge { background: var(--warm); color: var(--on-brand); }
.syskind { font-size: 11.5px; color: var(--ink-soft); }

/* Reference popups on the Advanced tab. */
.refdlg { max-width: 780px; width: 92vw; max-height: 82vh; overflow-y: auto;
  border: 1px solid var(--ink-faint); border-radius: var(--r-card); padding: 18px 20px;
  background: var(--surface); color: var(--ink); }
.refdlg::backdrop { background: rgba(0,0,0,0.45); }
.refhead { display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin-bottom: 8px; font-family: var(--font-display); font-size: 15px; }
.refitem { border-top: 1px solid var(--ink-faint); padding: 12px 0 4px; }
.refitemhead { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.refitemhead code { font-size: 13px; font-weight: 650; }
.refos { font-size: 11.5px; color: var(--ink-soft); }
.refdesc { font-size: 12.5px; color: var(--ink-soft); margin: 4px 0 6px; }
.refpre { margin: 0; padding: 10px 12px; background: var(--surface-2); border-radius: var(--r-btn);
  font-size: 11.5px; line-height: 1.5; overflow-x: auto; white-space: pre; cursor: copy; }
[data-copy-inline] { cursor: copy; }

/* Debug profiles: the usable ones first, the rest shut but reachable. */
.dbgeyebrow { font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: lowercase;
  color: var(--ink-soft); margin: 16px 0 10px; display: flex; align-items: center; gap: 8px; }
.dbgpending { margin-top: 18px; border: 1px solid var(--ink-faint); border-radius: var(--r-btn);
  padding: 12px 14px; }
.dbgpending > summary { cursor: pointer; font-weight: 650; display: flex; align-items: center;
  gap: 8px; flex-wrap: wrap; }
.dbgpendinghelp { font-weight: 400; font-size: 12px; color: var(--ink-soft); }

/* Content-caching form: an entry point, and a hierarchy. Twenty-eight equally-weighted
   fields behind six accordions named after Apple's key taxonomy is a reference manual. */
.cceyebrow { font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: lowercase;
  color: var(--ink-soft); margin: 0 0 8px; }
.ccessentials { padding: 12px 14px; border: 1px solid var(--brand-line); border-radius: var(--r-btn);
  background: var(--brand-soft); margin-bottom: 4px; }
.ccessentials .field { margin-bottom: 10px; }
.ccessentials .field:last-child { margin-bottom: 0; }
.ccgrouphelp { display: block; font-size: 12px; font-weight: 400; color: var(--ink-soft);
  margin-top: 2px; }
.ccpreview { margin: 10px 0 4px; border: 1px solid var(--ink-faint); border-radius: var(--r-btn);
  padding: 10px 12px; }
.ccpreview > summary { cursor: pointer; font-weight: 650; }
.ccpayload { margin: 8px 0 0; padding: 10px 12px; background: var(--surface-2);
  border-radius: var(--r-btn); font-size: 12px; line-height: 1.55; overflow-x: auto;
  white-space: pre; color: var(--ink-soft); }

.cclegend { list-style: none; margin: 9px 0 0; padding: 0; display: flex; flex-wrap: wrap;
  gap: 5px 18px; font-size: 12.5px; color: var(--ink-soft); }
.cclegend li { display: flex; align-items: center; gap: 6px; }
.cclegend b { color: var(--ink); font-weight: 650; }
.ccdot { width: 9px; height: 9px; border-radius: 3px; flex: none; }
.ccsaved { display: flex; align-items: baseline; gap: 9px; margin: 12px 0; }
.ccsaved-num { font-family: var(--font-display); font-size: 26px; font-weight: 800;
  letter-spacing: -0.01em; color: var(--ink); }
.cccat { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; margin: 10px 0; }
.ccpie { width: 88px; height: 88px; flex: none; }
.ccpie .ccslice { stroke: var(--surface); stroke-width: 2; }

/* Cache-clients filter bar + sortable headers. The list is one Mac's, filtered in the page
   (see device_detail.html) because a round trip would close the <details> it lives in. */
.ccfilters { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px; margin-top: 10px; }
.ccfilters label { display: flex; flex-direction: column; gap: 4px;
  font-size: 11px; text-transform: lowercase; letter-spacing: .06em;
  color: var(--ink-faint-solid); font-weight: 700; }
.ccfilters .field { font-size: 12.5px; padding: 6px 8px; min-width: 150px; }
.ccfilters-count { font-size: 12px; margin-left: auto; align-self: center; }
.ccclient-table th { padding: 0; }
.ccclient-table th [data-ccsort] {
  font: inherit; color: inherit; background: none; border: 0; cursor: pointer;
  padding: 9px 12px; width: 100%; text-align: inherit; display: inline-flex; gap: 5px;
  align-items: center; border-radius: var(--r-btn);
}
.ccclient-table th [data-ccsort]:hover { color: var(--ink); background: var(--surface-2); }
.ccclient-table th [data-ccsort]:focus-visible { outline: 2px solid var(--brand-line); outline-offset: -2px; }
/* The arrow is drawn from aria-sort rather than a second class, so the thing a screen reader
   announces and the thing a sighted reader sees cannot disagree. */
.ccclient-table th [data-ccsort]::after { content: ""; font-size: 9px; opacity: .5; }
.ccclient-table th[aria-sort="ascending"] [data-ccsort]::after { content: "\2191"; opacity: 1; }
.ccclient-table th[aria-sort="descending"] [data-ccsort]::after { content: "\2193"; opacity: 1; }

/* --- App artwork ------------------------------------------------------------------
   Paired with the app_icon() macro in _macros.html. One knob, --icon-size, drives width,
   height and corner radius together: Apple's icon grid puts the corner at about 22.5% of
   the width, so a radius fixed in pixels is only right at one size. Every app list in the
   console and the portal uses this, so they line up with each other.

   The placeholder is not a missing image -- it is the answer "we have no artwork for this",
   which is the normal case for an Installomator label or a .pkg. It keeps the row height
   and the text baseline identical to a row that does have artwork, so a mixed list does not
   look ragged. */
.appicon { width: var(--icon-size, 28px); height: var(--icon-size, 28px);
           border-radius: calc(var(--icon-size, 28px) * 0.225); flex: none;
           background: var(--surface-2); object-fit: cover; }
.appicon.noicon { display: grid; place-items: center; color: var(--ink-faint-solid);
                  border: 1px solid var(--line-soft); }
.appicon.noicon svg { width: 55%; height: 55%; }

/* ── Ops console: hairline lists ───────────────────────────────────────────────────────
   Rows are separated from each other, never from the edge of the thing containing them.
   A border on the last row draws a line immediately above the container's own border,
   which reads as a doubled edge at every list bottom in the app. */
.dlist > *:last-child,
.data-list > *:last-child,
.applist .approw:last-child,
.u-list .u-row:last-child { border-bottom: 0; }

/* Selection and caret pick up the accent, which is most of what makes a mono UI feel like a
   terminal rather than a spreadsheet. */
::selection { background: var(--brand-soft); }
:root { accent-color: var(--brand); caret-color: var(--brand); }

/* Numbers line up in a mono UI for free, but only if nothing overrides the family. Tabular
   figures still matter for proportional-width digits in some faces. */
.app { font-variant-numeric: tabular-nums; }

/* <code> and <pre> carry the browser's generic monospace unless told otherwise, which in a
   console already set in mono means one family sitting inside another -- close enough to look
   like a rendering fault rather than a choice. */
code, pre, kbd, samp { font-family: var(--font-mono); }

/* A native control does not inherit the page font. `.btn` said so from the start; every other
   control had to be told one at a time, which is how filter chips, small action buttons and
   dropdown lists ended up in Arial inside a console set entirely in mono. Said once, here. */
button, input, select, textarea, option { font-family: inherit; }

/* ── Callout ───────────────────────────────────────────────────────────────────────────
   The block for a sentence that has to be READ, as opposed to one that is available.

   The console had no such thing, so warnings were written as an ordinary paragraph with a
   bold first sentence -- and where that paragraph was also `.muted`, which several were, the
   bold only made a footnote heavier. "This Mac is encrypted and nobody here can recover it"
   was rendering at footnote size in footnote grey.

   Two changes do the work, and the first matters more than the colour: the lead takes its own
   LINE, so it reads as a heading rather than as the start of a sentence. Then it takes the
   severity colour and the body comes up from muted to --ink-soft, so the block is legible
   rather than merely present.

   The left rule rather than a filled panel, because this theme separates with borders. It is
   the same device the device header's status block uses, which is deliberate: one visual idea
   for "this edge means something". */
.callout { border-left: 2px solid var(--line); padding: 1px 0 1px 12px; margin: 10px 0;
           font-size: 12.5px; line-height: 1.6; color: var(--ink-soft); }
.callout > b:first-child, .callout > strong:first-child {
  display: block; color: var(--ink); font-size: 13px; font-weight: 700; margin-bottom: 2px; }
.callout.warn { border-left-color: var(--warn); }
.callout.warn > b:first-child, .callout.warn > strong:first-child { color: var(--warn); }
.callout.crit { border-left-color: var(--crit); }
.callout.crit > b:first-child, .callout.crit > strong:first-child { color: var(--crit); }
.callout.info { border-left-color: var(--brand); }
.callout.info > b:first-child, .callout.info > strong:first-child { color: var(--brand); }

/* Emphasis inside faint text. A <b> that inherits the same grey as the sentence around it is
   emphasis that does not emphasise -- it was carrying weight alone, which at 12px in a mono
   face is very nearly nothing. */
.muted > b, .sub > b, .ag-hint > b, .muted > strong, .sub > strong { color: var(--ink); }

/* Irreversible-action framing. Lived in device_detail.html's own <style> until the verb
   layer's conversion screen needed the same treatment -- two templates asking for one look
   is what a shared rule is for, and a second copy would have drifted. */
.danger-zone { border: 1px solid var(--crit); border-radius: var(--r-card); padding: 12px;
               margin-top: 8px; }
.danger-zone .dz-h { font-weight: 700; font-size: 12.5px; color: var(--crit);
                     margin-bottom: 4px; }

/* --- Home Screen layout canvas ------------------------------------------------------------
   Moved out of homescreen_build.html: 681 lines of template carrying its own stylesheet and
   its own application is past the size where either can be read. Living here also puts every
   class under tests/test_component_classes.py, which fails the suite for a class the
   stylesheet does not define. */
  .hsx { display: grid; grid-template-columns: 452px minmax(0, 1fr); gap: 20px; align-items: start; }
  @media (max-width: 1180px) { .hsx { grid-template-columns: minmax(0, 1fr); } }

  /* The iPad, to shape. Not decoration: the whole point of drawing it is that the 30-icon page
     and the separate Dock are the two facts that decide whether a layout survives contact with
     a device, and neither is visible in a comma-separated list. */
  /* Capped, so that when the two columns stack on a narrow window the "iPad" does not
     stretch to 720px and stop reading as a device. */
  .hs-pad { background: var(--surface-2); border: 1px solid var(--line); border-radius: 22px;
            padding: 14px; box-shadow: var(--shadow); max-width: 452px; }
  /* Transparent on purpose -- var(--ground) here used to paint an opaque panel directly over
     .hs-wall below, so the gradient only ever showed in the 14px margin around it and every
     icon actually sat on a flat app-surface colour, not the wallpaper the comment describes. */
  .hs-screen { background: transparent; border: 1px solid var(--line-soft); border-radius: 14px;
               padding: 12px 10px 10px; }
  .hs-page { display: grid; gap: 8px 4px; }
  /* touch-action: none is the other half of the pointer-event drag engine below, not
     decoration. Without it, the browser's own gesture recognizer gets first look at a finger
     drag, reads it as "the user wants to scroll", and answers with a pointercancel on the
     very first touchmove -- verified live: a real touch drag died after exactly one
     pointermove until this was added. Pointer events fire regardless, so mouse and trackpad
     dragging were never affected; only touch was silently broken. */
  .hs-slot { position: relative; display: flex; flex-direction: column; align-items: center;
             gap: 3px; padding: 2px 0; border-radius: 8px; cursor: grab; touch-action: none; }
  .hs-slot:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
  .hs-slot.drag { opacity: .35; }
  .hs-ic { width: 44px; height: 44px; border-radius: 11px; display: grid; place-items: center;
           background: var(--surface); border: 1px solid var(--line); overflow: hidden;
           font-size: 13px; font-weight: 700; color: var(--ink-soft); }
  .hs-ic img { width: 100%; height: 100%; object-fit: cover; }
  /* 9.5px, matching the smallest type this console uses anywhere. The first cut was 8.5px --
     smaller than anything else in the app, and the caption filled 51 of the slot's 53px, so
     nearly every app name clipped. The canvas is wider instead: the six columns are the truth
     about an iPad page and are not negotiable, so the pad grew rather than the grid shrinking. */
  /* Theme-aware by default -- .hs-cap is also the caption inside the OPEN-FOLDER STRIP below
     the canvas (renderOpen() reuses appTile()), and that strip sits on var(--surface), the
     console's own themed background, not the wallpaper. .hs-pad below pins it to a fixed
     white instead, but only inside the canvas, where the backdrop really is the fixed dark
     gradient in both console themes. Pinning white here unscoped made every caption (and the
     folder's "Empty..." placeholder) invisible on the light theme's white .hs-open panel. */
  .hs-cap { font-size: 9.5px; line-height: 1.2; text-align: center; color: var(--ink-soft);
            max-width: 62px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2;
            -webkit-box-orient: vertical; }
  .hs-pad .hs-cap { color: rgba(255, 255, 255, .82); }
  /* An icon whose app is not on every target. It still renders, because the layout still names
     it -- what it must not do is look like the ones that will actually appear. */
  .hs-slot.gap .hs-ic { border-style: dashed; border-color: var(--warn); color: var(--warn); }
  /* Placed twice. The error names it; this is what lets you FIND it -- five bundle ids in a
     sentence is not a way to locate five tiles in a grid of thirty. The border survives on
     .hs-ic itself (the squircle mask still lets a border through). The glow does not survive
     there in EITHER direction: an outer box-shadow paints outside the masked box and the mask
     erases it; an inset one paints inside the box but under normal-flow content, and once
     artwork loads, .hs-ic img fills the entire content box and sits on top of it. .hs-slot is
     the icon's unmasked, un-covered parent, so the halo goes there instead, as an ordinary
     outer box-shadow -- outside a box nothing clips or paints over. */
  .hs-slot.dup .hs-ic { border-color: var(--crit); border-style: solid; }
  .hs-slot.dup { box-shadow: 0 0 0 2px var(--crit-soft); }
  .hs-slot.dup .hs-cap { color: var(--crit); font-weight: 700; }
  .hs-x { position: absolute; top: -3px; right: 4px; width: 15px; height: 15px; line-height: 13px;
          border-radius: 50%; border: 1px solid var(--line); background: var(--surface);
          color: var(--ink-soft); font-size: 11px; text-align: center; cursor: pointer;
          opacity: 0; transition: opacity .1s; padding: 0; }
  .hs-slot:hover .hs-x, .hs-slot:focus-within .hs-x { opacity: 1; }

  .hs-dock { margin-top: 10px; background: rgba(255, 255, 255, .08);
             border: 1px solid rgba(255, 255, 255, .16); border-radius: 14px; padding: 8px 10px; }
  /* Same fixed-colour reasoning as .hs-cap above -- both sit directly on the wallpaper. */
  .hs-zonelab { font-size: 9.5px; letter-spacing: .06em; text-transform: uppercase;
                color: rgba(255, 255, 255, .55); font-weight: 700; margin: 0 0 6px; }
  /* Same scoping as .hs-cap above: theme-aware by default, because renderOpen() reuses this
     class for the open folder's own empty-state message on var(--surface), not the wallpaper. */
  .hs-empty { grid-column: 1 / -1; text-align: center; font-size: 11px; color: var(--ink-faint-solid);
              padding: 14px 0; border: 1px dashed var(--line); border-radius: 8px; }
  .hs-pad .hs-empty { color: rgba(255, 255, 255, .5); border-color: rgba(255, 255, 255, .25); }
  /* Where the DEVICE will break the page, drawn where it will happen rather than explained
     afterwards. Measured on an iPad Pro 13-inch (M4): page 1 held exactly 30. */
  .hs-break { grid-column: 1 / -1; display: flex; align-items: center; gap: 8px; margin: 4px 0 2px;
              font-size: 10px; color: var(--warn); font-weight: 700; }
  .hs-break::before, .hs-break::after { content: ""; flex: 1; height: 1px; background: var(--warn); opacity: .4; }

  .hs-tools { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin: 0 0 10px; }
  .hs-search { flex: 1; min-width: 150px; font: inherit; font-size: 12.5px; padding: 7px 10px;
               border-radius: var(--r-btn); border: 1px solid var(--line);
               background: var(--surface); color: var(--ink); }
  .hs-pick { display: grid; grid-template-columns: repeat(auto-fill, minmax(178px, 1fr)); gap: 7px;
             max-height: 460px; overflow-y: auto; padding-right: 4px; }
  /* pan-y, not none: unlike the canvas, .hs-pick is its OWN scroll container (overflow-y:
     auto, capped height) -- `none` here blocked a finger from scrolling the app list at all.
     pan-y lets the browser take a vertical-only touch gesture as a native scroll; anything
     else (a horizontal or diagonal move, which is what reaching the canvas requires) still
     reaches the pointer engine untouched, so lifting an app still works. */
  .hs-app { display: flex; align-items: center; gap: 9px; padding: 7px 9px; font: inherit;
            font-size: 12.5px; cursor: pointer; text-align: left; min-width: 0;
            background: var(--surface-2); border: 1px solid var(--line);
            border-radius: var(--r-btn); color: var(--ink); touch-action: pan-y; }
  .hs-app:hover { border-color: var(--brand-line); }
  .hs-app[data-used="1"] { opacity: .45; }
  .hs-app .hs-ic { width: 26px; height: 26px; border-radius: 7px; font-size: 10px; flex: none; }
  .hs-app b { font-weight: 600; display: block; overflow: hidden; text-overflow: ellipsis;
              white-space: nowrap; }
  .hs-cov { font-size: 10px; font-weight: 700; }
  .hs-cov.all { color: var(--ink-faint-solid); font-weight: 400; }
  .hs-cov.some { color: var(--warn); }
  .hs-cov.unknown { color: var(--ink-faint-solid); font-weight: 400; }

  .hs-lab { display: block; font-size: 10.5px; text-transform: lowercase; letter-spacing: .07em;
            color: var(--ink); font-weight: 700; margin-bottom: 5px; }
  .hs-field { width: 100%; box-sizing: border-box; font: inherit; font-size: 12.5px;
              padding: 8px 10px; border-radius: var(--r-btn); border: 1px solid var(--line);
              background: var(--surface); color: var(--ink); }
  .hs-zone-on { box-shadow: 0 0 0 2px var(--brand-soft); border-color: var(--brand); }

  /* A folder reads as a container, not an app: its tile shows the first few icons inside it on
     a lighter ground, the way iPadOS draws one. Without that it is another square and nobody
     can tell at a glance which squares hold things. */
  .hs-fic { background: var(--surface-2); display: grid; grid-template-columns: 1fr 1fr;
            gap: 2px; padding: 3px; align-content: center; }
  .hs-ic-mini { width: 100%; height: 100%; min-width: 0; min-height: 0; border-radius: 3px;
                border: 0; font-size: 6px; background: var(--surface); }
  .hs-fempty { display: block; width: 100%; height: 100%; grid-column: 1 / -1;
               border: 1px dashed var(--line); border-radius: 4px; }
  /* var(--ink) was fine when the folder sat on the console's own light surface; on the
     wallpaper it is dark-on-dark in the light theme. Fixed white instead, matching
     .hs-cap's reasoning above. */
  .hs-folder .hs-cap { color: #fff; font-weight: 600; }
  /* Same reasoning as the duplicate ring above -- the halo goes on the unmasked .hs-slot, not
     on the masked, artwork-covered .hs-ic. */
  .hs-folder.is-open .hs-ic { border-color: var(--brand); }
  .hs-folder.is-open { box-shadow: 0 0 0 2px var(--brand-soft); }

  /* The open folder's own strip. Deliberately outside the iPad frame: what is being edited is
     the CONTENTS of one icon, and drawing that inside the page grid would say it is a page. */
  .hs-open { margin-top: 12px; border: 1px solid var(--brand-line); border-radius: var(--r-card);
             background: var(--surface); padding: 11px 12px; }
  .hs-openhead { display: flex; gap: 8px; align-items: center; margin-bottom: 9px; }
  .hs-fname { flex: 1; min-width: 0; font-weight: 650; }
  /* Scoped, not just placed -- `.hs-page` below sets the SAME property with equal specificity,
     and source order alone decided which one drew the folder-editing strip. Scoping under
     `.hs-open` beats it outright regardless of where either rule sits in the file, which is
     the property this exact hazard (a canvas-width rule silently losing to another same-weight
     rule) keeps costing time to rediscover. */
  .hs-open .hs-openrow { grid-template-columns: repeat(6, 1fr); }

/* The grid width follows the device, so it is set from a custom property rather than fixed at
   six. The old rule said repeat(6, 1fr) in two places and would have drawn an iPhone as an
   iPad. */
.hs-page { grid-template-columns: repeat(var(--hs-cols, 6), 1fr); }

/* The Dock is its OWN grid, drawn with a separate custom property rather than falling back to
   --hs-cols above. #zone-dock is a .hs-page too (it reuses the class for its flex/gap rules),
   so without this it silently inherited the PAGE's column count -- CanvasShape.dock_slots was
   plumbed from the server through data-dock-slots and geo.dockSlots and consumed by nothing,
   and it only ever looked wired up because iPad happens to be 6 page columns / 6 Dock slots
   and iPhone happens to be 4 / 4. A device where those two numbers differ would draw a Dock
   the wrong width and nothing here would say why. */
.hs-dock .hs-page { grid-template-columns: repeat(var(--hs-dock-cols, 6), 1fr); }

.hs-pad { position: relative; }
.hs-ipad { max-width: 452px; }
.hs-iphone { max-width: 300px; }

/* Wallpaper. Not decoration: on a flat surface every icon with a light ground disappears into
   the page, and the whole point of the canvas is judging how the grid reads. */
.hs-wall { position: absolute; inset: 0; border-radius: 22px; pointer-events: none;
           background: linear-gradient(150deg, #1b2a4a, #3b2350 55%, #12233d);
           opacity: .9; }
.hs-screen, .hs-dock { position: relative; }

/* The squircle -- iOS's continuous-curve rounded square, not a plain border-radius. A
   border-radius rounds each corner on a circular arc, which reads visibly boxier at 44px
   beside a real icon. The mask is the superellipse; the border-radius under it is the
   fallback for a browser without mask-image, and is deliberately close rather than identical. */
:root {
  --squircle: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cpath d='M50 0c34 0 50 16 50 50s-16 50-50 50S0 84 0 50 16 0 50 0Z' fill='%23000'/%3E%3C/svg%3E");
}
/* NEVER put a box-shadow on .hs-ic (or any selector ending in .hs-ic). The mask below clips
   this element to the squircle, and an outer box-shadow paints OUTSIDE the box being masked,
   so the mask erases it before it reaches the screen; an inset one paints under the loaded
   artwork instead. Every ring in this file (.dup, .is-open, .hs-target) puts its box-shadow
   on the unmasked .hs-slot/.hs-folder parent instead, for that reason. This has been broken
   and re-fixed four times -- if you are about to add a fifth ring, put the shadow up there. */
.hs-ic { border-radius: 22.5%; overflow: hidden;
         -webkit-mask-image: var(--squircle); mask-image: var(--squircle);
         -webkit-mask-size: 100% 100%; mask-size: 100% 100%; }

.hs-pickic { position: relative; }
.hs-initials { position: absolute; inset: 0; display: grid; place-items: center; }
.hs-docklimit { font-size: 10.5px; color: var(--warn); font-weight: 700; margin: 6px 2px 0; }

/* The dragged icon itself, following the pointer. Fixed rather than absolute so it is not
   trapped by the canvas's own overflow -- the same constraint that stops tooltips escaping a
   scrolling panel elsewhere in this console. */
.hs-ghost { position: fixed; z-index: 9999; pointer-events: none; opacity: .92;
            transform: scale(1.08); filter: drop-shadow(0 6px 14px rgb(0 0 0 / .35)); }

/* The gap the icons part to make. A real grid cell, so the row genuinely reflows around it
   rather than the drop point being implied by a tint on a neighbour. */
.hs-gap { border-radius: 8px; min-height: 53px;
          border: 2px dashed var(--brand); background: var(--brand-soft); }

/* A folder the pointer is squarely over. Same reasoning as the duplicate ring and the
   is-open ring above: the masked, artwork-covered .hs-ic cannot show an outer box-shadow, so
   the ring goes on the unmasked .hs-slot (.hs-folder) instead. This is the fourth time this
   exact mistake has been made against this mask -- see the comment on .hs-ic's mask-image
   rule below. */
.hs-folder.hs-target .hs-ic { border-color: var(--brand); }
.hs-folder.hs-target { box-shadow: 0 0 0 3px var(--brand-soft); }

/* Text selection during a drag turns the whole page blue on the first sideways move. The
   cursor declaration here does nothing on its own: .hs-slot sets its OWN cursor (grab)
   directly on the tile, and a rule on body only supplies the cursor an element would
   otherwise INHERIT -- it never overrides a value the element declares itself, no matter how
   specific the body selector is. The explicit descendant rule below is what actually reaches
   the tiles. */
body.hs-dragging { user-select: none; -webkit-user-select: none; cursor: grabbing; }
body.hs-dragging .hs-slot { cursor: grabbing; }
