/* ==========================================================================
   Fleet dashboard.

   A dense operational tool for a small number of technical users. Optimised for
   scanning 250 rows and spotting the broken ones, not for a screenshot. The
   fleet table IS the product; the strip is the one place visual effort is spent;
   everything else is deliberately plain.

   --------------------------------------------------------------------------
   COLOUR IS NOT THE ONLY ENCODING, AND THAT IS A MEASURED DECISION
   --------------------------------------------------------------------------
   The palette below is fixed by the build plan. Run through the OKLab/Machado
   CVD checks against the panel surface (#1A1F27), it produces:

     ok    <-> warn      protan dE  6.0   <-- floor band
     fail  <-> unknown   protan dE  7.8   <-- floor band
     ok    <-> fail      deutan dE  8.2       barely over target
     unknown vs panel    2.76:1           <-- below the 3:1 contrast floor

   So for a protanopic reader a healthy tile and a degraded one are nearly the
   same colour, and — worse — a FAILING device and an UNKNOWN one converge.
   That second pair would quietly invert the rule the whole system rests on
   (§9.4: unknown is not failed), for those readers, on the one page that is
   supposed to tell you what is broken.

   The palette stays as specified. Instead every state carries a second,
   non-colour encoding:

     state      fill         height   geometry
     ok         solid        60%      calm, short, recedes
     warn       45deg hatch  100%     texture -- separates it from ok
     fail       solid        100%     tall and solid
     unknown    hollow       100%     outline only -- separates it from fail
     free       dashed       40%      barely there

   ok vs warn is solved by texture and height. fail vs unknown is solved by
   solid vs hollow, which survives any CVD simulation because it is geometry.
   Every tile also carries a title and an aria-label naming its state, and every
   table row carries the state as text.

   Chip label colours are likewise computed, not chosen: near-black on ok, warn
   and fail (7.01, 8.38, 4.62:1) and white on unknown (6.00:1). White on the
   amber warn chip measures 2.18:1 and is unreadable, which is not obvious by
   eye.
   ========================================================================== */

:root {
    /* Instrument-panel dark, borrowed from the electrical metering these
       devices actually do. Not a generic dark theme with one accent. */
    --bg:      #12151A;
    --panel:   #1A1F27;
    --rule:    #2A323D;
    --text:    #DCE3EC;
    --muted:   #7A8799;
    --ok:      #4FB286;
    --warn:    #E0A458;
    --fail:    #D9534F;
    --unknown: #5A6472;

    /* Derived. Measured: near-black clears 4.6:1 or better on all three
       saturated chips; white only clears on the grey one. */
    --on-color:   #12151A;
    --on-unknown: #FFFFFF;
    --panel-hi: #212734;   /* row hover, sticky header */
    --focus:    #7FB3FF;   /* focus ring only -- never a data colour */

    --mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
    --sans: "IBM Plex Sans Condensed", "IBM Plex Sans", Inter, system-ui,
            -apple-system, "Segoe UI", sans-serif;
}

*, *::before, *::after { box-sizing: border-box; }

html { color-scheme: dark; }

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font-family: var(--sans);
    /* Condensed, with tracking tightened, so 250 rows fit without horizontal
       scrolling at 1280px. */
    letter-spacing: -0.006em;
    font-size: 13px;
    line-height: 1.45;
    -webkit-font-smoothing: antialiased;
}

/* Misaligned digits make a fleet table unreadable -- every number, everywhere. */
table, .num, .tnum, input, .strip-legend, .stat { font-variant-numeric: tabular-nums; }

code, kbd, .mono, .mac, .ip, .rid { font-family: var(--mono); font-size: 0.92em; }

a { color: var(--text); text-decoration-color: var(--rule); text-underline-offset: 2px; }
a:hover { text-decoration-color: var(--muted); }

/* Focus must be visible -- it is part of the quality floor, and this is a
   keyboard-driven tool. Never remove the ring; only restyle it. */
:focus-visible {
    outline: 2px solid var(--focus);
    outline-offset: 1px;
    border-radius: 2px;
}

/* Respect reduced motion. Nothing here animates meaningfully anyway; this
   guarantees a future addition cannot regress it. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.001ms !important;
        transition-duration: 0.001ms !important;
    }
}

.skip-link {
    position: absolute; left: -9999px;
    background: var(--panel); color: var(--text);
    padding: 8px 12px; z-index: 100; border: 1px solid var(--rule);
}
.skip-link:focus { left: 8px; top: 8px; }

/* ---------------------------------------------------------------- layout -- */

