/* Mobile status page.
 *
 * Deliberately NOT status/styles.css: that is 41 KB of three-column desktop
 * layout whose only phone support is one media query that stacks the columns
 * into a single long scroll -- the thing this page exists to replace.
 *
 * Same palette and design language as the desktop pages (see status/CLAUDE.md),
 * but sized for a thumb: compact sticky header, >=44px tap targets, sections
 * collapsed by default, modals as full-screen sheets.
 *
 * The .modal / .modal-content rules at the bottom are REQUIRED, not optional --
 * status_gen_modal.js and status_pod_modal.js render into those classes and each
 * consuming page supplies its own copy (gen_manager.py does the same). That is
 * what lets this page style them as sheets without touching the desktop rules.
 */

:root {
    --bg: #1a1a1a;
    --panel: rgba(255,255,255,0.05);
    --edge: rgba(255,255,255,0.1);
    --text: #e0e0e0;
    --dim: #888;
    --good: #00ff00;
    --info: #4a9eff;
    --warn: #ff6600;
    --bad: #ff4444;
    --amber: #ffaa00;
}

* { box-sizing: border-box; -webkit-tap-highlight-color: rgba(255,255,255,0.06); }

body {
    background: var(--bg);
    color: var(--text);
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    margin: 0;
    padding: 0;
    line-height: 1.4;
    overflow-x: hidden;
    -webkit-text-size-adjust: 100%;
}

