/* Datonet CMS
 *
 * A support tool, not a marketing site (D30). Datonet's identity sets the
 * accent; the layout prioritises information density and legibility because
 * staff read this all day, often while on a call.
 *
 * Brand colours are placeholders until extracted from datonet.co.za (D36).
 * Only --brand and --brand-dark should need changing.
 *
 * Health colours are deliberately SEPARATE from brand colours - if a red brand
 * accent were reused, it would start reading as an alarm.
 */

/* ---------------------------------------------------------------------------
 * Theme — the palette from datonet.co.za, DARK BY DEFAULT.
 *
 * The public site defaults light and offers dark; the CMS is the other way
 * round, because it is looked at all day in an office and on a tower at night.
 * So the dark values live on bare `:root` and `[data-theme="light"]` is the
 * opt-in — which also means **no `prefers-color-scheme` here at all**. A staff
 * tool that silently changes colour because someone's laptop reached sunset is
 * worse than one that looks the same every time; the switch in the header is
 * the only thing that changes it, and it is remembered per browser.
 *
 * Token NAMES are the CMS's own (`--text`, `--surface`, `--brand`), not the
 * website's (`--ink`, `--primary`). 885 lines of rules below already use them,
 * so re-skinning is this block and nothing else. The website's names are given
 * beside each value so the two can be kept in step by eye.
 * ------------------------------------------------------------------------ */

:root {
    --brand:        #57a0ec;   /* site --primary      */
    --brand-dark:   #74b1f0;   /* site --primary-600  */
    --brand-tint:   #1d324a;   /* site --primary-tint */

    --accent:       #f0916f;   /* site --accent — used sparingly, as there  */
    --accent-tint:  #3a2018;   /* site --accent-tint                        */

    --bg:           #0d1825;   /* site --bg        */
    --bg-wash:      #0a131d;   /* site --bg-wash   */
    --surface:      #14202f;   /* site --surface   */
    --surface-2:    #1b2a3c;   /* site --surface-2 */
    --border:       #24364b;   /* site --border    */
    --border-soft:  #1c2c3e;   /* site --border-soft */
    --text:         #e9eff6;   /* site --ink       */
    --text-dim:     #9fb2c7;   /* site --ink-soft  */
    --text-faint:   #6b8099;   /* site --ink-faint */
    --logo:         #aab2bd;   /* site --logo — the grey wordmark */

    /* Status colours are the CMS's own: the site has one "good" green and no
       vocabulary for degraded or down, and a verdict of DEGRADED has to read
       as amber whatever the marketing palette says (D24, one visual language).
       Tuned for legibility on #0d1825 rather than inherited. */
    --green:        #3fbe84;
    --green-bg:     #102b22;
    --amber:        #d9a441;
    --amber-bg:     #2c2313;
    --red:          #ef6b60;
    --red-bg:       #331a19;
    --neutral:      #9fb2c7;
    --neutral-bg:   #1b2a3c;

    --shadow-sm:    0 1px 2px rgba(0,0,0,.4);
    --shadow:       0 6px 20px rgba(0,0,0,.45);

    /* The site rounds to 16-28px. This is a dense operations tool with tables
       and graph nodes in it, so it takes the site's SMALLEST radius and stops
       there: 6px read as a different product, 16px would waste rows. */
    --radius:       10px;
    --radius-pill:  999px;
    --gap:          16px;
    --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
    --sans: "Figtree", system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    --display: "Neuropol", "Figtree", var(--sans);

    /* The gateway globe, as a mask so it can be any colour the node is. A
       data: URI rather than a file: one request fewer, and an internal tool
       that keeps working when the internet does not (the fonts are local for
       the same reason). Encoded because `<`, `>` and `#` are not URL-safe. */
    --globe: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cg fill='none' stroke='%23000' stroke-width='1.5'%3E%3Ccircle cx='8' cy='8' r='6.3'/%3E%3Cellipse cx='8' cy='8' rx='2.9' ry='6.3'/%3E%3Cpath d='M1.7 8h12.6'/%3E%3C/g%3E%3C/svg%3E");

    /* The rest of the role marks, same technique for the same reasons. They are
       drawn at about 9px on a fitted graph, which is the whole design brief:
       each one is a SILHOUETTE that survives at that size - one or two strokes,
       no interior detail. Three horizontal lines inside a 9px circle fill it in
       solid, which is why the globe has one equator and not two parallels. */
    --icon-router:   url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 1.6 14.4 8 8 14.4 1.6 8Z' fill='none' stroke='%23000' stroke-width='1.6' stroke-linejoin='round'/%3E%3C/svg%3E");
    --icon-switch:   url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M2.2 5.4h11.6M2.2 10.6h11.6' stroke='%23000' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");
    --icon-ap:       url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M2.6 9.4a7.6 7.6 0 0 1 10.8 0' fill='none' stroke='%23000' stroke-width='1.7' stroke-linecap='round'/%3E%3Ccircle cx='8' cy='12.4' r='1.9' fill='%23000'/%3E%3C/svg%3E");
    /* A dish and its feed: an open curve with a dot at the focus. Drawn FILLED
       the half-disc and a feed ARM merged into an arrowhead with a tail at 9px
       and read as "this way", which is not something a role mark may ever say;
       open with an arm it still read as a thin one. The dot cannot. */
    --icon-cpe:      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cg fill='none' stroke='%23000' stroke-width='1.7' stroke-linecap='round'%3E%3Cpath d='M10.4 2.2a6.9 6.9 0 0 0 0 11.6'/%3E%3C/g%3E%3Ccircle cx='12.9' cy='8' r='1.6' fill='%23000'/%3E%3C/svg%3E");
    --icon-onu:      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='9.6' cy='8' r='3.9' fill='none' stroke='%23000' stroke-width='1.7'/%3E%3Cpath d='M1.4 8h4' stroke='%23000' stroke-width='1.7' stroke-linecap='round'/%3E%3C/svg%3E");
    --icon-backhaul: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M2 2.6 7.2 8 2 13.4Z' fill='%23000'/%3E%3Cpath d='M14 2.6 8.8 8 14 13.4Z' fill='%23000'/%3E%3C/svg%3E");
}

:root[data-theme="light"] {
    --brand:        #2563b4;
    --brand-dark:   #1e529c;
    --brand-tint:   #dce8f5;

    --accent:       #e07856;
    --accent-tint:  #fbe6dd;

    --bg:           #f4f7fb;
    --bg-wash:      #eaf1f9;
    --surface:      #ffffff;
    --surface-2:    #eef3fa;
    --border:       #e2eaf3;
    --border-soft:  #edf2f8;
    --text:         #16212e;
    --text-dim:     #51637a;
    --text-faint:   #8a9bb0;
    --logo:         #868c96;

    --green:        #1f9d6b;
    --green-bg:     #e4f5ed;
    --amber:        #9a6700;
    --amber-bg:     #fdf3d9;
    --red:          #b3261e;
    --red-bg:       #fbeae9;
    --neutral:      #51637a;
    --neutral-bg:   #eef3fa;

    --shadow-sm:    0 1px 2px rgba(26,58,92,.06), 0 2px 8px rgba(26,58,92,.05);
    --shadow:       0 4px 14px rgba(26,58,92,.08), 0 12px 32px rgba(26,58,92,.07);
}

/* Self-hosted, copied from the public site: no request to a third party on
   every page of an internal tool, and the CMS keeps working when the internet
   does not. `font-display: swap` so a cold cache shows text immediately. */
@font-face {
    font-family: "Figtree";
    src: url("/assets/fonts/figtree-latin.woff2") format("woff2");
    font-weight: 300 800;
    font-display: swap;
    unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,
                   U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
    font-family: "Figtree";
    src: url("/assets/fonts/figtree-latin-ext.woff2") format("woff2");
    font-weight: 300 800;
    font-display: swap;
    unicode-range: U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,
                   U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
    font-family: "Neuropol";
    src: url("/assets/fonts/neuropol.otf") format("opentype");
    font-weight: 400 700;
    font-display: swap;
}

* { box-sizing: border-box; }

/* **`display:` beats the browser's own `[hidden] { display: none }`, and this
   has now cost two features.** The connect form was on screen before anything
   had been picked, and the "a new device is on this site" banner was on screen
   before any device had arrived - both because the class that lays them out
   sets `display: flex`, which is a more specific declaration than the user-agent
   rule and simply wins.
   
   Fixing it per class means remembering, every time, that an element which is
   sometimes hidden must not be given a `display`. This is the rule that makes
   `el.hidden = true` mean what it says everywhere, once. Caught by rendering
   it, which is exactly what D41 asks for - twice now. */
[hidden] { display: none !important; }

body {
    margin: 0;
    font-family: var(--sans);
    font-size: 14px;
    line-height: 1.45;
    color: var(--text);
    background: var(--bg);
}

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

/* ---------- Top bar ---------- */

.topbar {
    display: flex;
    align-items: center;
    gap: var(--gap);
    padding: 10px 18px;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    position: sticky;
    top: 0;
    z-index: 50;
}

.brand { display: flex; align-items: center; gap: 9px; color: var(--text); }
.brand:hover { text-decoration: none; }
/* The wordmark is the public site's own artwork, and it is painted through a
 * CSS MASK rather than shown as an <img> - which is why it needs no light and
 * dark copy: `background: currentColor` means the shape takes whatever colour
 * the theme gives it. Straight from datonet.co.za, where the comment on the
 * same rule explains the raster source: corrected artwork, so the letterforms
 * are not at the mercy of font kerning.
 *
 * aspect-ratio keeps the box the right shape before the image has loaded, so
 * the topbar does not jump. */
.brand-logo {
    display: block;
    width: 118px;
    aspect-ratio: 866 / 106;
    background: currentColor;
    color: var(--logo);
    -webkit-mask: url(/assets/img/datonet-grey.png) center / contain no-repeat;
            mask: url(/assets/img/datonet-grey.png) center / contain no-repeat;
}
.brand:hover .brand-logo { color: var(--text); }

/* "CMS" is ours, not the brand's, so it is set apart rather than pretending to
 * be part of the wordmark: the display face, the brand blue, and a hairline to
 * separate it from a logo it does not belong to. */
.brand-tag {
    font-family: var(--display);
    font-size: 12px;
    letter-spacing: .08em;
    color: var(--brand);
    padding-left: 9px;
    border-left: 1px solid var(--border);
    line-height: 1;
}

.search { flex: 1; max-width: 620px; position: relative; }
.search input {
    width: 100%;
    padding: 8px 12px;
    font-size: 14px;
    font-family: inherit;
    color: var(--text);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}
.search input:focus {
    outline: 2px solid var(--brand);
    outline-offset: -1px;
    background: var(--surface);
}

