/* Copyright MMQR Development */
/* Nocturne — design-system tokens and component classes. This file is the source of truth for the system's look; retune it here and see readme.md. */
/* Inter, served from this application rather than from fonts.googleapis.com.
   A webfont fetched from a third party tells that third party the address and
   user agent of everybody who opens the page, on every load, before they have
   even signed in -- and makes somebody else's CDN an availability dependency
   for a typeface. Both are avoidable by shipping the file.

   Two files, not eight. Inter is a VARIABLE font: Google serves one woff2 per
   unicode subset covering the whole 100-900 weight axis, so asking for four
   weights returns the same file four times. `font-weight: 100 900` below is
   what tells the browser this face can be any of them.

   Two subsets, not seven. latin and latin-ext cover this console's own text
   and European names; Cyrillic, Greek and Vietnamese are left out at 85 KB
   each. A character outside these ranges falls back to system-ui for that
   character alone, which is what unicode-range is for.

   To refresh: see "Webfonts" in CONVERSION.md. Do not re-add the remote
   import -- the content security policy forbids it anyway. */
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("/static/fonts/inter-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191,
    U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("/static/fonts/inter-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
    U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
    U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
    U+A720-A7FF;
}

:root {
  /* This design is dark and only dark: there is not one prefers-color-scheme
     rule in this file, and every surface below is a fixed colour. Saying so
     is not decoration. Undeclared, the used scheme is `normal`, which means
     LIGHT -- so the browser drew every part of the page this file does not
     paint from its light palette: the scrollbars, the caret, the number
     spinners, the date picker, and, the one that was reported, the overlay
     Chrome paints on an autofilled field.

     That overlay outranks author CSS, so a saved login turned the username
     box into black-on-white on a machine with a credential stored and left
     it alone on one without. Same stylesheet, same browser, two different
     pages, and nothing to see in devtools because no rule of ours had
     changed. */
  color-scheme: dark;

  --color-bg: #161826;
  --color-surface: #232532;
  --color-text: #e9e9ed;
  --color-accent: #9184d9; /* review round 7 (Barron): the accent moves to
     the product's blurple — OKLCH hue 289.2, the hue of the app's own Pro
     accent (#9a8fe0, --om-accent-pro) — at the same lightness the steel
     accent held (L 0.660, so the documented ratios survive) and chroma
     in the product blurple's own range (its token sits at C 0.118; the
     accent takes 0.125, from the old steel's 0.030):
     the accent now reads as an accent rather than another neutral. */
  --color-accent-2: #a7a1db; /* the machine-derived stand-in follows: same
     hue, same L 0.734, chroma at the same 2:3 proportion it held */
  --color-divider: color-mix(in srgb, #e9e9ed 16%, transparent);

  /* Wrong, as opposed to merely emphasised.
   *
   * The invalid state used to borrow --color-accent-300, a pale lavender two
   * steps from the accent itself -- so a field the form had rejected looked
   * almost exactly like a field that had the cursor in it. Red is the one
   * colour nothing else here uses, which is the whole reason to spend it on
   * this. Muted rather than pillar-box, to sit on #161826 without glowing. */
  --color-danger: #f2706d;

  /* The thermometer's three states, on the same muted footing as the danger
     red above so none of them glows on #161826. Green and amber are spent
     here and nowhere else, which is what makes a red bar findable in a long
     table without reading a single number. */
  --color-ok: #5fbf8a;
  --color-warn: #e0b350;

  /* Tonal ramps — generated in OKLCH on one shared lightness scale, so the
     same step of any role matches the others in visual value. */
  --color-neutral-100: #f3f5fe;
  --color-neutral-200: #e4e7f5;
  --color-neutral-300: #cfd3e5;
  --color-neutral-400: #b2b6ca;
  --color-neutral-500: #9397ab;
  --color-neutral-600: #75798c;
  --color-neutral-700: #595d6c;
  --color-neutral-800: #3f424d;
  --color-neutral-900: #292b31;

  --color-accent-100: #f5f4ff;
  --color-accent-200: #e7e5fe;
  --color-accent-300: #d2cefd;
  --color-accent-400: #b5abfc;
  --color-accent-500: #968ae0;
  --color-accent-600: #796cbf;
  --color-accent-700: #5d5294;
  --color-accent-800: #423a6a;
  --color-accent-900: #2b2741;

  --color-accent-2-100: #f5f4ff;
  --color-accent-2-200: #e7e5fe;
  --color-accent-2-300: #d2cefd;
  --color-accent-2-400: #b5afe8;
  --color-accent-2-500: #9690c9;
  --color-accent-2-600: #7972a9;
  --color-accent-2-700: #5c5783;
  --color-accent-2-800: #423e5d;
  --color-accent-2-900: #2b293a;

  /* Deck section-divider ground — review round 3: saturation as presence.
     Dividers take a saturated deep indigo (hue 277, the neutral ramp's own
     family; C 0.094 — written when the accent held C 0.030, before the
     round-7 blurple raised it past the ground's) with a lighter bloom and
     a ghost tint of the same hue, while the slide grounds stay
     desaturated. Deck-scale fills only — not interface colors. */
  --color-section: #262a60;
  --color-section-glow: #353b80;
  --color-section-ghost: #4c5397;

  --font-heading: "Inter", system-ui, sans-serif;
  --font-heading-weight: 500;
  --font-body: "Inter", system-ui, sans-serif;

  --space-1: 2.8px;
  --space-2: 5.6px;
  --space-3: 8.4px;
  --space-4: 11.2px;
  /* 5 was missing from this scale and two rules asked for it anyway: the
     Overview's section gap and the sign-in page's demo note. A var() that
     names nothing makes the whole declaration invalid at computed-value time,
     so `gap` fell back to `normal` -- zero for a flex container -- and
     `margin-top` to 0. Both looked like a designer's choice and neither was
     one. 2.8 x 5, like every other step. */
  --space-5: 14px;
  --space-6: 16.8px;
  --space-8: 22.4px;

  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 14px;

  /* Elevation — derived from the ground: soft ink-tinted shadows on a
     light theme, a hairline edge + ambient darkness on a dark one. */
  --shadow-sm: 0 0 0 1px #3f424d;
  --shadow-md: 0 0 0 1px #595d6c, 0 6px 18px rgba(0,0,0,0.55);
  --shadow-lg: 0 0 0 1px #9397ab, 0 16px 40px rgba(0,0,0,0.65);
}

body {
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-body);
}
h1, h2, h3, h4 { font-family: var(--font-heading); font-weight: var(--font-heading-weight); }

.lighten{mix-blend-mode:lighten;background-color:transparent}

/* ══════════════════════════════════════════════════════════════════════════
   Components — built with the tokens above. Plain CSS
   on plain HTML: no JavaScript, no build step. Each class is documented in
   readme.md and demonstrated in foundations/ and components/.
   ══════════════════════════════════════════════════════════════════════ */

*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; font-size: 15px; line-height: 1.55; font-weight: 400; }

/* The `hidden` attribute has to WIN.
 *
 * The browser hides it with `[hidden] { display: none }`, which is one
 * attribute selector and loses to any class rule that sets `display` -- and
 * a panel or a menu is exactly the kind of thing that has a class setting
 * `display: flex`. The element then carries the attribute, reads as hidden
 * from script, and is painted anyway.
 *
 * That is not a subtle failure, but it is an INVISIBLE one from JavaScript:
 * `el.hidden` is true and `el.hidden = true` appears to work, so the bug
 * survives being tested with the property. The fragment tooltip on the modem
 * list shipped with every panel on the page open at once because of it.
 */
[hidden] { display: none !important; }
h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2);
}
h1 { font-size: 42px; }
h2 { font-size: 32px; }
h3 { font-size: 25px; }
h4 { font-size: 20px; }
h5 { font-size: 16px; }
h6 { font-size: 13px; }
h6 { letter-spacing: 0.08em; text-transform: uppercase; }
p { margin: 0 0 var(--space-3); }
a { color: var(--color-accent); text-underline-offset: 3px; }
img { display: block; max-width: 100%; }
figure { margin: 0; }
figcaption {
  font-size: 11px; margin-top: var(--space-1);
  color: color-mix(in srgb, var(--color-text) 55%, transparent);
}
.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }
:focus { outline: none; }
:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }

