:root{
  --bg:#1e1e2e;--surface:#2a2a3d;--border:#3d3d5c;
  --accent:#0794c8;--accent2:#af530c;--text:#e0e0f0;--muted:#888;
  --danger:#c84040;--success:#40c870;--warn:#c8a010;
  --row-alt:#252538;--sel:#0794c840;
  --sidebar-w:240px;   /* shared width for every left-hand list/nav panel */

  /* ── Chart tokens (static/js/charts.js) ──────────────────────────────────
     Series slots are assigned in fixed order and never cycled; --chart-1 is
     the UI accent so a chart reads as part of the page. --chart-2 is a
     lightened --accent2: the base #af530c sits at 2.7:1 against the card
     surface, under the 3:1 floor, while this step clears it and still passes
     the colourblind separation check against --chart-1 (ΔE 22 protan/deutan).
     Status slots are for STATE only (up/down/unknown) and are never reused as
     "series 3" — --chart-bad is likewise a lightened --danger, which at
     2.85:1 was too dim to sit on the card. */
  --chart-1:#0794c8;--chart-2:#cc6d1f;
  --chart-ok:#40c870;--chart-bad:#d9534f;--chart-idle:#888;
  --chart-grid:#3d3d5c;

  /* Slots 3-4 exist for the fleet donuts, which need four identities in one
     ring. They are NOT free hues: the separation gate is run on ADJACENT pairs
     around the ring (a donut is a stacked bar bent into a circle, so only
     neighbours touch — plus the wrap pair, which is a real pair here and is
     not on a bar), and the four slots pass it only in the order written. The
     ring order the charts use is 1 -> 2 -> 3 -> 4 -> neutral -> back to 1, and
     the numbers for it, against this --surface, are: worst adjacent CVD ΔE 12.8
     (protan/deutan, Machado 2009 at severity 1.0) and worst adjacent
     normal-vision ΔE 17.3, both clear of the 8 / 15 floors.

     Two consequences worth stating, because both are easy to undo by accident:
     re-ordering the slices re-orders the pairs and the guarantee goes with it
     (--chart-2 beside --chart-4 is orange beside yellow at ΔE 6.1 — a fail),
     and there is no --chart-5. A fifth identity is folded into "Other", which
     is what --chart-neutral is for.

     --chart-neutral is the de-emphasis slot: "Other", and (hatched, see
     .sw-unknown) "unknown". It sits below the chroma floor on purpose — it is
     not a series colour and must not read as one.

     SOLID, it may not sit next to --chart-1: ΔE 11.2, under the 15 floor. The
     ring satisfies that by construction rather than by luck — an "unknown"
     slice is always last and always HATCHED, so the pair that closes the ring
     back to slot 1 is separated by texture, not by hue, and a solid "Other"
     only ever lands mid-ring against --chart-4 (ΔE 17.6). Adding a solid
     neutral slice anywhere else re-opens the question. */
  --chart-3:#d55181;--chart-4:#c98500;--chart-neutral:#8d8da6;

  /* Round-trip bands, an ORDINAL ramp: one hue, monotone lightness, and the
     anchor flipped for a dark surface so more-latency reads lighter and the
     slow end is the one that catches the eye. Every step clears 2:1 against
     the card (the dimmest, --rtt-1, at 2.21:1), so the fast band stays visible
     rather than sinking into the background. Never reuse these as series
     colours: their meaning is the ORDER, not identity. */
  --rtt-1:#10687f;--rtt-2:#0a86b4;--rtt-3:#1ba5d6;--rtt-4:#62c3e8;--rtt-5:#a8def5;
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Segoe UI',sans-serif;background:var(--bg);color:var(--text);min-height:100vh;display:flex;flex-direction:column}

/* ── Login ── */
#loginScreen{position:fixed;inset:0;background:var(--bg);display:flex;align-items:center;justify-content:center;z-index:500}
#loginScreen.hidden{display:none}
.login-box{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:36px 40px;width:340px;display:flex;flex-direction:column;gap:14px}
.login-box h2{text-align:center;font-size:1.2rem}
.login-box .logo{text-align:center;font-size:2.4rem}
.login-logo{display:block;width:190px;max-width:78%;height:auto;margin:0 auto 2px;border-radius:6px}
.login-head{text-align:center;margin-bottom:2px}
.login-head h2{font-size:1.3rem;letter-spacing:.01em}
.login-sub{color:var(--muted);font-size:.8rem;margin-top:2px}
.login-box label{font-size:.82rem;color:var(--muted)}
.login-box input{width:100%;background:var(--bg);border:1px solid var(--border);color:var(--text);padding:8px 12px;border-radius:6px;font-size:.95rem;outline:none}
.login-box input:focus{border-color:var(--accent)}
.login-box .err{color:var(--danger);font-size:.82rem;min-height:1.1em;text-align:center}
.login-box button:not(.pw-toggle){width:100%;padding:9px;background:var(--accent);color:#fff;border:none;border-radius:6px;font-size:.95rem;font-weight:600;cursor:pointer}
.login-box button:not(.pw-toggle):hover{filter:brightness(1.12)}

/* ── Header ── */
header{background:linear-gradient(90deg,var(--accent) 0%,#5B7F79 55%,var(--accent2) 100%);padding:10px 16px;font-size:1rem;font-weight:600;color:#fff;display:flex;align-items:center;gap:12px;user-select:none}
header span.sub{opacity:.7;font-size:.83rem;font-weight:400}
.header-right{display:flex;align-items:center;gap:10px;font-size:.84rem}
.pill{padding:3px 10px;border-radius:20px;font-size:.76rem;font-weight:700}
.pill.admin{background:#af530c99}
.pill.user{background:#0794c899}
/* ── Hamburger menu ── */
.hdr-menu{position:relative}
.hdr-hamburger{background:rgba(255,255,255,.18);border:none;color:#fff;cursor:pointer;font-size:1.2rem;padding:3px 10px;border-radius:4px;line-height:1;margin-left:auto}
.hdr-hamburger:hover{background:rgba(255,255,255,.32)}
/* max-height is set on open by app-common.js::fitHdrDropdown (the room left
   below the header); this one only covers the moment before it runs. */
.hdr-dropdown{display:none;position:absolute;top:calc(100% + 8px);right:0;background:var(--surface);border:1px solid var(--border);border-radius:6px;min-width:200px;z-index:300;box-shadow:0 6px 24px #0009;max-height:calc(100vh - 64px);overflow-y:auto;overscroll-behavior:contain}
/* The account menu's button: the signed-in name, styled like the hamburger. */
.hdr-user{display:flex;align-items:center;gap:5px;background:none;border:none;color:#fff;cursor:pointer;font:inherit;padding:3px 8px;border-radius:4px}
.hdr-user:hover,.hdr-user-menu.open .hdr-user{background:rgba(255,255,255,.18)}
.hdr-user #headerUser{opacity:.85;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.hdr-caret{font-size:.7rem;opacity:.75}
.hdr-menu.open .hdr-dropdown{display:block}
.hdr-dropdown button{display:block;width:100%;text-align:left;background:none;border:none;color:var(--text);padding:9px 16px;cursor:pointer;font-size:.87rem;font-weight:400}
.hdr-dropdown button:hover{background:var(--accent);color:#fff}
.hdr-dropdown hr{border:none;border-top:1px solid var(--border);margin:3px 0}
.hdr-signout{color:var(--danger)!important}
.hdr-signout:hover{background:var(--danger)!important;color:#fff!important}
.save-status{font-size:.78rem;min-width:90px;text-align:right;opacity:0;transition:opacity .25s;color:#fff;margin-left:auto}
.save-status.show{opacity:.85}
.save-status.saving{color:#ffe9a8}
.save-status.saved{color:#9be8b6}
.save-status.error{color:#ffb0b0}

/* ── Toolbars ── */
.toolbar{background:var(--surface);border-bottom:1px solid var(--border);padding:6px 10px;display:flex;flex-wrap:wrap;align-items:center;gap:8px}
.toolbar label{font-size:.83rem;color:var(--muted)}
.toolbar select,.toolbar input[type=text]{background:var(--bg);border:1px solid var(--border);color:var(--text);padding:4px 8px;border-radius:4px;font-size:.85rem;outline:none}
.toolbar select:focus,.toolbar input[type=text]:focus{border-color:var(--accent)}
.toolbar select:disabled,.toolbar input[type=text]:disabled{opacity:.38;cursor:not-allowed}
.sep{flex:1}
button.btn{padding:4px 12px;border-radius:4px;border:none;cursor:pointer;font-size:.84rem;font-weight:600;transition:filter .15s;background:var(--accent);color:#fff}
button.btn:hover{filter:brightness(1.15)}
button.btn.danger{background:var(--danger)}
button.btn.warn{background:var(--warn)}
button.btn.success{background:var(--success)}
button.btn.plain{background:var(--border);color:var(--text)}
button.btn.plain:hover{background:#555}
button.btn.small{padding:3px 9px;font-size:.76rem}
button.btn:disabled{opacity:.35;cursor:not-allowed;filter:none}

/* ── Page notice ──
   A page-width banner for the reason a screen is empty or refused — not a
   toast: a toast is for something that just happened and then goes away, and
   this is a standing condition the reader has to act on (ask an administrator,
   sign in as someone else). Accent-bordered rather than red: being told what
   your login can do is information, not a fault. */
.page-notice{margin:10px 10px 0;padding:10px 14px;border:1px solid var(--border);
  border-left:3px solid var(--accent);border-radius:6px;background:var(--surface);
  font-size:.86rem;line-height:1.5;color:var(--text)}
.page-notice strong{color:var(--text)}
.page-notice span{color:var(--muted)}

/* ── Main panels ── */
main{flex:1;display:grid;grid-template-columns:1fr 5px 1fr;padding:10px;overflow:hidden}
.panel{display:flex;flex-direction:column;gap:6px;overflow:hidden}
.panel h2{text-align:center;font-size:1rem;padding:4px 0;border-bottom:1px solid var(--border)}
.panel-toolbar{display:flex;flex-wrap:wrap;gap:6px;align-items:center;justify-content:center;padding:4px 0}
.panel-toolbar select{background:var(--bg);border:1px solid var(--border);color:var(--text);padding:3px 8px;border-radius:4px;font-size:.82rem;outline:none}
.grid-wrap{flex:1;overflow:auto;border:1px solid var(--border);border-radius:4px}
table{width:100%;border-collapse:collapse;font-size:.84rem}
thead th{background:var(--surface);color:var(--muted);font-weight:600;text-align:left;padding:6px 8px;position:sticky;top:0;border-bottom:1px solid var(--border);cursor:pointer;user-select:none}
thead th:hover{color:var(--accent)}
tbody tr{border-bottom:1px solid var(--border)}
tbody tr:nth-child(even){background:var(--row-alt)}
tbody tr:hover{background:var(--sel);cursor:pointer}
tbody tr.selected{background:var(--sel);outline:1px solid var(--accent)}
td{padding:5px 8px}
td input,td select{background:transparent;border:none;color:var(--text);width:100%;outline:none;font-size:.84rem}
td input:focus,td select:focus{border-bottom:1px solid var(--accent)}
td select{background:var(--surface)}
.splitter{background:var(--border);width:5px;cursor:col-resize}
.splitter:hover{background:var(--accent)}
/* Vertical drag bar under the sensors area (shown on desktop only). */
.v-splitter{display:none}
/* Sidebar drag handle (Table Browser / API Admin Console) */
.side-splitter{flex:none;width:6px;margin:12px 5px;border-radius:3px;background:var(--border);cursor:col-resize;transition:background .12s}
.side-splitter:hover{background:var(--accent)}
/* Drag handle for the 2-column grid pages (Alert Profiles, Organization); sits
   as the middle grid column between the list panel and the detail panel. */
.grid-splitter{width:6px;margin:12px 0;border-radius:3px;background:var(--border);cursor:col-resize;transition:background .12s}
.grid-splitter:hover{background:var(--accent)}

/* ── Shared left-hand nav/list panel ─────────────────────────────────────────
   One "new style" system for every sidebar: a rounded bordered surface card
   with an uppercase header (with room for a right-aligned action) and soft
   rgba pill rows. Used by the Device Console, API console, and Table Browser
   (as .layout>aside.sidenav) AND by the Organization tree and Alert Profiles
   list — which is why .aside-title / .aside-list are standalone, not scoped
   under .sidenav. ── */
.layout>aside.sidenav{width:var(--sidebar-w);flex-shrink:0;background:var(--surface);
  border:1px solid var(--border);border-radius:8px;margin:12px 0 12px 12px;
  display:flex;flex-direction:column;overflow:hidden}
.aside-title{display:flex;align-items:center;gap:6px;padding:11px 14px 7px;
  font-size:.75rem;font-weight:700;color:var(--muted);
  text-transform:uppercase;letter-spacing:.06em;border-bottom:1px solid var(--border)}
.aside-title .sep{flex:1}
.aside-list{flex:1;overflow-y:auto;padding:6px}
.aside-list a{display:block;padding:7px 10px;margin:2px 0;border-radius:6px;
  font-size:.85rem;color:var(--text);text-decoration:none;cursor:pointer;border-left:none}
.aside-list a:hover{background:rgba(128,128,128,.12)}
.aside-list a.active{background:rgba(90,130,255,.22);color:var(--accent)}

/* ── Events panel ── */
/* height fallback covers the non-flex layout after showMainTab clears the
   container's inline display */
#eventsPanel{flex:0 0 30vh;height:30vh;min-height:150px;padding:0 10px 10px}
.evt-chip{display:inline-block;font-size:.72rem;padding:2px 8px;border-radius:10px;font-weight:700;white-space:nowrap}
.evt-chip.CLEAR{background:#1e8c4533;color:#5fd68a}
.evt-chip.ADVISORY{background:#f59e0b33;color:#f5b84b}
.evt-chip.WARNING{background:#dc262633;color:#f47272}
.evt-chip.STALE_DATA,.evt-chip.INSUFFICENT_DATA,.evt-chip.UNKNOWN{background:#1a73e833;color:#6aa5f2}
.evt-chip.NO_SENSORS,.evt-chip.baseline{background:#88888833;color:#aaa}
.evt-chip.TRANSFERRED{background:#7c3aed33;color:#b795f5}
.evt-arrow{color:var(--muted);padding:0 4px}
.evt-sensor{border:1px solid var(--border);border-radius:6px;padding:10px 12px;margin-bottom:10px}
.evt-sensor h4{font-size:.86rem;margin-bottom:6px}
.evt-sensor .lim{font-size:.78rem;color:var(--muted);margin-bottom:6px}
.evt-detail-table{width:100%;border-collapse:collapse;font-size:.82rem}
.evt-detail-table th{text-align:left;color:var(--muted);font-weight:600;padding:3px 8px;border-bottom:1px solid var(--border)}
.evt-detail-table td{padding:3px 8px;border-bottom:1px solid var(--border)}
.evt-detail-table td.breach{color:var(--danger);font-weight:600}
.evt-now{background:rgba(90,130,255,.08);border-radius:6px;padding:10px 12px;margin-bottom:14px}
.evt-section{font-size:.78rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;margin:2px 0 8px}
.evt-section .evt-chip{text-transform:none;letter-spacing:0;margin-left:4px}
.evt-note{font-size:.82rem;color:var(--muted)}
.evt-counts{display:flex;flex-wrap:wrap;gap:12px;align-items:center;font-size:.84rem;margin-bottom:8px}

/* ── Admin page sections ── */
.admin-section{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:16px}
.admin-section h3{font-size:.95rem;margin-bottom:12px;color:var(--accent)}
.admin-table{width:100%;border-collapse:collapse;font-size:.84rem}
.admin-table th{text-align:left;padding:6px 8px;color:var(--muted);border-bottom:1px solid var(--border)}
.admin-table td{padding:6px 8px;border-bottom:1px solid var(--border)20;vertical-align:middle}
.badge{font-size:.72rem;padding:2px 8px;border-radius:10px;font-weight:700}
.badge.admin{background:#af530c55;color:#f09050}
.badge.user{background:#0794c855;color:#50c0f0}
/* A caution on a row that is still usable — the sensor picker's "no limits",
   which says a sensor can be attached but will not raise a temperature alert
   until it has reported. Amber rather than red: nothing is broken. */
.badge.warn{background:#c47f0055;color:#e0a340}
.role-note{font-size:.78rem;color:var(--muted);font-style:italic;margin-top:4px}
/* A filter/action row INSIDE a card (Users → Sign-In History). Selects were
   only ever styled under .toolbar and .modal, so a card-level control rendered
   as a raw browser widget on the dark surface; this gives it the toolbar's
   look without the toolbar's page-spanning bar. */
.section-filters{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:8px 0}
.section-filters select{background:var(--bg);border:1px solid var(--border);color:var(--text);padding:5px 9px;border-radius:4px;font-size:.85rem;outline:none}
.section-filters select:focus{border-color:var(--accent)}
/* Outcome pills for the sign-in audit. Green reads as "this worked", which for
   a failed sign-in is the wrong signal in both directions — a red row is the
   one to look at, and a wall of green successes should not compete with it. */
.badge.ok{background:#1d7a4f55;color:#4fc98a}
.badge.bad{background:#a4242455;color:#f08080}

/* ── Toast ── */
#toast{position:fixed;bottom:20px;right:20px;background:var(--success);color:#fff;padding:10px 18px;border-radius:6px;font-size:.88rem;opacity:0;transition:opacity .3s;pointer-events:none;z-index:999}
#toast.show{opacity:1}
#toast.error{background:var(--danger)}
#toast.warn{background:var(--warn)}

/* ── Modals ── */
.modal-overlay{display:none;position:fixed;inset:0;background:#000a;z-index:400;align-items:center;justify-content:center}
.modal-overlay.show{display:flex}
/* Tall modals scroll internally instead of clipping; overscroll-behavior stops
   the scroll from chaining to the page behind. body.modal-open (toggled by
   app-common.js) locks the page itself while any modal is up. */
body.modal-open{overflow:hidden}
.modal{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:24px;min-width:340px;max-width:480px;width:90%;max-height:calc(100vh - 32px);max-height:calc(100dvh - 32px);overflow-y:auto;overscroll-behavior:contain}
.modal h3{margin-bottom:16px;font-size:1.05rem}
.modal label{display:block;font-size:.82rem;color:var(--muted);margin-bottom:3px}
.modal input,.modal select{width:100%;background:var(--bg);border:1px solid var(--border);color:var(--text);padding:7px 10px;border-radius:4px;font-size:.9rem;margin-bottom:14px;outline:none}
.modal input:focus,.modal select:focus{border-color:var(--accent)}
/* Checkboxes/radios are inline controls — the full-width text-input styling
   above stretches and drops them below their label; reset them so they sit
   flush to the left of the label text (e.g. the org-grants picker). */
.modal input[type=checkbox],.modal input[type=radio]{width:auto;margin:0 6px 0 0;padding:0;vertical-align:middle;flex:none}
.modal-footer{display:flex;gap:10px;justify-content:flex-end;margin-top:4px}
.form-err{color:var(--danger);font-size:.82rem;margin-bottom:10px;min-height:1em}

/* ── Confirm modal ── */
.confirm-msg{font-size:.92rem;margin-bottom:14px;line-height:1.45}
.confirm-msg b{color:var(--accent)}
.confirm-warn{font-size:.82rem;color:var(--warn);margin-bottom:14px;display:flex;gap:6px;align-items:flex-start}
.confirm-warn::before{content:"⚠";font-size:1rem;line-height:1}
#mConfirmTypeBox{margin-bottom:14px;display:none}
#mConfirmTypeBox.show{display:block}
#mConfirmTypeBox label{display:block;font-size:.78rem;color:var(--muted);margin-bottom:4px}
#mConfirmTypeBox input{width:100%;background:var(--bg);border:1px solid var(--border);color:var(--text);padding:7px 10px;border-radius:4px;font-size:.9rem;outline:none}
#mConfirmTypeBox input:focus{border-color:var(--danger)}
#mConfirmTypeBox input.match{border-color:var(--success)}

/* ── Password input toggle (eye) ── */
.pw-wrap{position:relative;margin-bottom:14px}
.pw-wrap input{margin-bottom:0!important;padding-right:36px!important;width:100%}
.pw-toggle{position:absolute;right:6px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;color:var(--muted);font-size:.95rem;padding:4px 6px;line-height:1;border-radius:3px}
.pw-toggle:hover{color:var(--text);background:var(--border)}
.pw-toggle:focus{outline:1px solid var(--accent);outline-offset:1px}

/* ── Password strength meter ── */
.pw-strength{margin:-8px 0 12px;font-size:.74rem;line-height:1.3}
.pw-strength-bar{height:4px;background:var(--border);border-radius:2px;overflow:hidden;margin-bottom:4px}
.pw-strength-bar > div{height:100%;width:0;transition:width .2s,background .2s;background:var(--danger)}
.pw-strength-label{color:var(--muted)}
.pw-strength.s1 .pw-strength-bar > div{width:25%;background:var(--danger)}
.pw-strength.s2 .pw-strength-bar > div{width:50%;background:var(--warn)}
.pw-strength.s3 .pw-strength-bar > div{width:75%;background:#5fb55f}
.pw-strength.s4 .pw-strength-bar > div{width:100%;background:var(--success)}
.pw-strength.s1 .pw-strength-label{color:var(--danger)}
.pw-strength.s2 .pw-strength-label{color:var(--warn)}
.pw-strength.s3 .pw-strength-label,
.pw-strength.s4 .pw-strength-label{color:var(--success)}

/* ── Filter inputs above tables ── */
.tbl-filter{position:relative;margin-bottom:8px;max-width:320px}
.tbl-filter.center{margin-left:auto;margin-right:auto}
.tbl-filter input{width:100%;background:var(--bg);border:1px solid var(--border);color:var(--text);padding:5px 28px 5px 10px;border-radius:4px;font-size:.83rem;outline:none}
.tbl-filter input:focus{border-color:var(--accent)}
.tbl-filter .clear{position:absolute;right:4px;top:50%;transform:translateY(-50%);background:none;border:none;color:var(--muted);cursor:pointer;font-size:1rem;line-height:1;padding:2px 6px;border-radius:3px;display:none}
.tbl-filter .clear.show{display:block}
.tbl-filter .clear:hover{color:var(--text)}

/* ── BluConsole organization picker (Customers modal) ── */
/* A tree of 547 checkboxes: it scrolls inside the modal rather than growing it,
   and the indent comes from an inline padding-left set to depth*18px. Three
   states beyond "tickable": .derived is covered by a ticked ancestor (checked,
   disabled), .context is only on screen as an ancestor of a filter match, and
   .taken is already linked to another customer — allowed since migrate_038, so
   it is a label rather than a block. */
#mAccBcOrg{max-height:min(46vh,340px);overflow-y:auto;border:1px solid var(--border);
           border-radius:4px;padding:6px 8px}
#mAccBcOrg label{display:block;padding:2px 0;font-size:.85rem;line-height:1.35;
                 cursor:pointer}
#mAccBcOrg label.derived{color:var(--muted);cursor:default}
#mAccBcOrg label.context{opacity:.6}
#mAccBcOrg label.taken{color:var(--warn)}
#mAccBcOrg .bc-via{color:var(--muted);font-style:italic;font-size:.78rem}
#mAccBcOrg .bc-none{color:var(--muted);font-size:.85rem}

/* Sensor id -> detail modal. Without a colour it falls back to the browser
   default link blue, which is close to unreadable on this background; --accent
   is what every other in-page link on the Configuration screen uses. */
.sensor-id{color:var(--accent);text-decoration:none}
.sensor-id:hover{text-decoration:underline}

/* ── Searchable combo box (createCombo) ── */
.combo{position:relative;display:inline-block;vertical-align:middle}
/* Selector carries .combo so page-level input[type=text] rules (e.g. Effect
   Designer) don't out-specify it and eat the caret padding. */
.combo .combo-input{width:100%;background:var(--bg);border:1px solid var(--border);color:var(--text);padding:6px 26px 6px 10px;border-radius:4px;font-size:.85rem;outline:none}
.combo .combo-input:focus{border-color:var(--accent)}
.combo-caret{position:absolute;right:8px;top:50%;transform:translateY(-50%);color:var(--muted);pointer-events:none;font-size:.7rem}
.combo-menu{position:absolute;z-index:50;left:0;right:0;top:calc(100% + 3px);max-height:240px;overflow-y:auto;background:var(--surface);border:1px solid var(--border);border-radius:6px;box-shadow:0 8px 24px rgba(0,0,0,.35);display:none}
.combo-menu.show{display:block}
.combo-opt{padding:7px 10px;font-size:.85rem;cursor:pointer;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.combo-opt:hover,.combo-opt.active{background:var(--accent);color:#fff}
.combo-opt .combo-sub{color:var(--muted);font-size:.78rem;margin-left:6px}
.combo-opt.active .combo-sub,.combo-opt:hover .combo-sub{color:rgba(255,255,255,.8)}
.combo-empty{padding:8px 10px;font-size:.82rem;color:var(--muted)}
/* The "Copy ID" link beside a device picker (copyToClipboard). A button, since
   it acts rather than navigates, dressed as a link so it reads as a small aside
   to the picker rather than a toolbar action competing with Save. */
.copy-link{background:none;border:none;padding:2px 0;font:inherit;font-size:.8rem;color:var(--accent);
           cursor:pointer;white-space:nowrap}
.copy-link:hover{text-decoration:underline}
.copy-link:focus-visible{outline:1px solid var(--accent);outline-offset:2px}
.copy-link:disabled{color:var(--muted);opacity:.5;cursor:default;text-decoration:none}
/* A device picker and its Copy ID link, side by side. The combo mounts into
   .pick-mount (createCombo clears its container, so the link cannot live in
   it). Configuration's light and Energy's meter share this width so the two
   read as one control; the mount gives way first when the row is tight. */
.device-pick{display:flex;align-items:center;gap:10px;min-width:0}
.device-pick .pick-mount{width:260px;flex:0 1 auto;min-width:0}
.device-pick .combo{width:100%}
/* In a toolbar the next thing is a field's label; keep the link off it. */
.toolbar .device-pick{margin-right:10px}

/* ── Spinner / empty rows ── */
.row-message td{padding:18px 12px;text-align:center;color:var(--muted);font-size:.85rem;font-style:italic}
.row-message.error td{color:var(--danger);font-style:normal}
.spinner{display:inline-block;width:12px;height:12px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .7s linear infinite;vertical-align:-2px;margin-right:6px}
@keyframes spin{to{transform:rotate(360deg)}}

/* ── Mobile (≤768px) ────────────────────────────────────────────────────── */
@media (max-width: 768px){
  /* Login: let the box shrink to the viewport */
  .login-box{width:auto;max-width:92vw;padding:24px 20px}

  /* Header: allow wrapping so buttons stay on-screen */
  header{flex-wrap:wrap;font-size:.95rem;padding:8px 10px;gap:8px;line-height:1.2}
  header span.sub{width:100%;order:10;font-size:.78rem}
  .header-right{flex-wrap:wrap;gap:6px;font-size:.78rem;width:100%;justify-content:flex-start;align-items:center}
  /* Save status: sit inline right after the title (override desktop's margin-left:auto) */
  .save-status{min-width:0;flex:0 1 auto;text-align:left;font-size:.72rem;margin-left:0}
  /* Menus: push the account menu (and the hamburger after it) to the right
     when header-right wraps on mobile, so both dropdowns open from the right
     edge as they do on desktop. */
  .hdr-user-menu{margin-left:auto}
  .hdr-user #headerUser{max-width:45vw}

  /* Toolbar: stack controls vertically and let selects/inputs fill width */
  .toolbar{padding:8px;gap:6px}
  .toolbar>label{width:100%;margin-top:4px}
  .toolbar select,.toolbar input[type=text]{width:100%!important;font-size:.92rem;padding:7px 10px}
  .toolbar .btn{flex:1 1 auto;padding:8px 10px;font-size:.88rem}
  .toolbar .sep{display:none}
  #lnkCtrl{width:100%;text-align:center;padding:4px 0}
  /* A toolbar's device picker fills the row, its Copy ID link beside it. */
  .toolbar .device-pick{width:100%;margin-right:0}
  .toolbar .device-pick .pick-mount{flex:1 1 auto;width:auto}

  /* Main: stack the two panels vertically instead of side-by-side */
  main{grid-template-columns:1fr;grid-template-rows:1fr auto 1fr;padding:6px;gap:6px;overflow:auto}
  .panel{min-height:300px}
  .splitter{width:100%;height:5px}
  #eventsPanel{flex:0 0 auto;max-height:none;min-height:220px}
  .panel-toolbar{gap:4px}
  .panel-toolbar .btn{flex:1 1 auto;padding:6px 8px;font-size:.8rem}
  .panel-toolbar select{flex:1 1 100%}

  /* Tables: slightly tighter, still readable; allow horizontal scroll if needed */
  .grid-wrap{overflow:auto}
  table{font-size:.82rem;min-width:420px}
  td,thead th{padding:8px 6px}
  td input,td select{font-size:.88rem;padding:4px 2px} /* bigger tap targets */

  /* Stacked section pages (Customers, Users): reflow tables as cards
     (one row = one card) */
  #customersTab,#usersTab{padding:10px;gap:12px}
  .admin-section{padding:12px}
  .admin-section h3{font-size:1rem}
  /* Hide the table header — each cell is self-labeling below */
  .admin-table thead{display:none}
  /* border-collapse:separate avoids quirks when rows become display:block */
  .admin-table{border-collapse:separate;border-spacing:0;width:100%}
  .admin-table,.admin-table tbody,.admin-table tr,.admin-table td{display:block;max-width:100%}
  .admin-table tr{
    border:1px solid var(--border);border-radius:6px;
    padding:8px 10px;margin-bottom:8px;background:var(--bg)
  }
  .admin-table td{
    border:none;padding:3px 0;
    white-space:normal;word-break:break-word;overflow-wrap:anywhere;
    font-size:.86rem
  }
  /* Label each cell with its column name via data-label (set below) */
  .admin-table td[data-label]::before{
    content:attr(data-label) ": ";
    color:var(--muted);font-size:.72rem;text-transform:uppercase;
    letter-spacing:.04em;font-weight:600;margin-right:4px
  }
  /* Actions cell: wrap buttons so they never overflow */
  .admin-table td[data-label="Actions"]{
    display:flex!important;flex-wrap:wrap;gap:6px;
    margin-top:6px;padding-top:8px;border-top:1px solid var(--border)
  }
  .admin-table td[data-label="Actions"]::before{display:none}
  .admin-table td[data-label="Actions"] .btn,
  .admin-table td[style*="display:flex"] .btn{
    flex:1 1 auto;padding:7px 10px!important;font-size:.82rem!important;min-width:80px
  }
  /* Fallback for the existing inline style="display:flex" action cells */
  .admin-table td[style*="display:flex"]{
    flex-wrap:wrap!important;gap:6px!important;
    margin-top:6px;padding-top:8px!important;border-top:1px solid var(--border)
  }

  /* Modals: full-width on phones */
  .modal{min-width:0;width:95vw;padding:18px}
  .modal input,.modal select{font-size:16px;padding:9px 10px} /* 16px prevents iOS zoom */
  .modal-footer{flex-wrap:wrap}
  .modal-footer .btn{flex:1 1 auto;padding:9px 12px}

  /* Filters */
  .tbl-filter{max-width:none}
  .tbl-filter input{font-size:16px;padding:7px 28px 7px 10px} /* 16px prevents iOS zoom */

  /* Toast */
  #toast{left:10px;right:10px;bottom:10px;text-align:center;font-size:.85rem}

  /* Login inputs: 16px prevents iOS zoom on focus */
  .login-box input{font-size:16px}
}

/* ── Shared-layout additions (shell iframe + hamburger <a> links + embedded) ── */
html.embedded header{display:none!important}
.app-title{font-weight:600}
.content-frame{flex:1;border:none;width:100%;background:var(--bg)}
.hdr-dropdown a{display:block;width:100%;text-align:left;background:none;border:none;color:var(--text);padding:9px 16px;cursor:pointer;font-size:.87rem;font-weight:400;text-decoration:none}
.hdr-dropdown a:hover{background:var(--accent);color:#fff}
.hdr-dropdown a.active{color:var(--accent);font-weight:600}
.hdr-dropdown a.active:hover{color:#fff}
.hdr-dropdown a.hdr-signout{color:var(--danger)}
.hdr-dropdown a.hdr-signout:hover{background:var(--danger);color:#fff}
/* Sectioned menu: Admin, then one heading per product. The rule above each
   heading is what separates the sections, so the first one does without. */
.hm-heading{padding:7px 16px 3px;font-size:.68rem;letter-spacing:.09em;text-transform:uppercase;color:var(--muted);border-top:1px solid var(--border);margin-top:3px}
.hm-section:first-child .hm-heading{border-top:none;margin-top:0}

/* ── Configuration page: let it scroll on desktop instead of cramming the
   sensors + events into one fixed viewport. Both regions get a definite
   height (so their internal table scroll works) and #configView scrolls when
   they overflow. Mobile (≤768px) keeps its own stacking rules above. ── */
@media (min-width:769px){
  main#mainContent{flex:none;height:62vh}
  /* Events grows to whatever height it needs; the page scrolls to reach it
     (no inner scrollbar). Sensor panels above keep their fixed-area scroll. */
  #eventsPanel{flex:none;height:auto;min-height:0;overflow:visible}
  #eventsPanel .grid-wrap{flex:none;overflow:visible}
  /* Drag bar to resize the sensors area height (paired with the column
     splitter for width). */
  .v-splitter{display:block;flex:none;height:6px;margin:2px 10px;border-radius:3px;
              background:var(--border);cursor:row-resize}
  .v-splitter:hover{background:var(--accent)}
}

/* ── Charts (static/js/charts.js) ─────────────────────────────────────────────
   Chrome stays recessive so the data is the only loud thing: hairline SOLID
   gridlines (dashes read as noise), muted axis text, and no frame around the
   plot. Marks carry the series colour; every piece of text here wears a text
   token instead. */
.chart-svg{display:block;overflow:visible}
.chart-grid{stroke:var(--chart-grid);stroke-width:1;shape-rendering:crispEdges}
/* pointer-events:none matters: an x-axis label sits directly over the
   below-baseline "no data" tick and would otherwise swallow its hover, making
   the one mark that most needs an explanation the one that cannot be asked. */
.chart-axis{fill:var(--muted);font-size:.68rem;pointer-events:none}
/* The one dashed line in the system, and deliberately so: a threshold is an
   annotation, not data, and the dash is what says "this is not a series". */
.chart-threshold{stroke:var(--muted);stroke-width:1;stroke-dasharray:3 3;opacity:.75}
.chart-bar{transition:filter .12s}
.chart-bar:hover{filter:brightness(1.25)}
.chart-nodata{fill:var(--muted);opacity:.5}
.chart-line{stroke-width:2;stroke-linejoin:round;stroke-linecap:round}
/* A series' lowest and highest (BluCharts.line, `extremes: true`): two dotted
   lines in the line's own colour, thinner than the average and under it —
   context around the line, never a second series competing with it. Round
   caps on a zero-length dash make round dots; the 4px gap keeps them from
   reading as a solid hairline. */
.chart-extreme{fill:none;stroke-width:1.5;stroke-dasharray:0 4;stroke-linecap:round;
               pointer-events:none}
/* The line-style key under a chart with extremes: the legend's voice, with
   each sample drawn in the current (muted) text colour so the key says
   "solid" and "dotted" without borrowing any series' colour. */
.chart-key{margin-top:6px;font-size:.72rem;color:var(--muted);display:flex;align-items:center;
           flex-wrap:wrap;gap:5px}
.chart-key svg{vertical-align:middle;overflow:visible}
.chart-key-sep{margin:0 3px}
/* 2px surface ring, so an end marker stays legible where it crosses its own
   line or another series. */
.chart-dot{stroke:var(--surface);stroke-width:2}
.chart-cross{stroke:var(--muted);stroke-width:1;opacity:.6;pointer-events:none}
.chart-hit{cursor:crosshair}
/* Drag-to-zoom (BluCharts, `onZoom`). pan-y hands a vertical touch drag to
   the page so it still scrolls; only a sideways one selects. The band is the
   accent at low opacity so it reads as a selection, not as a fifth series,
   and takes no pointer events so the drag it belongs to never lands on it. */
.chart-zoomable{touch-action:pan-y;user-select:none;-webkit-user-select:none}
.chart-brush{fill:var(--accent);opacity:.18;stroke:var(--accent);stroke-width:1;
             pointer-events:none}
/* Legend entries and the Min/max item as buttons (BluCharts.line, `toggles`).
   They keep the legend's own voice — same size, same muted text — and say
   their state two ways that are not colour: a hidden series' swatch is
   hollowed out (still in its colour, so it can be found again) and its name
   struck through; aria-pressed carries the same fact to a screen reader. */
.chart-toggle{background:none;border:0;padding:0 2px;margin:0 -2px;font:inherit;
              color:inherit;cursor:pointer;border-radius:3px;display:inline-block;
              line-height:inherit}
.chart-toggle:hover{color:var(--text)}
.chart-toggle:focus-visible{outline:1px solid var(--accent);outline-offset:2px}
.chart-toggle i{background:var(--sw)}
.chart-toggle[aria-pressed="false"]{text-decoration:line-through;opacity:.65}
.chart-toggle[aria-pressed="false"] i{background:transparent;
                                     box-shadow:inset 0 0 0 1.5px var(--sw)}
.chart-toggle[aria-pressed="false"] svg{opacity:.5}
.chart-seg{stroke:none}
.chart-seg:hover{filter:brightness(1.25)}
.chart-empty{padding:18px;text-align:center;color:var(--muted);font-style:italic;font-size:.8rem}
/* One chart + its title and legend. Stacked charts need a real break between
   them: with only margin, a legend sits as close to the NEXT chart's title as
   to its own plot and reads as belonging to the wrong one. The hairline rule
   is layout, not data chrome — it separates figures, it never crosses one. */
.chart-block{margin-top:20px;padding-top:16px;border-top:1px solid var(--border)}
/* The first block follows the stat tiles, so it is never :first-child —
   target it by what actually precedes it. */
.stat-row + .chart-block{margin-top:8px;padding-top:0;border-top:none}
.chart-legend{display:flex;flex-wrap:wrap;gap:12px;margin-top:8px;font-size:.72rem;color:var(--muted)}
.chart-legend i{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:5px;
                vertical-align:middle}
.chart-tip{position:fixed;z-index:600;pointer-events:none;background:var(--bg);
           border:1px solid var(--border);border-radius:5px;padding:6px 9px;font-size:.76rem;
           color:var(--text);box-shadow:0 4px 16px #0008;max-width:260px;line-height:1.5}
.chart-tip i{display:inline-block;width:8px;height:8px;border-radius:2px;margin-right:5px}
.chart-tip-range{color:var(--muted)}
.chart-title{font-size:.78rem;color:var(--muted);margin-bottom:4px;display:flex;
             justify-content:space-between;align-items:baseline;gap:8px}

/* ── Donut (BluCharts.donut) ──────────────────────────────────────────────────
   Ring on the left, legend on the right, stacking under it when the card gets
   too narrow to hold both. The legend is not decoration: a slice under ~9% has
   no room for a direct label, so its row is the only place its number is
   written, and hovering the row lifts the slice it names. */
.donut-wrap{display:flex;align-items:center;justify-content:center;gap:18px}
.donut-wrap.is-stacked{flex-direction:column;align-items:stretch;gap:8px}
.donut-svg{flex:0 0 auto;margin:0 auto}
/* A 2px gap in the surface colour between neighbours — the same separator the
   column chart uses, and never an outline around the mark. */
.donut-seg{stroke:var(--surface);stroke-width:2;transition:filter .12s}
.donut-seg:hover,.donut-seg.hot{filter:brightness(1.3)}
.donut-seg.is-clickable,.donut-row.is-clickable{cursor:pointer}
.donut-row.is-clickable:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
.donut-hatch{stroke:var(--chart-neutral);stroke-width:2.5}
.donut-label{fill:#fff;font-size:.72rem;font-weight:700;pointer-events:none;
             paint-order:stroke;stroke:#0006;stroke-width:2.5px}
.donut-center{fill:var(--text);font-size:1.35rem;font-weight:600;
              font-variant-numeric:tabular-nums}
.donut-center-sub{fill:var(--muted);font-size:.66rem;text-transform:uppercase;
                  letter-spacing:.05em}
/* Capped rather than flex:1. Each row is label / count / share on one line,
   and letting it fill a wide card strands the numbers a hand's width from the
   name they belong to — the reader has to track across empty space to pair
   them up. */
.donut-legend{flex:0 1 300px;min-width:0;display:flex;flex-direction:column;
              gap:3px;font-size:.75rem}
.donut-wrap.is-stacked .donut-legend{flex:1 1 auto}
.donut-row{display:flex;align-items:center;gap:7px;padding:2px 4px;border-radius:4px;
           cursor:default;font-variant-numeric:tabular-nums}
.donut-row:hover,.donut-row.hot{background:var(--bg)}
.donut-row i{flex:0 0 auto;width:10px;height:10px;border-radius:2px}
/* The hatch, repeated at swatch size so the legend key matches the slice. */
.donut-row i.sw-unknown{background:repeating-linear-gradient(45deg,
  var(--chart-neutral) 0 2px, transparent 2px 4px);border:1px solid var(--chart-neutral)}
.donut-row b{flex:1;min-width:0;font-weight:400;color:var(--text);
             overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.donut-row em{font-style:normal;font-weight:600;color:var(--text)}
.donut-row u{text-decoration:none;color:var(--muted);width:42px;text-align:right}
.donut-row .tag{flex:0 0 auto;font-size:.6rem;font-weight:700;letter-spacing:.04em;
                text-transform:uppercase;color:var(--accent);border:1px solid var(--accent);
                border-radius:3px;padding:0 4px}

/* ── Stat tiles ───────────────────────────────────────────────────────────────
   Label (sentence case) · value · optional note. The value is the mark here,
   so it may carry a status colour; the label and note stay in text tokens. */
.stat-row{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px}
.stat{flex:1 1 110px;background:var(--bg);border:1px solid var(--border);border-radius:6px;
      padding:8px 10px;min-width:110px}
.stat .lbl{font-size:.7rem;color:var(--muted);text-transform:none}
.stat .val{font-size:1.15rem;font-weight:600;line-height:1.35}
.stat .val.ok{color:var(--chart-ok)}
.stat .val.warn{color:var(--warn)}
.stat .val.bad{color:var(--chart-bad)}
.stat .val.none{color:var(--muted)}
.stat .note{font-size:.68rem;color:var(--muted)}