.suggest {
    position: absolute;
    top: calc(100% + 4px); left: 0; right: 0;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: 0 8px 24px rgba(0,0,0,.12);
    /* A tree is taller than a list was, so it scrolls rather than running off
       the bottom of the screen. `hidden` on the cross axis is what keeps the
       rounded corners cutting the first and last row.

       The height is a WHOLE NUMBER OF ROWS (17 of the 34px below) because a
       pixel height cuts the last one in half, and half a row of text reads as a
       rendering fault - seen in the headless shot of a search for ZP. Seventeen
       is one more than SUGGEST_ROWS plus its "everything matching" row, so on a
       normal screen the list does not scroll at all. */
    overflow: hidden auto;
    max-height: min(68vh, 578px);
    z-index: 60;
}

/* **Results are a tree** (D93): a customer, their premises indented under them,
   the equipment indented under each premises. The indent is the whole point, so
   it is drawn here and not in the JS, which only sets `data-depth`.

   Four things share one row: the name, a line of detail, a mono chip (a code, a
   tower's kind, a device's role) and a state pill. The name takes the slack and
   truncates, so the chip and the pill stay where the eye expects them. */
.suggest .sg {
    position: relative;
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto auto;
    align-items: center;
    gap: 10px;
    /* Fixed, so the scroller above can be a whole number of rows. Every row is
       one line: the name truncates rather than wrapping. */
    height: 34px;
    padding: 0 12px;
    color: var(--text);
    border-bottom: 1px solid var(--border);
    font-size: 13.5px;
}
.suggest .sg:last-child { border-bottom: 0; }
.suggest .sg:hover, .suggest .sg.sel { background: var(--brand-tint); text-decoration: none; }

.suggest .sg[data-depth="1"] { padding-left: 30px; }
.suggest .sg[data-depth="2"] { padding-left: 48px; }

/* The guides are hairlines standing in the gutter of each level, so rows at the
   same depth stack theirs into one continuous line. A depth-2 row draws its
   parent's as well - ::before is its own level, ::after its parent's. */
.suggest .sg[data-depth="1"]::before,
.suggest .sg[data-depth="2"]::before,
.suggest .sg[data-depth="2"]::after {
    content: ''; position: absolute; top: 0; bottom: 0; width: 1px;
    background: var(--border);
}
.suggest .sg[data-depth="1"]::before,
.suggest .sg[data-depth="2"]::after  { left: 18px; }
.suggest .sg[data-depth="2"]::before { left: 36px; }

.suggest .sg-name {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A customer heads a branch, and a row that did NOT match what was typed is
   there to say where the match stands - so it stays legible and stops competing. */
.suggest .sg[data-type="customer"] .sg-name { font-weight: 600; }
.suggest .is-context .sg-name { color: var(--text-dim); }

.suggest .sg-detail {
    color: var(--text-dim); font-size: 12px; white-space: nowrap;
    overflow: hidden; text-overflow: ellipsis; min-width: 0;
}
.suggest .sg-chip { font-family: var(--mono); font-size: 11.5px; color: var(--text-dim); }
.suggest .sg[data-type="customer"] .sg-chip { color: var(--brand); }
.suggest .pill { min-width: 52px; text-align: center; }

/* The characters they typed, inside the name they are reading. */
.suggest mark.hit { background: none; color: var(--brand); font-weight: 600; }

.suggest .sg-more { font-size: 12px; }
.suggest .sg-more .sg-name { color: var(--brand); }
.suggest .sg-empty { padding: 10px 12px; color: var(--text-dim); font-size: 13px; }

/* The box is the full width of a phone, where a role or a kind is the first
   thing worth losing: the pill says more in less. A customer's CODE stays - it
   is what people say to each other on the phone. */
@media (max-width: 560px) {
    .suggest .sg:not([data-type="customer"]) .sg-chip { display: none; }
}

/* The same tree on /search, where there is room for a table around it. The
   indent is a padding on the name cell, not a nested table: the row is still one
   row, so a table can still be read down its columns. */
.search-tree .search-name { padding-left: 10px; }
.search-tree tr[data-depth="1"] .search-name { padding-left: 32px; }
.search-tree tr[data-depth="2"] .search-name { padding-left: 54px; }
.search-tree tr.is-hit .search-name a { font-weight: 600; }
.search-tree tr.is-context .search-name a { color: var(--text-dim); }
.search-tree tr.is-context .search-name a:hover { color: var(--brand); }

/* **A card clips, and that clipped the type-ahead inside it.** `.card` is
   `overflow: hidden` so its rounded corners cut the head's background and any
   table flush to its edges - but a `.suggest` list is absolutely positioned
   BELOW its input, so on the Placement card it was sliced off at the card's
   border and only the first result or two could be seen. Same bug on the two
   device pickers in "Add a link".

   Any card holding a picker stops clipping, and there is no class to remember
   to add: put a type-ahead in a card and it works. What the clipping was doing
   for the corners is done explicitly instead - only the head needs it, being
   the one child with a background of its own. The graph containers do their own
   clipping (they must - a node left of the origin escapes and scrolls the whole
   page), so nothing there depends on this.

   Where :has() is unsupported the list is clipped exactly as it is today: the
   old bug, not a new one. */
.card:has(.suggest) { overflow: visible; }
.card:has(.suggest) > .card-head {
    border-radius: calc(var(--radius) - 1px) calc(var(--radius) - 1px) 0 0;
}

.nav { display: flex; gap: 4px; }
.nav a {
    padding: 6px 11px;
    border-radius: var(--radius);
    color: var(--text-dim);
    white-space: nowrap;
}
.nav a:hover { background: var(--surface-2); text-decoration: none; }
.nav a.active { background: var(--brand-tint); color: var(--brand); font-weight: 600; }

.user { display: flex; align-items: center; gap: 10px; margin-left: auto; }
.user-name { color: var(--text-dim); font-size: 13px; white-space: nowrap; }
a.user-name:hover { color: var(--brand); text-decoration: none; }

/* Per-permission results under "Test connection" */
.test-checks { list-style: none; margin: 6px 0 0; padding: 0; font-size: 12.5px; }
.test-checks li { margin-bottom: 2px; }
.test-checks li.ok   { color: var(--green); }
.test-checks li.fail { color: var(--red); }
.link {
    background: none; border: 0; padding: 0;
    color: var(--brand); cursor: pointer; font: inherit;
}

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

.page { padding: 18px; max-width: 1500px; margin: 0 auto; }

.grid { display: grid; gap: var(--gap); }
.grid.cols-2 { grid-template-columns: 2fr 1fr; }
.grid.cols-3 { grid-template-columns: repeat(3, 1fr); }
.grid.cols-4 { grid-template-columns: repeat(4, 1fr); }

@media (max-width: 1000px) {
    .grid.cols-2, .grid.cols-3, .grid.cols-4 { grid-template-columns: 1fr; }
    .topbar { flex-wrap: wrap; }
    .search { order: 3; flex-basis: 100%; max-width: none; }
}

.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
}
.card + .card { margin-top: var(--gap); }
.card-head {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    padding: 11px 14px;
    border-bottom: 1px solid var(--border);
    background: var(--surface-2);
}
.card-head h2 { margin: 0; font-size: 13px; text-transform: uppercase;
                letter-spacing: .05em; color: var(--text-dim); }
.card-body { padding: 14px; }
.card-body.tight { padding: 0; }

h1 { font-size: 22px; margin: 0 0 4px; letter-spacing: -0.01em; }
h3 { font-size: 14px; margin: 0 0 8px; }

.muted { color: var(--text-dim); }
.faint { color: var(--text-faint); }
.mono  { font-family: var(--mono); font-size: 12.5px; }
.right { text-align: right; }
.nowrap { white-space: nowrap; }

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

table { width: 100%; border-collapse: collapse; }
th {
    text-align: left; padding: 8px 14px;
    font-size: 11px; text-transform: uppercase; letter-spacing: .05em;
    color: var(--text-dim); background: var(--surface-2);
    border-bottom: 1px solid var(--border);
    position: sticky; top: 0;
}
td { padding: 8px 14px; border-bottom: 1px solid var(--border); vertical-align: top; }
tr:last-child td { border-bottom: 0; }
tbody tr:hover { background: var(--surface-2); }

.scroll-x { overflow-x: auto; }

/* ---------- Status ---------- */

.pill {
    display: inline-flex; align-items: center; gap: 5px;
    padding: 2px 8px;
    border-radius: 999px;
    font-size: 12px; font-weight: 600;
    white-space: nowrap;
}
/* Status is never colour alone (D30) - each pill carries a glyph too. */
.pill::before { font-family: var(--mono); font-size: 10px; }
.pill.green   { background: var(--green-bg);   color: var(--green); }
.pill.green::before { content: "\25CF"; }
.pill.amber   { background: var(--amber-bg);   color: var(--amber); }
.pill.amber::before { content: "\25B2"; }
.pill.red     { background: var(--red-bg);     color: var(--red); }
.pill.red::before { content: "\2715"; }
.pill.neutral { background: var(--neutral-bg); color: var(--neutral); }
.pill.neutral::before { content: "\2013"; }

/* ---------- Device history, read live from the source system ----------
 * Dense on purpose: it is a log, scanned downwards, and the useful column is
 * the one on the left. Capped so a chatty device cannot own the page.
 */
.history { max-height: 340px; overflow-y: auto; }
.history table { font-size: 12.5px; }
.history td { padding: 5px 12px; vertical-align: middle; }
.history .pill { font-size: 10.5px; padding: 1px 7px; text-transform: lowercase; }
.history tr.history-error td:first-child { box-shadow: inset 3px 0 0 var(--red); }
.history tr.history-warning td:first-child { box-shadow: inset 3px 0 0 var(--amber); }

/* ---------- Health tiles (D35) ---------- */

.tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 10px; }

.tile {
    padding: 11px 13px;
    border: 1px solid var(--border);
    border-left-width: 3px;
    border-radius: var(--radius);
    background: var(--surface);
}
.tile.green   { border-left-color: var(--green);   background: var(--green-bg); }
.tile.amber   { border-left-color: var(--amber);   background: var(--amber-bg); }
.tile.red     { border-left-color: var(--red);     background: var(--red-bg); }
.tile.neutral { border-left-color: var(--neutral); }

.tile-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.tile-label { font-size: 11px; text-transform: uppercase; letter-spacing: .05em;
              color: var(--text-dim); }
.tile-value { font-size: 19px; font-weight: 600; font-family: var(--mono); }
/* The plain-English meaning - the whole point of the tile (D35). */
.tile-meaning { margin-top: 5px; font-size: 12.5px; line-height: 1.4; color: var(--text-dim); }

/* ---------- First glance: verdict + topology (Q71) ----------
 * The top of the customer page answers "are they online" and "what is between
 * us and them", and nothing else. Everything else is in a drawer below.
 */