.wrap { max-width: 1600px; margin: 0 auto; padding: 0 16px 48px; }

header.top {
    display: flex; align-items: baseline; gap: 20px; flex-wrap: wrap;
    padding: 10px 16px;
    border-bottom: 1px solid var(--rule);
    background: var(--panel);
}
header.top h1 {
    font-size: 14px; font-weight: 600; margin: 0;
    letter-spacing: 0.02em; text-transform: uppercase;
}
header.top nav { display: flex; gap: 14px; flex-wrap: wrap; }
header.top nav a {
    color: var(--muted); text-decoration: none;
    padding: 2px 0; border-bottom: 2px solid transparent;
}
header.top nav a:hover { color: var(--text); }
header.top nav a[aria-current="page"] {
    color: var(--text); border-bottom-color: var(--muted);
}
header.top .spacer { flex: 1; }
header.top .who { color: var(--muted); }
header.top .who strong { color: var(--text); font-weight: 500; }

/* Header stat row. Numbers, not cards -- §11 resists dashboards composed of
   large KPI cards, and at 250 devices the counts belong in the chrome where
   they are always visible rather than occupying the top third of the page. */
.stats { display: flex; gap: 18px; flex-wrap: wrap; align-items: baseline; }
.stat { color: var(--muted); white-space: nowrap; }
.stat b { color: var(--text); font-weight: 600; font-size: 15px; }
.stat.is-fail b { color: var(--fail); }
.stat.is-warn b { color: var(--warn); }
.stat.is-ok   b { color: var(--ok); }

/* --------------------------------------------------------------- banners -- */

.banner {
    display: flex; gap: 10px; align-items: flex-start;
    margin: 10px 0; padding: 9px 12px;
    background: var(--panel);
    border: 1px solid var(--rule);
    border-left: 3px solid var(--muted);
}
.banner .icon { flex: 0 0 auto; font-family: var(--mono); font-weight: 700; }
.banner .body { flex: 1; }
.banner .detail { color: var(--muted); margin-top: 3px; }
/* Quiet by design. Acknowledging a banner should not compete for attention with
   the banner, and a prominent button beside a warning invites clearing it
   before reading it. */
.banner-dismiss { flex: 0 0 auto; }
.banner-dismiss button {
    background: transparent; border: 1px solid var(--rule);
    color: var(--muted); font-size: 0.85em; padding: 2px 8px; cursor: pointer;
}
.banner-dismiss button:hover { color: var(--fg); border-color: var(--muted); }
.banner.fail { border-left-color: var(--fail); }
.banner.warn { border-left-color: var(--warn); }
.banner.ok   { border-left-color: var(--ok); }
.banner.fail .icon { color: var(--fail); }
.banner.warn .icon { color: var(--warn); }
.banner.ok   .icon { color: var(--ok); }

/* =========================================================== fleet strip == */
/* The signature element. 253 tiles, one per suffix in the 2-254 address space
   -- not one per device that happens to exist. A gap is as informative as a
   failure: it is the remaining capacity of a fleet with three names to spare,
   and a strip built from live devices alone cannot show one.
   Ordered by suffix, so a run of red means one site or one firmware
   generation and scattered red means something else. */

.strip-panel {
    background: var(--panel);
    border: 1px solid var(--rule);
    padding: 10px 12px 8px;
    margin: 10px 0 14px;
}
.strip-head {
    display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap;
    margin-bottom: 8px;
}
.strip-head h2 {
    font-size: 11px; font-weight: 600; margin: 0;
    text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted);
}

.strip {
    display: grid;
    /* Fixed tracks so tiles stay aligned to the suffix sequence at any width;
       the strip wraps into rows rather than squeezing tiles to sub-pixel. */
    grid-template-columns: repeat(auto-fill, 14px);
    gap: 3px;
    align-items: end;
    /* No horizontal page scroll: the strip wraps instead. */
    max-width: 100%;
}

.tile {
    display: block;
    width: 14px; height: 22px;
    position: relative;
    text-decoration: none;
    border: 0; padding: 0;
    background: transparent;
}
/* The coloured mark inside the tile. Anchored to the baseline so height reads
   as a magnitude, with a 4px rounded top -- the data end, not the whole box. */