/* — rules — */
/* Rules fade to transparent at both ends instead of stopping cleanly — a
   Nocturne signature. The ramp is 48px an end (one deck baseline unit),
   painted from the same divider token. Freestanding rules fade; box
   outlines, in-control separators, and short accent marks stay solid. */
.hr {
  height: 1px; border: 0; margin: var(--space-4) 0;
  background: linear-gradient(to right,
    transparent, var(--color-divider) 48px,
    var(--color-divider) calc(100% - 48px), transparent);
}

/* — buttons — */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  cursor: pointer; text-decoration: none;
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 14px; line-height: 1.2; color: var(--color-text); /* matches the .input's 14px —
     the pair sits side by side in sign-up rows */
  background: transparent; border: 1px solid transparent;
  padding: var(--space-2) calc(var(--space-3) * 1.2);
  border-radius: var(--radius-md);
}
.btn svg { display: block; }
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
.btn-primary { color: var(--color-accent); border-color: var(--color-accent); }
.btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }
.btn-primary:active { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }
.btn-secondary { border-color: var(--color-divider); }
.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
.btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }
.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); }
.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }
.btn-ghost:active { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }
.btn-icon { width: 36px; height: 36px; padding: 0; }
.btn-block { width: 100%; margin-top: var(--space-2); }

/* — forms — */
.field > label {
  display: block; font-size: 12px; margin-bottom: 5px;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}
.input {
  width: 100%; min-height: 36px; padding: 6px 10px; font: inherit;
  font-size: 14px; color: var(--color-text); caret-color: var(--color-accent);
  background: var(--color-surface);
  border: 1px solid var(--color-divider); border-radius: var(--radius-md);
}
.input:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }
.input:focus-visible { border-color: var(--color-accent); outline-offset: 0; }

/* An autofilled field, drawn in the design's colours rather than the
   browser's.
 *
 * `color-scheme: dark` above is what stops this being white, and would be
 * enough on its own. This pins the exact colours anyway, because "enough"
 * here means Chrome's own dark autofill grey, which is not --color-surface
 * and sits next to a field that is.
 *
 * The background goes on as an inset shadow, not `background`: Chrome paints
 * the autofill fill in the UA layer, where a background declaration of ours
 * loses even with !important. A shadow spread wide enough to cover the
 * padding box is the one thing that reaches over it. Same for the text --
 * `color` loses, `-webkit-text-fill-color` wins.
 *
 * The bad-value ring below is a box-shadow too, and it is more specific, so
 * a field that is autofilled AND wrong keeps the ring and falls back to the
 * browser's dark fill. That is the right way round: the error has to show. */
.input:-webkit-autofill,
.input:-webkit-autofill:hover,
.input:-webkit-autofill:focus {
  box-shadow: inset 0 0 0 100px var(--color-surface);
  -webkit-text-fill-color: var(--color-text);
  caret-color: var(--color-accent);
}
/* The same again under the standard name, as its own rule rather than three
   more selectors on the one above. A selector list is thrown away WHOLE if
   any selector in it is unknown, so pairing the standard name with the
   -webkit- one would lose both in every browser that knows only one of
   them -- which is every browser, in one direction or the other. */
.input:autofill,
.input:autofill:hover,
.input:autofill:focus {
  box-shadow: inset 0 0 0 100px var(--color-surface);
  -webkit-text-fill-color: var(--color-text);
  caret-color: var(--color-accent);
}
textarea.input { min-height: 90px; resize: vertical; }

/* Show/hide password.
 *
 * password-toggle.js builds the wrapper, the button and the two icons at
 * runtime; nothing in any template mentions them, which is why these rules
 * were missed when that script was copied into this app. Without them the
 * button falls back to a browser default -- a white block sitting after the
 * field rather than inside it -- and both icons render at once, because the
 * only thing that hides one is the aria-pressed rule below.
 *
 * The padding on the input is what keeps a long password from running under
 * the button. */
.pw-wrap { position: relative; display: block; }
.pw-wrap > .input { padding-right: 38px; }
.pw-toggle {
  position: absolute; top: 0; right: 0; height: 100%; width: 34px;
  display: flex; align-items: center; justify-content: center;
  padding: 0; background: none; border: 0; cursor: pointer;
  color: color-mix(in srgb, var(--color-text) 55%, transparent);
}
.pw-toggle:hover { color: var(--color-text); }
.pw-toggle:focus-visible { outline: 2px solid var(--color-accent); outline-offset: -2px; }
/* An SVG with a viewBox and no size is laid out at 300x150. That is the white
 * block, not the button. */
.pw-toggle svg { width: 17px; height: 17px; display: block; }

.pw-toggle .pw-icon-hide { display: none; }
.pw-toggle[aria-pressed="true"] .pw-icon-show { display: none; }
.pw-toggle[aria-pressed="true"] .pw-icon-hide { display: block; }

/* .pw-masked -- a field that reads like a password field and is not one.
 *
 * On the Operators page two forms set SOMEBODY ELSE's password: the new
 * account form and the reset beside each row. A browser's password manager
 * offers to remember whatever is typed into an <input type="password"> when
 * the form is submitted, so an admin who resets six people's passwords in a
 * morning is asked six times whether to save a credential that is not theirs
 * -- and one absent-minded yes puts a colleague's password in their browser.
 *
 * There is no attribute that turns that prompt off. Chrome says so plainly,
 * and autocomplete="new-password" -- which those fields already carried --
 * only suppresses FILLING. What a password manager actually keys on is the
 * input's type, so these two fields are type="text" and are masked here
 * instead. The operator sees the same row of dots either way.
 *
 * The field the operator types their OWN password into, below on the same
 * page, is still a real password field. Chrome offering to save that one is
 * correct and useful, and it is the difference this rule exists to draw.
 *
 * The @supports block is the whole safety argument. -webkit-text-security is
 * unprefixed nowhere and Firefox only took it in version 132, so a browser
 * without it would otherwise render a password in plain sight. Making the
 * text transparent is not as good as dots -- there is nothing to count -- but
 * it is never readable, and the caret colour keeps the field usable. */
.pw-masked { -webkit-text-security: disc; }
@supports not (-webkit-text-security: disc) {
  .pw-masked { color: transparent; text-shadow: none; caret-color: var(--color-text); }
  .pw-masked::selection { color: transparent; }
}
/* The confirmation after a picture is pasted into the model editor's file
 * field. Built at runtime by image-paste.js -- no template mentions these, so
 * without these rules the thumbnail lays out at its natural size, which for a
 * phone photograph is most of the screen. */
.paste-note {
  display: flex; align-items: center; gap: 10px;
  margin-top: var(--space-2); font-size: 12px;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}
.paste-thumb {
  width: 44px; height: 44px; object-fit: cover; flex: none;
  border-radius: var(--radius-sm); border: 1px solid var(--color-divider);
  background: var(--color-surface);
}