.glance {
    display: grid; grid-template-columns: 230px 1fr; gap: var(--gap);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 14px;
    margin-bottom: var(--gap);
}
@media (max-width: 900px) { .glance { grid-template-columns: 1fr; } }

.glance-verdict {
    display: flex; flex-direction: column; justify-content: center;
    padding: 12px 14px; border-radius: var(--radius);
    border-left: 4px solid var(--neutral); background: var(--neutral-bg);
}
.glance-verdict.green { border-left-color: var(--green); background: var(--green-bg); }
.glance-verdict.amber { border-left-color: var(--amber); background: var(--amber-bg); }
.glance-verdict.red   { border-left-color: var(--red);   background: var(--red-bg); }

/* The word carries the meaning - the colour only reinforces it (D30). */
.glance-label { font-size: 25px; font-weight: 700; line-height: 1.1; letter-spacing: -0.01em; }
.glance-verdict.green .glance-label { color: var(--green); }
.glance-verdict.amber .glance-label { color: var(--amber); }
.glance-verdict.red   .glance-label { color: var(--red); }
.glance-verdict.neutral .glance-label { color: var(--neutral); }
.glance-detail { margin-top: 5px; font-size: 12.5px; color: var(--text-dim); }

.glance-topology { display: flex; flex-direction: column; gap: 8px;
                   justify-content: center; min-width: 0; }

/* Same renderer and same visual language as the /topology graph, just smaller -
   an agent should not have to learn two topology views (D24). */
/* Boxed nodes need more room than bare dots did. The site page's map is the main
   navigation view (D41), so it opens taller than an embedded panel. */
/* Taller than it used to be because it is narrower than it used to be: the map
   now shares its row with the equipment list, so a BFS tree that used to spread
   sideways has to go down instead. */
#site-graph {
    width: 100%; height: 520px;
    background: var(--surface-2);
    border: 1px solid var(--border); border-radius: var(--radius);
}
#customer-graph, #device-graph {
    width: 100%; height: 320px;
    background: var(--surface-2);
    border: 1px solid var(--border); border-radius: var(--radius);
}

/* ---------- Graph nodes are HTML ----------
 *
 * Cytoscape draws the edges on its canvas and decides where everything goes.
 * Every NODE is a div in an overlay above it, so the map uses this stylesheet
 * like the rest of the CMS: real type, the same `.pill` the tables use, both
 * themes for free, and is-peek / is-selected that are the same idea as the rows
 * in the equipment list beside it.
 *
 * The one rule for anything added here: **never change a node's box metrics on
 * a state class.** Cytoscape lays the graph out on sizes measured from these
 * divs before it exists, so a border or padding that grows on :hover or
 * .is-focus makes the div and the node it stands on disagree, and the edges stop
 * meeting the boxes. Rings are box-shadow, which costs no layout.
 */

/* overflow:hidden, or a node div placed left of the origin escapes the card and
   gives the whole page a horizontal scrollbar. */
#graph, #site-graph, #customer-graph, #device-graph { position: relative; overflow: hidden; }

.graph-nodes {
    position: absolute; inset: 0;
    /* One transform for the layer mirrors cytoscape's pan and zoom. The origin
       has to be the top-left or model space and screen space diverge. */
    transform-origin: 0 0;
    /* Empty space belongs to the canvas underneath, or panning stops working. */
    pointer-events: none;
}

.gnode {
    position: absolute;
    transform: translate(-50%, -50%);
    pointer-events: auto;
    cursor: pointer;
    box-sizing: border-box;
    width: max-content;

    display: flex; flex-direction: column; gap: 4px;
    padding: 7px 9px;
    border: 1.5px solid var(--neutral);
    border-radius: 8px;
    background: var(--neutral-bg);
    color: var(--text);
    font-size: 13px; line-height: 1.3;
}

.gnode.green  { border-color: var(--green);  background: var(--green-bg); }
.gnode.amber  { border-color: var(--amber);  background: var(--amber-bg); }
.gnode.red    { border-color: var(--red);    background: var(--red-bg); }

.gnode-head { display: flex; align-items: flex-start; gap: 6px; }

/* Its own flex item, so a long name wraps beside the glyph instead of pushing it
   onto a line of its own - which is exactly what a canvas label did, because
   there the two were one string and the space between them was a break. */
.gglyph { flex: 0 0 auto; font-size: 10px; line-height: 1.7; }
.gnode.green  .gglyph { color: var(--green); }
.gnode.amber  .gglyph { color: var(--amber); }
.gnode.red    .gglyph { color: var(--red); }
.gnode.neutral .gglyph { color: var(--neutral); }

.gname { font-weight: 600; overflow-wrap: anywhere; }

/* ---------- The role mark: what the box IS ----------
 *
 * Top-right of the node's head, opposite the status glyph, because WHAT a box
 * is and HOW it is doing are different questions and must not read as one row
 * of status lights. `margin-left: auto` rather than a place in the flow, so a
 * name that wraps to two lines cannot push the mark onto a line of its own.
 *
 * **Every drawing is in the stylesheet, once**, as a mask over `currentColor`,
 * rather than an SVG built in topology.js and a second copy pasted into the
 * legend template. Two copies of one glyph is how a map and its own legend come
 * to disagree about what a symbol looks like. An emoji was the other option and
 * is worse: it is drawn by the system's emoji font at its own weight and colour,
 * ignores these tokens entirely, and looks like a different product in each
 * theme (D24, one visual language).
 *
 * **The gateway keeps the state colour and everything else is dim, and that is
 * deliberate.** The globe meant something BECAUSE it was rare - the one box on
 * a customer site whose being down IS the customer being down (ServiceState).
 * Marking every role would have spent that: seven marks in one colour is a row
 * of badges and the important one stops shouting. So the globe is still drawn
 * in the box's own state colour - a red globe on a red box - and router,
 * switch, ap, cpe, onu and backhaul are quiet type markers in --text-dim.
 *
 * `other` gets NO mark. A device whose role we do not know is exactly the case
 * where a symbol would be an invention.
 */
.grole, .rolemark {
    flex: 0 0 auto;
    display: inline-block;
    width: 1.15em; height: 1.15em;
    background-color: currentColor;
    -webkit-mask: var(--mark) center / contain no-repeat;
            mask: var(--mark) center / contain no-repeat;
}
.grole { margin-left: auto; color: var(--text-dim); }

.is-gateway  { --mark: var(--globe); }
.is-router   { --mark: var(--icon-router); }
.is-switch   { --mark: var(--icon-switch); }
.is-ap       { --mark: var(--icon-ap); }
.is-cpe      { --mark: var(--icon-cpe); }
.is-onu      { --mark: var(--icon-onu); }
.is-backhaul { --mark: var(--icon-backhaul); }

/* The gateway, and only the gateway, takes the box's own state colour. */
.gnode.green  .grole.is-gateway { color: var(--green); }
.gnode.amber  .grole.is-gateway { color: var(--amber); }
.gnode.red    .grole.is-gateway { color: var(--red); }

/* In the legend the marks are inline with the words, at the text's own colour. */
.rolemark { vertical-align: -2px; color: var(--text-dim); }

/* Model left, chip right - and when they will not both fit, the chip wraps
   under and stays right rather than the model being cut short. The model is what
   the box IS; an ellipsis through it is worse than a second line. */
.gnode-foot {
    display: flex; flex-wrap: wrap;
    align-items: center; justify-content: space-between;
    gap: 3px 10px;
}
.gnode-foot .pill { margin-left: auto; }

/* Small and dim: subordinate to the name by size and colour, which a one-font
   canvas label could not express at all. Ellipsis rather than growth, so one
   long model cannot widen the box past what the layout was given. */