.tile::after {
    content: "";
    position: absolute; left: 0; right: 0; bottom: 0;
    border-radius: 3px 3px 1px 1px;
}

/* ok: solid, short. Healthy devices should recede -- 240 of these are the
   background against which the broken ones stand out. */
.tile.t-ok::after      { height: 60%; background: var(--ok); }

/* warn: 45deg hatch, full height. Texture is what separates it from ok for a
   protanopic reader, where the two colours measure dE 6.0. */
.tile.t-warn::after {
    height: 100%;
    background: repeating-linear-gradient(
        45deg,
        var(--warn) 0 2px,
        color-mix(in srgb, var(--warn) 45%, var(--panel)) 2px 4px);
}

/* fail: solid, full height. */
.tile.t-fail::after    { height: 100%; background: var(--fail); }

/* unknown: HOLLOW, full height. Solid-vs-hollow is geometry, so it survives
   every CVD simulation -- which matters because fail and unknown measure only
   dE 7.8 apart under protanopia, and unknown reading as failed is the one
   inversion this system must not make. */
.tile.t-unknown::after {
    height: 100%;
    background: transparent;
    border: 1.5px solid var(--unknown);
}

/* free / reserved: the address space that has no device. Barely there. */
.tile.t-free::after {
    height: 40%;
    background: transparent;
    border: 1px dashed color-mix(in srgb, var(--rule) 70%, var(--muted));
}
.tile.t-reserved::after {
    height: 40%;
    background: transparent;
    border: 1px solid var(--muted);
}

.tile:hover::after { filter: brightness(1.35); }
.tile:focus-visible { outline-offset: 2px; }

/* Legend is always present: identity is never colour-alone. */
.strip-legend {
    display: flex; gap: 14px; flex-wrap: wrap;
    margin-top: 9px; color: var(--muted); font-size: 12px;
}
.strip-legend .key { display: inline-flex; align-items: center; gap: 5px; }
.strip-legend .swatch {
    width: 11px; height: 14px; position: relative; display: inline-block;
}
.strip-legend .swatch::after {
    content: ""; position: absolute; left: 0; right: 0; bottom: 0;
    border-radius: 2px 2px 1px 1px;
}
.strip-legend .swatch.t-ok::after      { height: 60%;  background: var(--ok); }
.strip-legend .swatch.t-warn::after    { height: 100%; background:
    repeating-linear-gradient(45deg, var(--warn) 0 2px,
        color-mix(in srgb, var(--warn) 45%, var(--panel)) 2px 4px); }
.strip-legend .swatch.t-fail::after    { height: 100%; background: var(--fail); }
.strip-legend .swatch.t-unknown::after { height: 100%; background: transparent;
    border: 1.5px solid var(--unknown); }
.strip-legend .swatch.t-free::after    { height: 40%; background: transparent;
    border: 1px dashed var(--rule); }

/* ============================================================== controls == */

.controls {
    display: flex; gap: 10px; flex-wrap: wrap; align-items: center;
    margin: 0 0 10px;
}
.controls form { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }

input[type="text"], input[type="password"], input[type="search"], select {
    background: var(--bg);
    color: var(--text);
    border: 1px solid var(--rule);
    padding: 5px 8px;
    font: inherit;
    font-family: var(--sans);
    border-radius: 2px;
    min-height: 30px;
}
input::placeholder { color: var(--muted); }
select { padding-right: 24px; }

button, .btn {
    background: var(--panel-hi);
    color: var(--text);
    border: 1px solid var(--rule);
    padding: 5px 11px;
    font: inherit;
    border-radius: 2px;
    cursor: pointer;
    min-height: 30px;
}
button:hover, .btn:hover { border-color: var(--muted); }
button[disabled], .btn[disabled] {
    opacity: 0.45; cursor: not-allowed;
}
button.primary { border-color: var(--muted); }
button.danger  { border-color: var(--fail); color: var(--fail); }
button.danger:hover { background: color-mix(in srgb, var(--fail) 16%, var(--panel)); }

.toggle {
    display: inline-flex; align-items: center; gap: 6px;
    color: var(--muted); cursor: pointer; user-select: none;
}

/* ================================================================= table == */