.radio { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; }
.radio input, .seg-opt input {
  position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;
}
.radio .dot {
  width: 16px; height: 16px; flex: none; border-radius: 50%;
  border: 1.5px solid var(--color-divider);
}
.radio:hover .dot { border-color: var(--color-accent); }
.radio input:checked + .dot {
  border-color: var(--color-accent); background: var(--color-accent);
  box-shadow: inset 0 0 0 4px var(--color-bg);
}
.radio input:focus-visible + .dot { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.seg {
  display: inline-flex; overflow: hidden;
  border: 1px solid var(--color-divider); border-radius: var(--radius-md);
}
.seg-opt {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 12px; font-size: 13px; cursor: pointer;
}
.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }
.seg-opt:has(input:checked) { color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); }
.seg-opt:not(:has(input:checked)):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }

/* — cards — */
.card {
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-3); border-radius: var(--radius-md); background: var(--color-surface);
}
.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); }
.card-title {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 17px; line-height: 1.2;
}
.card-body { margin: 0; font-size: 13px; opacity: 0.8; flex: 1; }
.card-meta {
  display: flex; align-items: center; gap: 6px; font-size: 11px;
  color: color-mix(in srgb, var(--color-text) 50%, transparent);
}
.elev-sm { box-shadow: var(--shadow-sm); }
.elev-md { box-shadow: var(--shadow-md); }
.elev-lg { box-shadow: var(--shadow-lg); }

/* — tags — */
.tag {
  display: inline-flex; align-items: center; font-size: 11px;
  letter-spacing: 0.02em; padding: 3px 10px;
  border-radius: calc(var(--radius-md) * 0.75);
}
.tag-accent { background: var(--color-accent-800); color: var(--color-accent-100); }
.tag-accent-2 { background: var(--color-accent-2-800); color: var(--color-accent-2-100); }
.tag-neutral { background: var(--color-neutral-800); color: var(--color-neutral-100); }
.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }
/* "Compiled", and nothing else on this console is green. A neutral tag would
   read the same as "not compiled yet", which is the state it has to be told
   apart from at a glance. */
.tag-ok { background: color-mix(in srgb, var(--color-ok) 22%, transparent); color: var(--color-ok); }
/* "Deleted", on the change journal. The counterpart to .tag-ok and mixed the
   same way, because the two are read side by side in one column and a
   difference in weight would read as a difference in importance. */
.tag-bad { background: color-mix(in srgb, var(--color-danger) 22%, transparent); color: var(--color-danger); }

/* — navigation — */
.nav {
  display: flex; align-items: center; gap: var(--space-4);
  padding: var(--space-3) var(--space-4);
  border-bottom: none;
}
.nav-brand {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 18px; margin-right: auto;
}
.nav a { color: inherit; text-decoration: none; font-size: 14px; }
.nav a:hover, .nav a[aria-current='page'] { color: var(--color-accent); }

/* — tables — */
.table { width: 100%; border-collapse: collapse; font-size: 14px; }
.table th {
  text-align: left; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;
  color: color-mix(in srgb, var(--color-text) 60%, transparent);
  padding: var(--space-2); border-bottom: 1px solid transparent; /* keeps layout; the row paints the fading rule */
}
.table td {
  padding: var(--space-2);
  border-bottom: 1px solid transparent;
}
/* Row rules as row-level strips so the end-fade spans the row, not each cell. */
/* The rule under the head is painted by the LAST header row, not by every
   one of them.
 *
 * A grouped header is two rows with rowspan cells reaching through both, so a
 * rule at the first row's bottom edge lands halfway down those cells and
 * strikes through them. It showed up the moment the pool health table grew an
 * "Old leases" group above three sub-columns; the pools page had it quietly
 * all along. */
.table thead tr:last-child {
  background: linear-gradient(to right,
    transparent, var(--color-divider) 48px,
    var(--color-divider) calc(100% - 48px), transparent) no-repeat bottom / 100% 1px;
}
.table tbody tr {
  background: linear-gradient(to right,
    transparent, color-mix(in srgb, var(--color-text) 8%, transparent) 48px,
    color-mix(in srgb, var(--color-text) 8%, transparent) calc(100% - 48px), transparent) no-repeat bottom / 100% 1px;
}
.table tbody tr:hover {
  /* the hover tint layers over the row rule, which keeps painting */
  background:
    linear-gradient(color-mix(in srgb, var(--color-text) 4%, transparent),
                    color-mix(in srgb, var(--color-text) 4%, transparent)) no-repeat 0 0 / 100% 100%,
    linear-gradient(to right,
      transparent, color-mix(in srgb, var(--color-text) 8%, transparent) 48px,
      color-mix(in srgb, var(--color-text) 8%, transparent) calc(100% - 48px), transparent) no-repeat bottom / 100% 1px;
}

/* — dialog — */
.dialog-backdrop {
  position: fixed; inset: 0; display: grid; place-items: center;
  padding: var(--space-4);
  background: color-mix(in srgb, var(--color-neutral-900) 50%, transparent);
}
.dialog {
  width: min(440px, 100%); display: flex; flex-direction: column; gap: var(--space-3);
  padding: var(--space-4); border-radius: var(--radius-lg);
  background: var(--color-surface); box-shadow: var(--shadow-lg);
}
.dialog-title {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 20px;
}
.dialog-body { font-size: 14px; opacity: 0.85; }
.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-2); }


/* ===========================================================================
   docsis-admin application classes.

   Everything below replaces an interpolated style= attribute from the design
   export. See static/portal.comments for why that matters; the short version
   is that html/template CSS-sanitises a style attribute and silently writes
   the literal string ZgotmplZ for anything it cannot prove safe -- and
   var(--x) and color-mix() are both function calls it will not pass.
   =========================================================================== */

/* --- app frame ---------------------------------------------------------- */
.app-grid { height:100vh; display:grid; grid-template-rows:auto 1fr;
  grid-template-columns:196px 1fr; background:var(--color-bg); }
.app-header { grid-column:1 / -1; display:flex; align-items:center;
  gap:var(--space-4); padding:var(--space-2) var(--space-4);
  border-bottom:1px solid var(--color-divider); }
.app-nav { display:flex; flex-direction:column; gap:2px;
  padding:var(--space-4) var(--space-3) var(--space-4) 0;
  border-right:1px solid var(--color-divider); }
.app-main { overflow:auto; padding:var(--space-6) var(--space-8) var(--space-8); }
.app-main:focus { outline:none; }

.brand { display:flex; align-items:center; gap:8px; width:172px; }
.brand-dot { width:7px; height:7px; border-radius:50%;
  background:var(--color-accent); box-shadow:0 0 10px var(--color-accent); }
.brand-name { font-family:var(--font-heading); font-weight:500; font-size:15px; }
.nav-foot { margin-top:auto; padding:0 12px; font-size:11px;
  color:var(--color-neutral-500); line-height:1.5; }

/* --- sidebar nav items  (was navBase + active/inactive) ----------------- */
.nav-item { display:block; width:100%; text-align:left; background:transparent;
  border:0; border-left:2px solid transparent; padding:6px 14px; font:inherit;
  font-size:13px; color:var(--color-neutral-400); cursor:pointer;
  border-radius:0 6px 6px 0; }
.nav-item:hover { color:var(--color-accent-200);
  background:color-mix(in srgb, var(--color-accent) 5%, transparent); }
.nav-item.is-active { color:var(--color-accent);
  border-left-color:var(--color-accent);
  background:color-mix(in srgb, var(--color-accent) 9%, transparent); }

/* --- modem detail tabs  (was tabBase + active/inactive) ----------------- */
.tab { background:transparent; border:0; border-bottom:2px solid transparent;
  margin-bottom:-1px; padding:8px 12px; font:inherit; font-size:13px;
  color:var(--color-neutral-400); cursor:pointer; }