.ghw {
    min-width: 0; flex: 0 1 auto;
    font-size: 11px; color: var(--text-dim);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.gnode .pill { flex: 0 0 auto; font-size: 11px; padding: 1px 7px; }

/* A place, not a radio (D40). And a count, which is not a thing at all. */
.gnode-site    { border-style: dashed; }
/* Several customer sites one device feeds: places too, so dashed like one. */
.gnode-clients { border-style: dashed; }
/* How far the box's fan-out reaches. Same size and colour as the model line, so
   it stays subordinate to the name - a third line of the name's weight is what
   turns a node into a paragraph. */
.gfan { font-size: 11px; color: var(--text-dim); }

/* Rings, in the order they shout. box-shadow only - see the rule above. */
.gnode.is-peek   { box-shadow: 0 0 0 3px var(--surface-2); border-color: var(--text-dim); }
.gnode.is-mine   { box-shadow: 0 0 0 2px var(--brand); }
.gnode.is-focus  { box-shadow: 0 0 0 3px var(--brand); }
.gnode.is-picked { box-shadow: 0 0 0 3px var(--amber); }
@media (max-width: 900px) { #customer-graph, #device-graph { height: 240px; } }

/* ---------- The site bench: the map, the device picked on it, and the two
 *            columns underneath ----------
 *
 * Four cards, one job. The top row is the MAP and the DEVICE SELECTED ON IT:
 * "which box" and "what is that box doing" are two halves of one question and
 * they are answered side by side. The detail used to open a screen further
 * down, under the list and the edit card, so picking a box on the map meant
 * scrolling away from the map to read the answer.
 *
 * Under them, two equal columns: the DEVICE LIST and the site's LOG. Both are
 * read down a column and both are asked about in the same breath - "is
 * anything down" and "when did it drop" - so they sit level with each other
 * rather than one above the other.
 *
 * All four are in one .site-bench wrapper because under 1000px they become a
 * single column and the tabs reorder them, which only works while they share a
 * parent. The wrapper is what spaces the two rows; nothing relies on
 * `.card + .card` here.
 */
.site-bench { display: flex; flex-direction: column; gap: var(--gap); }

/* align-items:start, NOT stretch. A grid row takes the height of its tallest
   item, so with stretch a long device detail would size the row and push the
   map's own controls off the screen. The map decides the height and the panel
   is given exactly that much - which is why nothing that grows on its own may
   size this row: a row cannot be both its content's height and the map's. */
.site-workbench { grid-template-columns: 2fr 1fr; align-items: start; }
.site-workbench > .card { display: flex; flex-direction: column; min-height: 0; }
/* `.card + .card` stacks cards down a page with a gap between them, and these
   sit SIDE BY SIDE - it drew the second column 16px lower than the map it is
   meant to line up with. The grid's own gap already spaces them. Measured:
   listTop 34 against mapTop 18. */
.site-workbench > .card + .card { margin-top: 0; }
.site-underbench > .card + .card { margin-top: 0; }

/* --workbench-height is the MAP CARD's own height, kept up to date by sites.js,
   so the two halves of the top row end on the same line. The fallback is what
   a browser with no ResizeObserver gets, and it is only ever a little wrong.

   max-height, not height: the "nothing selected" card is three lines and has no
   business being 590px tall. A device detail is always longer than the map, and
   it SCROLLS inside this column rather than stretching the row - or opening a
   device would shove the list and the log half a screen down the page. */
.workbench-panel {
    display: flex; flex-direction: column;
    min-width: 0; min-height: 0;
    max-height: var(--workbench-height, 590px);
}
/* The shell is the flex box and #device-panel is the scroller, NOT this column:
   the bar carrying Close has to stay on screen while the detail scrolls under
   it, and a scroller on the column would take the bar with it. The shell has to
   BE a flex column for that - left a plain block it simply clips, which looks
   identical until you try to reach the bottom of a device. */
.workbench-panel > .device-shell { display: flex; flex-direction: column; min-height: 0; }

/* Under the map: what is standing here, and what it has been doing. Equal
   columns - the log's rows are a time, a device and a sentence, which is not
   the narrower half of anything. */
.site-underbench { grid-template-columns: 1fr 1fr; align-items: start; }
.site-underbench > .card { display: flex; flex-direction: column; min-height: 0; }
/* Capped rather than sized off the map: neither of these may push the other
   down the page, and a tower with 40 devices would otherwise run the page to
   three screens before the photos. Each keeps its own scroller. */
.site-devices, .site-history { max-height: 560px; }
.site-history > .card-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
/* `.history` caps itself at 340px, which was right when it was a card down a
   page on its own. Beside the device list it has to reach the same line, so the
   CARD's cap is the one that counts and the body is the single scroller - two
   nested scrollers is a scrollbar inside a scrollbar. */
.site-history .history { max-height: none; }

/* position:relative so a row's offsetTop is measured against this box: the
   selection scroll moves this scroller, never the page.

   `flex: 1 1 auto`, NOT `flex: 1`. The list card used to be given the map's own
   height, which is definite, so a zero basis had something to grow into. It is
   capped by max-height now and its height is otherwise its content's - and with
   `flex: 1` (basis 0) a content-sized flex container measures this scroller as
   nothing at all and collapses the card to its head. */
.equipment-scroll {
    flex: 1 1 auto; min-height: 0;
    position: relative; overflow-y: auto;
}
/* A head is a head. Without this it is shrinkable and a 40-device list steals
   the title row's last few pixels. */
.site-devices > .card-head, .site-history > .card-head { flex: none; }

/* Shrink-to-fit, so the device names get the rest of a narrow column. */
.equipment-table th:first-child,
.equipment-table td:first-child { width: 1%; }
.equipment-table td { vertical-align: middle; }
/* The vendor and model, which is what the row is actually asked for: dimmer
   than the name, brighter than the role beside it. */
.equipment-meta { font-size: 12px; margin-top: 3px; color: var(--text-faint); }
.equipment-hw { color: var(--text-dim); }
/* Firmware in the equipment list's meta line. Coloured text rather than a pill:
   the row already leads with a state pill, and a second one reads as a second
   state. Amber for a version on offer, red for a project that will never take it. */
/* Signal in the OLT's ONU table (D82). Coloured text with a glyph rather than a
   pill: the row already leads with a state pill, and a second pill reads as a
   second state. The glyph is what keeps it readable without colour (D30). */
.signal         { font-family: var(--mono); font-weight: 600; }
.signal.green   { color: var(--green); }
.signal.amber   { color: var(--amber); }
.signal.red     { color: var(--red); }
.signal.neutral { color: var(--text-dim); }
.signal-icon    { display: inline-block; min-width: 1.4em; font-size: 11px; opacity: .85; }
.onu-age        { font-size: 12px; margin-top: 3px; }
.fw-flag         { color: var(--amber); }
.fw-flag.is-warn { color: var(--amber); font-weight: 600; }
.fw-flag.is-bad  { color: var(--red);   font-weight: 600; }

/* How long it has been in that state, under the pill that names it. Wraps, so
   "no change in 48h" cannot widen the shrink-to-fit state column. */
.equipment-age {
    margin-top: 3px;
    font-size: 11px; color: var(--text-faint);
    white-space: normal; max-width: 96px;
}

/* Selected is a bar down the left rather than only a fill, so it survives the
   hover tint landing on top of it - the row you picked and the row under the
   pointer are frequently not the same row. */
.equipment-table tr.is-selected > td { background: var(--brand-tint); }
.equipment-table tr.is-selected > td:first-child { box-shadow: inset 3px 0 0 var(--brand); }
.equipment-table tr.is-peek > td { background: var(--surface-2); }

/* Picked in connect mode. Amber, matching node.picked in topology.js - the two
   ends of one link must never be drawn in two different colours. */
.equipment-table tr.is-picked > td { background: var(--amber-bg); }
.equipment-table tr.is-picked > td:first-child { box-shadow: inset 3px 0 0 var(--amber); }

/* ---------- Edit this site ----------
 * Both ways of changing a site - connecting two devices, and writing down a box
 * no API reports - in one card under the workbench. They were a button on the
 * map's head and a drawer under the device list, and neither was where anybody
 * looked. Each button reveals its own form; only one form is ever open.
 */
.edit-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.edit-form { margin-top: 12px; max-width: 520px; }
.edit-form .field { margin-bottom: 10px; }

/* Map | Device list, and only where they cannot both fit. Stacking them would
   put the length of a graph between the two things this layout exists to join. */
.workbench-tabs { display: none; gap: 6px; margin-bottom: var(--gap); }

@media (max-width: 1000px) {
    .workbench-tabs { display: flex; }

    /* One column, and the four cards are reordered so the phone reads: the map
       OR the list, then the device picked out of it, then the log. That order
       needs them to be siblings, which `display: contents` on the two grids is
       what makes them - the wrapper's flex gap then spaces all four. */
    .site-workbench, .site-underbench { display: contents; }
    .workbench-map   { order: 1; }
    .site-devices    { order: 2; }
    .workbench-panel { order: 3; }
    .site-history    { order: 4; }

    /* ONE at a time, which is what the tabs have always claimed to do - and
       did not: sites.js toggles .show-list and nothing here read it, so a phone
       got the map AND the list stacked, with the length of a graph between the
       two things this layout exists to join. Found by rendering it at 760px
       (D41: any change to the graphs must be rendered). */
    .site-bench:not(.show-list) .site-devices { display: none; }
    .site-bench.show-list .workbench-map { display: none; }

    /* Everything flows down the page instead of becoming a scrollbar inside a
       scrollbar on a phone. Matching the map's height means nothing when only
       one of them is on screen. */
    .site-devices, .site-history { max-height: none; }
    .workbench-panel { max-height: none; }
    .workbench-panel #device-panel { overflow: visible; }
    .equipment-scroll { flex: none; max-height: none; }
}

/* ---------- The selected device ----------
 *
 * Its own block with a bar of its own, because selecting used to be a one-way
 * door: a device opened under the map and nothing on the page put it away
 * again. The bar is what gives Close somewhere to live, and the outline is what
 * says where the device's detail starts and the site page continues.
 *
 * The detail inside is `templates/devices/_detail.php`, which is also the whole
 * device PAGE - so nothing here may be required for that detail to read
 * correctly. This shell is the site page's frame around it, and the device page
 * renders without one.
 */
.device-shell {
    border: 1px solid var(--brand);
    border-radius: var(--radius);
    background: var(--surface);
    overflow: hidden;
}
.device-shell-head {
    display: flex; align-items: center; gap: 12px;
    padding: 9px 14px;
    background: var(--brand-tint);
    border-bottom: 1px solid var(--border);
}
.device-shell-head h2 {
    margin: 0; font-size: 13px; text-transform: uppercase;
    letter-spacing: .05em; color: var(--text-dim);
}
.device-shell-head .faint { margin-right: auto; font-size: 12px; }
/* Padding here rather than on the panel: the detail is a grid of cards and they
   need to stop short of the outline, but it must stay unpadded on its own page. */
#device-panel { padding: 10px; }
/* The scroller, so the bar above it - and Close with it - stays on screen.
   `flex: 1 1 auto` for the reason .equipment-scroll has it: the shell's height
   is its content's, capped, and a zero basis measures as nothing. */
.workbench-panel .device-shell-head { flex: none; }
.workbench-panel #device-panel { flex: 1 1 auto; min-height: 0; overflow-y: auto; }

/* ---------- The selected device, compacted ----------
 *
 * The same detail as the device PAGE (`templates/devices/_detail.php`, one copy
 * two homes) in a third of the width, so it is laid out rather than rewritten:
 * nothing below changes what the panel SAYS, only how much room it takes.
 *
 * `display: contents` on the two columns is what makes that possible. The page
 * puts health, history and the path down the left and identity, customers,
 * placement and links down the right; dropping those two wrappers out of the
 * layout turns all of it into ONE column of cards this stylesheet can order -
 * so the answer comes first (what state, what box, whose) and the editing comes
 * last, instead of the page's left column being read out in full before the
 * serial number is reached.
 */
#device-panel .grid.cols-2 { grid-template-columns: 1fr; gap: 10px; }
#device-panel .grid.cols-2 > div { display: contents; }
/* .card + .card still matches inside those wrappers, and the grid's own gap
   already spaces them - without this every card after the first is 26px down. */
#device-panel .grid.cols-2 > div > .card + .card { margin-top: 0; }

/* The answer first, the corrections last. "Linked customers" is not in this
   list because the panel does not render it at all - the site this device is
   standing on is what says whose it is (D74), and on a site page that site is
   already on the screen. Path, Links and Placement are drawers here, closed. */
#device-panel .detail-health    { order: 1; }
#device-panel .detail-identity  { order: 2; }
#device-panel .detail-history   { order: 3; }
#device-panel .detail-path      { order: 4; }
#device-panel .detail-links     { order: 5; }
#device-panel .detail-placement { order: 6; }

/* The head: a name, a state and where it stands. One size down from the page's,
   because in this column an h1 at 22px wraps to three lines on its own. */
#device-panel .customer-head { margin-bottom: 10px; gap: 10px; }
#device-panel .customer-head h1 { font-size: 16px; }

#device-panel .card-head { padding: 8px 11px; }
/* :not(.tight), or this ID-weight rule beats `.card-body.tight { padding: 0 }`
   and puts 11px round every table in the panel. */
#device-panel .card-body:not(.tight) { padding: 11px; }

/* Two tiles to a row at this width rather than one. 230px is a page's
   measurement and leaves a single column of very wide tiles here. */
/* Two to a row here, not three. 230px is a page's measurement; 132px was this
   column's arithmetic and it put "168.7Mbps" through the side of its own tile -
   a label and a mono value on one line need about 200px. Wrapping is the
   backstop for the ones that still do not fit. */
#device-panel .tiles { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px; }
#device-panel .tile { padding: 8px 10px; }
#device-panel .tile-head { flex-wrap: wrap; row-gap: 2px; }
#device-panel .tile-value { font-size: 15px; }
#device-panel .tile-meaning { margin-top: 3px; font-size: 11.5px; }

#device-panel dl.facts { grid-template-columns: minmax(74px, auto) 1fr; gap: 4px 10px; }
#device-panel dl.facts dt { font-size: 12px; }

/* The device's own log is shorter here than anywhere else: the site's log is
   the card beside the device list now, so this one answers "and this box in
   particular", which is a glance rather than a read. Its own cap, not the card
   body's - `.history` is already the scroller and a second one around it is a
   scrollbar inside a scrollbar. */
#device-panel .detail-history .history { max-height: 240px; }

/* ONE COLUMN, not a table. A three-column table needs a page's width: in 430px
   the time and the pill hold their own on the left while "The wireless
   frequency for device had been changed from 68040 MHz to 66960 MHz" wraps to
   four ragged lines in what is left, and the eye has to find the start of each
   event twice. Stacked, an event is a heading line - when, and what kind - with
   its sentence under it at full width.

   It is still the same markup from the same renderer (D76): `display` is doing
   all of this, so a device's log and a site's cannot drift into two shapes of
   the same rows. */
#device-panel .history table,
#device-panel .history tbody { display: block; }
#device-panel .history tr {
    display: flex; flex-wrap: wrap; align-items: baseline; gap: 3px 8px;
    padding: 7px 12px;
    border-bottom: 1px solid var(--border);
}
#device-panel .history tr:last-child { border-bottom: 0; }
/* The cells lose their own padding and rules, or a block `td` draws three of
   them down every event. */
#device-panel .history td { display: block; padding: 0; border: 0; }
/* The message takes a line of its own, whatever is in front of it. */
#device-panel .history td:last-child { flex: 1 1 100%; }
/* The severity bar belongs to the ROW now: on a block first cell it is a 3px
   tick beside the timestamp rather than a stripe down the event. */
#device-panel .history tr.history-error td:first-child,
#device-panel .history tr.history-warning td:first-child { box-shadow: none; }
#device-panel .history tr.history-error   { box-shadow: inset 3px 0 0 var(--red); }
#device-panel .history tr.history-warning { box-shadow: inset 3px 0 0 var(--amber); }

.chain-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.chain { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; min-width: 0; }
.chain-arrow { color: var(--text-faint); }
.chain-note { font-size: 12px; color: var(--amber); }
.chain-legend { display: flex; gap: 14px; flex-wrap: wrap; align-items: center;
                font-size: 12px; margin-top: 2px; }
.chain-legend span { display: flex; align-items: center; gap: 6px; }

.hop {
    display: inline-flex; align-items: center; gap: 7px;
    padding: 5px 10px;
    border: 1px solid var(--border); border-radius: var(--radius);
    background: var(--surface-2); color: var(--text);
    max-width: 100%;
}
.hop:hover { text-decoration: none; border-color: var(--brand); }
.hop.amber { border-color: var(--amber); background: var(--amber-bg); }
.hop.red   { border-color: var(--red);   background: var(--red-bg); }
/* The customer's own device, so an agent can see at a glance which end is theirs. */
.hop.theirs { outline: 2px solid var(--brand); outline-offset: 1px; font-weight: 600; }

.hop-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 24ch; }
.hop-role { font-size: 11px; text-transform: uppercase; letter-spacing: .04em;
            color: var(--text-faint); }