.tablewrap {
    /* Wide content scrolls inside its own container. The page body must never
       scroll horizontally. */
    overflow-x: auto;
    border: 1px solid var(--rule);
    background: var(--panel);
}

table.fleet {
    width: 100%;
    border-collapse: collapse;
    /* At 1280px this fits without horizontal scroll; below that the wrapper
       scrolls rather than the page. */
    min-width: 1120px;
}

table.fleet thead th {
    position: sticky; top: 0; z-index: 2;
    background: var(--panel-hi);
    border-bottom: 1px solid var(--rule);
    text-align: left;
    font-size: 11px; font-weight: 600;
    text-transform: uppercase; letter-spacing: 0.06em;
    color: var(--muted);
    padding: 7px 9px;
    white-space: nowrap;
}
table.fleet tbody td {
    border-bottom: 1px solid color-mix(in srgb, var(--rule) 60%, var(--panel));
    padding: 5px 9px;
    white-space: nowrap;
    vertical-align: baseline;
}
table.fleet tbody tr:hover td { background: var(--panel-hi); }
table.fleet tbody tr:last-child td { border-bottom: 0; }
table.fleet td.num, table.fleet th.num { text-align: right; }
table.fleet td.wrap-ok { white-space: normal; }

/* A left edge that carries the tier, so the shape of the fleet is legible
   while scrolling even before reading a single label. */
table.fleet tbody tr { border-left: 3px solid transparent; }
table.fleet tbody tr.tier-fail    { border-left-color: var(--fail); }
table.fleet tbody tr.tier-warn    { border-left-color: var(--warn); }
table.fleet tbody tr.tier-unknown { border-left-color: var(--unknown); }
table.fleet tbody tr.tier-ok      { border-left-color: transparent; }

/* State chip. Always carries text -- never colour alone. Label colours are the
   measured ones: near-black on the saturated chips, white on the grey. */
.chip {
    display: inline-block;
    padding: 1px 7px;
    border-radius: 2px;
    font-size: 11px; font-weight: 600;
    letter-spacing: 0.02em;
    white-space: nowrap;
}
.chip.tier-ok      { background: var(--ok);      color: var(--on-color); }
.chip.tier-warn    { background: var(--warn);    color: var(--on-color); }
.chip.tier-fail    { background: var(--fail);    color: var(--on-color); }
.chip.tier-unknown { background: var(--unknown); color: var(--on-unknown); }

/* Outline variant for dense contexts where a solid block would dominate. */
.chip.ghost { background: transparent; border: 1px solid currentColor; }
.chip.ghost.tier-ok      { color: var(--ok); }
.chip.ghost.tier-warn    { color: var(--warn); }
.chip.ghost.tier-fail    { color: var(--fail); }
.chip.ghost.tier-unknown { color: var(--muted); }

/* An unknown value is an em dash in muted grey. Never 0, never "no", never
   red -- §9.4. */
.unk { color: var(--muted); }

.muted { color: var(--muted); }
.small { font-size: 12px; }

/* ---------------------------------------------------------------- utility --
   These exist so that NO template needs a style="" attribute.
   A strict Content-Security-Policy blocks inline style attributes as well as
   inline script, and `style-src 'self' 'unsafe-inline'` would have to be granted
   to the whole document to allow one `margin-top:0`. On a page with a Reboot
   button, spending the CSP on cosmetics is a bad trade — so the cosmetics move
   here and the policy stays strict. */
.mt0  { margin-top: 0; }
.mt8  { margin-top: 8px; }
.mt12 { margin-top: 12px; }
.mt14 { margin-top: 14px; }
.mb0  { margin-bottom: 0; }
.mb10 { margin-bottom: 10px; }
.tight { margin: 8px 0 0; }
.grow { flex: 1; }
.bare { text-decoration: none; }
.page-title { margin: 0; font-size: 16px; }
.row-hi { background: var(--panel-hi); }
.fail-text { color: var(--fail); }
.warn-text { color: var(--warn); }
/* A table that sits inside a .card and so needs no border of its own. */
.tablewrap.bare-wrap { border: 0; background: transparent; }
.truncate {
    max-width: 520px; overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap;
}

/* ============================================================== detail ==== */

.cols { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
@media (max-width: 1100px) { .cols { grid-template-columns: 1fr; } }

.card {
    background: var(--panel);
    border: 1px solid var(--rule);
    padding: 12px 14px;
    margin-bottom: 14px;
}
.card > h2 {
    font-size: 11px; font-weight: 600; margin: 0 0 9px;
    text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted);
}