.tab:hover { color:var(--color-accent-200); }
.tab.is-active { color:var(--color-accent); border-bottom-color:var(--color-accent); }

/* --- segmented controls  (was segBase + active/inactive) ---------------- */
.seg-btn { background:transparent; border:0; padding:6px 12px; font:inherit;
  font-size:13px; color:var(--color-neutral-400); cursor:pointer; }
.seg-btn + .seg-btn { border-left:1px solid var(--color-divider); }
.seg-btn:hover { color:var(--color-accent-200); }
.seg-btn.is-active { color:var(--color-accent);
  box-shadow:inset 0 0 0 1px var(--color-accent); }

/* --- fragment picker rows  (was fragBase + active/inactive) ------------- */
.frag-item { display:flex; align-items:center; gap:8px; width:100%;
  background:transparent; border:1px solid transparent; padding:6px 10px;
  font:inherit; font-size:13px; color:var(--color-neutral-300); cursor:pointer;
  border-radius:var(--radius-md); text-align:left; }
.frag-item:hover { border-color:var(--color-divider); }
.frag-item.is-active { background:var(--color-surface);
  border-color:var(--color-divider); color:var(--color-accent-200); }

/* --- pool usage bars ---------------------------------------------------- */
.bar { height:3px; border-radius:2px; background:var(--color-neutral-900);
  overflow:hidden; }
.bar > i { display:block; height:100%; border-radius:2px;
  background:var(--color-accent-600); }
.bar > i.is-full { background:var(--color-accent); }

/* The pool-health thermometer.
 *
 * Taller than the 3px bar the cards used, because here it is a column of its
 * own that gets scanned down rather than a hairline under a heading.
 *
 * The thresholds are in poolHealth() in views.go, not here: a colour is a
 * conclusion about a number, and the conclusion is the server's. The class
 * names say what they mean rather than naming the colour, so changing the
 * palette does not turn `.is-green` amber. */
.bar-lg { height:8px; border-radius:4px; }
.bar-lg > i { border-radius:4px; }
.bar > i.is-cool { background:var(--color-ok); }
.bar > i.is-warm { background:var(--color-warn); }
.bar > i.is-hot  { background:var(--color-danger); }
/* A range with nothing countable in it. No width, so nothing is painted; the
   rule exists so the class is not a silent no-op nobody can find. */
.bar > i.is-none { background:var(--color-neutral-700); }

/* A sortable heading. The whole cell is the target, which is why the cursor
   and the arrow are on the th and not on a button inside it. */
.table th[data-sort] { cursor:pointer; user-select:none; }
.table th[data-sort]:hover { color:var(--color-text); }
.table th[data-sort]::after { content:"\2195"; opacity:0.25; margin-left:6px; }
.table th[aria-sort="ascending"]::after { content:"\2191"; opacity:1; }
.table th[aria-sort="descending"]::after { content:"\2193"; opacity:1; }

/* --- editable table cells ----------------------------------------------- */
.cell { background:transparent; border-radius:4px;
  border:1px solid transparent; color:inherit; font:inherit; font-size:13px;
  padding:2px 6px; width:100%; }
.cell:hover { border-color:var(--color-divider); }
.cell:focus-visible { border-color:var(--color-accent);
  outline:2px solid var(--color-accent); outline-offset:1px; }
.cell-mono { font-family:ui-monospace,Menlo,monospace; }

/* --- a region a non-admin may read and not change ------------------------ */
/* <fieldset disabled> is the browser's own switch for this, and it is used
   here rather than a class on every control for a reason: it disables every
   descendant, including ones added later, and a disabled control fires no
   change event -- so the hx-post on each cell never runs. A rule that has to
   be repeated on forty inputs is a rule that will be missing from the
   forty-first.

   The fieldset's own defaults are reset away. min-width:0 is not cosmetic:
   fieldset carries a min-content minimum that overrides the flex and grid
   minimums around it, and without it every dialog it wraps stops shrinking. */
.ro-region { border:0; margin:0; padding:0; min-width:0; }

/* Disabled normally means "not yet", so it is dimmed. Here it means "yours to
   read", so it is not: the whole point of the read-only view is that a help
   desk operator can read the value out loud over the phone. Full contrast,
   and the cursor and the borders say it is not a field to type in. */
.ro-region .cell:disabled,
.ro-region .input:disabled,
.ro-region select:disabled,
.ro-region textarea:disabled {
  opacity:1; color:var(--color-text); -webkit-text-fill-color:var(--color-text);
  cursor:default;
}
.ro-region .cell:disabled { border-color:transparent; }
.ro-region .cell:disabled:hover { border-color:transparent; }
.ro-region .input:disabled,
.ro-region select:disabled,
.ro-region textarea:disabled {
  background:color-mix(in srgb, var(--color-surface) 60%, transparent);
  border-style:dashed;
}

/* Said once at the top of a read-only dialog, so nobody hunts for the Save
   button that is deliberately not there. */
.ro-note { font-size:12px; color:var(--color-neutral-400); margin:0; }

/* --- small helpers repeated across views -------------------------------- */
.mono { font-family:ui-monospace,Menlo,monospace; }
.mono-12 { font-family:ui-monospace,Menlo,monospace; font-size:12px; }
.mono-13 { font-family:ui-monospace,Menlo,monospace; font-size:13px; }

/* --- the fragment tooltip on the modem list --------------------------- */

/* The value that has something more to say on a hover, not the cell around
   it: a cell is mostly empty space beside its value. `position:relative` so
   the panel below is placed against the value and not against the page. */
.has-tip { position: relative; }

/* Hidden until a script unhides it, and hidden with the `hidden` attribute
   rather than a class, so it stays hidden for a reader with no JavaScript
   instead of appearing over the table. */
.tip {
  position: absolute; z-index: 40; left: 0; top: calc(100% + 6px);
  /* Not a hover target. The panel sits directly under the value it belongs
     to, so without this a pointer drifting downwards would enter the panel
     and the thing would never close. Read-only, so there is nothing in it to
     click. */
  pointer-events: none;
  min-width: 300px; max-width: 440px; padding: var(--space-3);
  display: flex; flex-direction: column; gap: 3px;
  background: var(--color-surface); border: 1px solid var(--color-divider);
  border-radius: var(--radius-md); box-shadow: var(--shadow-lg);
  font-family: var(--font-body); font-size: 12.5px; white-space: normal;
}
.tip-head {
  font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase;
  color: color-mix(in srgb, var(--color-text) 55%, transparent);
  margin-bottom: 4px;
}
.tip-row { display: flex; gap: 8px; align-items: baseline; }
.tip-id {
  flex: none; width: 42px; text-align: right;
  font-family: ui-monospace, Menlo, monospace; color: var(--color-accent);
}
/* Wraps rather than truncating. This panel exists to show the names, so a
   name cut off at the edge would defeat the whole thing. */
.tip-name { min-width: 0; }
/* A number no fragment answers to. Shown, not dropped: that row is the one
   somebody is looking for. */
.tip-missing { color: var(--color-danger); font-style: italic; }

/* --- the status dialog ------------------------------------------------- */

/* Said in the dialog, not only in a comment. A plausible number nobody
   labelled is worse than no number at all. */
.status-note {
  padding: 8px 10px; border-radius: var(--radius-md);
  border: 1px solid var(--color-accent-700);
  background: color-mix(in srgb, var(--color-accent) 8%, transparent);
}
.status-group { margin-top: var(--space-4); }

/* Downstream and upstream are two measurements of two different paths, and a
   reader who takes one for the other reads a healthy modem as a broken one. A
   heading alone stopped being enough separation once each side had four
   readings under it. */