/* Status is never colour alone (D30) - the dot carries the same glyph as a pill. */
.hop-dot { font-family: var(--mono); font-size: 10px; line-height: 1; }
.hop-dot.green::before   { content: "\25CF"; color: var(--green); }
.hop-dot.amber::before   { content: "\25B2"; color: var(--amber); }
.hop-dot.red::before     { content: "\2715"; color: var(--red); }
.hop-dot.neutral::before { content: "\2013"; color: var(--neutral); }

/* Detail on demand: a card that opens. The chevron replaces the browser's own
   marker so the head keeps the same layout as a plain card head. */
.drawer > summary { cursor: pointer; list-style: none; justify-content: flex-start; }
.drawer > summary > h2 { margin-right: auto; }
.drawer > summary::-webkit-details-marker { display: none; }
.drawer > summary::after { content: "\25BE"; color: var(--text-faint); font-size: 11px; }
.drawer[open] > summary::after { content: "\25B4"; }
.drawer > summary:hover { background: var(--brand-tint); }

/* ---------- The hygiene report's tabs ----------
 * The same `.nav` the site list uses for Sites | Customer sites (D24, one
 * visual language), with a count in each. The count is the TRUE total, so a
 * tab is safe to choose a morning's work from.
 */
.hygiene-tabs { flex-wrap: wrap; margin-bottom: 6px; }
.hygiene-tabs a { display: flex; align-items: center; gap: 7px; }
/* Smaller than a table's pill: it is a label on a label, not a status. */
.hygiene-tabs .pill { font-size: 11px; padding: 1px 7px; }
.hygiene-hint { margin: 0 0 var(--gap); font-size: 12.5px; }
/* The count and "showing 100" are one item in the card head: `.card-head` is
   space-between, and a third child there puts the pill in the middle of the
   bar with its own footnote at the far edge. */
.hygiene-count { display: flex; align-items: center; gap: 8px; }
.hygiene-count .faint { font-size: 12px; }

/* ---------- Customer page ---------- */

.customer-head {
    display: flex; align-items: flex-start; gap: 16px; flex-wrap: wrap;
    margin-bottom: var(--gap);
}
.customer-code {
    font-family: var(--mono); font-size: 13px; font-weight: 600;
    color: var(--brand); background: var(--brand-tint);
    padding: 3px 9px; border-radius: var(--radius);
}
.actions { margin-left: auto; display: flex; gap: 8px; flex-wrap: wrap; }