/* ---- header: compact + sticky, so "where am I" never scrolls away ---------- */
.mhead {
    position: sticky;
    top: 0;
    z-index: 900;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    background: #141414;
    border-bottom: 1px solid var(--edge);
}
.mhead .title {
    flex: 1;
    font-size: 1.15em;
    font-weight: 600;
    color: var(--good);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.mback {
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 38px;
    margin-left: -8px;
    border: none;
    background: none;
    color: var(--info);
    font-size: 1.5em;
    cursor: pointer;
}
.mconn { width: 9px; height: 9px; border-radius: 50%; background: var(--bad); flex: none; }
.mconn.on { background: var(--good); }

/* ---- fleet / site summary strip ------------------------------------------- */
.mstrip {
    display: flex;
    flex-wrap: wrap;
    gap: 2px 16px;
    padding: 10px 14px;
    background: var(--panel);
    border-bottom: 1px solid var(--edge);
    font-size: 0.82em;
    color: var(--dim);
}
.mstrip b { color: var(--text); font-size: 1.25em; font-weight: 600; display: block; }
.mstrip .k { flex: 1 1 auto; min-width: 74px; }

/* ---- generic list rows ----------------------------------------------------- */
.msec { margin: 14px 10px 0 10px; }
.msec > h2 {
    font-size: 0.72em;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--dim);
    margin: 0 0 6px 4px;
    font-weight: 600;
}
.mrow {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    min-height: 52px;
    padding: 9px 12px;
    margin-bottom: 6px;
    background: var(--panel);
    border: 1px solid var(--edge);
    border-radius: 8px;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
}
.mrow:active { background: rgba(255,255,255,0.1); }
.mrow .nm { flex: 1; min-width: 0; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sub { display: block; font-weight: 400; font-size: 0.76em; color: var(--dim); margin-top: 1px; }
.mrow .val { text-align: right; font-size: 0.9em; white-space: nowrap; }
.mrow .val .sub { text-align: right; }
.mrow .chev { color: var(--dim); font-size: 0.85em; flex: none; }

.dot { width: 10px; height: 10px; border-radius: 50%; flex: none; }
.dot.online, .dot.ok { background: var(--good); }
.dot.offline, .dot.bad { background: var(--bad); }
.dot.stuck, .dot.warn { background: var(--warn); }
.dot.idle { background: var(--dim); }

.g { color: var(--good); } .b { color: var(--info); }
.o { color: var(--warn); } .r { color: var(--bad); } .d { color: var(--dim); }

/* ---- pod rows: uplink icons + the per-pod miner map -------------------------
 * The map is the same per-position colour grid the desktop sites-overview draws
 * (status_overview_grid_cgi.py -> StatusWidgets.drawPixmap), at a bigger pixel
 * scale so it is readable on a phone. One pixel per miner slot:
 *   green = hashing, yellow = low, red = zero-hash, blue = sleeping,
 *   purple = dead board, white = offline, transparent = not installed. */
.podrow { display: block; padding: 9px 12px 8px 12px; }
.podtop { display: flex; align-items: center; gap: 8px; }
.podtop .nm { flex: 1; min-width: 0; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.uplink { display: inline-flex; align-items: center; gap: 3px; flex: none; }
.uplink:empty { display: none; }

/* Three signals, three shapes -- circle = pod, router = peplink, dish = Starlink.
 * Colours match status/styles.css so a colour means the same thing on both pages.
 * They sit in a flex row with a fixed box and centred glyph: vertical-align does
 * nothing to a flex item, which is why the dish looked a pixel high before. */
.uplink i {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 15px;
    height: 15px;
    font-size: 12px;
    line-height: 1;
}
.starlink-dish.up, .pep-icon.up { color: var(--good); }
.starlink-dish.down, .pep-icon.down { color: var(--bad); }
.status-icon.dot.inline { display: block; }

canvas.pod-pixmap {
    display: block;
    margin: 7px 0 1px 0;
    max-width: 100%;
    image-rendering: pixelated;   /* keep miner pixels crisp when scaled */
}
canvas.pod-pixmap[width="0"], canvas.pod-pixmap:not([width]) { display: none; }

/* ---- gen group accordion --------------------------------------------------- */
.grp { margin-bottom: 6px; border: 1px solid var(--edge); border-radius: 8px; overflow: hidden; background: var(--panel); }
.grp > .hd { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 52px; padding: 9px 12px;
             background: none; border: none; color: inherit; font: inherit; text-align: left; cursor: pointer; }
.grp > .hd .nm { flex: 1; font-weight: 600; }
.grp > .bd { display: none; border-top: 1px solid var(--edge); }
.grp.open > .bd { display: block; }
.grp.open > .hd .chev { transform: rotate(90deg); }
.grp .chev { transition: transform 0.15s; }
.gen { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 48px; padding: 8px 12px 8px 14px;
       background: none; border: none; border-top: 1px solid rgba(255,255,255,0.05); color: inherit; font: inherit;
       text-align: left; cursor: pointer; }
.gen:first-child { border-top: none; }
.gen:active { background: rgba(255,255,255,0.08); }
.gen .nm { flex: 1; min-width: 0; font-size: 0.9em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gen .kw { font-variant-numeric: tabular-nums; font-size: 0.9em; }

.empty { padding: 26px 14px; text-align: center; color: var(--dim); font-size: 0.9em; }

/* ---- reused desktop modals, dressed as mobile sheets ------------------------
 * status_gen_modal.js renders into .modal / .modal-content. Left alone it is a
 * desktop dialog: centred box, dense 13px label/value tables, close button in a
 * corner -- which reads as a completely different app from the gen sheet you
 * just came from. Same bottom-sheet shape, same row rhythm, same header as
 * .sheet so the two feel like one thing.
 *
 * The tables are inline-styled by the JS (padding 4px 8px, colours, width 50%),
 * so matching the sheet's row rhythm needs !important. Values only -- no layout
 * is being fought here, just spacing and colour. */
.modal {
    display: none;
    position: fixed;
    top: 0; right: 0; bottom: 0; left: 0;   /* before inset: iOS Safari < 14.1 */
    inset: 0;
    z-index: 1000;
    background: rgba(0,0,0,0.72);
}
.modal.show, .modal[style*="block"] { display: block; }

.modal-content {
    position: absolute;
    left: 0; right: 0; bottom: 0;
    width: 100%;
    max-height: 88vh;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    background: #1e1e1e;
    border: none;
    border-top: 1px solid var(--edge);
    border-radius: 14px 14px 0 0;
    margin: 0;
    padding: 0 14px env(safe-area-inset-bottom, 20px) 14px;
    color: var(--text);
    animation: sheetup 0.16s ease-out;
}
@media (prefers-reduced-motion: reduce) { .modal-content { animation: none; } }

/* The modal's <h2> is its title; make it the sticky header .sheet-hd gives the
   other sheets. Negative margins let it span the sheet's padding. */
.modal-content > div > h2,
.modal-content h2 {
    position: sticky;
    top: 0;
    z-index: 4;
    margin: 0 -14px 8px -14px !important;
    padding: 14px 52px 10px 14px;
    background: #1e1e1e;
    border-bottom: 1px solid var(--edge);
    font-size: 1.05em !important;
    font-weight: 600;
    color: var(--good) !important;
}

/* Close sits with the header rather than floating over the page. It is the first
   child of .modal-content, so sticky+float keeps it pinned top-right while the
   body scrolls under it. */
.modal-content .close, .modal .close {
    position: sticky;
    top: 0;
    z-index: 6;
    float: right;
    width: 40px;
    height: 40px;
    margin: 7px -6px 0 0;
    line-height: 38px;
    text-align: center;
    border-radius: 50%;
    color: var(--dim);
    font-size: 26px;
    cursor: pointer;
}

/* Label/value tables -> the same rhythm as .prow in the sheets. */
.modal-content table { width: 100% !important; border-collapse: collapse; font-size: 0.88em !important; }
.modal-content table tr { border-bottom: 1px solid rgba(255,255,255,0.05) !important; }
.modal-content table td { padding: 9px 0 !important; vertical-align: top; }
.modal-content table td:first-child { color: var(--dim) !important; width: 45% !important; }
.modal-content table td:last-child { color: var(--text) !important; text-align: right; font-weight: 600; }
.modal-content th { color: var(--good); text-align: left; padding: 6px 0; }
.modal-content canvas { max-width: 100%; }

/* Wider phones / small tablets: two columns of rows rather than one long list. */
@media (min-width: 620px) {
    .msec .rows { display: grid; grid-template-columns: 1fr 1fr; gap: 0 8px; }
}

/* ---- pod sheet --------------------------------------------------------------
 * Bottom sheet, not a centred dialog: it opens under the thumb and does not
 * cover the row you tapped. Actions are two-tap (arm, then confirm) because
 * these are real miner commands on a device that lives in a pocket. */
#podsheet { position: fixed; top: 0; right: 0; bottom: 0; left: 0; inset: 0; z-index: 1100; }
.sheet-back { position: absolute; top: 0; right: 0; bottom: 0; left: 0; inset: 0; background: rgba(0,0,0,0.6); }
.sheet {
    position: absolute;
    left: 0; right: 0; bottom: 0;
    max-height: 88vh;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    background: #1e1e1e;
    border-top: 1px solid var(--edge);
    border-radius: 14px 14px 0 0;
    padding-bottom: env(safe-area-inset-bottom, 12px);
    animation: sheetup 0.16s ease-out;
}
@keyframes sheetup { from { transform: translateY(14px); opacity: 0.6; } to { transform: none; opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .sheet { animation: none; } }

.sheet-hd {
    position: sticky; top: 0;
    display: flex; align-items: center; gap: 8px;
    padding: 14px 12px 10px 14px;
    background: #1e1e1e;
    border-bottom: 1px solid var(--edge);
}
.sheet-hd b { font-size: 1.05em; }
.sheet-hd .d { flex: 1; font-size: 0.78em; }
.sheet-x {
    min-width: 40px; min-height: 40px;
    background: none; border: none; color: var(--dim);
    font-size: 26px; line-height: 1; cursor: pointer;
}
.sheet-bd { padding: 6px 14px 16px 14px; }

.prow {
    display: flex; justify-content: space-between; gap: 12px;
    padding: 7px 0;
    border-bottom: 1px solid rgba(255,255,255,0.05);
    font-size: 0.88em;
}
.prow > span { color: var(--dim); }
.prow > b { font-weight: 600; text-align: right; }

.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0 4px 0; }
.chip {
    padding: 4px 9px; border-radius: 999px;
    font-size: 0.72em; font-weight: 600; letter-spacing: 0.02em;
    border: 1px solid var(--edge); color: var(--dim);
}
.chip.on { background: rgba(255,170,0,0.14); border-color: var(--amber); color: var(--amber); }

.acts { display: flex; flex-direction: column; gap: 8px; margin-top: 14px; }
.pact {
    min-height: 46px;
    border-radius: 8px;
    border: 1px solid var(--edge);
    background: var(--panel);
    color: var(--text);
    font: inherit; font-weight: 600;
    cursor: pointer;
}
.pact:active { background: rgba(255,255,255,0.1); }
.pact.armed { color: #111; }
.pact.warn.armed { background: var(--warn); border-color: var(--warn); }
.pact.good.armed { background: var(--good); border-color: var(--good); }
.pact.bad.armed  { background: var(--bad);  border-color: var(--bad); color: #fff; }

.actmsg { min-height: 20px; margin-top: 10px; font-size: 0.85em; text-align: center; color: var(--dim); }
.actmsg.good { color: var(--good); }
.actmsg.warn { color: var(--warn); }
.actmsg.bad  { color: var(--bad); }

/* ---- pod sheet: actionable pills + group kW ---------------------------------- */
.sublabel {
    margin: 16px 0 7px 0;
    font-size: 0.7em; letter-spacing: 0.08em; text-transform: uppercase;
    color: var(--dim); font-weight: 600;
}
button.chip {
    min-height: 34px;
    font: inherit; font-size: 0.74em; font-weight: 600;
    cursor: pointer;
    background: var(--panel);
}
button.chip.armed {
    background: var(--amber); border-color: var(--amber); color: #111;
}
.kwrow { display: flex; align-items: center; gap: 8px; }
.kwbtn {
    width: 46px; min-height: 46px;
    border-radius: 8px; border: 1px solid var(--edge);
    background: var(--panel); color: var(--text);
    font: inherit; font-size: 1.3em; line-height: 1; cursor: pointer;
}
.kwbtn:active { background: rgba(255,255,255,0.12); }
.kwval { flex: 1; text-align: center; }
.kwval b { display: block; font-size: 1.35em; color: var(--amber); line-height: 1.1; }
.kwval span { font-size: 0.7em; color: var(--dim); }
.kwsave {
    min-height: 46px; padding: 0 14px;
    border-radius: 8px; border: 1px solid var(--edge);
    background: var(--panel); color: var(--text);
    font: inherit; font-weight: 600; cursor: pointer;
}
.kwsave.idle { opacity: 0.45; }
.kwsave.armed { background: var(--amber); border-color: var(--amber); color: #111; }

/* ---- gen sheet --------------------------------------------------------------- */
#gensheet { position: fixed; top: 0; right: 0; bottom: 0; left: 0; inset: 0; z-index: 1100; }
.pact[disabled] { opacity: 0.4; }
.sublabel.o { color: var(--warn); text-transform: none; letter-spacing: 0; font-size: 0.8em; }

/* ---- reused desktop gen modal ------------------------------------------------
 * status_gen_modal.js is a two-column desktop layout. styles.css has a
 * max-width:768px rule that stacks it, but this page does not load styles.css
 * (it is 41 KB of three-column desktop layout), so on a phone the modal arrived
 * with NO column rules at all and read as a wall. These mirror the desktop's own
 * mobile rules -- same intent, so the modal reads the same way on both. */
.gen-modal-cols { display: flex; flex-direction: column; gap: 16px; }
.gen-modal-col-left { max-height: none; overflow-y: visible; }
.gen-modal-col-right { min-width: 0; }
.modal-content > *, .gen-modal-cols > * { min-width: 0; max-width: 100%; }

/* The gen action picker (status_gen_modal.js GenActionPicker) is appended to
 * <body>, NOT into .modal-content -- an earlier rule scoped under .modal-content
 * never applied, which is why it stayed cramped. It also sets position/width via
 * inline style.cssText, so overriding those needs !important.
 * On a phone it becomes a full-width bottom sheet with real tap targets. */
#genActionPicker {
    left: 0 !important;
    right: 0 !important;
    top: auto !important;
    bottom: 0 !important;
    width: 100% !important;
    min-width: 0 !important;
    max-width: 100% !important;
    max-height: 82vh;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    border: none !important;
    border-top: 1px solid var(--edge) !important;
    border-radius: 14px 14px 0 0 !important;
    padding-bottom: env(safe-area-inset-bottom, 10px) !important;
    /* The picker centres itself with left/top 50% AND translate(-50%,-50%).
     * Overriding left/top alone left the transform in place, so the full-width
     * sheet was still shifted half its own width left and half its height up --
     * mostly off-screen, which reads as "an empty popup pinned to the left". */
    transform: none !important;
    background: #1e1e1e !important;
    box-shadow: none !important;
}
/* Its header is a plain div with inline padding/colour/size. Match .sheet-hd so
 * the picker reads as the same surface you arrived from, not a second design. */
#genActionPicker > div:first-child {
    /* sticky is a positioned value, so it anchors the injected .picker-x */
    position: sticky;
    top: 0;
    z-index: 2;
    background: #1e1e1e;
    padding: 14px 14px 11px 14px !important;
    border-bottom: 1px solid var(--edge) !important;
}
#genActionPicker > div:first-child > div:first-child {
    font-size: 1.05em !important;
    font-weight: 600 !important;
}
#genActionPicker > div:first-child > div:last-child {
    font-size: 0.78em !important;
    color: var(--dim) !important;
    margin-top: 3px !important;
}

.gen-action-item {
    min-height: 54px;
    padding: 15px 18px !important;
    font-size: 15px !important;
    color: var(--text) !important;
    border-bottom: 1px solid rgba(255,255,255,0.06);
    gap: 14px !important;
}
.gen-action-item:last-child { border-bottom: none; }
.picker-x {
    position: absolute;
    top: 6px;
    right: 6px;
    background: none;
    border: none;
}
.gen-action-item:active { background: rgba(255,255,255,0.1); }
.gen-action-item i { font-size: 17px; width: 22px !important; text-align: center; }

/* The modal renders dense label/value pairs; stop long values forcing a
   horizontal scroll. */
.modal-content pre,
.modal-content code { white-space: pre-wrap; word-break: break-word; }
.modal-content img,
.modal-content svg { max-width: 100%; height: auto; }

/* The schematic is a fixed-geometry drawing -- let it scroll sideways rather
   than squashing it into an unreadable sliver. */
.gen-schematic { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* ---- whole-group gen control ------------------------------------------------
 * Sits at the top of an expanded gen group. The chip in the header means an
 * action is available without having to expand to find out. */
.ggc-chip {
    flex: none;
    padding: 3px 8px;
    border-radius: 999px;
    font-size: 0.66em;
    font-weight: 700;
    letter-spacing: 0.03em;
    color: #fff;
}
.ggc {
    display: block;
    width: calc(100% - 20px);
    min-height: 50px;
    margin: 10px;
    padding: 9px 12px;
    border: none;
    border-radius: 8px;
    color: #fff;
    font: inherit;
    font-weight: 700;
    cursor: pointer;
    text-align: center;
}
.ggc span { display: block; font-weight: 400; font-size: 0.76em; opacity: 0.85; margin-top: 2px; }
.ggc:active { filter: brightness(1.15); }
.ggc.busy { background: #444; color: #bbb; cursor: default; }

/* The shared restart confirm needs no override: #genGroupConfirm is the
 * full-screen overlay (left/top 0, 100%x100%, flex-centred) and its inner panel
 * already carries max-width:360px; width:90%, which is correct on a phone.
 * Styling the overlay as though it were the panel would shrink the backdrop --
 * the same half-override that put the action picker off-screen. */
#genGroupToast { max-width: 92vw; }

/* ---- per-device site filter --------------------------------------------------
 * Gear in the header opens a sheet of tick boxes. Hiding a site is a display
 * preference stored on this phone -- it is NOT the view.py allowlist, and a deep
 * link to a hidden site still opens. */
.mgear {
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 40px;
    min-height: 38px;
    border: none;
    background: none;
    color: var(--dim);
    font-size: 1.05em;
    cursor: pointer;
    flex: none;
}
.mgear:active { color: var(--text); }

/* "3 of 13 sites" in the summary strip, so a filtered total never reads as a
   fleet total. Tappable -- it is the fastest way back to the picker. */
.mstrip .k.filtered {
    color: var(--amber);
    cursor: pointer;
    flex: none;
}
.mstrip .k.filtered b { color: var(--amber); }

.prefbtns { display: flex; gap: 8px; margin-bottom: 4px; }
.prefbtns .pact { flex: 1; }

.prefrow {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    min-height: 50px;
    padding: 8px 2px;
    background: none;
    border: none;
    border-bottom: 1px solid rgba(255,255,255,0.05);
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
}
.prefrow:active { background: rgba(255,255,255,0.06); }
.prefrow .nm { font-weight: 600; }
.cbox {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    flex: none;
    border: 1px solid var(--edge);
    border-radius: 5px;
    color: #111;
    font-size: 14px;
}
.cbox.on { background: var(--good); border-color: var(--good); }

/* Bottom padding on the scroll container, not a spacer div in one view.
 * The sites overview had none, so its last row ran under the iOS home indicator
 * and could not be scrolled clear -- you hit the end of the page with the row
 * still half-covered. env() needs the viewport-fit=cover already set in m.py and
 * view.py; the fallback covers browsers without a safe area. */
#app {
    padding-bottom: calc(28px + env(safe-area-inset-bottom, 0px));
}

/* ---- gen maintenance tag + disabled dimming ---------------------------------
 * Mirrors the desktop: a gen carrying a maintenance status gets a small tag in
 * its own colour, and a DISABLED gen is dimmed rather than badged. The dimming
 * already reads at a glance, and these rows are busy enough. */
.gtag {
    flex: none;
    padding: 2px 6px;
    border-radius: 3px;
    font-size: 0.62em;
    font-weight: 700;
    letter-spacing: 0.02em;
    white-space: nowrap;
    background: var(--panel);
    color: var(--dim);
}
.gen.dim { opacity: 0.5; }

/* ---- a tech is on this gen ---------------------------------------------------
 * Set and cleared from the Mesa field app. Unlike every other flag on a gen row
 * this one is an INTERLOCK, not information: gen_control refuses start/onload/
 * auto on the gen AND on its whole group, and auto-restart holds the group.
 *
 * Same treatment as the desktop status page and the Mesa app, deliberately, so
 * "a tech is on this" looks identical wherever it is seen. It marks the row with
 * a PATTERN rather than a colour -- every hue here is already spoken for, and
 * hazard striping means "work area" in the physical world these gens live in.
 * And it cancels .gen.dim: by the time someone is working on a gen it has almost
 * always been DISABLED first, so a serviced row is the one thing on a dim row
 * that has to stay legible. Defined after .gen.dim so its opacity:1 wins. */
/* The bar is a PSEUDO-ELEMENT, not border-image. border-image paints every
   border the element has, and .gen already carries a 1px border-top as its row
   separator -- so the stripe was landing across the top of the row as well,
   which read as a second, horizontal piece of tape. An absolutely positioned
   bar can only ever paint the left edge, and it leaves the row separator its
   own colour. Each row keeps its original left padding, so flagging a gen no
   longer nudges its contents sideways either. */
.gen.servicing,
.grp.servicing > .hd {
    position: relative;
    opacity: 1;
    background: rgba(255, 179, 0, 0.07);
}
.gen.servicing::before,
.grp.servicing > .hd::before {
    content: '';
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 5px;
    background: repeating-linear-gradient(45deg, #ffb300 0 7px, #20180a 7px 14px);
}
/* The one animated thing on the page. This is the only state where acting on
 * stale information can hurt somebody, so it earns what nothing else gets. */
.gtag.svc {
    background: #ffb300;
    color: #17130a;
    animation: svc-pulse 1.8s ease-in-out infinite;
}
@keyframes svc-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }
@media (prefers-reduced-motion: reduce) { .gtag.svc { animation: none; } }

.mrow .sub .svc { color: #ffb300; }

/* Gen sheet banner: read before the action, not as a 409 after it. */
.sheet .svcnow {
    margin: 10px 0 2px;
    padding: 9px 11px;
    border-radius: 6px;
    background: rgba(255, 179, 0, 0.12);
    color: #ffd98a;
    font-size: 0.86em;
    line-height: 1.45;
    position: relative;
    overflow: hidden;
    padding-left: 16px;
}
.sheet .svcnow::before {
    content: '';
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 5px;
    background: repeating-linear-gradient(45deg, #ffb300 0 7px, #20180a 7px 14px);
}
.sheet .svcnow b { color: #ffb300; }

/* Group control held because a tech is on a set in this group. Amber text on the
   same inert grey the "Working…"/"Settling" states use, so it reads as the same
   kind of thing -- a button that is deliberately not available -- rather than an
   error. */
.ggc.held { background: #333 !important; color: #ffb300 !important; cursor: not-allowed; }
.ggc.held span { color: #bbb; }

/* ---- why this group is where it is ------------------------------------------
 * Groups collapse by default here, so the control state has to read from the
 * HEADER: someone looking at a pod full of sleeping miners should not have to
 * expand anything to learn that a gen is on a coolant hold. The detail and the
 * individual holds go in the body, where there is room for a sentence. */
.grp > .hd .ctl {
    display: block;
    font-weight: 400;
    font-size: 0.76em;
    margin-top: 1px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.ctlwhy {
    padding: 8px 12px 2px;
    font-size: 0.78em;
    line-height: 1.45;
}
.ctlwhy .d { color: var(--dim); }
.ctlwhy .r { margin-top: 4px; }
.ctlwhy .r i { width: 13px; text-align: center; }

/* A gen holding headroom back. Amber like the gas/hold family, and distinct from
 * the hazard striping, which means a PERSON is in danger rather than a machine
 * being nursed. */
/* Same class the desktop uses, from the same shared builder -- only the sizing
   is local, because that is the one thing that genuinely differs between a
   mouse row and a thumb row. */
.gen-hold-badge {
    flex: none;
    padding: 2px 5px;
    border-radius: 3px;
    font-size: 0.62em;
    font-weight: 700;
    letter-spacing: 0.02em;
    white-space: nowrap;
    background: rgba(255,170,0,.16);
    color: #ffc14d;
    border: 1px solid rgba(255,170,0,.45);
}
.sheet .holdnow {
    margin: 10px 0 2px;
    padding: 9px 11px 9px 16px;
    border-radius: 6px;
    background: rgba(255,170,0,.10);
    color: #ffd9a0;
    font-size: 0.86em;
    line-height: 1.45;
    position: relative;
    overflow: hidden;
}
.sheet .holdnow::before {
    content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
    background: #ffaa00;
}
.sheet .holdnow b { color: #ffc14d; }

/* ---- wifi bridge sheet --------------------------------------------------------
 * Field troubleshooting view for the gen wifi bridges: one plain sentence per gen
 * saying which link of the chain is broken, worst first. Opened from the wifi
 * button in the header (site view only).
 *
 * Deliberately NOT built on .mrow: these rows carry two lines of prose under the
 * headline and expand in place, which .mrow's single-line ellipsis layout fights. */
.mwifi {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 40px;
    min-height: 38px;
    border: none;
    background: none;
    color: var(--dim);
    font-size: 1.05em;
    cursor: pointer;
    flex: none;
}
.mwifi:active { color: var(--text); }
.mwifi.alert { color: var(--warn); }
.mwifi .bdg:empty { display: none; }
.mwifi .bdg {
    position: absolute;
    top: 1px; right: 0;
    min-width: 15px;
    padding: 0 3px;
    border-radius: 8px;
    background: var(--warn);
    color: #111;
    font-size: 10px;
    font-weight: 700;
    line-height: 15px;
    text-align: center;
}

#bridgesheet { position: fixed; top: 0; right: 0; bottom: 0; left: 0; inset: 0; z-index: 1100; }
#bridgesheet .sheet { max-height: 92vh; }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }

.brchips { margin: 8px 0 4px 0; }
.brchips .chip.bad { background: rgba(255,68,68,0.14); border-color: var(--bad); color: var(--bad); }
.brchips .chip.good { background: rgba(0,255,0,0.12); border-color: var(--good); color: var(--good); }
.brchips .chip.warn { background: rgba(255,102,0,0.14); border-color: var(--warn); color: var(--warn); }

/* Antenna down = we are blind behind it, so gens there read as offline when they
   are fine. Loudest thing in the sheet on purpose. */
.brwarn {
    margin: 8px 0 2px;
    padding: 9px 11px 9px 16px;
    border-radius: 6px;
    background: rgba(255,68,68,0.10);
    color: #ffb3b3;
    font-size: 0.86em;
    line-height: 1.45;
    position: relative;
    overflow: hidden;
}
.brwarn::before {
    content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
    background: var(--bad);
}
.brwarn b { color: #ff7b7b; display: block; margin-bottom: 3px; }

.brallgood {
    margin: 14px 0 4px;
    padding: 14px 12px;
    border-radius: 8px;
    background: rgba(0,255,0,0.06);
    border: 1px solid rgba(0,255,0,0.2);
    color: var(--good);
    font-size: 0.88em;
    text-align: center;
}
.brnote {
    margin: 2px 0 8px;
    font-size: 0.8em;
    line-height: 1.45;
    color: var(--dim);
}

/* Collapsed groups (off gens, no-bridge, reporting-fine). Same accordion feel as
   the gen groups on the site page so the gesture is already learned. */
.brsec { margin: 10px 0 0; border: 1px solid var(--edge); border-radius: 8px; overflow: hidden; }
.brsec-hd {
    display: flex; align-items: center; gap: 10px;
    width: 100%; min-height: 44px; padding: 8px 12px;
    background: var(--panel); border: none; color: var(--dim);
    font: inherit; font-size: 0.8em; font-weight: 600; text-align: left; cursor: pointer;
}
.brsec-hd .nm { flex: 1; }
.brsec-hd .chev { transition: transform 0.15s; }
.brsec.open > .brsec-hd { color: var(--text); border-bottom: 1px solid var(--edge); }
.brsec.open > .brsec-hd .chev { transform: rotate(90deg); }
.brsec-bd { display: none; padding: 8px 10px 10px; }
.brsec.open > .brsec-bd { display: block; }

.brrow {
    margin-bottom: 6px;
    border: 1px solid var(--edge);
    border-left: 3px solid var(--dim);
    border-radius: 8px;
    background: var(--panel);
    overflow: hidden;
}
.brrow.bad { border-left-color: var(--bad); }
.brrow.warn { border-left-color: var(--warn); }
.brrow.info { border-left-color: var(--info); }
.brrow.good { border-left-color: var(--good); }
.brrow.dim { border-left-color: var(--dim); }
.brrow-hd {
    display: flex; align-items: flex-start; gap: 9px;
    width: 100%; padding: 9px 11px;
    background: none; border: none; color: inherit;
    font: inherit; text-align: left; cursor: pointer;
}
.brrow-hd:active { background: rgba(255,255,255,0.06); }
.brrow-hd .dot { margin-top: 4px; }
.brrow-hd .nm { flex: 1; min-width: 0; font-weight: 600; font-size: 0.92em; }
/* The headline wraps -- it is a sentence, not a label, and truncating it would
   throw away the half that says what is wrong. */
.brrow-hd .nm .sub { white-space: normal; }
.brrow-hd .nm .sub.hl { color: var(--text); font-weight: 600; font-size: 0.85em; margin-top: 3px; }
.brrow.bad .nm .sub.hl { color: #ff9c9c; }
.brrow.warn .nm .sub.hl { color: #ffb782; }
.brrow.good .nm .sub.hl { color: var(--dim); font-weight: 400; }
.brrow-hd .chev { color: var(--dim); font-size: 0.85em; flex: none; margin-top: 2px; transition: transform 0.15s; }
.brrow.open .brrow-hd .chev { transform: rotate(90deg); }
.brrow-bd { display: none; padding: 2px 11px 10px; }
.brrow.open .brrow-bd { display: block; }
.brsay { font-size: 0.85em; line-height: 1.5; color: var(--dim); margin-bottom: 8px; }
.bract {
    font-size: 0.85em; line-height: 1.5;
    padding: 8px 10px; margin-bottom: 8px;
    border-radius: 6px;
    background: rgba(74,158,255,0.08);
    color: #cfe4ff;
}
.bract b { color: var(--info); }
.brnone {
    margin: 14px 0 4px;
    padding: 14px 12px;
    border-radius: 8px;
    background: var(--panel);
    border: 1px solid var(--edge);
    color: var(--dim);
    font-size: 0.88em;
    text-align: center;
    line-height: 1.5;
}
/* Shortlist of gens a stray bridge could belong to. Sits inside the row detail,
   under the MAC it is matched against. */
.brcand {
    margin: 8px 0 4px;
    padding: 9px 11px;
    border-radius: 6px;
    background: rgba(255,255,255,0.04);
    border: 1px solid var(--edge);
    font-size: 0.85em;
    line-height: 1.5;
}
.brcand .l { margin-top: 6px; }
.brcand .l b { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