.status-boxed {
  border: 1px solid var(--color-divider);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4) var(--space-4);
  background: color-mix(in srgb, var(--color-surface) 45%, transparent);
}
.status-boxed .status-title { margin-top: 0; }
.status-title {
  margin: 0 0 var(--space-2); font-size: 13px; letter-spacing: 0.04em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--color-text) 62%, transparent);
}
.status-facts {
  display: grid; grid-template-columns: 110px 1fr; gap: 4px 12px;
  margin: 0 0 var(--space-3); font-size: 13px;
}
.status-facts dt { color: color-mix(in srgb, var(--color-text) 62%, transparent); }
.status-facts dd { margin: 0; }

.reading { margin-bottom: var(--space-3); }
.reading-top { display: flex; align-items: baseline; gap: 8px; font-size: 13px; }
.reading-label { flex: 1; }
.reading-value { font-family: ui-monospace, Menlo, monospace; font-weight: 600; }
.reading-ok   { color: var(--color-ok); }
.reading-warn { color: var(--color-warn); }
.reading-bad  { color: var(--color-danger); }
.reading-foot { display: flex; gap: 8px; margin-top: 3px; }
.reading-note { flex: 1; text-align: center; }

/* The scale, and the mark on it.

   The bands are the numbers a DOCSIS technician works to, so the colour under
   the mark is a judgement rather than a decoration. The mark is a triangle
   pointing down at the band it landed in — the shape in the request,
   _____\/__ — drawn with a border trick so it needs no image and no glyph. */
.gauge { position: relative; margin-top: 5px; padding-top: 14px; }
.gauge-track {
  display: flex; height: 6px; border-radius: 3px; overflow: hidden;
  background: var(--color-divider);
}
.gauge-band { display: block; height: 100%; }
.gauge-ok   { background: var(--color-ok); }
.gauge-warn { background: var(--color-warn); }
.gauge-bad  { background: var(--color-danger); }
/* The marker, twice the size it was: at 10px across it was a speck on a
   200px scale, and the whole point of it is to be findable at a glance. The
   padding above the track is what makes room for it, so the two move
   together. */
.gauge-mark {
  position: absolute; top: 0; width: 0; height: 0; margin-left: -10px;
  border-left: 10px solid transparent; border-right: 10px solid transparent;
  border-top: 14px solid var(--color-text);
}
.gauge-mark-ok   { border-top-color: var(--color-ok); }
.gauge-mark-warn { border-top-color: var(--color-warn); }
.gauge-mark-bad  { border-top-color: var(--color-danger); }

/* --- a column of counts under a heading that spans several of them --- */

/* Centred, not right-aligned. Right is where figures belong when they are read
   against each other DOWN the column; these are read across the row -- Offers
   against Acks for one interface -- and centring puts each number under the
   heading that names it. Wide enough for "15 min", which is what the heading
   says now that "15m" turned out to read as a quantity of something. */
/* "15 min" is one thing and must read as one. A plain `width` is a
   suggestion to a table layout, not an instruction: at 1000px the browser
   squeezed these to 55px and broke the heading over three lines. min-width is
   the floor, and nowrap is what stops the break. */
.num-col {
  min-width: 66px;
  white-space: nowrap;
  text-align: center;
  font-weight: 400;
}

/* The HEADING has to be centred too, and it was not.
 *
 * `.table th` sets text-align:left and is one class plus one element, which
 * outweighs a lone class -- so .num-col centred the numbers and left the "5
 * min" above them sitting hard left. Every count on the DHCP/TFTP table was
 * a few pixels adrift of the heading that names it, which is the whole thing
 * .num-col exists to line up. */
.table th.num-col { text-align: center; }

/* .center-col centres a heading and its cells together, for a column that is
 * read ACROSS the row rather than down it -- the same argument as .num-col,
 * for columns that are not counts and do not want its width floor. */
.table th.center-col, .table td.center-col { text-align: center; }

/* A sortable heading carries an arrow in ::after, and an arrow only on the
 * right shifts the words left of the column's true centre -- so a centred
 * heading is not quite over its numbers, and a right-aligned one is pushed
 * clear of them entirely. This is the same arrow on the other side, holding
 * the same width and painting nothing.
 *
 * visibility:hidden rather than opacity:0: both keep the space, and only
 * this one keeps the glyph out of the accessibility tree, where it would
 * otherwise be read out before every heading. */
.table th.num-col[data-sort]::before,
.table th.center-col[data-sort]::before {
  content: "\2195"; visibility: hidden; margin-right: 6px;
}

/* Closes a group of columns. Twelve counts across a row are four things, and
   without a rule between them they are twelve. */
.col-end { border-right: 1px solid var(--color-divider); }

/* Except on the last column, where the same rule would be drawing a border
   down the right-hand edge of the table rather than dividing anything.
   `:last-child` rather than a column number, so a column added later cannot
   leave the edge ruled and the new boundary bare. */
.table tr > :last-child.col-end { border-right: none; }
.f12 { font-size:12px; }
.f13 { font-size:13px; }
.push { margin-left:auto; }
.row { display:flex; align-items:center; gap:8px; }
.col-4 { display:flex; flex-direction:column; gap:var(--space-4); }
.col-6 { display:flex; flex-direction:column; gap:var(--space-6); }
.field-label { color:var(--color-neutral-500); margin:0 0 var(--space-2);
  font-size:13px; }
.skip-link { position:absolute; left:-9999px; top:0; z-index:10;
  background:var(--color-surface); color:var(--color-text); padding:10px 16px;
  border-radius:var(--radius-md); border:1px solid var(--color-accent);
  text-decoration:none; }
.skip-link:focus { left:8px; top:8px; }

/* --- the login page ----------------------------------------------------- */
.login-page { min-height:100vh; display:grid; place-items:center;
  background:radial-gradient(ellipse 60% 50% at 50% 0%,
    color-mix(in srgb, var(--color-accent) 9%, transparent), transparent 70%),
    var(--color-bg); }
.login-form { width:340px; display:flex; flex-direction:column;
  gap:var(--space-4); }
/* The banner htmx-errors.js raises when the server refuses a write.
 *
 * Fixed rather than in the flow: every screen here is swapped into #app-body
 * by htmx, and a message living inside the thing being replaced is a message
 * that disappears with it. */
.app-alert {
  position:fixed; z-index:60; left:50%; transform:translateX(-50%);
  bottom:var(--space-4); max-width:min(680px, calc(100vw - 2 * var(--space-4)));
  display:flex; align-items:flex-start; gap:var(--space-3);
  padding:10px 12px; font-size:13px; line-height:1.4;
  color:var(--color-text); background:var(--color-surface);
  border:1px solid var(--color-danger); border-radius:var(--radius-md);
  box-shadow:var(--shadow-lg);
}
.app-alert-text { flex:1; }
.app-alert-close {
  flex:none; padding:0 4px; font-size:16px; line-height:1; cursor:pointer;
  background:none; border:0; color:color-mix(in srgb, var(--color-text) 55%, transparent);
}
.app-alert-close:hover { color:var(--color-text); }
.app-alert-close:focus-visible { outline:2px solid var(--color-accent); outline-offset:1px; }

/* --- a panel beside a dialog -------------------------------------------- */
/* The fragment list on the modem editor. Outside the .dlg so it takes no width
   from the form, and gone below the width where there is nothing to sit beside.
   Its own scroll: a plant with two hundred fragments must not make the dialog
   taller than the window. */
.dlg-aside {
  width: 260px; flex: none;
  max-height: min(70vh, 620px); overflow-y: auto;
  padding: var(--space-3);
  background: var(--color-surface); border: 1px solid var(--color-divider);
  border-radius: var(--radius-md); box-shadow: var(--shadow-lg);
}
@media (max-width: 1100px) { .dlg-aside { display: none; } }