.btn {
    display: inline-block;
    padding: 6px 13px;
    font: inherit; font-size: 13px;
    color: var(--text); background: var(--surface);
    border: 1px solid var(--border); border-radius: var(--radius);
    cursor: pointer;
}
.btn:hover { background: var(--surface-2); text-decoration: none; }
.btn.primary { background: var(--brand); border-color: var(--brand); color: #fff; }
.btn.primary:hover { background: var(--brand-dark); }
.btn.danger { background: var(--red-bg); border-color: var(--red); color: var(--red); }
.btn.danger:hover { background: var(--red); color: #fff; }
.btn:disabled { opacity: .5; cursor: not-allowed; }

dl.facts { display: grid; grid-template-columns: minmax(110px, auto) 1fr;
           gap: 5px 14px; margin: 0; }
dl.facts dt { color: var(--text-dim); font-size: 12.5px; }
dl.facts dd { margin: 0; }

.device {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    margin-bottom: 12px;
    background: var(--surface);
}
.device-head {
    display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
    padding: 10px 13px;
    border-bottom: 1px solid var(--border);
    background: var(--surface-2);
}
.device-name { font-weight: 600; }
/* Beside the state pill on a customer's device: "for 14d 6h", "since 09:12". */
.device-age { font-size: 12px; color: var(--text-dim); white-space: nowrap; }

.device-meta { color: var(--text-dim); font-size: 12.5px; }
.device-body { padding: 13px; }

/* Capability notice - never leave an agent guessing whether an empty list
   means "nothing there" or "we cannot see in" (D16). */
.capability {
    margin-top: 10px; padding: 9px 12px;
    border-radius: var(--radius); font-size: 12.5px;
    border: 1px dashed var(--border);
    color: var(--text-dim);
}
.capability.none    { border-color: var(--amber); background: var(--amber-bg); color: var(--amber); }
.capability.partial { border-color: var(--border); }

.banner {
    padding: 11px 14px; border-radius: var(--radius); margin-bottom: var(--gap);
    border: 1px solid transparent;
}
.banner.info  { background: var(--brand-tint); border-color: var(--brand); }
.banner.warn  { background: var(--amber-bg);   border-color: var(--amber); color: var(--amber); }
.banner.error { background: var(--red-bg);     border-color: var(--red);   color: var(--red); }
.banner.ok    { background: var(--green-bg);   border-color: var(--green); color: var(--green); }

.stat { text-align: center; padding: 6px 0; }
.stat-value { font-size: 26px; font-weight: 600; line-height: 1.1; }
.stat-label { font-size: 11px; text-transform: uppercase; letter-spacing: .05em;
              color: var(--text-dim); margin-top: 3px; }

.stale {
    display: inline-block; font-size: 11px; padding: 1px 7px;
    background: var(--amber-bg); color: var(--amber);
    border-radius: 999px; font-weight: 600;
}

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

.auth-wrap { display: grid; place-items: center; min-height: 100vh; padding: 20px; }
.auth-card { width: 100%; max-width: 380px; background: var(--surface);
             border: 1px solid var(--border); border-radius: var(--radius); padding: 26px; }
.auth-card h1 { text-align: center; margin-bottom: 20px; }
/* Relative so a type-ahead dropdown inside a form field anchors to the field
   rather than to the page (the device pickers on a site page). */
.field { margin-bottom: 13px; position: relative; }
.field label { display: block; font-size: 12.5px; color: var(--text-dim); margin-bottom: 4px; }
.field input, .field select, .field textarea {
    width: 100%; padding: 8px 11px; font: inherit;
    color: var(--text); background: var(--surface-2);
    border: 1px solid var(--border); border-radius: var(--radius);
}
.field input:focus { outline: 2px solid var(--brand); outline-offset: -1px; }
.code-input { font-family: var(--mono); font-size: 22px !important;
              text-align: center; letter-spacing: .3em; }
/* The white is inside the SVG; the border radius only rounds the quiet zone */
.totp-qr {
    display: block; margin: 4px auto 10px; width: 200px; height: 200px;
    border-radius: var(--radius);
}
.totp-secret {
    font-family: var(--mono); font-size: 15px; word-break: break-all;
    background: var(--surface-2); padding: 11px; border-radius: var(--radius);
    border: 1px solid var(--border); text-align: center; user-select: all;
}

/* ---------- Wallboard (D12) ---------- */

body.wallboard {
    background: #0a0d10; color: #e6edf3;
    font-size: 17px; padding: 22px; overflow: hidden;
}
.wb-head { display: flex; align-items: baseline; justify-content: space-between;
           margin-bottom: 18px; }
.wb-head h1 { font-size: 30px; }
.wb-grid { display: grid; grid-template-columns: 1.3fr 1fr; gap: 22px; }
.wb-panel { background: #12171c; border: 1px solid #232b33; border-radius: 8px; padding: 16px; }
.wb-panel h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .12em;
               color: #7d8894; margin: 0 0 12px; }
.wb-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin-bottom: 22px; }
.wb-stat { background: #12171c; border: 1px solid #232b33; border-radius: 8px;
           padding: 16px; text-align: center; }
.wb-stat .v { font-size: 42px; font-weight: 700; line-height: 1; }
.wb-stat .l { font-size: 12px; text-transform: uppercase; letter-spacing: .12em;
              color: #7d8894; margin-top: 7px; }
.wb-stat.down .v { color: #ef6b60; }
.wb-stat.up .v   { color: #4ac776; }
.wb-row { display: flex; justify-content: space-between; gap: 12px;
          padding: 8px 0; border-bottom: 1px solid #1d242b; }
.wb-row:last-child { border-bottom: 0; }
.wb-affected { font-weight: 700; color: #ef6b60; }

/* A stale wallboard showing all-green because polling died is worse than no
   wallboard at all. So it says so, loudly (D12). */
.wb-stale {
    background: #3d1a17; border: 2px solid #ef6b60; color: #ff9d94;
    padding: 14px 18px; border-radius: 8px; margin-bottom: 18px;
    font-size: 19px; font-weight: 700; text-align: center;
}

/* ---------- Topology ---------- */

#graph { width: 100%; height: 720px; background: var(--surface);
         border: 1px solid var(--border); border-radius: var(--radius); }
.legend { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 10px;
          font-size: 12.5px; color: var(--text-dim); }
.legend span { display: flex; align-items: center; gap: 6px; }
.swatch { width: 11px; height: 11px; border-radius: 50%; display: inline-block; }

/* A line in the legend is drawn, not spelled. "- -" set in the page font is a
   different thickness, colour and rhythm from what cytoscape strokes, so it
   taught the wrong mark; a border does the same job in the same weight. */
.line-swatch { width: 24px; height: 0; display: inline-block; flex: none;
               border-top: 2px solid var(--text-dim); }
.line-swatch.is-wireless { border-top-style: dashed; }
.line-swatch.is-relation { border-top-style: dotted; }

/* ---------- The heartbeat: a change, and a page that stopped updating ----------
 *
 * A device whose state moved while somebody was looking flashes ONCE and then
 * gets out of the way. The row tints and the node takes a ring, which is the
 * same pair of treatments hover and selection already use on this page - one
 * visual language (D24), not a new one for this.
 *
 * **The node's is a box-shadow, like every other node state.** A border or a
 * padding that changes on a class makes the div disagree with the node
 * cytoscape laid out underneath it, and the edges stop meeting the boxes.
 */
@keyframes cms-changed-row {
    from { background: var(--amber-bg); }
    to   { background: transparent; }
}
@keyframes cms-changed-node {
    from { box-shadow: 0 0 0 3px var(--amber); }
    to   { box-shadow: 0 0 0 3px transparent; }
}
.equipment-table tr.is-changed > td { animation: cms-changed-row 1.6s ease-out; }
.gnode.is-changed { animation: cms-changed-node 1.6s ease-out; }

/* Somebody who prefers less motion still needs to SEE the change, so the tint
   stays and only the fade goes. */
@media (prefers-reduced-motion: reduce) {
    .equipment-table tr.is-changed > td { animation-duration: 0.01s; background: var(--amber-bg); }
    .gnode.is-changed { animation-duration: 0.01s; box-shadow: 0 0 0 3px var(--amber); }
}

/* "1 new device on this site." The page never reloads itself - this is how it
   says so and hands the decision over. */
.live-new { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
            margin-bottom: var(--gap); padding: 10px 14px;
            border: 1px solid var(--amber); border-radius: var(--radius);
            background: var(--amber-bg); font-size: 13px; }

/* The heartbeat stopped getting through. Every pill on the page is now a claim
   nothing is checking, so it says so rather than showing stale confidence. */
.live-dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block;
            background: var(--green); opacity: 0.55; flex: none; }
.is-stale .live-dot { background: var(--red); opacity: 1; }
.live-flag { display: inline-flex; align-items: center; gap: 6px;
             font-size: 12px; color: var(--text-dim); }
.live-flag .live-words::after { content: 'live'; }
.is-stale.live-flag .live-words::after { content: 'not updating'; }

.empty { padding: 34px 18px; text-align: center; color: var(--text-dim); }
.empty strong { display: block; margin-bottom: 5px; color: var(--text); }

/* ---------- Photos (D43) ----------
 *
 * The CMS is the only copy of these, so the grid is built to be read rather
 * than skimmed: a real thumbnail, what it shows, and who put it there.
 */

.photo-upload {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 12px;
    margin-bottom: var(--gap);
}

.photo-upload-row {
    display: flex;
    gap: 8px;
    align-items: center;
    flex-wrap: wrap;
}

.photo-upload-row input[type="text"],
.photo-upload-row select {
    padding: 8px 10px;
    font: inherit;
    color: var(--text);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.photo-upload-row input[type="text"] { flex: 1; min-width: 180px; }

.photo-pick { cursor: pointer; }

/* Thumb-sized targets: this is used one-handed, on a roof. */
.photo-actions { display: flex; gap: 8px; margin-bottom: 10px; }
.photo-actions .btn.big,
.btn.big {
    flex: 1;
    padding: 14px 16px;
    font-size: 16px;
    text-align: center;
}

.photo-queue { margin-top: 10px; display: grid; gap: 4px; }

.photo-queue-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 6px 8px;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    font-size: 13px;
}

.photo-queue-name {
    flex: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.photo-queue-state { color: var(--text-dim); }
.photo-queue-row.done   { border-color: var(--green); }
.photo-queue-row.done   .photo-queue-state { color: var(--green); }
.photo-queue-row.failed { border-color: var(--red); background: var(--red-bg); }
.photo-queue-row.failed .photo-queue-state { color: var(--red); }

.photo-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
    gap: 12px;
}

.photo-tile {
    margin: 0;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
}

.photo-open {
    display: block;
    width: 100%;
    padding: 0;
    border: 0;
    background: var(--neutral-bg);
    cursor: zoom-in;
}

.photo-open img {
    display: block;
    width: 100%;
    aspect-ratio: 4 / 3;
    object-fit: cover;
}

.photo-tile figcaption { padding: 8px 10px; }
.photo-meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.photo-caption { margin-top: 4px; }
.photo-sub { margin-top: 2px; font-size: 12px; }

.photo-edit { border-top: 1px solid var(--border); }
.photo-edit summary {
    padding: 6px 10px;
    cursor: pointer;
    color: var(--text-dim);
    font-size: 12px;
}
.photo-edit form { padding: 0 10px 10px; }

.lightbox {
    position: fixed;
    inset: 0;
    z-index: 200;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 12px;
    padding: 20px;
    background: rgba(0, 0, 0, .88);
}

.lightbox img {
    max-width: 100%;
    max-height: calc(100vh - 90px);
    object-fit: contain;
    border-radius: var(--radius);
}

.lightbox-bar {
    display: flex;
    align-items: center;
    gap: 10px;
    color: #fff;
    max-width: 100%;
    flex-wrap: wrap;
    justify-content: center;
}

/* The field app is a phone screen first. */
.field-app .page { max-width: 680px; }
.field-app .photo-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }

@media (max-width: 600px) {
    .photo-upload-row { flex-direction: column; align-items: stretch; }
    .photo-upload-row input[type="text"] { min-width: 0; }
}

/* "Don't ask for a code on this device" (D44). A checkbox on the auth cards,
   which are otherwise all full-width fields - and "Ping it from the CMS" in a
   site's add-device form, where `.edit-form input` would stretch the box
   across the field with its words underneath (width: auto). */
.remember {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 4px 0 14px;
    font-size: 13px;
    color: var(--text-dim);
    cursor: pointer;
}
.remember input { margin: 0; width: auto; }

/* Connect mode on a site's topology: pick two devices, link them. The form sits
   directly under the map so the picture and the thing you are editing are never
   on separate screens. */
.connect-form {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
    margin-top: 10px;
    padding: 10px 12px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--surface-2);
}

/* The global `[hidden]` rule at the top of this file covers this now - it was
   written here first, for this form, and then the same bug arrived on the "new
   device" banner. Kept as a comment rather than a duplicate so the next person
   looks up rather than adding a third copy. */

.connect-form input[type="text"] { flex: 1 1 200px; min-width: 0; }

#connect-bar { margin-bottom: 10px; }

/* ---------------------------------------------------------------------------
 * Theme switch
 *
 * Which glyph shows is decided by CSS on the root attribute, not by JavaScript,
 * so it is correct on the FIRST frame - the inline script in <head> has already
 * set the attribute by the time this is applied. A toggle that renders the wrong
 * icon for one frame on every page load is the kind of small wrongness people
 * stop trusting the rest of the page for.
 * ------------------------------------------------------------------------ */
.theme-toggle {
    display: grid;
    place-items: center;
    background: none;
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    color: var(--text-dim);
    cursor: pointer;
    line-height: 0;
    padding: 6px;
    margin-right: 10px;
}
.theme-toggle svg { display: block; }
.theme-toggle:hover { color: var(--text); border-color: var(--brand); }

/* Dark is the default, so the SUN shows (press it for light). */
.theme-toggle .t-moon { display: none; }
:root[data-theme="light"] .theme-toggle .t-sun  { display: none; }
:root[data-theme="light"] .theme-toggle .t-moon { display: block; }


/* The sign-in page wears the wordmark rather than a letter in a box: it is the
   first thing anyone sees and it should look like the company. Bigger than the
   topbar's, and centred with the card. */
.auth-brand {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    margin: 0 0 18px;
}
.auth-brand .brand-logo { width: 168px; }
.auth-brand .brand-tag { font-size: 13px; }

/* ---------------------------------------------------------------------------
   The API budget block on Admin > Integrations.

   Only a metered source draws one, so this is Ruijie's 5,000 a day and nothing
   else today. The bar uses the same green/amber/red the pills do, and takes
   them from the same tokens - a budget at 92% has to read as the same kind of
   trouble as a device that is down (D24, one visual language).
   -------------------------------------------------------------------------- */
.budget { margin: 0 0 14px; }
.budget-head { font-size: 13px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.budget-bar {
    margin-top: 6px;
    height: 6px;
    border-radius: 3px;
    background: var(--surface-2);
    overflow: hidden;
}
/* A span rather than a pseudo-element: the width is the one thing the server
   computes, and it has to land on an element the template can set it on. */
.budget-bar > span { display: block; height: 100%; border-radius: 3px; }
.budget-bar > span.green { background: var(--green); }
.budget-bar > span.amber { background: var(--amber); }
.budget-bar > span.red   { background: var(--red); }
.budget-detail { font-size: 11.5px; margin-top: 5px; }

/* ---------------------------------------------------------------------------
   Network Power (/power, and the Power card on a site page).

   One card per Cerbo. The number is the answer, so it is the biggest thing on
   the card; the bar and its colour say how worried to be, from the same
   tokens as the pills (D24). A card whose reading is old is greyed where it
   stands rather than moved - it is still the best thing known about that site.
   -------------------------------------------------------------------------- */
.power-section { margin-bottom: 26px; }
.power-section-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
                      margin-bottom: 10px; }
.power-section-head h2 { margin: 0; font-size: 13px; text-transform: uppercase;
                         letter-spacing: .06em; color: var(--text-dim); }
.power-section-head .faint { font-size: 12px; }
.power-none { font-size: 13px; padding: 4px 0; }

.power-grid { display: grid; gap: 14px; align-items: start;
              grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); }

.power-card { display: block; padding: 12px 14px 11px; color: var(--text);
              background: var(--surface); border: 1px solid var(--border);
              border-left: 3px solid var(--power-tone, var(--border));
              border-radius: var(--radius); box-shadow: var(--shadow-sm);
              text-decoration: none; }
/* A card opens in place (a <details>); the summary is the whole closed card.
   align-items: start on the grid, or opening one card stretches its row. */
.power-card > summary { display: block; list-style: none; cursor: pointer; }
.power-card > summary::-webkit-details-marker { display: none; }
.power-card > summary > span { display: block; }
.power-card > summary > .power-card-head { display: flex; }
.power-card:hover, .power-card[open] { border-color: var(--brand); border-left-color: var(--power-tone, var(--brand)); }
.power-card > summary:focus-visible { outline: 2px solid var(--brand); outline-offset: 4px; border-radius: 4px; }
.power-more { margin-top: 11px; padding-top: 11px; border-top: 1px solid var(--border-soft); }
/* The open card: headed groups of label-left, value-right rows, the value in
   the same right-hand column as the countdown times above it. */
.power-group { display: grid; grid-template-columns: 1fr auto; column-gap: 12px; row-gap: 4px; }
.power-group + .power-group { margin-top: 12px; }
.power-group-h { grid-column: 1 / -1; margin-bottom: 1px; font-size: 10.5px; font-weight: 600;
                 letter-spacing: .08em; text-transform: uppercase; color: var(--text-faint); }
.power-kv { display: contents; }
.power-kv > span:first-child { font-size: 13px; color: var(--text-dim); }
.power-kv > span:first-child small { font-size: 11.5px; color: var(--text-faint); }
.power-kv > span:first-child small::before { content: "· "; }
.power-kv > :last-child { font-size: 13px; text-align: right; white-space: nowrap;
                          font-variant-numeric: tabular-nums; }
.power-kv > strong { font-size: 13.5px; }
.power-foot { display: flex; justify-content: flex-end; margin-top: 12px; }
.power-card.level-red   { --power-tone: var(--red); }
.power-card.level-amber { --power-tone: var(--amber); }
.power-card.level-brand { --power-tone: var(--brand); }
.power-card.level-green { --power-tone: var(--green); }
.power-card.is-stale { opacity: .5; filter: grayscale(1); }
.power-card.is-stale .power-read { color: var(--red); }

.power-card-head { display: flex; align-items: center; gap: 8px; }
.power-site { font-weight: 600; font-size: 14px; overflow: hidden;
              text-overflow: ellipsis; white-space: nowrap; }
.power-arrow { margin-left: auto; font-size: 18px; line-height: 1; font-weight: 700; color: var(--power-tone); }

.power-soc { font-family: var(--display); font-size: 34px; line-height: 1.1;
             margin: 6px 0 8px; color: var(--power-tone, var(--text)); }
.power-soc small { font-size: 16px; margin-left: 2px; color: var(--text-dim); }

.power-bar { position: relative; height: 7px; border-radius: 4px;
             background: var(--surface-2); overflow: hidden; }
.power-bar > span { display: block; height: 100%; border-radius: 4px;
                    background: var(--power-tone, var(--neutral)); }
/* Where "dead" is. Drawn over the bar, so a battery near it reads as near it. */
.power-cutoff { position: absolute; top: 0; bottom: 0; width: 2px; margin-left: -1px;
                background: var(--text); opacity: .7; }

.power-meta { display: flex; flex-direction: column; gap: 1px; margin-top: 3px;
              font-size: 11.5px; }

/* On a site page the card sits inside the Power drawer, beside its form. */
.site-power-card { max-width: 280px; margin-bottom: 14px; }
.site-power-form .grid.cols-3 { gap: 10px; }

/* A solar site's battery runs down every night by design, so its card carries
   a sun: "on backup" there is the plan, not a fault. Drawn like the role marks
   (a mask over currentColor, never an emoji - D24), and in the amber the
   stylesheet already has rather than a new colour. */
:root {
    --icon-sun: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3' fill='%23000'/%3E%3Cg stroke='%23000' stroke-width='1.6' stroke-linecap='round'%3E%3Cpath d='M8 1.2v1.8M8 13v1.8M1.2 8h1.8M13 8h1.8M3.2 3.2l1.3 1.3M11.5 11.5l1.3 1.3M3.2 12.8l1.3-1.3M11.5 4.5l1.3-1.3'/%3E%3C/g%3E%3C/svg%3E");
}
.power-supply { display: inline-flex; align-items: center; gap: 4px; flex: none;
                padding: 1px 7px 1px 5px; border-radius: var(--radius-pill);
                background: var(--amber-bg); color: var(--amber);
                font-size: 11px; font-weight: 600; white-space: nowrap; }
.power-sun { width: 13px; height: 13px; display: inline-block; background: currentColor;
             -webkit-mask: var(--icon-sun) center / contain no-repeat;
                     mask: var(--icon-sun) center / contain no-repeat; }

/* "If power fails: ≈ 19h to 20%" on a site that is charging or full. The live
   estimate above it is the headline; this is the reserve behind it, so it is a
   quieter line in the same weight family, not a second headline. */

/* A POWERCHECK site's percentage is worked out, not read: "≈ 64%" and a small
   word under it, so it can never be mistaken for a Cerbo's measurement. */
.power-est { display: block; margin-top: 1px; font-family: var(--sans); font-size: 10.5px;
             letter-spacing: .06em; text-transform: uppercase; color: var(--text-faint); }
.power-card.level-neutral { --power-tone: var(--neutral); }
/* Not set up: a mains light and a name, nothing to measure yet, so a small card. */
.power-unset .power-grid { grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 8px; }
.power-unset .power-card { padding: 9px 12px; }
.power-unset .power-card > summary > .power-lines { margin-top: 4px; }
.power-estimate-fields { margin: 4px 0 6px; padding-top: 10px; border-top: 1px solid var(--border-soft); }
.power-estimate-fields h3 { margin: 0 0 8px; }

/* The countdowns: what happens on the left, when on the right, one row each.
   The rows share one grid so the two times line up; the headline row is the
   bold one, "If power fails" the quiet one. */
.power-card > summary > .power-lines { display: grid; grid-template-columns: 1fr auto;
                                       column-gap: 12px; row-gap: 3px; margin-top: 9px; }
.power-line { display: contents; }
.power-line > span:first-child { font-size: 12.5px; color: var(--text-dim); }
.power-line.is-main > span:first-child { font-size: 14px; font-weight: 600; color: var(--text); }
.power-at { font-size: 12.5px; color: var(--text-dim); text-align: right; white-space: nowrap;
            font-variant-numeric: tabular-nums; align-self: baseline; }

/* ---------- Contract templates (D105) ---------- */

.contract-upload { display: grid; grid-template-columns: 2fr 1fr 2fr auto; gap: 12px; align-items: end; }
.contract-upload .field { margin: 0; }
.contract-errors { margin: 6px 0 0; padding-left: 18px; }
.contract-naming { margin-top: 16px; }
.contract-naming summary { cursor: pointer; color: var(--text-dim); font-size: 13px; }
.contract-naming table { margin-top: 8px; }
.contract-actions { display: flex; gap: 8px; align-items: center; margin-bottom: var(--gap); }
.contract-actions form { margin: 0; }
.contract-fields td { vertical-align: middle; }
.contract-fields td.center { text-align: center; }
.contract-fields input[type="text"], .contract-fields select {
    width: 100%; min-width: 150px; padding: 6px 9px; font: inherit; font-size: 13px;
    color: var(--text); background: var(--surface-2);
    border: 1px solid var(--border); border-radius: var(--radius);
}
.contract-field-name { font-size: 11.5px; margin-top: 3px; }
/* Staff rows stand out: those are the ones support fills in for every contract. */
.contract-fields tr.is-staff > td:first-child { box-shadow: inset 3px 0 0 var(--brand); }

@media (max-width: 760px) {
    .contract-upload { grid-template-columns: 1fr; }
}

/* ---------- A customer's contract (D105) ---------- */

.contract-pick .field { max-width: 520px; margin: 0; }
.contract-staff-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 0 14px; }
.field .req { color: var(--red); }
.field.has-error input { border-color: var(--red); }
.field-error { color: var(--red); font-size: 12.5px; margin-top: 4px; }
.contract-link { border-color: var(--brand); }
.contract-once { margin-top: 0; }
.contract-code { font-size: 20px; letter-spacing: .12em; }
.contract-url { word-break: break-all; }
.contract-message {
    display: block; width: 100%; margin-top: 4px; padding: 8px 11px; font: inherit; font-size: 13px;
    color: var(--text); background: var(--surface-2);
    border: 1px solid var(--border); border-radius: var(--radius); resize: vertical;
}
.contract-copy { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.contract-offer { font-size: 11px; word-break: break-all; }
.contract-void { margin-top: 14px; display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; }
.contract-void .field { flex: 1 1 220px; margin: 0; }
.contract-new { padding: 10px 14px; border-top: 1px solid var(--border); }
/* One rhythm for the contract pages: every block, cards inside a form or a
   grid included, is a gap apart. The global .card + .card margin is cancelled
   here because inside a grid it pushes the second column's card down. */
.contract-stack > *, .contract-stack > form > .card { margin-bottom: var(--gap); }
.contract-stack .card + .card { margin-top: 0; }
.contract-stack .grid { align-items: start; }

/* ---------- Error pages (404, 500): staff and customers both land here ---------- */

.error-card { text-align: center; }
.error-card h1 { margin-bottom: 14px; }
.error-card h2 { margin: 0 0 8px; font-size: 18px; }
.error-card p { margin: 0 0 14px; }
.error-card p:last-child { margin-bottom: 0; }
.error-contact { font-size: 13.5px; }

/* ---------- The customer's signing pages, /esign (D105) ---------- */
/* Light always (layout-esign sets data-theme). Phone first. The customer fills
   in ON the pages; a bar fixed to the bottom does the typing and drawing. */

body.esign { background: var(--bg); }
.esign-top { display: flex; justify-content: center; padding: 18px 16px 6px; }
.esign-top .brand-logo { width: 150px; }
.esign-main { max-width: 860px; margin: 0 auto; padding: 8px 16px 24px; }
.esign-foot { text-align: center; color: var(--text-faint); font-size: 12px; padding: 0 16px 28px; }
.esign-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
              padding: 18px; margin-bottom: 16px; }
.esign-card h1 { font-size: 21px; margin: 0 0 10px; }
.esign-narrow { max-width: 460px; margin: 24px auto 16px; }
.esign-alt { color: var(--text-dim); font-weight: normal; }
.esign-second { border-top: 1px solid var(--border); margin-top: 16px; padding-top: 16px; }
.esign-second h1 { font-size: 17px; }
.esign-wide { width: 100%; padding: 12px; font-size: 15px; }
.esign-title { font-size: 22px; margin: 10px 0 2px; }
.esign-sub { margin: 0 0 10px; }
.esign-intro { margin: 0 0 14px; }
.banner ul { margin: 6px 0 0; padding-left: 18px; }

/* The contract pages, with every value laid over its field. */
.esign-pages { display: grid; gap: 18px; }
.esign-page { position: relative; width: 100%; padding-top: var(--ratio);
              background: #fff; border: 1px solid var(--border); border-radius: 4px; overflow: hidden;
              container-type: inline-size; }
.esign-page > img { position: absolute; inset: 0; width: 100%; height: 100%; }
.esign-page-link { display: inline-block; margin-top: 4px; font-size: 12.5px; }
/* 10 pt on a 595 pt page is 1.68% of its width: the size the PDF prints. */
.esign-ov { position: absolute; display: flex; align-items: center; overflow: hidden; white-space: nowrap;
            font-size: 1.6cqw; line-height: 1; color: #0b1f4a; padding: 0 .3cqw; }
.esign-ov[style*="text-align:center"] { justify-content: center; }
.esign-ov[style*="text-align:right"] { justify-content: flex-end; }
/* Yellow: still for the customer to do. Pale green: done. Blue ring: now. */
.esign-ov[data-target] { background: rgba(255, 214, 0, .32); cursor: pointer; }
.esign-ov[data-target].is-filled,
.esign-ov[data-target].is-done { background: rgba(31, 157, 85, .10); }
.esign-ov[data-target]:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.esign-ov.is-active { box-shadow: 0 0 0 2px var(--brand); background: rgba(46, 125, 214, .14); }
.esign-ov.is-mark { justify-content: center; }
.esign-ov.is-mark img { max-width: 100%; max-height: 100%; }

/* The bar: label, one control, Back / Next. Fixed to the bottom, never wider
   than the page column, and as short as the step allows. */
.esign-bar { position: fixed; left: 0; right: 0; bottom: 0; z-index: 20;
             background: var(--surface); border-top: 1px solid var(--border);
             box-shadow: 0 -6px 20px rgba(15, 30, 60, .12);
             padding: 12px 16px calc(12px + env(safe-area-inset-bottom)); }
.esign-bar-in { max-width: 828px; margin: 0 auto; }
.esign-bar-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 8px; }
.esign-bar-head strong { font-size: 15px; }
.esign-bar-req { font-size: 12px; color: var(--text-faint); }
.esign-bar-count { margin-left: auto; font-size: 12.5px; color: var(--text-dim); white-space: nowrap; }
.esign-bar-input { width: 100%; padding: 11px 12px; font: inherit; font-size: 16px; /* 16px: no iOS zoom */
                   color: var(--text); background: var(--surface-2);
                   border: 1px solid var(--border); border-radius: var(--radius); }
.esign-bar-input:focus { outline: 2px solid var(--brand); outline-offset: -1px; }
.esign-bar-hint { margin: 0 0 8px; font-size: 13px; color: var(--text-dim); }
.esign-bar-panel + .esign-bar-panel:not([hidden]) { margin-top: 10px; }
.esign-pad-box { display: flex; gap: 8px; align-items: flex-end; }
.esign-pad { display: block; flex: 1 1 auto; min-width: 0; width: 100%; height: 120px; background: #fff; touch-action: none;
             border: 1px dashed var(--text-faint); border-radius: var(--radius); cursor: crosshair; }
.esign-pad.is-initials { max-width: 320px; height: 110px; }
.esign-pad.is-signature { height: 140px; }
.esign-pad-clear { flex: none; padding: 5px 10px; font-size: 12.5px; }
.esign-clause-text { margin: 0 0 4px; padding: 10px 12px; font-size: 15px; line-height: 1.45;
                     background: var(--surface-2); border-left: 3px solid var(--amber); border-radius: 3px; }
.esign-clause-crop { position: relative; width: 100%; overflow: hidden; background: #fff;
                     outline: 1px solid var(--border); border-radius: 3px; }
.esign-clause-crop img { position: absolute; max-width: none; }
.esign-place { margin-top: 8px; }
.esign-placed { display: inline-flex; margin-top: 8px; color: var(--green); font-weight: 600; }
.esign-bar-nav { display: flex; justify-content: space-between; gap: 10px; margin-top: 12px; }
.esign-bar-nav .btn { min-width: 110px; padding: 9px 14px; }
.esign-missing { margin-bottom: 10px; font-size: 13.5px; }
.esign-missing-list { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.esign-missing-item { padding: 4px 10px; font-size: 12.5px; border-color: var(--amber); }
.esign-agree { display: flex; gap: 10px; align-items: flex-start; margin: 0 0 12px; cursor: pointer; font-size: 14px; }
.esign-agree input { width: 20px; height: 20px; margin-top: 1px; flex: none; }
.esign-agree.is-needed { color: var(--red); }

.esign-done { text-align: center; }
.esign-tick { width: 54px; height: 54px; margin: 4px auto 10px; border-radius: 50%; display: grid; place-items: center;
              background: var(--green-bg); color: var(--green); font-size: 28px; }

@media (max-width: 620px) {
    .esign-card { padding: 14px; }
    .esign-pad.is-initials { max-width: none; }
}
.esign-demo { text-align: center; }
.esign-demo-table td { padding: 6px 8px; }
.esign-demo-mark { max-height: 60px; max-width: 260px; background: #fff; border: 1px solid var(--border); border-radius: 3px; }
.contract-demo { margin-bottom: var(--gap); }
.contract-demo .contract-copy form { margin: 0; }
.esign-demo-pdf { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 14px; }

/* ---------- Modules: the DATONET ▾ switcher and the launcher (D106) ---------- */

.switcher { position: relative; }
.switcher > summary { list-style: none; cursor: pointer; padding: 4px 6px; margin: -4px -6px;
                      border-radius: var(--radius); }
.switcher > summary::-webkit-details-marker { display: none; }
.switcher > summary:hover, .switcher[open] > summary { background: var(--surface-2); }
.switcher-caret { color: var(--text-dim); font-size: 12px; line-height: 1; }
.switcher-panel {
    position: absolute; top: calc(100% + 10px); left: -6px; z-index: 60;
    width: min(380px, calc(100vw - 24px)); padding: 12px;
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
    box-shadow: 0 10px 30px rgba(0, 0, 0, .25);
}
.switcher-home { display: block; font-size: 12.5px; color: var(--text-dim); margin: 0 0 10px 2px; }
.switcher-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.switcher-item {
    display: flex; align-items: center; gap: 8px; padding: 9px 10px; border-radius: var(--radius);
    color: var(--text); border: 1px solid transparent; font-size: 14px;
}
a.switcher-item:hover { background: var(--brand-tint); text-decoration: none; }
.switcher-item.is-current { border-color: var(--brand); background: var(--brand-tint); }
.switcher-item.is-soon { color: var(--text-faint); cursor: default; }
.switcher-glyph { display: inline-grid; place-items: center; width: 26px; height: 26px; flex: none;
                  border-radius: 6px; background: var(--surface-2); font-size: 15px; }
.switcher-label { display: flex; flex-direction: column; line-height: 1.2; min-width: 0; }
.switcher-soon { font-size: 10px; text-transform: uppercase; letter-spacing: .05em; }

/* The module's own name, where "CMS" used to stand: which part of the system
   this page is, in the brand's display face. */
.module-name {
    font-family: var(--display); font-size: 12px; letter-spacing: .08em; text-transform: uppercase;
    color: var(--brand); padding-left: 10px; border-left: 1px solid var(--border); line-height: 1.6;
    white-space: nowrap;
}
.module-name:hover { text-decoration: none; color: var(--text); }

.launcher-hello { margin-bottom: 2px; }
.launcher-sub { margin: 0 0 18px; }
.launcher { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: var(--gap); }
.launcher-tile {
    display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto auto; column-gap: 12px;
    padding: 16px; border-radius: var(--radius); background: var(--surface); border: 1px solid var(--border);
    color: var(--text); min-height: 112px;
}
a.launcher-tile:hover { border-color: var(--brand); text-decoration: none; }
.launcher-glyph { grid-row: 1 / span 3; display: grid; place-items: center; width: 42px; height: 42px;
                  border-radius: 10px; background: var(--brand-tint); color: var(--brand); font-size: 21px; }
.launcher-name { font-weight: 600; font-size: 16px; }
.launcher-blurb { font-size: 13px; color: var(--text-dim); margin-top: 2px; }
.launcher-line { font-size: 13px; margin-top: 8px; color: var(--text); }
.launcher-line.is-amber { color: var(--amber); }
.launcher-line.is-red { color: var(--red); }
.launcher-tile.is-soon { opacity: .55; border-style: dashed; }
.launcher-tile.is-soon .launcher-glyph { background: var(--surface-2); color: var(--text-dim); }
.launcher-soon { font-size: 10.5px; font-weight: normal; text-transform: uppercase; letter-spacing: .05em;
                 color: var(--text-faint); margin-left: 6px; }

@media (max-width: 760px) {
    .module-name { display: none; }
}

/* Per-module access on the Users page (D106). */
.user-access { display: flex; flex-wrap: wrap; gap: 8px 14px; align-items: flex-end; margin-top: 10px;
               padding: 10px 12px; background: var(--surface-2); border-radius: var(--radius); }
.user-access-label { font-size: 12px; text-transform: uppercase; letter-spacing: .05em; color: var(--text-dim);
                     align-self: center; }
.user-access-module { display: flex; flex-direction: column; gap: 3px; font-size: 12.5px; color: var(--text-dim); }
.user-access-module select { padding: 5px 8px; font: inherit; font-size: 13px; color: var(--text);
                             background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); }
.user-access-note { margin: 8px 0 0; font-size: 13px; }
/* "Datonet eSign" (Daniel, 2026-10-16): the product's name beside the logo,
   set like the CMS's module name, so a customer sees one name everywhere. */
.esign-top { align-items: center; gap: 10px; }
.esign-mark { font-family: var(--display); font-size: 14px; letter-spacing: .08em; color: var(--brand);
              padding-left: 10px; border-left: 1px solid var(--border); line-height: 1.4; }
