/* File: public/app/app.css
 *
 * ---------------------------------------------------------------------------
 * TWO SURFACES, TWO DIFFERENT RULES
 * ---------------------------------------------------------------------------
 * This is the part worth understanding before changing anything here, because
 * the two themes are deliberately governed by opposite principles.
 *
 * .theme-desk     COLOURFUL. Somebody is sitting at it, working, clicking
 *                 into detail. Colour carries meaning: it tells tanks from
 *                 generators from flow meters at a glance, it makes a long
 *                 asset list scannable, and it makes the product feel like a
 *                 product. Every asset kind owns a colour.
 *
 * .theme-control  GREY, per ISA-101. Nobody is sitting at it. It hangs on a
 *                 wall and is read from across a room by somebody walking
 *                 past. Here a normal plant renders grey, and saturated colour
 *                 is RESERVED for abnormal states - because on a screen where
 *                 everything is colourful, nothing is urgent.
 *
 * The same asset therefore looks different on the two screens, on purpose.
 * The kind colours that help you scan a desk list would, on a wall display,
 * compete with the one red tile that actually needs somebody to walk over.
 * ---------------------------------------------------------------------------
 */

/* --- asset kind colours ------------------------------------------------- */
/* Used freely on the desk. Reduced to near-grey on the control surface. */

:root {
    --k-tank:      #0d6efd;
    --k-genset:    #6f42c1;
    --k-meter:     #0dcaf0;
    --k-dispenser: #fd7e14;
    --k-temp:      #dc3545;
    --k-humidity:  #20c997;
    --k-pressure:  #d63384;
    --k-generic:   #6c757d;

    /* Severity. The only three colours allowed on the control surface. */
    --sev-high:   #dc3545;
    --sev-medium: #fd7e14;
    --sev-low:    #0dcaf0;

    --st-good:    #198754;
    --st-warning: #fd7e14;
    --st-alarm:   #dc3545;
    --st-stale:   #adb5bd;

    --fluid-diesel: #4a7fb5;
    --fluid-petrol: #c9973f;
    --fluid-other:  #7a8794;

    --radius: 6px;
}

.theme-desk {
    --bg:        #f2f4f7;
    --panel:     #ffffff;
    --panel-alt: #f8fafc;
    --line:      #dde3ea;
    --line-soft: #eef1f5;
    --ink:       #16202b;
    --ink-soft:  #566173;
    --ink-faint: #8794a5;
    --shell:     #c3ccd6;
    --shell-in:  #eef2f6;
    --accent:    #0d6efd;
}

/*
 * The control surface. Every kind colour collapses to grey here, and the
 * severity colours are lifted for contrast against the darker ground.
 *
 * Grey rather than black: black behind bright text causes halation - the glow
 * that makes edges bloom - on a projector, and it is genuinely harder to read
 * from a distance.
 */
.theme-control {
    --bg:        #23272c;
    --panel:     #2c3136;
    --panel-alt: #33383e;
    --line:      #3d444b;
    --line-soft: #363c42;
    --ink:       #e8eaed;
    --ink-soft:  #a8b0b8;
    --ink-faint: #79828b;
    --shell:     #464d55;
    --shell-in:  #1d2126;
    --accent:    #8ab4f8;

    --k-tank:      #9aa3ac;
    --k-genset:    #9aa3ac;
    --k-meter:     #9aa3ac;
    --k-dispenser: #9aa3ac;
    --k-temp:      #9aa3ac;
    --k-humidity:  #9aa3ac;
    --k-pressure:  #9aa3ac;
    --k-generic:   #9aa3ac;

    --st-good:    #4ade80;
    --st-warning: #fbbf24;
    --st-alarm:   #f87171;
    --st-stale:   #6b7280;

    --sev-high:   #f87171;
    --sev-medium: #fbbf24;
    --sev-low:    #7dd3fc;
}

/* --- foundation --------------------------------------------------------- */

/*
 * Inter, self-hosted.
 *
 * The system stack was legible but noisy - the interface rendered in a
 * different face on every machine, and Segoe UI's numerals in particular sit
 * awkwardly next to each other in a column of readings.
 *
 * Inter was drawn for screen interfaces at small sizes, which is what almost
 * every number in this product is. It is served from our own server rather
 * than a font CDN: a page that fetches a font from somebody else's domain
 * hands them a log of every customer who opened it, and stops rendering
 * properly if that domain has a bad day.
 *
 * font-display: swap so text is readable immediately in the fallback and
 * simply re-renders when the file arrives - a dashboard that shows nothing for
 * 300ms is worse than one that shifts slightly.
 */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url('vendor/fonts/inter-latin.woff2') format('woff2');
}

* { box-sizing: border-box; }

body {
    margin: 0;
    background: var(--bg);
    color: var(--ink);
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI Variable Text",
                 "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    font-size: 14px;
    line-height: 1.55;
    letter-spacing: -0.006em;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;

    /*
     * cv11 gives the single-storey 'a' a straighter tail and, more usefully,
     * disambiguates 1 / l / I - which matters on a screen full of asset names
     * and identifiers. ss01 tightens the punctuation.
     */
    font-feature-settings: 'cv11' 1, 'ss01' 1;
}

a { color: var(--accent); }

/*
 * Every reading uses tabular figures.
 *
 * In a proportional font the digit 1 is narrower than 8, so a value going from
 * 1,118 to 8,881 changes width and the row twitches. On a wall display
 * refreshing every thirty seconds that twitch is the most visually obvious
 * thing in the room, drawing the eye to nothing at all.
 */
.readout, .metric-value, .tank-figure, table.data td.num, .kv-value,
.stamp, .alarm-row .when, .toast-meta {
    font-variant-numeric: tabular-nums;

    /* tnum for equal widths, ss02 for the flat-topped 1 and slashed 0 that
       stop a column of readings blurring together at a glance. */
    font-feature-settings: 'tnum' 1, 'ss02' 1, 'cv11' 1;
    letter-spacing: -0.01em;
}

.muted { color: var(--ink-soft); }
.faint { color: var(--ink-faint); }

.label {
    font-size: 10.5px;
    letter-spacing: 0.085em;
    text-transform: uppercase;
    color: var(--ink-faint);
    font-weight: 600;
    line-height: 1.4;
}

/* --- panels ------------------------------------------------------------- */

.panel {
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
}

.panel-head {
    padding: 11px 15px;
    border-bottom: 1px solid var(--line);
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
}