.frag-pick {
  display: flex; gap: 8px; align-items: baseline;
  padding: 3px 6px; border-radius: 4px; font-size: 12.5px;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}
.frag-pick-id { flex: none; width: 38px; text-align: right; opacity: 0.7; }
.frag-pick-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Named in the box. Slight, as asked: enough to find while typing, not enough
   to turn the list into a second thing competing for attention. */
.frag-pick.is-picked {
  background: color-mix(in srgb, var(--color-accent) 16%, transparent);
  color: var(--color-text);
}
.frag-pick.is-picked .frag-pick-id { opacity: 1; color: var(--color-accent); }

/* --- the retire dialog's day buttons ------------------------------------ */
/* A pressed one, marked by the attribute that already says so to a screen
   reader rather than by a class kept beside it. Two things saying the same
   thing is two things that can disagree, and the attribute is the one that
   has to be right. */
.rt-pick[aria-pressed="true"] {
  border-color: var(--color-accent);
  color: var(--color-accent);
  background: color-mix(in srgb, var(--color-accent) 14%, transparent);
}

/* --- fragment groups ---------------------------------------------------- */
/* A collapsible group heading. The whole row is the target, and the arrow is
   a character rather than an icon so it inherits the text's colour and size
   without a second thing to keep in step. */
.frag-group { display:flex; align-items:center; gap:6px; }
.frag-group-head {
  display:flex; align-items:center; gap:6px; flex:1; min-width:0;
  padding:4px 2px; font-size:13px; color:var(--color-text);
  text-decoration:none; border-radius:var(--radius-md);
}
.frag-group-head:hover { background:color-mix(in srgb, var(--color-text) 7%, transparent); }
.frag-group-head:focus-visible { outline:2px solid var(--color-accent); outline-offset:-2px; }
.frag-arrow { flex:none; width:12px; opacity:0.6; }
.frag-group-name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

/* The explanation above the examples. Pre-wrapped because the longer entries
   line field names up against their ranges, and reflowing that as a paragraph
   turns a small table into a wall of text. Not monospace: it is prose with
   some columns in it, and the columns are what the wrapping has to keep. */
.frag-help-text {
  margin:var(--space-3) 0 0; max-width:82ch;
  font-size:13px; line-height:1.55; white-space:pre-wrap;
  color:color-mix(in srgb, var(--color-text) 78%, transparent);
}

/* A worked example under the fragment box. Selectable and wrapped rather than
   scrolled: it is there to be copied into the box above it. */
.frag-example {
  margin:0; padding:var(--space-3);
  background:var(--color-surface); border:1px solid var(--color-divider);
  border-radius:var(--radius-md);
  font-family:ui-monospace,Menlo,monospace; font-size:12.5px; line-height:1.5;
  color:var(--color-neutral-300); white-space:pre-wrap;
}

.login-error { font-size:13px; color:var(--color-danger); }

/* --- the header search --------------------------------------------------- */
.hs { position:relative; display:flex; align-items:center; gap:8px;
  flex:0 1 420px; min-width:220px; }
.hs-input { width:100%; min-height:30px; padding-top:3px; padding-bottom:3px;
  font-size:13px; }
/* The red state. A 1px border alone is easy to miss at 13px, so the ring
   carries it and the border reinforces. */
.hs-input.hs-invalid { border-color:var(--color-danger);
  box-shadow:0 0 0 2px color-mix(in srgb, var(--color-danger) 35%, transparent); }
.hs-help { position:absolute; left:-9999px; }

.hs-results { position:absolute; top:calc(100% + 6px); left:0; right:0;
  z-index:40; display:none; max-height:60vh; overflow:auto;
  background:var(--color-surface); border:1px solid var(--color-divider);
  border-radius:var(--radius-md); box-shadow:var(--shadow-lg); }
.hs-results.is-open { display:block; }
.hs-list { list-style:none; margin:0; padding:4px; }
.hs-hit { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr) auto;
  gap:2px 10px; padding:7px 10px; border-radius:6px; text-decoration:none;
  color:var(--color-text); }
.hs-hit:hover, .hs-hit:focus-visible { background:color-mix(in srgb, var(--color-accent) 12%, transparent);
  color:var(--color-text); }
.hs-addr { font-size:13px; }
.hs-mac  { font-size:12px; color:var(--color-neutral-400); }
.hs-meta { grid-column:1 / 3; font-size:11px; color:var(--color-neutral-500); }
.hs-srv  { grid-row:1 / 3; align-self:center; font-size:11px;
  color:var(--color-neutral-400); white-space:nowrap; }
.hs-note { padding:9px 12px; font-size:12px; color:var(--color-neutral-400); }
.hs-note.hs-bad { color:var(--color-accent-300); }

/* --- the CMTS tree -------------------------------------------------------- */
.cmts-card { border:1px solid var(--color-divider); border-radius:var(--radius-md);
  background:var(--color-surface); overflow:hidden; }
/* Four columns, the same four on every row whatever its depth.
   The address columns only mean anything if they line up, and two things used
   to stop them: the indent was padding on the ROW, which moved the whole grid
   in by a level, and the actions column was `auto`, so a row offering "+ add
   Sub-Bundle VRF" pushed the addresses left of one offering only "Edit". The
   indent now lives on the name cell and the actions column is fixed. */
.cmts-row { display:grid;
  grid-template-columns:minmax(0,1fr) 132px 190px 210px;
  gap:12px; align-items:center; padding:9px 14px; }
.cmts-row + .cmts-row { border-top:1px solid color-mix(in srgb, var(--color-divider) 60%, transparent); }
.cmts-row.is-cmts   { background:color-mix(in srgb, var(--color-accent) 8%, transparent); }
.cmts-name   { font-size:14px; }
.cmts-row.is-bundle .cmts-name { padding-left:20px; }
.cmts-row.is-vrf    .cmts-name { padding-left:46px; }
.cmts-row.is-cmts .cmts-name { font-weight:500; }
.cmts-row.is-vrf  .cmts-name { font-size:13px; color:var(--color-neutral-300); }
.cmts-gi { font-family:ui-monospace,Menlo,monospace; font-size:13px;
  color:var(--color-neutral-300); }
/* An IPv6 address is long, and wrapping it mid-address makes it unreadable.
   It gets its own column and is allowed to shrink instead. */
.cmts-link6 { font-family:ui-monospace,Menlo,monospace; font-size:12px;
  color:var(--color-neutral-400); overflow:hidden; text-overflow:ellipsis;
  white-space:nowrap; }
/* The heading row that names the two address columns, once per card. */
.cmts-row.is-head { padding-top:6px; padding-bottom:6px;
  border-bottom:1px solid var(--color-divider); }
.cmts-row.is-head span { font-size:11px; letter-spacing:0.06em;
  text-transform:uppercase; color:var(--color-neutral-500); }
.cmts-actions { display:flex; gap:6px; justify-content:flex-end; }
.cmts-empty { padding:12px 14px; font-size:13px; color:var(--color-neutral-500); }

/* --- model catalogue ------------------------------------------------------ */
.model-thumbs { display:flex; gap:10px; flex-wrap:wrap; margin-top:var(--space-3); }
.model-thumb { border:1px solid var(--color-divider); border-radius:var(--radius-md);
  overflow:hidden; background:var(--color-bg); }
.model-thumb img { display:block; max-width:220px; max-height:170px; cursor:zoom-in; }
.model-thumb img:focus-visible { outline:2px solid var(--color-accent); outline-offset:2px; }