/* Key/value grid for device facts. */
dl.kv { display: grid; grid-template-columns: auto 1fr; gap: 3px 14px; margin: 0; }
dl.kv dt { color: var(--muted); white-space: nowrap; }
dl.kv dd { margin: 0; }

table.plain { width: 100%; border-collapse: collapse; }
table.plain th {
    text-align: left; font-size: 11px; font-weight: 600; color: var(--muted);
    text-transform: uppercase; letter-spacing: 0.06em;
    padding: 4px 8px 4px 0; border-bottom: 1px solid var(--rule);
}
table.plain td {
    padding: 4px 8px 4px 0;
    border-bottom: 1px solid color-mix(in srgb, var(--rule) 55%, var(--panel));
    vertical-align: baseline;
}

pre.raw {
    margin: 0;
    padding: 10px;
    background: var(--bg);
    border: 1px solid var(--rule);
    overflow-x: auto;
    font-family: var(--mono);
    font-size: 12px;
    line-height: 1.5;
    max-height: 420px;
}

/* Command outcome.
   'no_response' is deliberately NOT styled as a failure: the command may well
   have run and the ack been lost, and saying "failed" asserts something nobody
   knows (§9.4). It gets the muted grey that every other unknown gets.
   'not_sent' IS known -- the broker rejected the publish -- but it is our fault
   rather than the device's, so it is a warning, not a device failure. */
.outcome-ok          { color: var(--ok); }
.outcome-failed      { color: var(--fail); }
.outcome-no_response { color: var(--muted); }
.outcome-not_sent    { color: var(--warn); }
.outcome-pending     { color: var(--warn); }

/* Action rows. One form per command so each carries its own confirmation field;
   a single form with a command dropdown would let a mistyped confirmation apply
   to the wrong action. */
.actions { display: flex; flex-direction: column; gap: 6px; }
.actions .action {
    display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    padding: 4px 0;
    border-bottom: 1px solid color-mix(in srgb, var(--rule) 45%, var(--panel));
}
.actions .action:last-child { border-bottom: 0; }
.actions .action button { min-width: 168px; text-align: left; }
.actions .action .small { flex: 1; min-width: 220px; }

details > summary { cursor: pointer; }
details > summary:focus-visible { outline: 2px solid var(--focus); }

/* =============================================================== login ==== */

.login-wrap {
    max-width: 340px; margin: 12vh auto; padding: 0 16px;
}
.login-wrap h1 {
    font-size: 14px; text-transform: uppercase; letter-spacing: 0.08em;
    margin: 0 0 4px;
}
.login-wrap .card { padding: 18px; }
.login-wrap label { display: block; color: var(--muted); margin: 10px 0 3px; }
.login-wrap input { width: 100%; }
.login-wrap button { width: 100%; margin-top: 16px; }

/* ============================================================== empty ===== */
/* Empty states say what happened and what to do. They never apologise and are
   never vague. */
.empty {
    padding: 26px 14px; text-align: center; color: var(--muted);
}
.empty strong { color: var(--text); display: block; margin-bottom: 4px;
                font-weight: 600; }

/* ============================================================== a11y ====== */

.visually-hidden {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* Forced-colors / high-contrast: drop our surfaces and let the OS win, but keep
   the geometry that carries state so the encoding survives. */
@media (forced-colors: active) {
    .chip { border: 1px solid CanvasText; }
    .tile.t-fail::after    { background: CanvasText; }
    .tile.t-ok::after      { background: CanvasText; }
    .tile.t-warn::after    { background: CanvasText; }
    .tile.t-unknown::after { background: transparent; border: 1.5px solid CanvasText; }
    table.fleet tbody tr { border-left-color: CanvasText; }
}

/* Print: the fleet table, legibly, on white. Used for taking a list into the
   field where there is no signal. */
@media print {
    :root { --bg: #fff; --panel: #fff; --text: #000; --muted: #444;
            --rule: #999; }
    header.top nav, .controls, .strip-panel { display: none; }
    .tablewrap { border: 0; }
    table.fleet { min-width: 0; font-size: 10px; }
    .chip { border: 1px solid #000; color: #000 !important;
            background: transparent !important; }
}