.panel-head h2 { font-size: 13.5px; font-weight: 640; margin: 0;
                 letter-spacing: -0.008em; }
.panel-body { padding: 15px; }

/* --- kind badge --------------------------------------------------------- */
/*
 * What sort of thing this is, said in words AND colour AND an icon.
 *
 * Three channels rather than one because roughly one man in twelve cannot
 * reliably separate red from green, and because a colour alone has to be
 * learned before it means anything. The word never disappears.
 */
.kind {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    padding: 2px 7px 2px 5px;
    border-radius: 20px;
    background: color-mix(in srgb, var(--kind-colour) 13%, transparent);
    color: var(--kind-colour);
    border: 1px solid color-mix(in srgb, var(--kind-colour) 28%, transparent);
    white-space: nowrap;
}

.kind svg { width: 11px; height: 11px; fill: currentColor; flex: 0 0 auto; }

/* On the wall display the badge stays, but quietly. It is orientation, not
   emphasis - the emphasis belongs to whatever is in alarm. */
.theme-control .kind {
    background: transparent;
    border-color: var(--line);
    color: var(--ink-faint);
}

/* --- status ------------------------------------------------------------- */

.dot {
    display: inline-block;
    width: 8px; height: 8px;
    border-radius: 50%;
    background: var(--st-stale);
    flex: 0 0 auto;
}

.dot.is-good    { background: var(--st-good); }
.dot.is-warning { background: var(--st-warning); }
.dot.is-alarm   { background: var(--st-alarm); }
.dot.is-stale   { background: var(--st-stale); }

/* Only an alarm moves. Nothing else on either screen animates, so movement
   itself carries meaning rather than being ornament. */
.dot.is-alarm { animation: pulse 1.6s ease-in-out infinite; }

@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }

@media (prefers-reduced-motion: reduce) {
    .dot.is-alarm { animation: none; }
    .viz .flow-dash { animation: none !important; }
    .toast { animation: none !important; }
}

.state-text { font-size: 11px; font-weight: 650; }
.state-text.is-good    { color: var(--st-good); }
.state-text.is-warning { color: var(--st-warning); }
.state-text.is-alarm   { color: var(--st-alarm); }
.state-text.is-stale   { color: var(--st-stale); }

/* --- metric strip ------------------------------------------------------- */

.metrics {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(126px, 1fr));
    gap: 1px;
    background: var(--line);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    overflow: hidden;
}

/*
 * The metric card.
 *
 * position:relative and the overflow are what hold the watermark icon; the
 * generous bottom padding is what stops the figure being clipped.
 *
 * That clipping was a real fault rather than a nicety: the metric strip on the
 * control surface had a fixed-ish height and 25-30px digits, so the bottoms of
 * numerals - and the whole descender on a comma - were cut off. On a wall
 * display read at three metres, a clipped digit is genuinely ambiguous.
 */
.metric {
    background: var(--panel);
    padding: 11px 14px 15px;
    position: relative;
    overflow: hidden;
    min-height: 72px;
}

/*
 * The watermark.
 *
 * Deliberately very faint. It is there to make a card recognisable at a glance
 * - the eye finds "the one with the tank on it" faster than it reads the word
 * ASSETS - but a metric card exists to show a number, and an icon that
 * competes with the number has failed at its job.
 */
.metric .watermark {
    position: absolute;
    right: -6px;
    bottom: -8px;
    width: 58px;
    height: 58px;
    opacity: 0.07;
    pointer-events: none;
}

.metric .watermark svg { width: 100%; height: 100%; fill: currentColor; }

.metric.clickable .watermark { opacity: 0.09; }
.metric.is-active .watermark { opacity: 0.16; }

.metric .label, .metric .metric-value { position: relative; z-index: 1; }

/* A metric that filters the list below it is a control, and has to look like
   one before it is clicked, not after. */
.metric.clickable { cursor: pointer; }
.metric.clickable:hover { background: var(--panel-alt); }

.metric.is-active {
    background: color-mix(in srgb, var(--accent) 10%, var(--panel));
    box-shadow: inset 0 -3px 0 var(--accent);
}

.metric-value {
    font-size: 26px;
    font-weight: 620;
    line-height: 1.3;      /* room for the comma in 3,557 */
    letter-spacing: -0.028em;
    padding-bottom: 1px;
}

.metric-value .unit {
    font-size: 12.5px; font-weight: 500;
    color: var(--ink-faint); margin-left: 3px; letter-spacing: 0;
}

.metric-value.is-good    { color: var(--st-good); }
.metric-value.is-warning { color: var(--st-warning); }
.metric-value.is-alarm   { color: var(--st-alarm); }

/* --- filter bar --------------------------------------------------------- */

.filters { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }

.chip {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-size: 12px;
    font-weight: 600;
    padding: 4px 11px;
    border-radius: 20px;
    border: 1px solid var(--line);
    background: var(--panel);
    color: var(--ink-soft);
    cursor: pointer;
    white-space: nowrap;
}

.chip:hover { background: var(--panel-alt); color: var(--ink); }

.chip.is-on {
    background: color-mix(in srgb, var(--chip-colour, var(--accent)) 14%, var(--panel));
    border-color: var(--chip-colour, var(--accent));
    color: var(--chip-colour, var(--accent));
}

.chip svg { width: 11px; height: 11px; fill: currentColor; }
.chip .count { opacity: 0.65; font-variant-numeric: tabular-nums; }

/* --- asset tiles -------------------------------------------------------- */

.asset-grid {
    display: grid;
    gap: 11px;
    grid-template-columns: repeat(auto-fill, minmax(212px, 1fr));
}

.asset-tile {
    background: var(--panel);
    border: 1px solid var(--line);
    border-top: 3px solid var(--kind-colour, var(--line));
    border-radius: var(--radius);
    padding: 11px 12px 9px;
    display: flex;
    flex-direction: column;
    gap: 8px;
    min-width: 0;
    text-decoration: none;
    color: inherit;
}

a.asset-tile:hover { border-color: var(--accent); border-top-color: var(--kind-colour); }

/* On the wall display the kind stripe goes. An alarm stripe replaces it, so
   the only coloured edges in the room belong to things that need somebody. */
.theme-control .asset-tile { border-top: 1px solid var(--line); }

.asset-tile.is-alarm   { border-left: 3px solid var(--st-alarm); }
.asset-tile.is-warning { border-left: 3px solid var(--st-warning); }