/* A photograph at full size, over everything.
 *
 * Appended to <body> rather than beside the thumbnail: a photograph lives
 * inside .dlg-backdrop, which is position:fixed and therefore its own
 * stacking context -- an overlay inside one can never cover the dialog it
 * came from, whatever z-index it is given. 70 puts it over the 60 the
 * dialogs use.
 *
 * The image is `contain`, so a tall photograph of the back of a modem fits
 * top to bottom and a wide one fits side to side, and neither is cropped.
 * Cropping the thing somebody opened full size to read would be the one
 * unforgivable bug in this file. */
.img-zoom {
  position:fixed; inset:0; z-index:70;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:var(--space-4); padding:24px;
  background:color-mix(in srgb, #000 88%, transparent);
  cursor:zoom-out;
  opacity:0;
  transition:opacity 120ms ease-out;
}
.img-zoom.is-open { opacity:1; }
.img-zoom img {
  max-width:100%; max-height:calc(100% - 40px);
  object-fit:contain;
  border-radius:var(--radius-md);
  transform:scale(0.94);
  transition:transform 120ms ease-out;
}
.img-zoom.is-open img { transform:scale(1); }
.img-zoom-hint { margin:0; font-size:12px; color:var(--color-neutral-400); }

/* Somebody who has asked for less movement gets the picture and not the
   animation. The opacity is what carries the change; without this rule the
   scale is a jump either way, which is worse than no motion at all. */
@media (prefers-reduced-motion: reduce) {
  .img-zoom, .img-zoom img { transition:none; }
  .img-zoom img { transform:none; }
}
.model-thumb figcaption { font-size:11px; color:var(--color-neutral-400);
  padding:4px 8px; text-align:center; }
.yn-yes { color:var(--color-accent-200); }
.yn-no  { color:var(--color-neutral-500); }

/* --- dialogs -------------------------------------------------------------- */
.dlg-backdrop { position:fixed; inset:0; z-index:60; display:grid;
  place-items:start center; padding:40px 16px; overflow:auto;
  background:color-mix(in srgb, #000 62%, transparent); }
/* A dialog with a panel beside it. Its own class rather than a change to the
   rule above: every other dialog on this console is one box centred in the
   grid, and making them all flex rows to suit this one would move all of
   them. */
.dlg-backdrop.has-aside { display:flex; align-items:flex-start;
  justify-content:center; gap:var(--space-4); }
.dlg { width:min(760px, 100%); background:var(--color-surface);
  border:1px solid var(--color-divider); border-radius:var(--radius-md);
  box-shadow:var(--shadow-lg); }
.dlg-head { display:flex; align-items:center; gap:12px; padding:14px 18px;
  border-bottom:1px solid var(--color-divider); }
.dlg-title { margin:0; font-size:17px; }
.dlg-body { padding:18px; display:flex; flex-direction:column; gap:var(--space-4); }
.dlg-foot { display:flex; gap:10px; align-items:center; padding:14px 18px;
  border-top:1px solid var(--color-divider); }
.dlg-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(200px,1fr));
  gap:var(--space-4); }
.dlg-close { margin-left:auto; }


/* --- push-button toggles --------------------------------------------------
   A real radio group wearing a segmented control's clothes: the input keeps
   keyboard traversal, checked state and form posting, and :has() paints the
   selection. Rebuilding this with buttons and JS would mean re-implementing
   all three. */
.tog { display:inline-flex; border:1px solid var(--color-divider);
  border-radius:var(--radius-md); overflow:hidden; }
.tog-opt { position:relative; }
.tog-opt input { position:absolute; opacity:0; inset:0; margin:0; cursor:pointer; }
.tog-opt span { display:block; padding:6px 14px; font-size:13px;
  color:var(--color-neutral-400); cursor:pointer; }
.tog-opt + .tog-opt { border-left:1px solid var(--color-divider); }
.tog-opt:has(input:checked) span { color:var(--color-accent);
  background:color-mix(in srgb, var(--color-accent) 14%, transparent); }
.tog-opt:has(input:focus-visible) span { outline:2px solid var(--color-accent);
  outline-offset:-2px; }
.tog-opt:not(:has(input:checked)):hover span { color:var(--color-accent-200); }

/* --- validated inputs ------------------------------------------------------ */
/* The server's verdict, delivered as a marker span beside the field.
 *
 * :has() lets the outline follow a sibling, which is what keeps the input
 * itself out of the swap -- replacing it would take whatever is being typed
 * with it. A browser without :has() still shows the message, just not the
 * outline. */
.chk { font-size: 12px; color: var(--color-danger); margin-left: 6px; }
.chk-muted { color: var(--color-neutral-500); }
.field:has(> .chk.check-bad) .input {
  border-color: var(--color-danger);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-danger) 30%, transparent);
}
.field:has(label > .chk.check-bad) .input {
  border-color: var(--color-danger);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-danger) 30%, transparent);
}

.input.is-bad { border-color:var(--color-danger);
  box-shadow:0 0 0 2px color-mix(in srgb, var(--color-danger) 30%, transparent); }
.field-note { font-size:12px; color:var(--color-neutral-500); margin:4px 0 0; }
.field-note.is-bad { color:var(--color-danger); }
.usage-line { display:flex; gap:18px; font-size:13px; color:var(--color-neutral-300); }
.usage-line b { color:var(--color-text); font-weight:500; }

/* The visible "Lookup" label beside the header search. It is a real <label>
   pointing at the input, so it names the box for a screen reader as well as
   for a person -- the off-screen label it replaced did the first job only. */
.hs-tag { font-size:11px; letter-spacing:0.08em; text-transform:uppercase;
  color:var(--color-neutral-400); white-space:nowrap; cursor:pointer; }
.hs > .hs-input { flex:1; }

/* Capability toggles on the CM model dialog.
   A checkbox with the box itself hidden: the state is the checkbox's own, so
   the form posts, restores and keyboard-navigates with no script, and the
   colour follows :checked rather than a class something has to remember to
   keep in step. */
.cap-row { display:flex; flex-wrap:wrap; gap:8px; margin:2px 0 var(--space-4); }
.cap { display:inline-flex; cursor:pointer; }
.cap > input { position:absolute; opacity:0; width:0; height:0; }
.cap > span {
  display:inline-block; padding:6px 14px; border-radius:999px;
  font-size:13px; line-height:1.2; border:1px solid transparent;
  background:#3a2323; color:#f0b8b8; border-color:#573232;
  transition:background 120ms ease, color 120ms ease, border-color 120ms ease;
  user-select:none;
}
.cap > input:checked + span {
  background:#1f3a29; color:#a8e0bd; border-color:#2f5c41;
}
/* Focus has to stay visible with the real box hidden, or the toggles become
   unreachable by keyboard for anyone who cannot see the pointer. */
.cap > input:focus-visible + span { outline:2px solid var(--color-accent); outline-offset:2px; }


/* An outlined neutral button. .btn-ghost carries no border, which on a dialog
   footer leaves Cancel reading as a link beside a bordered Save rather than as
   its counterpart. */
.btn-outline { color: var(--color-text); border-color: var(--color-divider); }
.btn-outline:hover { background: color-mix(in srgb, var(--color-text) 8%, transparent); }
.btn-outline:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }

/* Delete. Loud enough to find deliberately and not to press by accident, which
   is the whole job -- and now in this design's colours.
 *
 * It was three fixed light values, #f3d6d6 on #3d1a57, from a light theme this
 * app has never had: a pale pink block sitting on #161826. It went unnoticed
 * while it appeared once, on a dialog footer; it stops being possible to miss
 * beside Save on the fragment editor.
 *
 * Built the way .btn-primary is, from the danger token rather than the accent,
 * with a tint behind it so it reads as heavier than a plain outlined button
 * without being the brightest thing on the page. */
.btn-danger {
  color: var(--color-danger); border-color: var(--color-danger);
  background: color-mix(in srgb, var(--color-danger) 12%, transparent);
}
.btn-danger:hover { background: color-mix(in srgb, var(--color-danger) 22%, transparent); }
.btn-danger:active { background: color-mix(in srgb, var(--color-danger) 32%, transparent); }


/* A value that is shown but never typed. It reads as text, not as a box that
   is refusing to accept input -- which is what a readonly <input> looks like,
   and why the config fragment name is not one. */
.field-readonly {
  margin: 0; padding: var(--space-2) 0;
  font-size: 14px; line-height: 1.2; color: var(--color-text);
  min-height: 30px; display: flex; align-items: center;
}

/* The "sent in every reply" preview under a pool's option pulldowns. Set back
   from the fields above it: it is a consequence of them, not another one. */
.opt-preview {
  margin-top: var(--space-4);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-divider);
}
.opt-preview-table { font-size: 13px; }
.opt-preview-table td, .opt-preview-table th { padding-top: 4px; padding-bottom: 4px; }
/* The group rows sit in a second tbody so the browser can replace them without
   touching the always-sent ones; the rule keeps the join visible. */
.opt-preview-table tbody + tbody tr:first-child td { border-top: 1px solid var(--color-divider); }

/* The CM model dialog's config-fragment fields follow the toggle that reveals
   them. This was a script; it is a selector, because the state it reads is a
   checkbox's own and the DOM says where the fields are relative to it.

   A browser without :has() shows the fields when they should be hidden, which
   is the harmless direction: the toggle still decides whether a fragment is
   saved, and that decision is the server's. */
.cap-row:has([data-frag-toggle]:not(:checked)) + [data-frag-fields] { display: none; }

/* --- the DHCP option editor ---------------------------------------------- */

/* Eight columns of boxes plus a picker, which the 760px a dialog of fields
   wants cannot hold. */
.dlg-wide { width:min(1120px, 100%); }
.opt-edit td { padding:5px 6px; vertical-align:top; }
.opt-edit td .input { width:100%; min-width:0; }
.opt-edit th { white-space:nowrap; }
/* The group's name reads first and takes the width; the number is four
   characters beside it. */
.opt-head { display:flex; gap:var(--space-4); align-items:flex-start; }
.opt-head > .field:first-child { flex:1 1 auto; min-width:0; }
.opt-head > .field:last-child { flex:0 0 110px; }

/* The option is chosen rather than typed, so the cell is a control that looks
   like one: the name, and a chevron that says it opens something. */
.opt-choose {
  display:flex; width:100%; gap:6px; align-items:center; justify-content:space-between;
  background:var(--color-surface); border:1px solid var(--color-divider);
  border-radius:var(--radius-sm); padding:5px 8px; font:inherit; font-size:13px;
  color:var(--color-text); text-align:left; cursor:pointer; min-width:0;
}
.opt-choose:hover { border-color:var(--color-accent); }
.opt-choose:focus-visible { outline:2px solid var(--color-accent); outline-offset:-2px; }
.opt-choose:disabled { cursor:not-allowed; }
.opt-choose-name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.opt-chevron { color:var(--color-accent); flex:0 0 auto; font-size:11px; }
/* What the row will be sent as, under the option it belongs to. The pulldown
   this replaced only appears for an option the RFCs here do not name. */
.opt-typelabel { font-size:11px; color:var(--color-neutral-400); margin-top:2px; }
.opt-dtype { margin-top:3px; font-size:12px; }
.opt-expect { font-size:11px; color:var(--color-neutral-400); margin-top:2px; }
.opt-pick-other { color:var(--color-neutral-400); margin-left:auto; }
.opt-len { text-align:right; color:var(--color-neutral-400); padding-top:11px; }
/* A length the RFC does not allow for that option number. Said in colour AND
   in the title, because colour alone is not a message. */
.opt-len-bad { color:var(--color-danger); font-weight:600; cursor:help; }

/* The minus. Red, at the far right of the row, and the same size as the boxes
   beside it so the row does not step. */
.btn-minus {
  width:26px; height:26px; padding:0; line-height:1;
  background:transparent; border:1px solid var(--color-divider);
  border-radius:var(--radius-sm); color:var(--color-danger);
  font:inherit; font-size:16px; cursor:pointer;
}
.btn-minus:hover { border-color:var(--color-danger);
  background:color-mix(in srgb, var(--color-danger) 14%, transparent); }
.btn-minus:focus-visible { outline:2px solid var(--color-accent); outline-offset:2px; }

/* The option picker: the RFCs' numbers and names, in as many columns as fit. */
.opt-pick {
  border:1px solid var(--color-divider); border-radius:var(--radius-sm);
  background:var(--color-surface-2, var(--color-surface));
}
.opt-pick-head { display:flex; gap:10px; align-items:baseline;
  padding:8px 10px; border-bottom:1px solid var(--color-divider); }
.opt-pick-grid {
  display:grid; grid-template-columns:repeat(auto-fill, minmax(210px, 1fr));
  gap:1px 8px; padding:8px 10px; max-height:320px; overflow-y:auto;
}
.opt-pick-item {
  display:grid; grid-template-columns:34px 1fr auto; gap:6px; align-items:baseline;
  background:transparent; border:0; border-radius:var(--radius-sm);
  padding:3px 4px; font:inherit; font-size:12px; text-align:left;
  color:var(--color-text); cursor:pointer;
}
.opt-pick-item:hover { background:color-mix(in srgb, var(--color-accent) 14%, transparent); }
.opt-pick-item:focus-visible { outline:2px solid var(--color-accent); outline-offset:-2px; }
.opt-pick-code { color:var(--color-accent); text-align:right; }
.opt-pick-name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.opt-pick-len { color:var(--color-neutral-400); }

/* What the chooser does not offer, under the dialog's own buttons. */
.opt-baked { padding:10px 18px 14px; border-top:1px solid var(--color-divider); }
.opt-baked-lead { color:var(--color-neutral-400); margin-right:6px; }
.opt-baked-item {
  display:inline-block; border:1px solid var(--color-divider);
  border-radius:var(--radius-sm); padding:1px 7px; margin:2px 4px 2px 0;
  color:var(--color-neutral-400); white-space:nowrap;
}
.opt-baked-item .mono { color:var(--color-accent); margin-right:5px; }
.opt-baked-name { color:var(--color-neutral-400); }

/* The public demo's own sign-in note: what this deployment is, and the two
   accounts anybody may use. It renders on one host and nowhere else -- see
   isDemoHost -- so it is styled as an aside rather than as a warning: nothing
   here is wrong, it is only worth knowing before signing in.

   The rule above the block does the separating, so the note needs no border of
   its own and does not compete with the Sign in button it sits under. */
.demo-note { margin-top:var(--space-5); padding-top:var(--space-4);
  border-top:1px solid var(--color-divider);
  font-size:12.5px; line-height:19px; color:var(--color-neutral-400); }
.demo-note p { margin:0; }
.demo-note dl { margin:10px 0 0; display:grid; gap:2px 10px; }
.demo-note dt { color:var(--color-text); margin-top:8px; }
.demo-note dt:first-child { margin-top:0; }
.demo-note dd { margin:0; }
.demo-note .mono { font-family:ui-monospace, Menlo, monospace;
  color:var(--color-accent); }

/* A sub-option catalogue with nothing in it: the container is real and its
   contents are the enterprise's, so the band says that rather than sitting
   empty. */
.opt-pick-none {
  grid-column: 1 / -1;
  margin: 0;
  padding: 6px 2px;
}