/* Not reporting drains the tile of contrast. It reads as "no information"
   rather than "a problem with the contents", which is the honest distinction:
   we do not know what is in there. */
.asset-tile.is-stale { opacity: 0.5; }

.asset-name {
    font-size: 12.5px; font-weight: 650; line-height: 1.25;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.asset-site {
    font-size: 10.5px; color: var(--ink-faint);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.asset-figures { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }

.tank-figure { font-size: 19.5px; font-weight: 620; line-height: 1.05;
               letter-spacing: -0.028em; }
.tank-figure .unit { font-size: 11px; font-weight: 500; color: var(--ink-faint); margin-left: 2px; }

.tank-pct { font-size: 13px; font-weight: 650; color: var(--ink-soft); }
.tank-pct.is-warning { color: var(--st-warning); }
.tank-pct.is-alarm   { color: var(--st-alarm); }

.tile-foot {
    display: flex; align-items: center; gap: 6px;
    margin-top: auto; padding-top: 6px;
    border-top: 1px solid var(--line-soft);
}

/* --- drawings ----------------------------------------------------------- */

.viz { display: block; width: 100%; height: auto; }

.viz .shell      { fill: none; stroke: var(--shell); stroke-width: 1.5; }
.viz .shell-fill { fill: var(--shell-in); }
.viz .fluid      { fill: var(--fluid-diesel); }
.viz .fluid-top  { fill: var(--fluid-diesel); filter: brightness(1.2); }
.viz .fluid-petrol, .viz .fluid-petrol.fluid-top { fill: var(--fluid-petrol); }
.viz .fluid-other,  .viz .fluid-other.fluid-top  { fill: var(--fluid-other); }

.viz .band { stroke: var(--ink-faint); stroke-width: 1; stroke-dasharray: 3 3; opacity: 0.7; }
.viz .band-text { fill: var(--ink-faint); font-size: 7px; letter-spacing: 0.04em; }

.viz .pipe { stroke: var(--shell); stroke-width: 1.5; fill: var(--shell-in); }

.viz .flow-dash {
    stroke: var(--k-meter); stroke-width: 5;
    stroke-dasharray: 7 9; stroke-linecap: round;
    animation: flow 1.1s linear infinite;
}

@keyframes flow { to { stroke-dashoffset: -16; } }

.viz .idle { stroke: var(--ink-faint); opacity: 0.35; }
.viz .genset-bar { fill: var(--k-genset); }

/* --- charts ------------------------------------------------------------- */

.chart { display: block; width: 100%; height: auto; }
.chart .grid-line { stroke: var(--line); stroke-width: 1; }
.chart .axis-text { fill: var(--ink-faint); font-size: 9px; }

/* The min-max envelope, drawn behind the average line.
   Without it a smoothed average looks like the whole truth, and every spike
   the buckets contain becomes invisible. */
.chart .band-area { fill: var(--accent); opacity: 0.16; }
.chart .avg-line  { fill: none; stroke: var(--accent); stroke-width: 1.75; }
.chart .event-mark { stroke-width: 1; stroke-dasharray: 2 2; }

/* --- toasts ------------------------------------------------------------- */
/*
 * Live alerts, top right.
 *
 * Colour comes from the severity the person chose when they wrote the rule -
 * High, Medium or Low. That is the one place in this interface where colour is
 * chosen by a user rather than by the software, and it is the right place: the
 * person writing the rule knows what waking somebody at 3am is worth.
 */
.toast-stack {
    position: fixed;
    top: 14px; right: 14px;
    z-index: 1080;
    display: flex; flex-direction: column; gap: 9px;
    width: 360px; max-width: calc(100vw - 28px);
    pointer-events: none;
}

.toast {
    pointer-events: auto;
    background: var(--panel);
    border: 1px solid var(--line);
    border-left: 4px solid var(--sev-colour);
    border-radius: var(--radius);
    box-shadow: 0 6px 22px rgba(15, 25, 40, 0.18);
    padding: 11px 12px;
    cursor: pointer;
    animation: toast-in 0.22s ease-out;
}

.toast:hover { border-color: var(--sev-colour); }

@keyframes toast-in {
    from { opacity: 0; transform: translateX(18px); }
    to   { opacity: 1; transform: none; }
}

.toast-head { display: flex; align-items: center; gap: 7px; margin-bottom: 4px; }

.toast-sev {
    font-size: 9.5px; font-weight: 800; letter-spacing: 0.09em;
    text-transform: uppercase; color: var(--sev-colour);
}

.toast-close {
    margin-left: auto; border: none; background: none;
    color: var(--ink-faint); cursor: pointer;
    font-size: 15px; line-height: 1; padding: 0 2px;
}

.toast-title { font-size: 12.5px; font-weight: 650; line-height: 1.3; }
.toast-msg   { font-size: 12px; color: var(--ink-soft); line-height: 1.4; margin-top: 2px; }

.toast-meta {
    display: flex; flex-wrap: wrap; gap: 4px 12px;
    margin-top: 7px; padding-top: 6px;
    border-top: 1px solid var(--line-soft);
    font-size: 10.5px; color: var(--ink-faint);
}

.toast-meta b { color: var(--ink-soft); font-weight: 600; }

/* --- map ---------------------------------------------------------------- */

.map-holder { height: 380px; width: 100%; border-radius: var(--radius); overflow: hidden; }
.theme-control .map-holder { height: 100%; }

/* The map's own tiles are photographic and cannot follow the theme, so on the
   dark surface they are dimmed to sit with everything else rather than
   glowing out of the wall. */
.theme-control .leaflet-tile { filter: brightness(0.62) saturate(0.7); }

.leaflet-container { background: var(--shell-in); font: inherit; }

.map-pin {
    display: flex; align-items: center; justify-content: center;
    width: 28px; height: 28px;
    border-radius: 50% 50% 50% 4px;
    transform: rotate(-45deg);
    border: 2px solid #fff;
    box-shadow: 0 2px 6px rgba(0,0,0,0.35);
}

.map-pin svg { width: 13px; height: 13px; fill: #fff; transform: rotate(45deg); }

.map-cluster {
    display: flex; align-items: center; justify-content: center;
    width: 36px; height: 36px; border-radius: 50%;
    background: var(--accent); color: #fff;
    font-weight: 700; font-size: 13px;
    border: 3px solid #fff;
    box-shadow: 0 2px 8px rgba(0,0,0,0.3);
    font-variant-numeric: tabular-nums;
}

.map-cluster.has-alarm { background: var(--st-alarm); }

.leaflet-popup-content { margin: 11px 13px; font-size: 13px; min-width: 190px; }
.leaflet-popup-content h4 { margin: 0 0 3px; font-size: 13.5px; }

/* --- control-room shell ------------------------------------------------- */

.control-shell { height: 100vh; display: flex; flex-direction: column; overflow: hidden; }

.control-head {
    display: flex; align-items: center; gap: 16px;
    padding: 9px 16px;
    background: var(--panel);
    border-bottom: 1px solid var(--line);
    flex: 0 0 auto; flex-wrap: wrap;
}

.control-title { font-size: 15px; font-weight: 650; }

.control-body {
    flex: 1 1 auto; padding: 11px 16px; overflow: hidden;
    display: flex; flex-direction: column; gap: 11px;
}

.control-body .asset-grid { flex: 1 1 auto; align-content: start; overflow: hidden; }

/* Read from three metres, not fifty centimetres. Everything grows together or
   the hierarchy collapses. */
.theme-control .metric-value { font-size: 29px; line-height: 1.32; }
.theme-control .metric { min-height: 84px; padding: 12px 15px 17px; }
.theme-control .asset-name   { font-size: 14px; }
.theme-control .tank-figure  { font-size: 22px; }
.theme-control .label        { font-size: 11px; }

.control-foot {
    flex: 0 0 auto; padding: 6px 16px;
    border-top: 1px solid var(--line); background: var(--panel);
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    font-size: 11.5px; color: var(--ink-faint);
}

.pager { display: flex; align-items: center; gap: 5px; }

.pager button {
    border: 1px solid var(--line); background: var(--panel);
    color: var(--ink-soft); border-radius: 4px;
    min-width: 26px; height: 24px; font-size: 11.5px; font-weight: 650;
    cursor: pointer; padding: 0 7px;
}

.pager button:hover:not(:disabled) { background: var(--panel-alt); color: var(--ink); }
.pager button.is-on { background: var(--accent); border-color: var(--accent); color: #fff; }
.pager button:disabled { opacity: 0.4; cursor: default; }

/* --- desk chrome -------------------------------------------------------- */

.side {
    width: 218px; flex: 0 0 218px;
    background: var(--panel); border-right: 1px solid var(--line);
    height: 100vh; position: sticky; top: 0;
    display: flex; flex-direction: column;
}

.side .brand {
    padding: 14px 16px; border-bottom: 1px solid var(--line);
    font-weight: 750; font-size: 14.5px; letter-spacing: -0.01em;
    display: flex; align-items: center; gap: 8px;
}

.side .brand .mark {
    width: 24px; height: 24px;
    display: inline-flex; flex: 0 0 auto;
    align-items: center; justify-content: center;
}

/* The app logo carries its own colours, so it sits on the panel with no
   coloured tile behind it - a green box would fight the green in the mark. */
.side .brand .mark img { width: 100%; height: 100%; object-fit: contain; display: block; }
.side .brand .mark svg { width: 12px; height: 12px; fill: #fff; }

.side nav { padding: 8px; flex: 1 1 auto; overflow-y: auto; }

.side nav a {
    display: flex; align-items: center; gap: 9px;
    padding: 7px 10px; border-radius: 5px;
    color: var(--ink-soft); text-decoration: none;
    font-size: 13px; margin-bottom: 1px;
}

.side nav a svg { width: 14px; height: 14px; fill: currentColor; opacity: 0.85; }
.side nav a:hover { background: var(--panel-alt); color: var(--ink); }

.side nav a.active {
    background: color-mix(in srgb, var(--accent) 11%, transparent);
    color: var(--accent); font-weight: 650;
}

.side .side-foot { padding: 10px 15px; border-top: 1px solid var(--line); font-size: 11.5px; }

.top {
    display: flex; align-items: center; gap: 13px;
    padding: 11px 20px; background: var(--panel);
    border-bottom: 1px solid var(--line);
    position: sticky; top: 0; z-index: 5; flex-wrap: wrap;
}

.top h1 { font-size: 17px; font-weight: 640; margin: 0; letter-spacing: -0.018em; }
.work { padding: 17px 20px 60px; }

/* --- tables ------------------------------------------------------------- */

table.data { width: 100%; border-collapse: collapse; font-size: 13px; }

table.data th {
    text-align: left; font-size: 10.5px; text-transform: uppercase;
    letter-spacing: 0.06em; color: var(--ink-faint); font-weight: 650;
    padding: 7px 10px; border-bottom: 1px solid var(--line); white-space: nowrap;
}

table.data td { padding: 8px 10px; border-bottom: 1px solid var(--line-soft); vertical-align: middle; }
/* A numeric column's heading follows its values.

   td.num was right aligned and th.num was not, so every number sat under a
   heading pushed to the far left of the column - which reads as the values
   being indented rather than as the heading being misplaced. The PDF renderer
   always had both rules, so only the screen was wrong, which made it look like
   a data problem rather than a stylesheet one. */
table.data th.num,
table.data td.num { text-align: right; }
table.data tbody tr:hover { background: var(--panel-alt); }
table.data a { text-decoration: none; font-weight: 600; }

/* --- key/value list ----------------------------------------------------- */

/*
 * The key/value list on a detail page.
 *
 * Given more room than a table row because this is where somebody stops and
 * reads rather than scans - the asset page is the screen people sit with. The
 * key is set back and the value carries the weight, so the eye lands on the
 * answer rather than the question.
 */
.kv {
    display: grid;
    grid-template-columns: minmax(112px, auto) 1fr;
    gap: 9px 18px;
    font-size: 13.5px;
    align-items: baseline;
}

.kv .kv-key {
    color: var(--ink-faint);
    white-space: nowrap;
    font-size: 12.5px;
    letter-spacing: -0.002em;
}

.kv .kv-value { font-weight: 560; color: var(--ink); }
.kv .kv-value a { font-weight: 560; }

/* Body copy on a detail page, as opposed to a list. */
.panel-body p { font-size: 13.5px; line-height: 1.6; }

table.data { font-size: 13.2px; }
table.data td { padding: 9px 10px; }
table.data th { letter-spacing: 0.07em; }

.asset-name { letter-spacing: -0.012em; }

/* --- buttons and forms -------------------------------------------------- */

.btn-primary {
    background: var(--accent); border: 1px solid var(--accent); color: #fff;
    font-weight: 650; font-size: 14px; padding: 9px 16px;
    border-radius: var(--radius); width: 100%; cursor: pointer;
}

.btn-primary:hover:not(:disabled) { filter: brightness(0.92); }
.btn-primary:disabled { opacity: 0.6; cursor: default; }

.btn-quiet {
    background: var(--panel); border: 1px solid var(--line); color: var(--ink-soft);
    font-size: 12px; font-weight: 650; padding: 5px 11px;
    border-radius: 5px; cursor: pointer; text-decoration: none;
    display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
}

.btn-quiet:hover { background: var(--panel-alt); color: var(--ink); }
.btn-quiet svg { width: 12px; height: 12px; fill: currentColor; }

.form-label { font-size: 12px; font-weight: 650; color: var(--ink-soft); margin-bottom: 4px; display: block; }

.form-control {
    border: 1px solid var(--line); border-radius: 5px; padding: 9px 11px;
    font-size: 14px; background: var(--panel); color: var(--ink); width: 100%;
}

.form-control:focus {
    outline: none; border-color: var(--accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent);
}

.form-error {
    background: #fdeaea; border: 1px solid #f5c2c2; color: #8b1f1f;
    border-radius: 5px; padding: 9px 11px; font-size: 13px; margin-bottom: 14px;
}

/* --- login -------------------------------------------------------------- */

.login-wrap {
    min-height: 100vh; display: flex; align-items: center;
    justify-content: center; padding: 20px;
    background: linear-gradient(160deg, #eef2f8 0%, #f6f8fb 55%, #eaeff6 100%);
}

.login-card { width: 100%; max-width: 386px; }
.login-card .panel-body { padding: 26px 24px; }
.login-brand { font-size: 20px; font-weight: 750; letter-spacing: -0.02em; margin-bottom: 3px; }

/* --- alarm rows --------------------------------------------------------- */

.alarm-row {
    display: flex; align-items: baseline; gap: 9px;
    padding: 8px 0; border-bottom: 1px solid var(--line-soft);
    font-size: 12.5px; text-decoration: none; color: inherit;
}

.alarm-row:last-child { border-bottom: none; }

/* The severity icon on each row. Round, filled with the severity colour, so a
   list of alerts is scannable by shape and colour before a word is read. */
.alarm-row .sev-icon {
    width: 24px; height: 24px; border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    background: color-mix(in srgb, var(--sev-colour) 16%, transparent);
    color: var(--sev-colour);
    flex: 0 0 auto;
}

.alarm-row .sev-icon svg { width: 13px; height: 13px; fill: currentColor; }

.alarm-row .body { flex: 1 1 auto; min-width: 0; }

/* Where the TICKET is, which is a different question from where the condition
   is. A tank refilled overnight closes the condition; it says nothing about
   whether anybody found out why it got low. */
.ticket-pill {
    font-size: 9.5px; font-weight: 750; letter-spacing: 0.06em;
    text-transform: uppercase; padding: 2px 6px; border-radius: 3px;
    border: 1px solid var(--line); color: var(--ink-faint);
    white-space: nowrap;
}

.ticket-pill.is-open {
    border-color: color-mix(in srgb, var(--st-alarm) 45%, transparent);
    color: var(--st-alarm);
}

.ticket-pill.is-ack {
    border-color: color-mix(in srgb, var(--st-warning) 45%, transparent);
    color: var(--st-warning);
}

.ticket-pill.is-resolved {
    border-color: color-mix(in srgb, var(--st-good) 45%, transparent);
    color: var(--st-good);
}

/* --- the ticket thread -------------------------------------------------- */

.thread { display: flex; flex-direction: column; gap: 12px; }

.thread-entry { display: flex; gap: 10px; }

.thread-entry .avatar {
    width: 28px; height: 28px; border-radius: 50%;
    background: var(--line-soft); color: var(--ink-soft);
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 11px; font-weight: 700; flex: 0 0 auto;
}

.thread-entry .bubble {
    flex: 1 1 auto; min-width: 0;
    background: var(--panel-alt);
    border: 1px solid var(--line-soft);
    border-radius: var(--radius);
    padding: 8px 11px;
}

.thread-entry .who {
    display: flex; align-items: baseline; gap: 8px;
    font-size: 11.5px; margin-bottom: 3px;
}

.thread-entry .who strong { font-size: 12.5px; }
.thread-entry .who .when { margin-left: auto; color: var(--ink-faint); font-size: 10.5px; }
.thread-entry .text { font-size: 13px; line-height: 1.45; white-space: pre-wrap; }

/* State changes read differently from comments - they are what happened to
   the ticket, not what somebody said about it. */
.thread-entry.is-event .bubble {
    background: transparent;
    border-style: dashed;
}

.thread-entry.is-event .avatar { background: transparent; border: 1px dashed var(--line); }

.kind-tag {
    font-size: 9.5px; font-weight: 750; letter-spacing: 0.06em;
    text-transform: uppercase; padding: 1px 5px; border-radius: 3px;
    background: var(--line-soft); color: var(--ink-soft);
}

textarea.form-control { font-family: inherit; resize: vertical; min-height: 74px; }
.alarm-row:hover { background: var(--panel-alt); }
.alarm-row .when { margin-left: auto; color: var(--ink-faint); font-size: 11px; white-space: nowrap; }

.sev-pill {
    font-size: 9.5px; font-weight: 800; letter-spacing: 0.07em;
    text-transform: uppercase; padding: 2px 6px; border-radius: 3px;
    background: color-mix(in srgb, var(--sev-colour) 15%, transparent);
    color: var(--sev-colour); white-space: nowrap;
}

/* --- responsive --------------------------------------------------------- */

/*
 * The mobile menu button and the drawer backdrop are injected by app.js on
 * every page, and hidden here on the desktop layout where the sidebar is
 * always on screen and neither is needed.
 */
.nav-toggle   { display: none; }
.nav-backdrop { display: none; }

@media (max-width: 980px) {

    /*
     * The sidebar becomes a drawer that slides in from the left.
     *
     * On desktop it is a sticky column in the flow; here it is lifted out and
     * parked off the left edge, shown by adding .nav-open to the body. The
     * content underneath keeps the full width it already had - nothing about
     * the desktop layout of the content itself changes, it just gets the room
     * the sidebar was using.
     */
    .side {
        display: flex;
        position: fixed;
        top: 0; left: 0; bottom: 0;
        width: 268px; max-width: 82vw;
        height: 100vh;
        z-index: 60;
        transform: translateX(-100%);
        transition: transform 0.24s ease;
        box-shadow: 4px 0 26px rgba(12, 18, 24, 0.20);
    }

    body.nav-open .side { transform: translateX(0); }

    .nav-backdrop {
        display: block;
        position: fixed;
        inset: 0;
        z-index: 55;
        background: rgba(10, 15, 20, 0.44);
        opacity: 0;
        pointer-events: none;
        transition: opacity 0.24s ease;
    }
    body.nav-open .nav-backdrop { opacity: 1; pointer-events: auto; }

    /* The menu button, at the start of the page's top bar. */
    .nav-toggle {
        display: inline-flex;
        align-items: center; justify-content: center;
        flex: 0 0 auto;
        width: 36px; height: 36px;
        padding: 0; margin-right: 3px;
        border: 1px solid var(--line);
        border-radius: 9px;
        background: var(--panel);
        color: var(--ink);
        cursor: pointer;
    }
    .nav-toggle svg { width: 20px; height: 20px; }
    .nav-toggle:active { background: var(--panel-alt); }

    /* A touch tighter, for the narrower screen. */
    .top  { padding: 9px 13px; }
    .work { padding: 14px 13px 60px; }

    /*
     * Wide tables scroll inside their own box instead of stretching the page
     * sideways. display:block turns the table into a horizontal scroll
     * container while its rows still lay out as a table within it.
     */
    table.data {
        display: block;
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
        white-space: nowrap;
    }

    /*
     * The slide-in editors (people, sites) go full-width. They carry an inline
     * width of 400px for the desktop, so the override has to be forceful.
     */
    #editor {
        width: 100vw !important;
        max-width: 100vw !important;
    }
}

/*
 * Two-column page layouts stack on a phone.
 *
 * Several pages lay their content out as an inline grid with a flexible main
 * column and a fixed-width side panel - the dashboard's map beside its alerts,
 * a form beside a preview, and so on:
 *
 *     grid-template-columns: minmax(0,1fr) 320px
 *
 * That is right on a wide screen and wrong on a narrow one, where it crushes
 * the flexible column to a sliver next to the fixed panel - which is exactly
 * the clustered look a phone shows. Forcing a single column below this width
 * makes every one of them stack cleanly instead.
 *
 * It is written against the inline style because that is where these layouts
 * live, and !important because an inline style wins otherwise. The card grids
 * that use repeat(auto-fit, ...) are already one column at this width, so this
 * changes nothing about them - it only unpacks the fixed-panel splits.
 */
@media (max-width: 640px) {
    [style*="grid-template-columns"] {
        grid-template-columns: 1fr !important;
    }

    /* The map is generous on a desktop; on a phone it should not eat the
       whole screen before the content below it. */
    .map-holder { height: 260px; }
}

/* A phone rather than a small tablet: give back the last of the side room. */
@media (max-width: 560px) {
    .top h1     { font-size: 16px; }
    .work       { padding: 12px 11px 56px; }
    .panel-body { padding: 12px; }

    /*
     * The summary cards go two across instead of three or four.
     *
     * The auto-fit rule packs in as many 126px cards as fit, which on a phone
     * is three cramped columns. Two roomy ones read far better on a small
     * screen, and the number of cards is small enough that a couple of rows
     * is no burden to scan.
     */
    .metrics { grid-template-columns: 1fr 1fr; }
}

/* The drawer and backdrop slide; honour a request for less motion. */
@media (prefers-reduced-motion: reduce) {
    .side, .nav-backdrop { transition: none; }
}

@media (min-width: 1900px) {
    .theme-control .asset-grid { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); }
    .theme-control .metric-value { font-size: 34px; line-height: 1.3; }
    .theme-control .metric { min-height: 94px; }
}

/* --- timestamps --------------------------------------------------------- */
/*
 * Every timestamp is shown as the exact moment and how long ago it was:
 *
 *     2026-08-29 20:00:43 (1 day 20 mins ago)
 *
 * The stamp is the fact - what somebody quotes in an email or matches against
 * a delivery note. The relative part is the reading aid, and it is set back so
 * a column of timestamps stays scannable rather than doubling the text
 * competing for attention.
 */
.stamp {
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum" 1;
    white-space: nowrap;
}

.stamp-ago {
    color: var(--ink-faint);
    font-size: 0.86em;
    margin-left: 6px;
    white-space: nowrap;
}

/* In a narrow table cell the two halves stack rather than forcing the column
   wide enough for both on one line. */
table.data td .stamp-ago { display: inline-block; margin-left: 5px; }


/* The reading time under a status word. Set smaller and quieter than the
   word itself: the state is what you scan for, the time is what you check
   once the state has caught your eye. */
.stamp-line {
    margin-top: 2px;
    font-size: 11.5px;
    color: var(--ink-faint);
}

.stamp-line .stamp-ago { margin-left: 4px; }


/* The filter row on the reports page.

   align-items:flex-end puts every control on one baseline whatever the label
   above it wraps to. Nothing inside a filter item may be taller than its
   control - a description under a select makes that item taller than its
   neighbours, and the row stops lining up. */
.filter-row {
    display: flex;
    gap: 14px;
    flex-wrap: wrap;
    align-items: flex-end;
}

.filter-row > div { display: flex; flex-direction: column; }

.filter-row .form-label { margin-bottom: 4px; }


/* --- the download note ---------------------------------------------------
   Bottom right, above everything, and never in the way. Exporting a report is
   something somebody sets going and then carries on around: the page keeps
   scrolling, the report stays readable, and the note can be dismissed by
   cancelling the thing it describes. */

.dl-stack {
    position: fixed;
    right: 18px;
    bottom: 18px;
    z-index: 1400;
    display: flex;
    flex-direction: column;
    gap: 8px;

    /* The stack itself catches nothing - only the notes in it do. Otherwise an
       invisible column would sit over the corner of the page swallowing
       clicks meant for whatever is underneath. */
    pointer-events: none;
}

.dl-note {
    pointer-events: auto;
    display: flex;
    align-items: center;
    gap: 11px;
    min-width: 290px;
    padding: 11px 13px;
    border-radius: var(--radius);
    background: var(--panel);
    border: 1px solid var(--line);
    box-shadow: 0 8px 26px rgba(15, 25, 40, 0.18);
    transition: opacity .3s ease, transform .3s ease;
}

.dl-note.is-going { opacity: 0; transform: translateY(6px); }

.dl-spin {
    width: 17px;
    height: 17px;
    flex: 0 0 auto;
    border-radius: 50%;
    border: 2px solid var(--line);
    border-top-color: var(--accent, #0f766e);
    animation: dl-turn .7s linear infinite;
}

.dl-note.is-done .dl-spin {
    animation: none;
    border-color: var(--st-good);
    border-top-color: var(--st-good);
}

@keyframes dl-turn { to { transform: rotate(360deg); } }

/* Somebody who has asked for less movement gets a still marker rather than a
   spinner. The note still says what is happening. */
@media (prefers-reduced-motion: reduce) {
    .dl-spin { animation: none; border-top-color: var(--line); }
}

.dl-text { flex: 1 1 auto; min-width: 0; }
.dl-title { font-size: 13px; font-weight: 600; }
.dl-sub { font-size: 11.5px; color: var(--ink-faint); }

.dl-cancel {
    flex: 0 0 auto;
    border: 1px solid var(--line);
    background: transparent;
    color: var(--ink-mid, var(--ink-faint));
    border-radius: var(--radius);
    padding: 4px 9px;
    font-size: 12px;
    cursor: pointer;
}

.dl-cancel:hover { background: var(--panel-alt); color: var(--ink); }


/* --- chart points and their tooltip ------------------------------------- */

/* The layer that catches the pointer. Invisible, and covering the whole plot,
   so a reading can be taken from anywhere along the line rather than only
   where a marker happens to sit. */
.chart-hit {
    fill: transparent;
    cursor: crosshair;
}

/* The marker that follows the pointer along the average line. */
.chart-cursor {
    fill: var(--panel);
    stroke: var(--accent, #0f766e);
    stroke-width: 2;
    pointer-events: none;
}

.chart-tip {
    position: absolute;
    z-index: 1200;
    min-width: 168px;
    padding: 9px 11px;
    border-radius: var(--radius);
    background: var(--panel);
    border: 1px solid var(--line);
    box-shadow: 0 6px 20px rgba(15, 25, 40, 0.16);
    font-size: 12px;
    pointer-events: none;
    opacity: 0;

    /* Kept out of the layout entirely while hidden, so a tooltip parked off
       screen cannot lengthen the page. */
    visibility: hidden;
    transition: opacity .12s ease;
}

.chart-tip.is-on { opacity: 1; visibility: visible; }

.chart-tip-at {
    font-weight: 650;
    margin-bottom: 5px;
    padding-bottom: 5px;
    border-bottom: 1px solid var(--line-soft);
    font-variant-numeric: tabular-nums;
}

.chart-tip-row {
    display: flex;
    justify-content: space-between;
    gap: 14px;
    line-height: 1.7;
}

.chart-tip-row span { color: var(--ink-faint); }
.chart-tip-row b { font-variant-numeric: tabular-nums; font-weight: 600; }


/* --- reports ------------------------------------------------------------
   A report is drawn from a payload the server built, so these styles serve
   every report there is and every one added later. They deliberately match
   HtmlRenderer's, which is what the PDF is made from - a figure that looks
   different on screen from in the document reads as two different figures. */

/* A wide table scrolls inside its own panel rather than pushing the page
   sideways. Report tables run to ten columns and the window does not. */
.table-scroll {
    overflow-x: auto;
    max-width: 100%;
}

table.data tr.totals td {
    font-weight: 650;
    background: var(--panel-alt);
    border-top: 2px solid var(--line);
    border-bottom: none;
}

/* Cell status, set by the report rather than by the page: a drain, a variance
   outside tolerance, a tank near its minimum. The report decides what is
   worth colouring; this only says what the colours are. */
table.data td.st-good { color: var(--st-good); }
table.data td.st-warn { color: var(--st-warning); font-weight: 600; }
table.data td.st-bad  { color: var(--st-alarm);   font-weight: 600; }

/* Things the reader needs in order to judge the numbers. A warning is a
   finding - thin coverage, an open incident, a tank about to run dry. A note
   is context, such as which timezone the days were counted in. */
.warn-line, .note-line {
    font-size: 12.5px;
    line-height: 1.5;
    margin: 0 0 8px;
    padding: 7px 11px;
    border-radius: var(--radius);
    border-left: 3px solid;
}

.warn-line {
    border-left-color: var(--st-alarm);
    background: var(--panel-alt);
    color: var(--ink);
}

.note-line {
    border-left-color: var(--accent, var(--st-good));
    background: var(--panel-alt);
    color: var(--ink-mid, var(--ink-faint));
}


/* --- asset photographs -------------------------------------------------- */
/*
 * Images are fetched through the API, not from a public folder, so every one
 * of these <img> tags is an authenticated request. That is why they are served
 * by a route rather than sitting in /storage - a photograph of a customer's
 * tank farm is their data.
 */
.gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(90px, 1fr)); gap: 8px; }

.shot {
    position: relative;
    aspect-ratio: 4 / 3;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--panel-alt);
}

.shot img { width: 100%; height: 100%; object-fit: cover; display: block; }

.shot .badge-primary {
    position: absolute; top: 4px; left: 4px;
    background: var(--accent); color: #fff;
    font-size: 9px; font-weight: 700; letter-spacing: 0.05em;
    text-transform: uppercase; padding: 2px 5px; border-radius: 3px;
}

/* The controls sit over the image and appear on hover, so a wall of
   photographs stays a wall of photographs rather than a wall of buttons. */
.shot .shot-tools {
    position: absolute; inset: auto 0 0 0;
    display: flex; gap: 4px; padding: 4px;
    background: linear-gradient(transparent, rgba(0,0,0,0.55));
    opacity: 0; transition: opacity 0.12s;
}

.shot:hover .shot-tools { opacity: 1; }

.shot .shot-tools button {
    flex: 1 1 auto; border: none; border-radius: 3px;
    background: rgba(255,255,255,0.9); color: #16202b;
    font-size: 10px; font-weight: 650; padding: 3px 0; cursor: pointer;
}

.shot .shot-tools button:hover { background: #fff; }

.hero-shot {
    width: 100%; aspect-ratio: 4 / 3; object-fit: cover;
    border-radius: var(--radius); border: 1px solid var(--line);
    display: block; margin-bottom: 12px;
}


/* --- sidebar sections --------------------------------------------------- */
/*
 * Settings sits below the day-to-day pages rather than beside them. The
 * separation is the point: somebody watching tanks should not be one
 * mis-click away from the screen that changes who can see what.
 */
.navgroup-label {
    font-size: 10px;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--ink-faint);
    font-weight: 700;
    margin: 16px 0 5px;
    padding: 8px 10px 0;
    border-top: 1px solid var(--line);
}

/* ---------------------------------------------------------------------------
   The account menu at the foot of the sidebar
   ---------------------------------------------------------------------------
   The person's name is the way in to everything about their own account -
   profile, password, the people on the company, asset and site management.
   That is where every other application puts them, and it is where somebody
   looks for "change my password" without being told.
--------------------------------------------------------------------------- */

.side-account {
    display: flex; align-items: center; gap: 8px; width: 100%;
    background: none; border: 1px solid transparent; border-radius: var(--radius);
    padding: 8px 9px; cursor: pointer; font-family: inherit; font-size: 13px;
    color: inherit; text-align: left;
}

.side-account:hover { background: var(--side-hover, rgba(255,255,255,0.06)); }

.side-account[aria-expanded="true"] {
    background: var(--side-hover, rgba(255,255,255,0.06));
    border-color: var(--line-soft);
}

.side-account-chevron { font-size: 10px; opacity: 0.7; flex: 0 0 auto; }

.side-menu {
    display: flex; flex-direction: column; gap: 1px;
    margin-top: 5px; padding: 4px;
    background: var(--panel); border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: 0 6px 18px rgba(0,0,0,0.18);
}

.side-menu[hidden] { display: none; }

.side-menu a,
.side-menu button {
    display: block; width: 100%; text-align: left;
    background: none; border: none; border-radius: 4px;
    padding: 7px 10px; font-size: 12.5px; font-family: inherit;
    color: var(--ink); text-decoration: none; cursor: pointer;
}

.side-menu a:hover,
.side-menu button:hover { background: var(--panel-alt); }

.side-menu button { color: var(--danger, #b4232c); }

/* ---------------------------------------------------------------------------
   Navigation progress
   ---------------------------------------------------------------------------
   A thin bar while the next page is on its way. Without it a slow request
   looks like a click that did nothing, and people click again.

   Deliberately not an overlay: the page being left stays readable and usable
   until the next one is ready, which is the whole point of navigating without
   a reload.
--------------------------------------------------------------------------- */

.nav-progress {
    position: fixed; top: 0; left: 0; height: 2px; width: 0;
    background: var(--accent); z-index: 300;
    opacity: 0; transition: width 0.2s ease, opacity 0.25s ease;
    pointer-events: none;
}

.nav-progress.is-going {
    width: 82%; opacity: 1;
    /* Creeps rather than completing: it cannot know how long the request will
       take, and a bar that reaches the end and waits is a bar that is lying. */
    transition: width 2.4s cubic-bezier(0.1, 0.8, 0.2, 1), opacity 0.1s ease;
}

@media (prefers-reduced-motion: reduce) {
    .nav-progress { transition: opacity 0.1s ease; }
    .nav-progress.is-going { width: 100%; }
}

/* ---------------------------------------------------------------------------
   A tank we cannot currently vouch for
   ---------------------------------------------------------------------------
   An offline tank still has fuel in it, so it is drawn with the level we last
   heard - faded, so it never reads as a live measurement. Showing an empty
   cylinder instead was not caution, it was a wrong answer.
--------------------------------------------------------------------------- */

.viz .fluid.is-stale,
.viz .fluid-top.is-stale { opacity: 0.32; }

/* ---------------------------------------------------------------------------
   Online, and saying so
   ---------------------------------------------------------------------------
   A pulse means "this is arriving now". It is reserved for assets that are
   genuinely reporting, and stops the moment they are not - an animation that
   keeps running on a dead device teaches the room that the animation means
   nothing.
--------------------------------------------------------------------------- */

.live {
    display: inline-flex; align-items: center; gap: 5px;
    font-size: 11px; font-weight: 600; color: var(--st-good);
    white-space: nowrap;
}

.live .pip {
    width: 7px; height: 7px; border-radius: 50%;
    background: var(--st-good); flex: 0 0 7px;
    position: relative;
}

.live .pip::after {
    content: ""; position: absolute; inset: 0;
    border-radius: 50%; background: var(--st-good);
    animation: live-pulse 2s ease-out infinite;
}

@keyframes live-pulse {
    0%   { transform: scale(1);   opacity: 0.65; }
    70%  { transform: scale(2.6); opacity: 0; }
    100% { transform: scale(2.6); opacity: 0; }
}

.live.is-off { color: var(--ink-faint); }
.live.is-off .pip { background: var(--st-stale); }

/* A device nobody is watching should not animate. Respecting the setting also
   removes the pulse for anybody who finds it distracting on a wall display. */
@media (prefers-reduced-motion: reduce) {
    .live .pip::after { animation: none; }
}

/* ---------------------------------------------------------------------------
   The extra facts on an asset tile
--------------------------------------------------------------------------- */

.tile-facts {
    display: flex; flex-wrap: wrap; gap: 2px 12px;
    font-size: 11px; color: var(--ink-faint);
    margin-top: 5px; line-height: 1.5;
}

.tile-facts b {
    font-weight: 600; color: var(--ink-soft);
    font-variant-numeric: tabular-nums;
}

.tile-capacity {
    font-size: 11px; color: var(--ink-faint);
    font-variant-numeric: tabular-nums;
}

/* The strip under the asset grid. Hidden by the page when everything fits -
   a pager reading "1 of 1" is a control that does nothing, sitting where a
   control should be. */
.asset-pager {
    display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
    padding: 12px 4px 2px; font-size: 12.5px;
    border-top: 1px solid var(--line-soft); margin-top: 14px;
}

.asset-pager-end { margin-left: auto; display: flex; align-items: center; gap: 8px; }
.asset-pager button:disabled { opacity: 0.45; cursor: default; }

/* Said in words when the figures on screen are the last ones received rather
   than current. A faded drawing and a grey dot are easy to miss on a page
   somebody opened to read a number off. */
.note-stale {
    background: var(--warn-bg, #fdf3e4);
    border: 1px solid var(--warn-line, #f2d4ab);
    color: var(--warn-ink, #7a3c05);
    border-radius: var(--radius); padding: 8px 10px;
    font-size: 11.5px; line-height: 1.5; margin-top: 10px;
}
