/* LiveIAQ Dashboard — "Mosaic Grid Architecture" (Technical Minimalist).
   Flat, bordered, blueprint-inspired, two-tone: a dark sidebar rail
   (--side-bg) against the original light-paper main content area
   (--paper), white cards, and a fresh "clean-air" sky blue (--brand) as
   the one accent hue. Panels use a crisp, no-blur offset shadow
   (drafting-table style) rather than a soft Material lift. Revised
   2026-08-04, iteratively: panels got a shadow/glow so they'd stop
   flowing into the page; the accent hue moved from forest green to sky
   blue; the page went through dark-charcoal and mid-gray main-area phases
   before landing on the original light paper for the main area, paired
   with a dark sidebar rail for contrast between the nav and the work
   area. Deliberately single-theme — this identity IS the design, not a
   light-mode default awaiting a dark counterpart. */
:root{
  /* Two zones, two tones: the main content area is the original light
     paper (--paper) with dark text. The sidebar is its own darker pocket
     (--side-bg) with light text, giving it real contrast against the
     lighter main area instead of just a hairline border. .side{} below
     re-scopes --ink/--line/--accent-text locally to light-on-dark for
     everything inside it; .card/.inset need no special casing since white
     panels already contrast fine against the root's dark-text/light-paper
     defaults. */
  --paper:#F7F7F5; --side-bg:#201F1D; --panel:#FFFFFF; --panel-hover:#E3F6FC; --brand:#1C7FB0; --grid:#3A3A38;
  --sky:#0FA8D6;
  /* ⚠️ THE TEXT-WEIGHT COMPANION TO --sky, and it exists for the same reason
     --moderate is a deep gold rather than yellow: --sky measures 2.77:1 on
     white, so it fails small text badly. This is 4.59:1. Same rule as the
     status ladder — the vivid token is for shapes, this one is for anything
     that has to be READ. */
  --sky-ink:#0A7FA3;
  --line:rgba(58,58,56,.18); --line-faint:rgba(58,58,56,.1);
  --ink:#1A2420; --ink-soft:#5B6660; --ink-faint:#8A928C;
  --accent-text:var(--brand);
  --coral:#FF8C69; --mint:#9EFFBF; --gold:#F4D35E;
  /* Semantic status. Revised 2026-08-08: the previous set ran
     green -> dark-goldenrod -> ORANGE -> brick, which had two problems. The
     four hues sat within about 60 degrees of each other, so "moderate",
     "unhealthy" and "hazardous" were three shades of the same warm smear
     rather than three distinct signals; and every one of them was desaturated
     enough to read as washed out. Worse, orange over the dark hero panel
     mixes to BROWN.
     
     The ladder is now green -> yellow -> red -> purple: EPA's own AQI sequence
     with its orange rung dropped. Four unmistakably different hues, all
     high-chroma.
     
     ⚠️ TWO TOKENS PER STATUS, and they are not interchangeable:
       --x        text and hairlines. Dark enough to hold 4.5:1 on white, which
                  is why "moderate" here is a deep gold rather than yellow —
                  small yellow text on white is unreadable at any weight.
       --x-fill   dots, badges, bars, chart strokes. The vivid one. Only ever
                  used behind white text or as a solid shape.
     Using --moderate-fill for text, or --moderate for a 6px dot, breaks one of
     those two constraints every time. */
  --good:#00874A;      --good-fill:#00B85C;
  --moderate:#9C7000;  --moderate-fill:#FFC42E;
  /* ⚠️ TRUE RED, hue 0deg, no blue and no green in the fill. The first attempt
     used #F5333B / #D01B1B, which carried enough blue to read pink against the
     yellow band beside it. Anything that drifts toward #F05 reads pink and
     anything that drifts toward #E44 reads salmon; both undercut the one
     colour on the page that has to mean "stop". Keep G and B equal and low. */
  --unhealthy:#C40000; --unhealthy-fill:#E60000;
  --hazardous:#7A1FA2; --hazardous-fill:#A63DD9;
  --danger:#C40000;
  --accent:var(--brand);

  /* ── Admin section accents ──────────────────────────────────────────────
     ⚠️ DELIBERATELY NOT THE STATUS LADDER, and that is the whole point. The
     four hues above MEAN something — green is "this air is fine", purple is
     "hazardous" — so painting a nav item or a console row with them would say
     something about air quality that nobody intended. A green "Sites" row
     reads as "the sites are good". These four are a separate, parallel set
     whose only job is to tell four SECTIONS apart.

     Orange is available here precisely because the status ladder dropped it
     (see the note above: EPA's sequence with the orange rung removed), so it
     is the one vivid hue in this palette that can never be mistaken for a
     reading. Access is indigo rather than violet to keep clear of
     --hazardous #A63DD9, which is the nearest neighbour and the one collision
     worth actively avoiding. */
  --sec-sites:#2E9E7A;
  --sec-nodes:#0FA8D6;
  --sec-alarms:#E8912B;
  --sec-access:#6E56CF;
  /* Console Home is the index the four sections hang off, not a fifth section,
     so it gets a neutral grey rather than a hue of its own. Anything coloured
     here would read as a peer of Sites/Nodes/Alarms/Access; grey reads as the
     parent. Light enough to hold against the fixed dark sidebar. */
  --sec-console:#9AA29C;
  --r:2px; /* the only corner radius this system uses, per spec: 0 or 2px */

  --font-display:"Space Grotesk",system-ui,sans-serif;
  --font-mono:"JetBrains Mono",ui-monospace,"SF Mono",Menlo,monospace;
  --font-body:"General Sans",system-ui,-apple-system,sans-serif;
}
*{box-sizing:border-box;margin:0;padding:0}
body{
  background-color:var(--paper); color:var(--ink);
  font-family:var(--font-body); line-height:1.55; -webkit-font-smoothing:antialiased;
  /* Mosaic background: seamless tile of interlocking panels divided by hairlines.
     Dark stroke on the light paper main area (the dark sidebar has its own
     opaque background and never shows this pattern). */
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240' viewBox='0 0 240 240'%3E%3Cg fill='none' stroke='%233A3A38' stroke-opacity='0.14' stroke-width='0.75'%3E%3Crect x='0' y='0' width='140' height='140'/%3E%3Crect x='140' y='0' width='100' height='90'/%3E%3Crect x='140' y='90' width='100' height='60'/%3E%3Crect x='0' y='140' width='90' height='100'/%3E%3Crect x='90' y='140' width='80' height='55'/%3E%3Crect x='90' y='195' width='80' height='45'/%3E%3Crect x='170' y='150' width='70' height='90'/%3E%3C/g%3E%3C/svg%3E");
  background-repeat:repeat;
}
h1,h2,h3,.disp{font-family:var(--font-display);font-weight:700;letter-spacing:-.02em;line-height:.98;text-wrap:balance}
a{color:var(--accent-text);text-decoration:none}
.muted{color:var(--ink-soft)}.accent{color:var(--accent-text)}.tiny{font-size:.8rem}
.row{display:flex;gap:1rem;align-items:center;flex-wrap:wrap}
.spread{display:flex;justify-content:space-between;align-items:center;gap:1rem;flex-wrap:wrap}
.grow{flex:1}
:focus-visible{outline:2px solid var(--accent-text);outline-offset:2px}

/* eyebrow / mono label convention used throughout for technical metadata */
.eyebrow,.mono-label{font-family:var(--font-mono);font-size:.68rem;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-faint)}

/* surfaces — panels are plain white (--panel) against the light-paper main
   area (--paper), shifting to a fresh pale-blue wash (--panel-hover, --sky)
   on hover. Depth comes from two stacked shadows: a crisp no-blur offset
   (drafting-table style, keeps the flat/technical personality) plus a soft
   ambient glow underneath (real lift, not just an outline) — together they
   make panels pop off the page instead of reading as a bordered rectangle. */
.card{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);padding:1.5rem;
  box-shadow:4px 4px 0 rgba(0,0,0,.14), 0 14px 32px -10px rgba(0,0,0,.35)}
.card.sm{padding:1rem}
.inset{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);
  box-shadow:2px 2px 0 rgba(0,0,0,.10), 0 8px 20px -9px rgba(0,0,0,.28)}

/* buttons — ghost + solid brand blue, sharp corners, no shadow */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:.4rem;
  font-family:var(--font-mono);font-weight:600;font-size:.72rem;letter-spacing:.08em;text-transform:uppercase;
  min-height:38px;padding:.5rem 1rem;cursor:pointer;border-radius:var(--r);
  background:transparent;color:var(--ink);border:1px solid var(--grid);
  transition:background-color .15s linear,color .15s linear,border-color .15s linear}
.btn:hover{border-color:var(--brand);color:var(--brand)}
.btn.primary{background:var(--brand);color:#fff;border-color:var(--brand)}
.btn.primary:hover{background:#125E82;color:#fff}
.btn.sm{min-height:30px;padding:.3rem .7rem;font-size:.66rem}
.btn.active{background:var(--brand);color:#fff;border-color:var(--brand)}
/* Destructive actions. Outlined rather than filled: the button that opens a
   confirmation should not look more urgent than the one that carries it out. */
.btn.danger{color:var(--danger);border-color:var(--danger)}
.btn.danger:hover{background:var(--danger);color:#fff;border-color:var(--danger)}
.btn:disabled{opacity:.4;cursor:not-allowed}
.btn.danger:disabled:hover{background:transparent;color:var(--danger)}

/* Admin tables — dense, directly editable rows. Replaces a stack of tall cards
   that made it impossible to see a whole fleet at once, which is how one site
   ended up carrying another site's coordinates unnoticed. */
.admin-h2{font-family:var(--font-display);font-size:1rem;margin:1.4rem 0 .5rem}
.admin-table td{padding:.35rem .5rem;vertical-align:middle}
.admin-table th{padding:.5rem .5rem}
.admin-table input,.admin-table select{min-height:30px;padding:.25rem .45rem;font-size:.82rem}
.admin-table td.actions{white-space:nowrap}
/* ⚠️ THE `width:auto` IS THE WHOLE RULE; nowrap on the cell does nothing on its
   own. Selects inherit `width:100%` from the base input rule further down, so
   inside a table the picker takes the column's width and the column is sized by
   the table — which rendered "Aria Sense Wall Pod" as "Aria Sense Wall P". Two
   models whose names share a nine-character prefix must not be told apart by
   where the text happens to stop. The table already scrolls horizontally, so
   the picker can have its natural width. */
.admin-table td.model-cell{white-space:nowrap}
.admin-table td.model-cell select{width:auto}
.admin-table td.actions .btn{margin-right:.25rem}
.admin-table .mono-cell{font-family:var(--font-mono);font-size:.74rem}
/* A retired site stays listed and fully editable — just visibly demoted, so
   "where did that site go?" never becomes a support question. */
.admin-table tr.is-inactive{opacity:.6}
/* Missing coordinates are the one gap worth flagging in the row itself: the
   site silently loses outdoor AQI and its whole outdoor history. */
.admin-table input.missing{border-color:var(--moderate);background:#FDF8EC}
.warn-box{padding:.75rem .9rem;margin-bottom:1rem;border-left:3px solid var(--moderate);font-size:.85rem}

/* Per-row feedback lives in its own row so a message never reflows the table. */
/* ⚠️ THE `.rtable` PREFIX IS LOAD-BEARING, NOT REDUNDANT. `.rtable td` further
   down this file sets padding:.65rem .9rem at the SAME specificity (0,0,1,1)
   as a bare `.msg-row td`, and it is declared LATER, so it wins on source
   order — the plain rule below is inert on any table that is also an .rtable.
   The symptom is silent and easy to miss: `.row-msg:empty{display:none}` hides
   the message correctly, but the empty row still claims ~22px of td padding, so
   every data row in an admin table carries a band of dead space under it. On a
   fleet page whose whole purpose is scanning many rows at once that is a real
   cost. Raising specificity here fixes it wherever the idiom is used without
   reordering the file. */
.rtable .msg-row td{padding:0 .5rem;border-bottom:none}
.msg-row td{padding:0 .5rem;border-bottom:none}
.row-msg:empty{display:none}
.row-msg{font-size:.78rem;padding:.15rem 0 .4rem}
.row-msg.ok{color:var(--good)}
.row-msg.bad{color:var(--danger)}
.confirm-box{border:1px solid var(--danger);border-radius:var(--r);padding:.6rem .7rem;margin:.2rem 0 .5rem}
.confirm-title{color:var(--danger);font-weight:700;font-size:.82rem;margin-bottom:.25rem}
.confirm-box .c-in{display:inline-block;width:auto;min-width:190px;margin-right:.4rem}

/* ── Operating hours ───────────────────────────────────────────────────────
   The summary cell doubles as the toggle. Three visual states, because they
   mean genuinely different things: real hours someone set, the untouched
   Mon–Fri 8–5 seed, and nothing at all. The last two both produce occupancy
   numbers that LOOK measured, so neither may read as ordinary data. */
.hours-cell{max-width:230px}
.btn-hours{width:100%;text-align:left;font:inherit;font-size:.76rem;line-height:1.3;
  cursor:pointer;padding:.3rem .45rem;border:1px solid var(--line);border-radius:var(--r);
  background:transparent;color:var(--ink)}
.btn-hours:hover{background:var(--panel-hover);border-color:var(--brand)}
.btn-hours.seeded{border-style:dashed;color:var(--ink-soft)}
.btn-hours.unset{border-color:var(--moderate);background:#FDF8EC;color:#7A5B0B}
.btn-hours .hint{font-size:.68rem;text-transform:uppercase;letter-spacing:.04em;opacity:.7}

.hours-row td{padding:0 .5rem .6rem;border-bottom:none}
.hours-edit{border:1px solid var(--brand);border-radius:var(--r);padding:.7rem .8rem;
  background:var(--panel-hover)}
.hours-head{font-size:.85rem;margin-bottom:.55rem}
.hours-head .tiny{display:block;font-weight:400}
/* Seven fixed columns so the checkboxes line up as a week and an irregular day
   is visible as a break in the pattern rather than as text to read. */
.hours-days{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:.4rem}
@media (max-width:820px){.hours-days{grid-template-columns:repeat(2,minmax(0,1fr))}}
.hday{display:flex;flex-direction:column;gap:.2rem;padding:.4rem;border-radius:var(--r);
  background:var(--panel);border:1px solid var(--line)}
.hday.off{opacity:.5;background:transparent}
.hday-on{display:flex;align-items:center;gap:.3rem;font-size:.78rem;font-weight:700;cursor:pointer}
.hday-on input{min-height:0;margin:0;width:14px;height:14px;accent-color:var(--brand)}
.admin-table .hday input[type=time]{width:100%;min-height:26px;font-size:.74rem;padding:.15rem .25rem}
.hdash{display:none}
.hours-foot{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;margin-top:.6rem}
.hours-foot label{display:flex;align-items:center;gap:.3rem}
.h-msg.ok{color:var(--good)}
.h-msg.bad{color:var(--danger)}

/* ── Interventions ─────────────────────────────────────────────────────────
   A dated log of what was done to the building, rendered as vertical markers on
   the trend chart. The list is a grid so dates and kinds line up into columns —
   scanning "when did we last change filters" should not mean reading prose. */
.intv-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.15rem}
.intv{display:grid;grid-template-columns:10px 6.5rem 8rem 1fr auto 1.4rem;
  align-items:center;gap:.5rem;padding:.3rem .1rem;border-bottom:1px solid var(--line);font-size:.82rem}
.intv:last-child{border-bottom:none}
.intv-dot{width:9px;height:9px;border-radius:50%}
.intv-date{font-family:var(--font-mono);font-size:.74rem;color:var(--ink-soft)}
.intv-kind{font-weight:700;font-size:.76rem}
.intv-note{min-width:0;overflow-wrap:anywhere}
.intv-del{border:none;background:transparent;color:var(--ink-soft);cursor:pointer;
  font-size:1.05rem;line-height:1;padding:.1rem .3rem;border-radius:var(--r)}
.intv-del:hover{color:#fff;background:var(--danger)}
.intv-del:disabled{opacity:.4;cursor:default}
@media (max-width:760px){
  .intv{grid-template-columns:10px 1fr 1.4rem;row-gap:.15rem}
  .intv-kind,.intv-note,.intv-by{grid-column:2}
}
/* Marker key under the chart — only the kinds actually present are listed, so
   it describes this site's history rather than the schema's vocabulary. */
.intv-key{display:flex;flex-wrap:wrap;gap:.75rem;align-items:center;margin-top:.5rem;font-size:.74rem}
.intv-key i{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:.3rem}

/* Endpoint rows are read-only and navigate on click — the table is for
   scanning, and mutation happens on the endpoint's own screen where there is
   room to say what each action actually does. */
.admin-table tr.clickable{cursor:pointer}
.admin-table tr.clickable:hover td{background:var(--panel-hover)}
.provision-card{display:block;padding:1rem 1.25rem;border-left:3px solid var(--brand);
  text-decoration:none;margin-bottom:1.5rem;max-width:420px}
.provision-card .name{font-family:var(--font-display);font-weight:700;color:var(--brand)}
.provision-card:hover{background:var(--panel-hover)}
.danger-card{border-color:var(--danger)}

/* Add-a-row forms sit directly under their table and mirror its column feel. */
.add-row{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;margin-bottom:.4rem}
.add-row input,.add-row select{width:auto;flex:1;min-width:130px;min-height:32px}

/* inputs */
label{display:block;font-family:var(--font-mono);font-size:.68rem;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-faint);margin:.6rem 0 .3rem}
input,select{width:100%;font-family:var(--font-body);font-size:.92rem;color:var(--ink);background:var(--panel);
  border:1px solid var(--grid);border-radius:var(--r);padding:.6rem .85rem}
input:focus,select:focus{outline:none;border-color:var(--brand)}

/* layout */
.app{display:grid;grid-template-columns:230px 1fr;min-height:100vh}
/* The sidebar is its own dark pocket, contrasting against the lighter
   main content area — re-scopes ink/line/accent back to light-on-dark
   locally, same pattern the surfaces used to use for panels. */
/* ⚠️ THE RAIL IS PINNED TO THE VIEWPORT, NOT TO THE PAGE. It used to scroll
   away with the content, so on a long dashboard — 38 sites, a full report, a
   node list — the navigation was simply gone until you scrolled back to the
   top. `position:sticky` + `height:100vh` + `align-self:start` is what does it:
   in a grid the item would otherwise stretch to the row's full height, and a
   sticky box as tall as its container has nowhere to stick.
   `overflow-y:auto` keeps the nav reachable on a short viewport, and
   `overscroll-behavior:contain` stops a scroll that runs off the end of the nav
   from continuing into the page behind it. Undone at the mobile breakpoint
   below, where the sidebar is a stacked banner rather than a column. */
.side{background:var(--side-bg);padding:1.5rem 1rem;display:flex;flex-direction:column;gap:.3rem;
  border-right:1px solid rgba(255,255,255,.08);box-shadow:2px 0 16px rgba(0,0,0,.25);
  position:sticky;top:0;align-self:start;height:100vh;overflow-y:auto;overscroll-behavior:contain;
  scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.22) transparent;
  --ink:#ECEDEA; --ink-soft:#A9ACA8; --ink-faint:#84867F; --accent-text:#5DC8EC;
  color:var(--ink)}
.side::-webkit-scrollbar{width:8px}
.side::-webkit-scrollbar-thumb{background:rgba(255,255,255,.18);border-radius:4px}
.side::-webkit-scrollbar-track{background:transparent}
/* ── Brandmark ─────────────────────────────────────────────────────────────
   The mark already HAS its own frame, so the old brand-blue tile behind it is
   gone — a rounded square inside a rounded square reads as a mistake. `.dot`
   keeps its name (it is referenced from app.js in four places) but is now just
   a transparent 26px box holding the SVG. */
.brandmark{display:flex;align-items:center;gap:.55rem;font-family:var(--font-display);
  font-weight:700;font-size:1.05rem;padding:.5rem .6rem 1.1rem}
.brandmark .dot{width:26px;height:26px;display:grid;place-items:center;flex:0 0 auto}
.brandmark .dot svg{width:26px;height:26px;display:block}

/* ⚠️ THE MARK AND THE WORDMARK ARE NEVER SHOWN TOGETHER.
   The mark's dot field and the wordmark's three-dot separator are the same
   idea, so side by side they read as a stutter rather than a lockup. One per
   surface, chosen by what the surface needs:

     sidebar     MARK only  — the tab title already names the product and the
                              user has just come through a sign-in that did too
     auth card   WORDMARK only — someone arriving cold from a reset link needs
                              the name, not an icon they have never seen
     report      WORDMARK only — a document that leaves the building has to
                              identify itself in words

   Alone in the sidebar the mark carries the whole slot, so it is drawn larger
   than it was when it sat beside text. */
/* Alone in the slot the wordmark carries it, so it is set larger than it was
   when a mark sat beside it. */
.brandmark.wordmark-only{font-size:1.32rem;padding:.7rem .6rem 1.5rem}

.brandmark.mark-only{padding:.7rem .6rem 1.4rem}
.brandmark.mark-only .dot,
.brandmark.mark-only .dot svg{width:46px;height:46px}

/* The wordmark. The separator is the SWEEP itself, shrunk into the gap where
   the middot used to sit — the same curve as the mark, so the two are one
   gesture rather than two related ones.

   ⚠️ Sized in `em` throughout so it tracks the wordmark's font-size. The
   sidebar sets 1.32rem and the sign-in card sets 1.05rem; a px separator would
   need a second number kept in sync with each of them.

   ⚠️ preserveAspectRatio="none" on the SVG means the box IS the shape — the
   sweep stretches to whatever width and height are set here rather than
   letter-boxing inside them. That is deliberate: at this size a faithful
   aspect ratio leaves slack on one side and the separator drifts toward one
   word. */
.wm{display:inline-flex;align-items:center;letter-spacing:-.01em}
.wm-sep{display:inline-block;width:.86em;height:.58em;margin:0 .16em;
  position:relative;top:-.02em}
.wm-sep svg{width:100%;height:100%;display:block}
/* ── Sidebar navigation ──────────────────────────────────────────────────────
   FRAMED, not bare text. Previously these were unadorned words on the dark
   panel, distinguishable from the surrounding chrome only by hover — so the
   sidebar read as a caption block rather than as a set of controls, and the
   only way to discover it was clickable was to point at it.

   Each link is now its own bordered plate: brighter ink, a visible edge, and a
   faint fill, so the boxes carry the affordance at rest instead of on hover.
   Hover then has somewhere to go (edge and fill both lift) rather than being
   the thing that reveals the control exists at all.

   ⚠️ The frame colours are rgba-white on purpose, not palette tokens. .side
   re-scopes --ink/--line locally to a light-on-dark set, and the sidebar is the
   one surface whose background is fixed regardless of theme — so these have to
   key off the panel itself, not off tokens that flip with the page. */
.nav a.nav-link{display:block;padding:.6rem .75rem;margin-bottom:.4rem;border-radius:var(--r);
  border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.045);color:var(--ink);
  font-family:var(--font-mono);font-weight:700;font-size:.74rem;letter-spacing:.08em;text-transform:uppercase;
  transition:background .12s ease,border-color .12s ease,color .12s ease}
.nav a.nav-link:hover{color:var(--accent-text);background:rgba(255,255,255,.1);
  border-color:rgba(255,255,255,.34)}

/* The page you are ON. A filled plate with an accent edge — the distinction has
   to survive next to :hover, so it is carried by the left rule and the fill
   together rather than by colour alone. */
.nav a.nav-link.is-current{color:var(--accent-text);background:rgba(255,255,255,.12);
  border-color:rgba(255,255,255,.28);box-shadow:inset 3px 0 0 var(--accent-text)}

/* ── Grouped nav ─────────────────────────────────────────────────────────
   The sidebar was one undifferentiated column of blue, so the operational
   screens and the administrative ones looked like a single flat list of
   seven things. The rail carries the grouping: operational items share the
   brand colour, each admin section carries its own accent, and the accent is
   the SAME one that section's rows use on the Admin page — so the colour is
   a cross-reference, not decoration.

   ⚠️ THE RAIL IS ALWAYS PRESENT, at 2px for an idle item and 3px when
   current. Making it appear only on the current item would mean the group
   colouring vanished exactly when you needed to see where you were. */
.nav a.nav-link{box-shadow:inset 2px 0 0 var(--nav-accent,transparent)}
.nav a.nav-link:hover{box-shadow:inset 2px 0 0 var(--nav-accent,transparent)}
.nav a.nav-link.is-current{box-shadow:inset 3px 0 0 var(--nav-accent,var(--accent-text))}
.nav a.nav-link.is-current .nav-dot{opacity:1}
.nav .nav-dot{display:inline-block;width:6px;height:6px;border-radius:50%;
  margin-right:.5rem;vertical-align:middle;background:var(--nav-accent,var(--accent-text));
  opacity:.75}

/* A quiet divider + label between the operational group and the admin group.
   Cheaper than a second nav component and it survives the sidebar being a
   plain flex column. */
.nav .nav-group{margin:.9rem 0 .45rem;padding:0 .75rem;font-family:var(--font-mono);
  font-size:.62rem;letter-spacing:.14em;text-transform:uppercase;
  color:rgba(255,255,255,.42)}
.nav .nav-group::before{content:'';display:block;height:1px;margin-bottom:.7rem;
  background:rgba(255,255,255,.14)}

/* ── Administration fold ─────────────────────────────────────────────────
   Console Home is the HEADER of the admin group, not a fifth peer in a flat
   column of seven. The four sections nest under it inside a <details>, indented
   behind a guide rule and set a size down, so weight tracks depth: the three
   operational links stay the loudest things in the rail, the header sits level
   with them, and the sections read as its contents.

   ⚠️ The header is a <summary> that CONTAINS an anchor, because the row has two
   jobs — the label navigates to #/admin, the caret and the rest of the row open
   and close the fold. See the click handler in app.js that keeps them apart.

   The default marker is removed twice on purpose: `list-style:none` covers
   Firefox and current Chrome, `::-webkit-details-marker` covers Safari, which
   still ignores the first. */
.nav details.nav-fold{margin-bottom:.4rem}
.nav summary.nav-fold-head{display:flex;align-items:center;cursor:pointer;list-style:none;
  padding:.6rem .75rem;border-radius:var(--r);
  border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.045);color:var(--ink);
  font-family:var(--font-mono);font-weight:700;font-size:.74rem;letter-spacing:.08em;text-transform:uppercase;
  box-shadow:inset 2px 0 0 var(--nav-accent,transparent);
  transition:background .12s ease,border-color .12s ease,color .12s ease}
.nav summary.nav-fold-head::-webkit-details-marker{display:none}
.nav summary.nav-fold-head:hover{color:var(--accent-text);background:rgba(255,255,255,.1);
  border-color:rgba(255,255,255,.34)}
.nav summary.nav-fold-head.is-current{color:var(--accent-text);background:rgba(255,255,255,.12);
  border-color:rgba(255,255,255,.28);box-shadow:inset 3px 0 0 var(--nav-accent,var(--accent-text))}
.nav summary.nav-fold-head.is-current .nav-dot{opacity:1}
/* The label fills the row so the whole width is a navigation target, and the
   caret keeps its own hit area at the end. Colour is inherited so the anchor
   does not fight the summary's hover and current states. */
.nav .nav-fold-link{flex:1 1 auto;min-width:0;color:inherit}
.nav .nav-fold-link:hover{color:inherit}
/* CSS triangle rather than a glyph: it inherits the row's colour, needs no
   font, and rotates cleanly. */
.nav .nav-caret{flex:0 0 auto;margin-left:.5rem;width:0;height:0;
  border-left:5px solid currentColor;border-top:4px solid transparent;border-bottom:4px solid transparent;
  opacity:.55;transition:transform .15s ease,opacity .12s ease}
.nav summary.nav-fold-head:hover .nav-caret{opacity:.9}
.nav details.nav-fold[open] > summary .nav-caret{transform:rotate(90deg)}
/* The children. Indented behind a guide rule that ties them to the header, and
   stepped down in size, padding and frame weight — a submenu, not four more
   top-level plates. */
.nav .nav-sub{margin:.4rem 0 0 .55rem;padding-left:.6rem;
  border-left:1px solid rgba(255,255,255,.14)}
.nav .nav-sub a.nav-link{padding:.42rem .6rem;margin-bottom:.28rem;
  font-size:.66rem;letter-spacing:.06em;
  background:rgba(255,255,255,.025);border-color:rgba(255,255,255,.11)}
.nav .nav-sub a.nav-link:last-child{margin-bottom:0}
.nav .nav-sub a.nav-link:hover{background:rgba(255,255,255,.085);border-color:rgba(255,255,255,.26)}
.nav .nav-sub a.nav-link.is-current{background:rgba(255,255,255,.11);border-color:rgba(255,255,255,.24)}
.nav .nav-sub .nav-dot{width:5px;height:5px;margin-right:.42rem}

/* Sign out is NOT a destination, so it is deliberately left unframed and pushed
   to the bottom — see SIGN_OUT_LINK in app.js. Framing it would make leaving
   look like a fifth place to go. */
.nav a.nav-exit{display:block;margin-top:auto;padding:.6rem .75rem;color:var(--ink-soft);
  border-top:1px solid rgba(255,255,255,.1);border-radius:0;
  font-family:var(--font-mono);font-weight:700;font-size:.74rem;letter-spacing:.08em;text-transform:uppercase}
.nav a.nav-exit:hover{color:var(--accent-text)}
.main{padding:2rem;max-width:1200px}
.crumbs{font-family:var(--font-mono);font-size:.72rem;letter-spacing:.04em;text-transform:uppercase;color:var(--ink-faint);margin-bottom:1rem}
.crumbs a:hover{color:var(--accent-text)}
h1.page{font-size:clamp(1.6rem,3vw,2.1rem);margin-bottom:.3rem;color:var(--brand)}
.sub{font-family:var(--font-mono);font-size:.78rem;color:var(--ink-soft);margin-bottom:1.5rem}
/* Stacked banner at this width: the viewport-height lock above would give the
   nav the whole first screen and push the content off the bottom. */
@media(max-width:820px){.app{grid-template-columns:1fr}.side{position:sticky;top:0;z-index:5;
  height:auto;align-self:stretch;overflow:visible;border-right:none;border-bottom:1px solid rgba(255,255,255,.08)}}

/* site / EP grid — real gap (not a 1px hairline) so each card's own shadow
   and glow is visible on all sides instead of being cancelled out by a
   shared bento background. .tile (EP detail's small metric readout grid)
   stays flat/bordered — a unified instrument cluster, not independent
   navigable panels, so it keeps the tighter old bento treatment. */
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:1.1rem}
.tile{border:none}
.site-card,.ep-card{padding:1.25rem;cursor:pointer;transition:background-color .15s linear,border-color .15s linear,box-shadow .15s linear;border-left:3px solid transparent}
.site-card:hover,.ep-card:hover{background:var(--panel-hover);border-left-color:var(--sky);
  box-shadow:4px 4px 0 rgba(58,58,56,.10), 0 14px 32px -8px rgba(44,95,138,.35)}
.site-card .name,.ep-card .name{font-family:var(--font-display);font-weight:700;font-size:1.05rem;margin-bottom:.15rem;color:var(--ink)}
.site-card .loc,.ep-card .loc{font-family:var(--font-mono);font-size:.7rem;letter-spacing:.04em;text-transform:uppercase;color:var(--ink-faint);margin-bottom:.75rem}

/* Provenance for the dashed alarm line. Sits directly under the chart because a
   limit with no stated source invites the reader to assume a regulatory one. */
.threshold-note{margin-top:.5rem;line-height:1.45;border-left:2px solid var(--line);
  padding-left:.6rem}
.threshold-note:empty{display:none}

/* ── Fleet insights ────────────────────────────────────────────────────────
   Deliberately not another line chart. A calendar answers "which days", a
   stacked bar answers "how much of the time", a ranked table answers "which
   places" — questions a time series makes you infer rather than read. */
.cal{width:100%;height:auto;max-width:100%}
.cal-month{font-family:var(--font-mono);font-size:7px;fill:var(--ink-faint)}
.cal-key{display:flex;flex-wrap:wrap;gap:.9rem;margin-top:.7rem;
  font-family:var(--font-mono);font-size:.62rem;text-transform:uppercase;
  letter-spacing:.06em;color:var(--ink-soft)}
.cal-key span{display:flex;align-items:center;gap:.35rem}
.cal-key i{width:9px;height:9px;border-radius:1px;display:inline-block}

.band-bar{display:flex;height:22px;border-radius:var(--r);overflow:hidden;
  background:var(--line-faint);margin-bottom:.4rem}
.bb-seg{height:100%}

.grid-2{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1rem;margin-bottom:.5rem}

.occ-pair{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:1.2rem}
.occ-n{font-family:var(--font-display);font-size:2rem;font-weight:700;line-height:1;
  letter-spacing:-.02em;color:var(--ink)}
.occ-n.good{color:var(--good)}.occ-n.moderate{color:var(--moderate)}
.occ-n.usg{color:var(--unhealthy)}.occ-n.unhealthy{color:var(--hazardous)}

/* Inline magnitude bar inside a table row — carries the comparison the numbers
   beside it can only imply. */
.minibar{height:9px;background:var(--line-faint);border-radius:1px;overflow:hidden;min-width:90px}
.minibar i{display:block;height:100%}

/* Hour-of-day profile. A recurring shift-change peak is a SHAPE; a trend line
   averages it into nothing. */
.hourbars{display:flex;align-items:flex-end;gap:2px;height:120px}
.hb{flex:1;display:flex;flex-direction:column;justify-content:flex-end;align-items:center;height:100%}
.hb i{width:100%;display:block;border-radius:1px 1px 0 0;min-height:2px}
.hb span{font-family:var(--font-mono);font-size:.55rem;color:var(--ink-faint);margin-top:.25rem}

/* Live-refresh marker. Deliberately static — a pulsing dot here would borrow the
   visual language the threshold alarm uses, and "the page is updating" must not
   read as "something is wrong". */
.live-badge{position:fixed;right:1rem;bottom:1rem;z-index:20;display:flex;align-items:center;gap:.4rem;
  font-family:var(--font-mono);font-size:.62rem;letter-spacing:.08em;text-transform:uppercase;
  color:var(--ink-soft);background:var(--panel);border:1px solid var(--line);
  border-radius:var(--r);padding:.3rem .55rem;box-shadow:2px 2px 0 rgba(58,58,56,.08)}
.live-badge .dot{width:6px;height:6px;background:var(--good);border-radius:0}
@media(max-width:820px){.live-badge{display:none}}

/* Threshold alarm. A breached endpoint card pulses slowly so it is noticeable
   from across a room without being frantic — 2.6 s, eased, and it moves only
   colour, never size or position, so it cannot make surrounding text jitter. */
@keyframes ep-alarm{
  0%,100%{background:var(--panel);box-shadow:4px 4px 0 rgba(178,58,46,.14)}
  50%    {background:#FBE9E6;box-shadow:4px 4px 0 rgba(178,58,46,.42), 0 0 0 3px rgba(178,58,46,.20)}
}
/* Endpoint cards, and the site cards that roll them up — a site alarms when any
   endpoint inside it does, so the condition is visible without drilling in. */
.ep-card.alarm,.site-card.alarm{border-left-color:var(--danger);animation:ep-alarm 2.6s ease-in-out infinite}
.ep-card.alarm:hover,.site-card.alarm:hover{border-left-color:var(--danger)}

/* The single offending metric inside the live-snapshot grid. .tile is flat and
   borderless by design (a unified instrument cluster, not separate panels), so
   the alarm gets its own lighter keyframes: no offset shadow to pulse, just the
   wash and a ring, keeping the cluster's flat treatment intact. */
@keyframes tile-alarm{
  0%,100%{background:#FBE9E6;box-shadow:inset 0 0 0 1px rgba(178,58,46,.35)}
  50%    {background:#F6D5CF;box-shadow:inset 0 0 0 2px rgba(178,58,46,.75)}
}
.tile.alarm{animation:tile-alarm 2.6s ease-in-out infinite;border-radius:var(--r)}
.tile.alarm .n{color:var(--danger)}
.tile-alarm{font-family:var(--font-mono);font-size:.58rem;letter-spacing:.06em;
  text-transform:uppercase;color:var(--danger);margin-top:.15rem}
.alarm-banner{font-family:var(--font-mono);font-size:.62rem;letter-spacing:.06em;text-transform:uppercase;
  color:#fff;background:var(--danger);border-radius:var(--r);padding:.25rem .5rem;margin:.5rem 0 .25rem;
  display:inline-block}
/* ⚠️ A slow full-card colour pulse is exactly the kind of motion that triggers
   discomfort for vestibular and photosensitive users, and this is a healthcare
   product. Honour the OS setting: keep the card unmistakably red, drop the
   animation entirely. The alarm must not depend on movement to be legible. */
@media (prefers-reduced-motion: reduce){
  .ep-card.alarm,.site-card.alarm{animation:none;background:#FBE9E6;box-shadow:4px 4px 0 rgba(178,58,46,.42)}
  .tile.alarm{animation:none;background:#FBE9E6;box-shadow:inset 0 0 0 2px rgba(178,58,46,.75)}
}

/* status badge — square dot + mono uppercase, per spec (no pill/shadow) */
.status{display:inline-flex;align-items:center;gap:.45rem;font-family:var(--font-mono);font-size:.66rem;font-weight:600;
  letter-spacing:.1em;text-transform:uppercase;padding:.25rem .6rem;border-radius:var(--r);border:1px solid currentColor}
.status .dot{width:8px;height:8px;border-radius:0;background:currentColor}
.status.good{color:var(--good)}
.status.moderate{color:var(--moderate)}
.status.unhealthy{color:var(--unhealthy)}
.status.hazardous{color:var(--hazardous)}
.status.stale{color:var(--ink-faint)}

/* dense metric readout */
.metrics-mini{display:flex;flex-wrap:wrap;gap:.6rem;margin-top:.6rem;font-family:var(--font-mono);font-size:.72rem;color:var(--ink-soft)}
.metrics-mini b{color:var(--ink);font-weight:700}
.spark{display:block;margin-top:.6rem}
.spark path.fill{fill:var(--brand);opacity:.1}
.spark path.line{fill:none;stroke:var(--brand);stroke-width:1.4}
.spark circle{fill:var(--brand)}

/* full metric tiles (EP detail) — numbers in mono (instrument-readout feel) */
.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:1px;background:var(--line);margin-bottom:1.5rem}
.tile{background:var(--panel);padding:1.1rem;text-align:left}
.tile .n{font-family:var(--font-mono);font-weight:700;font-size:1.5rem;color:var(--ink)}
.tile .n.good{color:var(--good)}.tile .n.moderate{color:var(--moderate)}
.tile .n.unhealthy{color:var(--unhealthy)}.tile .n.hazardous{color:var(--hazardous)}
.tile .l{font-family:var(--font-mono);font-size:.66rem;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-faint);margin-top:.2rem}

/* chart card — the one place this system spends its "blueprint annotation"
   boldness: L-shaped corner markers, like a schematic frame. */
.chart-card{position:relative;padding:1.5rem}
.chart-card::before,.chart-card::after,.chart-card .corner-tl,.chart-card .corner-br{content:"";position:absolute;width:10px;height:10px;pointer-events:none}
.chart-card::before{top:-1px;left:-1px;border-top:2px solid var(--brand);border-left:2px solid var(--brand)}
.chart-card::after{bottom:-1px;right:-1px;border-bottom:2px solid var(--brand);border-right:2px solid var(--brand)}
.range-picker{display:flex;gap:.6rem;align-items:center;flex-wrap:wrap;margin-bottom:1rem}
.range-select{width:auto;min-width:170px;font-family:var(--font-mono);font-size:.72rem;font-weight:600;
  letter-spacing:.04em;text-transform:uppercase;padding:.5rem .7rem;min-height:38px}
.custom-range{display:inline-flex;gap:.5rem;align-items:center}
.custom-range input[type="date"]{width:auto;min-height:38px;padding:.4rem .6rem;font-size:.82rem}
.chart-wrap{position:relative;height:340px}
canvas{max-width:100%}

/* Building status header. This figure is an AGGREGATE — the worst status across
   every endpoint at the site — and the old .eyebrow gave no hint of that, so it
   read as if it were one more sensor. Promoted to a real heading with the scope
   spelled out underneath; the per-endpoint cards still follow below unchanged. */
.building-title{font-family:var(--font-display);font-size:1.2rem;font-weight:700;
  letter-spacing:-.01em;color:var(--ink);margin-bottom:.2rem}
.building-scope{font-family:var(--font-mono);font-size:.7rem;letter-spacing:.06em;
  text-transform:uppercase;color:var(--ink-faint);max-width:34ch}
.building-scope b{color:var(--ink);font-weight:700}
.building-pill{display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;margin-top:.6rem}

/* outdoor air quality panel — sits between the building status and the trend
   chart, so the indoor numbers are read against outside without leaving the
   page. Uses the same EPA bands as the AQI value it displays. */
.outdoor{display:flex;flex-direction:column;gap:1rem;
  padding:1.1rem 0;border-top:1px solid var(--line-faint);border-bottom:1px solid var(--line-faint);
  margin-bottom:1rem}

/* The headline figures, as discrete blocks.
   These were a flat row of text sharing the panel's background, which made them
   genuinely hard to read: .eyebrow is --ink-faint at .68rem, so nothing anchored
   the eye and every number had to be hunted for against its neighbours. They now
   borrow .site-card's idiom exactly — white panel on --paper, hairline border,
   3px band-coloured left edge, offset hard shadow — so each metric reads as its
   own object. auto-fit means 4 across on a wide screen and 2x2 on a narrow one
   without a breakpoint.
   All four still share .outdoor-aqi, so size/family/weight cannot drift apart;
   only the band colour differs. */
.outdoor-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(192px,1fr));gap:.9rem}
.stat-block{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);
  border-left:3px solid var(--ink-faint);padding:.95rem 1.05rem;
  box-shadow:4px 4px 0 rgba(58,58,56,.08)}
/* Label has to carry weight here — at --ink-faint it disappeared entirely. */
.stat-block .eyebrow{display:block;color:var(--ink-soft);font-weight:600;margin-bottom:.5rem}
/* Eyebrows stay pinned to the top so they line up across the row; only the
   figure below floats to the middle. align-items:flex-start stops the delta
   chip being stretched to the block's full width by the flex cross-axis
   default. */
.stat-block.compact{display:flex;flex-direction:column;align-items:flex-start}
.stat-block.compact .outdoor-aqi{margin-top:auto}
.stat-block.compact > :last-child{margin-bottom:auto}

/* Condition artwork. Fixed height + preserveAspectRatio="none" on the SVG so
   every scene occupies exactly the same band whatever the block width, which is
   what keeps the two "now" blocks aligned with each other. */
.stat-scene{display:block;width:100%;height:74px;border-radius:var(--r);
  border:1px solid var(--line-faint);margin-bottom:.55rem}
/* The dial carries the reading; the number below it stays the thing you read. */
.stat-dial{display:block;width:100%;max-width:190px;height:auto;margin:0 0 .1rem -6px}
.dial-tick{font-family:var(--font-mono);font-size:10.5px;fill:var(--ink-faint);text-anchor:middle}
/* The category name is the sentence a non-specialist actually acts on, so it is
   bold and carries the band colour rather than sitting muted next to the AQI. */
.stat-block .cond{font-weight:700;color:var(--ink)}
.stat-block.good .cond{color:var(--good)}
.stat-block.moderate .cond{color:var(--moderate)}
.stat-block.usg .cond,.stat-block.unhealthy .cond{color:var(--unhealthy)}
.stat-block.very_unhealthy .cond,.stat-block.hazardous .cond{color:var(--hazardous)}
.dial-needle{stroke:var(--ink);stroke-width:3.4;stroke-linecap:round}
.dial-hub{fill:var(--ink)}
.stat-block.good{border-left-color:var(--good)}
.stat-block.moderate{border-left-color:var(--moderate)}
.stat-block.usg,.stat-block.unhealthy{border-left-color:var(--unhealthy)}
.stat-block.very_unhealthy,.stat-block.hazardous{border-left-color:var(--hazardous)}
.outdoor-aqi{font-family:var(--font-display);font-size:2.6rem;font-weight:700;line-height:1;
  letter-spacing:-.02em;color:var(--ink)}
.outdoor-aqi.good{color:var(--good)}
.outdoor-aqi.moderate{color:var(--moderate)}
.outdoor-aqi.usg{color:var(--unhealthy)}
.outdoor-aqi.unhealthy{color:var(--unhealthy)}
.outdoor-aqi.very_unhealthy{color:var(--hazardous)}
.outdoor-aqi.hazardous{color:var(--hazardous)}
/* Indoor-vs-outdoor delta. Deliberately NOT an AQI band colour — it encodes a
   direction (is the building helping?), not a health category, so band colours
   here would read as "this number is unhealthy" rather than "this went up". */
.outdoor-delta{display:inline-block;margin-top:.55rem;padding:.16rem .45rem;
  font-family:var(--font-display);font-size:1rem;font-weight:700;letter-spacing:-.01em;
  border:1px solid currentColor;border-radius:var(--r)}
.outdoor-delta.better{color:var(--good)}
.outdoor-delta.worse{color:var(--hazardous)}
.outdoor-delta.same{color:var(--ink-soft)}
.outdoor-detail{display:flex;flex-direction:column;gap:.28rem;font-size:.88rem}
.outdoor-detail .mono-label{display:inline-block;min-width:9.5rem}
/* Licence-required attribution. Muted, but never hidden. */
.outdoor-attrib{margin:-.4rem 0 1rem}

/* endpoint "logger" affordances — status strip above the snapshot tiles, and
   the readout rail under the trace. The readout is a FIXED strip rather than a
   floating tooltip so values stay in one place while scrubbing, and it reserves
   its height up front (min-height) so the chart doesn't jump when a hover
   swaps the hint line for a longer multi-metric reading. */
.logger-head{display:flex;align-items:center;gap:.9rem;flex-wrap:wrap;
  padding:.6rem 0 1rem;border-bottom:1px solid var(--line-faint);margin-bottom:1.2rem}
.readout{min-height:2.2rem;display:flex;align-items:center;flex-wrap:wrap;
  margin-top:.8rem;padding-top:.7rem;border-top:1px solid var(--line-faint);
  font-family:var(--font-mono);font-size:.72rem;color:var(--ink-soft)}
.readout b{color:var(--ink);font-weight:600}
/* Scrubbing reads better with a crosshair than an arrow, and the wheel is bound
   to zoom here so the cursor should advertise that the plot is interactive. */
.chart-wrap canvas{cursor:crosshair}

.center{min-height:100vh;display:grid;place-items:center;padding:1.5rem}
.hide{display:none}

/* health-context callout — sourced reference info, distinct from a plain
   .card so it reads as "annotation," not another data panel */
.callout{background:var(--panel);border-left:3px solid var(--brand);border-radius:var(--r);
  padding:1rem 1.1rem;margin-bottom:1.5rem;box-shadow:2px 2px 0 rgba(0,0,0,.08)}
.callout .label{font-family:var(--font-mono);font-size:.66rem;letter-spacing:.1em;text-transform:uppercase;
  color:var(--brand);margin-bottom:.5rem;font-weight:700}
.callout p{font-size:.88rem;line-height:1.6}

/* report tables — hairline rows, mono tabular numbers so columns line up */
.rtable{width:100%;border-collapse:collapse;font-size:.86rem}
.rtable th{text-align:left;font-family:var(--font-mono);font-size:.66rem;letter-spacing:.08em;text-transform:uppercase;
  color:var(--ink-faint);padding:.7rem .9rem;border-bottom:1px solid var(--line)}
.rtable td{padding:.65rem .9rem;border-bottom:1px solid var(--line-faint)}
.rtable tr:last-child td{border-bottom:none}
.rtable td.num{font-family:var(--font-mono);font-variant-numeric:tabular-nums;text-align:right}
/* A numeric column's HEADER has to be right-aligned too. th was unconditionally
   left-aligned while td.num is right-aligned, so every figure in every report
   sat adrift of the label describing it — the wider the column, the worse.
   Mark numeric headers `class="num"` and the two edges line up. */
.rtable th.num{text-align:right}
/* Numeric cells only — a figure split across two lines stops lining up with the
   one above it. Text columns are left free to wrap; applying nowrap to every
   cell just forces long prose columns into a horizontal scroll instead. */
.rtable th.num,.rtable td.num{white-space:nowrap}
.rtable td.num.good{color:var(--good)}.rtable td.num.moderate{color:var(--moderate)}
.rtable td.num.unhealthy{color:var(--unhealthy)}.rtable td.num.hazardous{color:var(--hazardous)}

/* one-shot credential panels — temporary passwords and single-use reset URLs.
   Deliberately loud and persistent: the server cannot reproduce these values, so
   an admin who scrolls past one has to reissue it and re-confuse the recipient. */
.secret-callout{margin-top:.75rem;padding:.75rem;border:1px solid var(--brand);
  border-radius:var(--r);background:rgba(28,127,176,.06)}
.secret-title{font-size:.72rem;font-weight:600;color:var(--brand);margin-bottom:.4rem}
.secret-row{display:flex;align-items:center;gap:.5rem;margin-bottom:.35rem}
/* min-width:0 lets a long reset URL shrink and wrap inside the flex row instead of
   forcing the whole panel wider than the card holding it. */
.secret-value{flex:1;min-width:0;font-family:var(--font-mono);font-size:.8rem;
  word-break:break-all;user-select:all}

/* ── Monthly facility report ───────────────────────────────────────────────
   The one page built to leave the screen. Export is the browser's own
   Print → Save as PDF: a Worker cannot run headless Chrome, and hand-rolling a
   PDF writer would be a great deal of code to reproduce what every browser
   already does correctly.

   ⚠️ CONSEQUENCE: the @media print block below IS the export format. If the PDF
   is wrong, this is the file to fix — the on-screen appearance is secondary. */
.report-picker{display:flex;gap:.75rem;align-items:flex-end;flex-wrap:wrap;margin-bottom:1.2rem}
.report-picker label{display:flex;flex-direction:column;gap:.2rem}
/* ⚠️ NO max-width. This used to be 52rem, which is a sound measure for running
   prose and the wrong one for this document: the body is tables of eight and
   nine columns, and capping the article at 832px squeezed the letterhead into a
   column so narrow that the timestamp and the timezone each wrapped onto two
   lines and sat hard against the right edge. The page it prints onto already
   bounds the measure (@page margin below); on screen the sheet does. */
.report-doc{max-width:none}
/* ⚠️ NO flex-wrap HERE. It looks like the right narrow-screen safety valve and
   it is not: with .report-meta at nowrap, the natural widths on a Letter page
   (a ~516px title beside a ~205px letterhead, in 725px of column) already
   exceed the line, so wrapping fires ON PAPER — the letterhead drops beneath
   the title and prints as a stray centred block. The title shrinking to two
   lines is the correct outcome there, which is what .report-ident's flex-shrink
   does. The genuine narrow-screen case is handled by stacking, below. */
.report-head{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem 2rem;
  border-bottom:2px solid var(--ink);padding-bottom:.6rem;margin-bottom:1rem}
/* min-width:0 is what actually lets it shrink — a flex item's floor is its
   min-content width until you say otherwise, and a 51-character facility name
   has a wide one. */
.report-ident{flex:1 1 auto;min-width:0}
.report-title{font-family:var(--font-display);font-size:1.5rem;margin:0}
.report-sub{margin:.15rem 0 0;color:var(--ink-soft);font-size:.9rem}
/* ⚠️ nowrap, and flex:none so the header cannot squeeze it. Left to shrink, this
   column collapses to its min-content width — "Generated 8/16/2026, 8:42:00 AM"
   breaks after the seconds and "Times local — America/Los_Angeles" breaks after
   the dash, turning a four-line letterhead into six lines of ragged right-edge
   text. The lines are short and fixed in length; there is nothing here that
   ever needs to wrap. */
.report-meta{flex:none;text-align:right;white-space:nowrap;
  color:var(--ink-soft);line-height:1.5}
/* Below roughly a phone the two columns genuinely do not both fit, so the
   header becomes one column with the letterhead under the address — still
   left-aligned, so it reads as part of the same block rather than as a caption
   that lost its table. Screen only: the printed page is never this narrow. */
@media(max-width:560px){
  .report-head{flex-direction:column;gap:.75rem}
  .report-meta{text-align:left;white-space:normal}
  .report-brand{justify-content:flex-start}
}
.report-section{margin-bottom:1.6rem}
.report-section h2{font-family:var(--font-display);font-size:1rem;margin:0 0 .4rem;
  border-bottom:1px solid var(--line);padding-bottom:.2rem}
.report-table{width:100%;margin-top:.5rem;font-size:.8rem}
.report-table.daily{font-size:.74rem}
/* The compiled report's cover table. Eight columns — two more than any other
   report table — and the widest of them holds a facility name.
   ⚠️ AT THE SHARED .9rem CELL PADDING THAT IS 230px OF PADDING ALONE, and the
   table came out 782px wide in a 725px Letter column: it ran off the right edge
   of every printed cover sheet, which no amount of looking at the screen would
   have shown. Tighter cells, smaller type, and headers allowed to wrap onto two
   lines. td.num keeps the inherited nowrap — a figure split across two lines
   stops lining up with the one above it, which is the whole point of a
   right-aligned numeric column. */
.report-table.rollup{font-size:.74rem}
.report-table.rollup th,.report-table.rollup td{padding:.5rem .55rem}
.report-table.rollup th{white-space:normal;vertical-align:bottom}
/* A flagged row must survive being printed in greyscale, so it carries a left
   rule as well as a tint — a faint background alone vanishes on a mono printer. */
.report-table .row-flag td{background:rgba(178,58,46,.07)}
.report-table .row-flag td:first-child{border-left:3px solid var(--danger)}
.report-table .row-empty td{color:var(--ink-soft);opacity:.6}
.report-caveat{border:1px solid var(--moderate);border-left-width:4px;border-radius:var(--r);
  padding:.6rem .75rem;margin-bottom:1rem;font-size:.83rem;background:#FDF8EC}
.report-foot{border-top:1px solid var(--line);padding-top:.5rem;margin-top:1.5rem}
.kpis{display:flex;flex-wrap:wrap;gap:1.4rem;margin:.6rem 0 .3rem}
.kpi{min-width:8rem}
.kpi-n{font-family:var(--font-display);font-size:1.6rem;line-height:1.1}
.kpi-n.good{color:var(--good)}.kpi-n.moderate{color:var(--moderate)}
.kpi-n.unhealthy{color:var(--unhealthy)}
.kpi-l{font-size:.74rem;color:var(--ink-soft);line-height:1.35;margin-top:.15rem}

/* One facility inside the compiled organization report. Carries .report-break
   too, so on paper it starts its own sheet — this rule is only about the screen,
   where the compiled report is a single scroll and, without it, the previous
   facility's footer runs straight into the next one's title with nothing at all
   between them. The double rule is the one place this system uses one: it says
   "a document ended and another began", which a single hairline (already the
   separator INSIDE a report, under every h2) cannot say. */
.report-compiled{margin-top:2.75rem;padding-top:2rem;border-top:3px double var(--line)}

/* ── The report sheet ──────────────────────────────────────────────────────
   Every report — not just the monthly one — is a DOCUMENT, and a document
   belongs on paper. Without this the mosaic hairlines in body{background-image}
   run straight through the tables and the body text: on the dashboard that
   texture reads as the desk the cards sit on, but a report has no cards, so the
   grid ends up running through the content itself and the page reads as
   unfinished rather than as something you would hand to a surveyor.

   Applied to .main rather than to a wrapper element so it covers the loading
   and error states too — those render through the same shell, and a report that
   is still fetching should not flash the mosaic and then paint a sheet over it.
   route() sets the class; see the toggle there for which routes qualify.

   ⚠️ THE REPORTS INDEX IS DELIBERATELY EXCLUDED. It is a grid of white .cards,
   and white cards on a white sheet lose every edge they have. */
body.on-report .main{
  background:var(--panel);
  border:1px solid var(--line);
  border-radius:var(--r);
  box-shadow:2px 2px 0 rgba(0,0,0,.10), 0 8px 20px -9px rgba(0,0,0,.28);
  margin:1.25rem;
  padding:1.75rem 2rem 2.25rem;
}
@media(max-width:820px){
  /* The sidebar is a stacked banner at this width, so a floating sheet with an
     air gap on both sides wastes the little horizontal room a phone has. */
  body.on-report .main{margin:0;border-left:none;border-right:none;
    border-radius:0;padding:1.25rem 1.1rem 1.75rem}
}

@media print{
  /* Chrome honours this for margins and orientation. */
  @page{margin:14mm 12mm}
  /* The app chrome is navigation, and navigation on paper is noise. */
  .side,.crumbs,.no-print{display:none !important}
  .app{display:block}
  .main{padding:0 !important;margin:0 !important;max-width:none}
  /* ⚠️ THE SHEET MUST LEAVE NO TRACE ON PAPER. body.on-report paints .main as a
     white card with a border and a drafting shadow; on paper the page IS the
     sheet, so the border would print as a rule around every page and the shadow
     as a grey smear down two edges — and *{print-color-adjust:exact} below
     guarantees Chrome renders both rather than dropping them. The padding and
     margin are already zeroed by the rule above; these three are not. */
  body.on-report .main{background:none;border:none;box-shadow:none;border-radius:0}
  .report-doc{max-width:none}
  body{background:#fff}
  /* Without this Chrome drops every background fill, taking the flagged-row tint
     and the caveat box with it — the two things a reader most needs to notice. */
  *{-webkit-print-color-adjust:exact;print-color-adjust:exact}
  /* Tables must not be sliced mid-row, and a header must reprint on each page it
     continues onto — a headerless page of numbers is unreadable. */
  .report-table{page-break-inside:auto}
  .report-table thead{display:table-header-group}
  .report-table tr{page-break-inside:avoid;page-break-after:auto}
  .report-section{page-break-inside:avoid}
  /* The daily table is the long one; it gets its own page rather than being
     split across a section boundary. */
  .report-break{page-break-before:always;page-break-inside:auto}
  /* ⚠️ The compiled report's between-facility separator is a SCREEN device. The
     page break above already separates them on paper, and left in place this
     would print 2rem of dead space and a double rule across the top of every
     facility's first sheet — under *{print-color-adjust:exact}, visibly. */
  .report-compiled{margin-top:0;padding-top:0;border-top:none}
  a{text-decoration:none;color:inherit}
}

/* ── Back link ─────────────────────────────────────────────────────────────
   Sits at the top of a page body, above the h1. Deliberately quiet — it is a
   way out, not a call to action — but with a real hit area, because the thing
   people do most often on a detail screen is leave it. */
.back-link{display:inline-flex;align-items:center;gap:.4rem;
  font-family:var(--font-mono);font-size:.7rem;letter-spacing:.08em;
  text-transform:uppercase;color:var(--ink-soft);
  padding:.35rem .6rem .35rem .45rem;margin:0 0 .9rem -.45rem;
  border:1px solid transparent;border-radius:var(--r)}
.back-link:hover{color:var(--brand);border-color:var(--line);background:var(--panel)}
.back-caret{font-size:.95rem;line-height:1}
@media print{.back-link{display:none}}

/* ── Admin console ─────────────────────────────────────────────────────────
   Replaces the org-card grid. With one tenant that grid was a single card whose
   only content was three links — a container adding nothing. The four areas are
   the actual destinations, so they are what the page presents. */
.console-org{display:flex;align-items:baseline;gap:.75rem;flex-wrap:wrap;
  padding-bottom:.6rem;border-bottom:2px solid var(--ink);margin-bottom:1.2rem}
.console-org h2{font-family:var(--font-display);font-size:1.25rem;margin:0}
.console-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(15rem,1fr));
  gap:1rem;margin-bottom:2rem}
.console-tile{display:block;padding:1.1rem 1.2rem;text-decoration:none;
  background:var(--panel);border:1px solid var(--line);border-left:3px solid var(--brand);
  border-radius:var(--r);
  box-shadow:2px 2px 0 rgba(0,0,0,.10), 0 8px 20px -9px rgba(0,0,0,.28)}
.console-tile:hover{background:var(--panel-hover)}

/* ── Management rows ─────────────────────────────────────────────────────
   The Admin landing page was a grid of equal tiles, which gave four unrelated
   destinations the same visual weight and no scan order — you read it as a
   poster rather than as a console. These are rows, and they borrow the
   dashboard's compact-row language on purpose: same glass panel, same 3px
   left rail, same translateX on hover. Someone who has learned to read the
   dashboard already knows how to read this.

   ⚠️ THE RAIL COLOUR IS THE SECTION ACCENT, NOT A STATUS. On the dashboard
   the identical rail encodes air quality; here it encodes which section the
   row belongs to, and the two palettes are kept disjoint (see --sec-* in the
   token block) so the same visual device can never be misread across pages. */
.mgmt-list{display:flex;flex-direction:column;gap:.4rem;margin-bottom:1.5rem}
.mgmt-row{position:relative;display:grid;align-items:center;gap:1rem;
  grid-template-columns:minmax(9rem,1fr) auto 1.2rem;
  padding:.7rem 1rem .7rem 1.3rem;border-radius:10px;text-decoration:none;color:var(--ink);
  background:linear-gradient(180deg,rgba(255,255,255,.94),rgba(255,255,255,.8));
  -webkit-backdrop-filter:blur(14px) saturate(160%);backdrop-filter:blur(14px) saturate(160%);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.9),
             inset 0 0 0 1px rgba(16,24,40,.07),
             0 1px 2px rgba(16,24,40,.045);
  transition:transform .14s cubic-bezier(.2,.7,.3,1),box-shadow .16s ease}
.mgmt-row:hover{transform:translateX(3px);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.96),
             inset 0 0 0 1px rgba(16,24,40,.1),
             0 8px 18px -10px rgba(16,24,40,.4)}
.mgmt-row::before{content:'';position:absolute;left:0;top:8px;bottom:8px;width:3px;
  border-radius:0 3px 3px 0;background:var(--rail,#B9C0BC)}
.mgmt-row .m-name{font-family:var(--font-display);font-weight:700;
  font-size:clamp(.98rem,.8rem + .4vw,1.12rem);line-height:1.2}
.mgmt-row .m-stat{font-family:var(--font-mono);font-size:.72rem;letter-spacing:.06em;
  text-transform:uppercase;color:var(--ink-soft);white-space:nowrap;text-align:right}
.mgmt-row .m-go{color:var(--ink-faint);font-size:1.1rem;transition:transform .14s ease,color .14s ease}
.mgmt-row:hover .m-go{color:var(--rail,var(--brand));transform:translateX(2px)}
@media (max-width:760px){
  .mgmt-row{grid-template-columns:1fr auto;gap:.3rem .8rem}
  .mgmt-row .m-go{display:none}
}

/* Section heading above a block of management rows, carrying the same accent
   so the grouping reads before any of the labels are actually read. */
.mgmt-head{display:flex;align-items:baseline;gap:.6rem;margin:1.4rem 0 .55rem}
.mgmt-head h2{font-family:var(--font-display);font-size:.82rem;font-weight:700;
  letter-spacing:.1em;text-transform:uppercase;color:var(--ink-soft);margin:0}
.mgmt-head .m-rule{flex:1;height:1px;background:var(--line-faint)}
.mgmt-head .m-key{width:8px;height:8px;border-radius:2px;background:var(--rail,#B9C0BC)}

/* ── Report scope banners ────────────────────────────────────────────────
   The wildfire report's primary control. Deliberately NOT a <select>: two of
   the three readings would then be hidden behind a click, with no hint that
   they differ from the one on screen — which on a smoke day is the whole
   finding. Flat, all three visible, the selected one filled.

   ⚠️ THE SELECTED STATE IS FILL + RAIL WIDTH, NOT COLOUR ALONE. Each banner
   already carries its own accent, so "which one is active" cannot also be
   carried by hue without the three accents and the selection state competing
   for the same channel. */
.scope-banners{display:grid;gap:.5rem;margin:0 0 1.25rem;
  grid-template-columns:repeat(auto-fit,minmax(11rem,1fr))}
.scope-banner{position:relative;display:flex;flex-direction:column;gap:.15rem;
  padding:.7rem .9rem .7rem 1.1rem;border-radius:10px;text-decoration:none;color:var(--ink);
  background:linear-gradient(180deg,rgba(255,255,255,.9),rgba(255,255,255,.72));
  box-shadow:inset 0 1px 0 rgba(255,255,255,.9),
             inset 0 0 0 1px rgba(16,24,40,.07),
             0 1px 2px rgba(16,24,40,.045);
  transition:transform .14s cubic-bezier(.2,.7,.3,1),box-shadow .16s ease}
.scope-banner::before{content:'';position:absolute;left:0;top:7px;bottom:7px;width:3px;
  border-radius:0 3px 3px 0;background:var(--rail,#B9C0BC);opacity:.55}
.scope-banner:hover{transform:translateY(-1px);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.96),
             inset 0 0 0 1px rgba(16,24,40,.1),
             0 8px 18px -10px rgba(16,24,40,.4)}
.scope-banner.is-on{background:linear-gradient(180deg,rgba(255,255,255,1),rgba(255,255,255,.88));
  box-shadow:inset 0 0 0 1px rgba(16,24,40,.16),0 2px 6px -2px rgba(16,24,40,.25)}
.scope-banner.is-on::before{width:5px;opacity:1;top:0;bottom:0;border-radius:0}
.scope-banner .sb-label{font-family:var(--font-display);font-weight:700;font-size:.98rem;
  line-height:1.15}
.scope-banner.is-on .sb-label{color:var(--rail)}
.scope-banner .sb-sub{font-family:var(--font-mono);font-size:.68rem;letter-spacing:.05em;
  text-transform:uppercase;color:var(--ink-soft)}

/* ── Site smoke map ──────────────────────────────────────────────────────
   Inline SVG, no tiles — see smokeMapSvg() for why (img-src 'self' data:).
   ⚠️ THE HALO USES *-fill AND THE CORE IS PAPER, per the two-token rule: the
   halo is a solid shape so it takes the vivid fill, and the small core is a
   knockout rather than a second colour, so overlapping sites stay countable. */
.smoke-map{padding:.75rem .75rem .4rem}
/* ⚠️ NO BLANKET `svg` RULE INSIDE .smoke-map. This used to be
   `.smoke-map svg{width:100%;height:auto}`, left over from when this card held
   a hand-drawn SVG scatter. Leaflet draws circleMarkers into an
   absolutely-positioned SVG in its overlay pane, which is a DESCENDANT of this
   card — so the rule stretched that overlay to the card's width and collapsed
   its height, and every marker vanished. Tiles survived because they are
   <img>, which is precisely why the map looked healthy and empty at the same
   time. Any styling here must target the map's own classes, never the tag. */
/* Basemap. Muted on purpose: it is orientation, not data — the dots carry the
   reading, and a strong land fill would compete with the status colours. */
.sm-water{fill:#EAF1F4}
.sm-land{fill:#F7F7F5;stroke:#B9C0BC;stroke-width:1;stroke-linejoin:round}
/* Satellite backdrop. Slightly dimmed so the status discs stay the brightest
   thing in the frame — full-strength imagery competes with them badly. */
.sm-photo{opacity:.92}
.smoke-dot{cursor:pointer}
.smoke-dot .sd-halo{fill:#B9C0BC;opacity:.42;transition:opacity .14s ease,r .14s ease}
.smoke-dot.b-good .sd-halo{fill:var(--good-fill)}
.smoke-dot.b-moderate .sd-halo{fill:var(--moderate-fill)}
.smoke-dot.b-unhealthy .sd-halo{fill:var(--unhealthy-fill)}
.smoke-dot.b-hazardous .sd-halo{fill:var(--hazardous-fill)}
.smoke-dot .sd-core{fill:var(--paper);stroke:rgba(16,24,40,.35);stroke-width:1}
/* A ring only where a node actually reports — the difference between "we model
   this address" and "we measure it". */
.smoke-dot .sd-core.has-node{stroke:var(--ink);stroke-width:2.2}
.smoke-dot:hover .sd-halo{opacity:.85}
.smoke-dot:focus-visible .sd-halo{opacity:.95;stroke:var(--ink);stroke-width:2}
.smoke-legend{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem 1rem;
  padding:.55rem .35rem .25rem;font-family:var(--font-mono);font-size:.68rem;
  letter-spacing:.05em;text-transform:uppercase;color:var(--ink-soft)}
.smoke-legend i.sl{display:inline-block;width:9px;height:9px;border-radius:50%;
  margin-right:.35rem;vertical-align:middle}
.smoke-legend i.b-good{background:var(--good-fill)}
.smoke-legend i.b-moderate{background:var(--moderate-fill)}
.smoke-legend i.b-unhealthy{background:var(--unhealthy-fill)}
.smoke-legend i.b-hazardous{background:var(--hazardous-fill)}
.smoke-legend .smoke-note{margin-left:auto;text-transform:none;letter-spacing:0;
  color:var(--ink-faint)}

/* Contour layer switch. Sits under the legend rather than floating over the map
   as a Leaflet control: the map is already busy, and a control pinned inside it
   would cover exactly the corner the Spokane cluster occupies at default zoom. */
.smoke-layers{display:flex;flex-wrap:wrap;align-items:center;gap:.4rem;
  padding:.15rem .35rem .55rem;font-family:var(--font-mono);font-size:.68rem}
.smoke-layers .mono-label{margin-right:.2rem}
.smoke-layers button{font:inherit;letter-spacing:.05em;text-transform:uppercase;
  padding:.22rem .55rem;border:1px solid var(--line);border-radius:3px;
  background:transparent;color:var(--ink-soft);cursor:pointer}
.smoke-layers button:hover{border-color:var(--brand);color:var(--brand)}
/* The active state has to survive the hover rule above it, hence the later
   declaration rather than a higher-specificity selector. */
.smoke-layers button.on{background:var(--brand);border-color:var(--brand);color:#fff}
.smoke-layers .contour-stamp{margin-left:auto;text-transform:none;letter-spacing:0}

/* ── Current Air Quality Conditions ──────────────────────────────────────
   ⚠️ THE TONE RAIL IS THE ONE PLACE THE STATUS LADDER *IS* CORRECT HERE, and
   that is not a contradiction of the --sec-* rule: this panel really is
   reporting air quality, so green/amber/red mean exactly what they mean
   everywhere else in the app. The admin sections do not, which is why they
   have their own palette. */
.cond-card{border-left:3px solid var(--tone,#B9C0BC);padding:1rem 1.1rem}
.cond-card.b-good{--tone:var(--good-fill)}
.cond-card.b-moderate{--tone:var(--moderate-fill)}
.cond-card.b-unhealthy{--tone:var(--unhealthy-fill)}
.cond-head{font-family:var(--font-display);font-weight:700;font-size:1.05rem;line-height:1.2}
.cond-detail{margin:.4rem 0 0;font-size:.9rem;color:var(--ink-soft);line-height:1.5;max-width:62ch}
.cond-fires{display:flex;flex-wrap:wrap;gap:.4rem;margin-top:.75rem}
.cond-fire{font-family:var(--font-mono);font-size:.7rem;letter-spacing:.03em;
  padding:.22rem .5rem;border-radius:2px;border:1px solid var(--line-faint);
  color:var(--ink-soft);background:rgba(16,24,40,.02)}
.cond-fire.is-upwind{border-color:var(--tone,var(--moderate));color:var(--ink);
  background:rgba(255,196,46,.12)}
/* Citations stay quiet: they establish provenance, they are not the reading.
   Still links, still open in a new tab — just not competing with the fires. */
.cond-src{display:flex;flex-wrap:wrap;align-items:center;gap:.35rem;margin-top:.9rem;
  padding-top:.6rem;border-top:1px solid var(--line-faint)}
.cond-src .cs-label{font-family:var(--font-mono);font-size:.64rem;font-weight:700;
  letter-spacing:.08em;text-transform:uppercase;color:var(--ink-faint);margin-right:.15rem}
.cond-src .cs-link{display:inline-flex;align-items:baseline;gap:.3rem;
  font-family:var(--font-mono);font-size:.64rem;letter-spacing:.06em;text-transform:uppercase;
  color:var(--ink-soft);text-decoration:none;border-bottom:1px solid var(--line-faint)}
.cond-src .cs-link:hover{color:var(--brand);border-bottom-color:var(--brand)}
.cond-src .cs-what{display:none}
.cond-src .cs-ext{font-size:.85em;opacity:.6}

/* ── The fires: the substance of this card ───────────────────────────────
   These are what an operator actually acts on — "is anything upwind of us" —
   so they get real size, and the upwind state is carried by a word as well as
   by the fill. */
.cond-fires-head{margin-top:1rem}
.cond-fires-head .cf-title{display:block;font-family:var(--font-display);font-weight:700;
  font-size:.95rem;color:var(--ink)}
.cond-fires-head .cf-note{display:block;margin-top:.2rem;font-size:.82rem;line-height:1.45;
  color:var(--ink-soft);max-width:62ch}
.cond-fires{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:.7rem}
.cond-fire{display:flex;flex-direction:column;gap:.12rem;min-width:11rem;
  padding:.55rem .75rem;border-radius:4px;border:1px solid var(--line);
  background:var(--panel)}
.cond-fire .cf-name{font-family:var(--font-display);font-weight:700;font-size:1rem;
  line-height:1.2;color:var(--ink)}
.cond-fire .cf-meta{font-family:var(--font-mono);font-size:.72rem;letter-spacing:.03em;
  color:var(--ink-soft)}
/* Incident detail from IRWIN. Set as separated chips rather than one run-on
   line so a reader scanning four cards can compare the same fact across them
   without reading a sentence each time. */
.cond-fire .cf-stats{display:flex;flex-wrap:wrap;gap:.2rem .5rem;margin-top:.2rem;
  font-family:var(--font-mono);font-size:.66rem;letter-spacing:.02em;color:var(--ink-soft)}
.cond-fire .cf-stats .cf-stat + .cf-stat::before{content:'·';margin-right:.5rem;
  color:var(--ink-faint)}
/* Barely-contained is the one number that should stop the eye. Weight AND
   colour, never colour alone — the same rule the bearing bands follow. */
.cond-fire .cf-stats .cf-stat.is-low{color:var(--unhealthy-fill);font-weight:700}
.cond-fire .cf-flag{margin-top:.15rem;font-family:var(--font-mono);font-size:.64rem;
  font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-faint)}
/* Three bands, and each is carried by border weight, fill AND the flag word —
   never by colour alone. The hues are the status ladder's own red and amber,
   which is correct here in a way it is not on the admin pages: this really is
   an air-quality judgement, so red meaning "this one may be doing it to you"
   reads the same as red everywhere else in the app.
   ⚠️ Downwind is left UNMARKED rather than given a third colour. A green or
   blue chip would read as a fourth status; absence of marking is what "nothing
   to look at" should look like. */
.cond-fire.is-upwind{border-color:var(--unhealthy);border-left-width:4px;
  background:rgba(230,0,0,.09)}
.cond-fire.is-upwind .cf-flag{color:var(--unhealthy)}
.cond-fire.is-crosswind{border-color:var(--moderate);border-left-width:4px;
  background:rgba(255,196,46,.13)}
.cond-fire.is-crosswind .cf-flag{color:var(--moderate)}
.cond-fire.is-downwind{border-left-width:1px}

.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0 0 0 0);white-space:nowrap;border:0}

/* Hero variant: light-on-dark, one line. Its own rules rather than an override
   of .cond-card — the report card's --ink-soft text and light fill are
   unreadable here, and inheriting then undoing them is how a later edit to the
   report card silently breaks the hero. */
.dh-cond{display:flex;align-items:center;gap:.55rem;flex-wrap:wrap;
  margin-top:.85rem;padding:.6rem .8rem;border-radius:8px;text-decoration:none;
  background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.14);
  transition:background .14s ease,border-color .14s ease,transform .14s ease}
a.dh-cond:hover{background:rgba(255,255,255,.13);border-color:rgba(255,255,255,.3);
  transform:translateY(-1px)}
a.dh-cond:hover .dhc-more{color:#fff}
a.dh-cond:focus-visible{outline:2px solid rgba(255,255,255,.7);outline-offset:2px}
.dh-cond .dhc-dot{width:9px;height:9px;border-radius:50%;flex:0 0 auto;
  background:var(--good-fill)}
.dh-cond .dhc-dot.t-warn{background:var(--moderate-fill)}
.dh-cond .dhc-dot.t-bad{background:var(--unhealthy-fill)}
.dh-cond .dhc-text{font-size:.86rem;color:rgba(255,255,255,.9);line-height:1.35}
.dh-cond .dhc-more{margin-left:auto;font-family:var(--font-mono);font-size:.68rem;
  letter-spacing:.06em;text-transform:uppercase;color:rgba(255,255,255,.62);
  white-space:nowrap;transition:color .14s ease}

/* ── The outdoor row: smoke line + pollen ─────────────────────────────────
   Two readings about the air outside the building, so they share a row rather
   than stacking as unrelated bars. The margin lives here, not on .dh-cond —
   either child can be hidden (smoke needs a live fetch, pollen needs a cron
   that has run), and a margin on the child leaves a gap where a hidden element
   used to be.

   ⚠️ THEY ARE SIBLINGS BECAUSE .dh-cond IS ITSELF AN ANCHOR. Nesting the pollen
   strip inside it would be a nested <a>; see the note at the markup. */
.dh-outdoor{display:flex;gap:.6rem;flex-wrap:wrap;margin-top:1.1rem}
.dh-outdoor > .dh-cond{margin-top:0;flex:1 1 26rem}

/* Same chip language as .dh-cond — translucent fill, hairline edge — so the two
   read as one row of instrumentation and not as a panel with an ornament next
   to it. Not a link: there is no pollen report to open, and a hover state that
   leads nowhere is a promise the interface does not keep. */
.dh-pollen{display:flex;align-items:center;gap:.55rem;flex-wrap:wrap;flex:1 1 20rem;
  padding:.6rem .8rem;border-radius:8px;
  background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.14)}
/* The header. Brighter than .dhc-more's quiet trailing label because this one
   NAMES the panel rather than annotating it — and because the box now has to be
   legible when it contains nothing but a sentence saying so. */
.dh-pollen .dhp-head{display:inline-flex;align-items:center;gap:.4rem;
  font-family:var(--font-mono);font-size:.68rem;letter-spacing:.09em;
  text-transform:uppercase;color:rgba(255,255,255,.82);white-space:nowrap}
.dh-pollen .dhp-head .ico{width:1.05rem;height:1.05rem;flex:0 0 auto;
  color:rgba(255,255,255,.6)}
/* An empty state is a sentence, not a reading: sentence case, no mono, no
   letterspacing — so it cannot be mistaken for a value at a glance. */
.dh-pollen .dhp-empty{font-size:.82rem;color:rgba(255,255,255,.62);line-height:1.35}
.dh-pollen .dhp-chips{display:flex;gap:.5rem;flex-wrap:wrap}
.dh-pollen .dhp-chip{display:inline-flex;align-items:center;gap:.32rem;
  font-size:.82rem;color:rgba(255,255,255,.9);white-space:nowrap}
.dh-pollen .dhp-t{color:rgba(255,255,255,.62)}
/* Tabular figures: three numbers side by side that jump around as they change
   read as three different fields rather than one row of readings. */
.dh-pollen .dhp-v{font-family:var(--font-mono);font-variant-numeric:tabular-nums;
  font-weight:700;color:var(--pband,rgba(255,255,255,.5))}
.dh-pollen .dhp-dot{width:9px;height:9px;border-radius:50%;flex:0 0 auto;
  background:var(--pband,rgba(255,255,255,.28))}
.dh-pollen .dhp-note{margin-left:auto;font-size:.78rem;color:rgba(255,255,255,.58);
  text-align:right}

/* ⚠️ THE DARK-FIELD LADDER, not --good/--moderate/--unhealthy. Those are the
   text-weight tokens tuned for 4.5:1 on WHITE; against this near-black panel
   they sink. These are the same values .dash-hero.tone-* and .dash-stat.t-*
   already use a few hundred lines down — one ladder for the whole dark header.
   Nothing here is orange, per the status-token note, and the red keeps G and B
   equal so it cannot drift toward pink.

   ⚠️ b-none IS "NOT MEASURED" AND b-zero IS "MEASURED AT ZERO". They must not
   look alike: the first is an em dash in grey, the second is a real green
   reading. Collapsing them would report an unmonitored allergen as a clean one. */
.dh-pollen .b-good{--pband:#2EE07F}
.dh-pollen .b-warn{--pband:#FFCE3D}
.dh-pollen .b-alarm{--pband:#FF2B2B}
.dh-pollen .b-hazardous{--pband:#C77DFF}
.dh-pollen .b-zero{--pband:#2EE07F}
.dh-pollen .b-none{--pband:rgba(255,255,255,.28)}
.dh-pollen .b-none .dhp-v{color:rgba(255,255,255,.38)}
.console-tile .t-name{font-family:var(--font-display);font-weight:700;font-size:1rem;
  color:var(--brand);margin-bottom:.2rem}
.console-tile .t-count{font-family:var(--font-mono);font-size:.72rem;
  letter-spacing:.06em;text-transform:uppercase;color:var(--ink-faint);margin-bottom:.35rem}
.console-tile .t-desc{font-size:.82rem;color:var(--ink-soft);line-height:1.4}
/* Tenant switcher. Only rendered when there is more than one org — a picker
   with a single option is a decision the user does not have. */
.org-switch{display:flex;align-items:center;gap:.5rem;margin-bottom:1.2rem;flex-wrap:wrap}
.org-switch select{min-width:12rem}

/* ── Node console (#/admin/:org/console) ─────────────────────────────────
   Reuses the semantic status colours already defined for AQI bands rather
   than introducing a second palette: --good/--moderate/--danger already mean
   "fine / watch / broken" everywhere else in this app, and a node's health
   is the same kind of judgement. Deliberately dense — this is an operations
   screen meant to be scanned across a fleet, not a reporting page. */
.con-summary{display:flex;gap:.4rem;flex-wrap:wrap;margin:0 0 1rem}
.con-pill{display:inline-block;padding:.15rem .55rem;border-radius:2px;font-size:.75rem;
  font-family:var(--font-mono);border:1px solid var(--line);white-space:nowrap}
.con-pill.v-ok{color:var(--good);border-color:var(--good)}
.con-pill.v-warn{color:var(--moderate);border-color:var(--moderate)}
.con-pill.v-bad{color:var(--danger);border-color:var(--danger)}
.con-pill.v-off{color:var(--ink-soft);border-color:var(--line)}
.v-warn-t{color:var(--moderate)}

.con-controls{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;margin:0 0 1rem}
.con-controls select{width:auto;min-width:12rem;margin:0}
.con-controls .btn{margin:0}

.con-card{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);
  padding:1rem 1.15rem;margin-bottom:.85rem}
.con-head{display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap}
.con-name{font-family:var(--font-display);font-weight:700;margin-right:.5rem}
.con-sub{margin-top:.15rem}
.con-reasons{margin:.6rem 0 0;padding-left:1.1rem}
.con-reasons li{font-size:.82rem;color:var(--danger);margin:.1rem 0}

/* auto-fit rather than a fixed 4-up: the sectors stay readable when the
   viewport narrows instead of forcing the page to scroll sideways. */
.con-sectors{display:grid;gap:.9rem;margin-top:.85rem;
  grid-template-columns:repeat(auto-fit,minmax(11rem,1fr))}
.con-sector{border-top:1px solid var(--line-faint);padding-top:.5rem}
.con-sector .eyebrow{margin-bottom:.35rem}
.con-pending{opacity:.55}
.con-kv{display:flex;justify-content:space-between;gap:.5rem;
  font-size:.8rem;font-family:var(--font-mono);padding:.1rem 0}
.con-kv span{color:var(--ink-soft)}
.v-bad-t{color:var(--danger)}

/* Quota gauges. The number lives INSIDE the bar so the reading survives at any
   fill level — a label beside the track is unreadable at 0.03% and collides
   with the fill at 90%. Minimum fill width keeps a sliver visible when usage
   is near zero, so an empty gauge still reads as "measured" not "broken". */
.gauge-wrap{display:grid;gap:.6rem;margin:0 0 1.25rem;
  grid-template-columns:repeat(auto-fit,minmax(15rem,1fr))}
.gauge-top{display:flex;justify-content:space-between;align-items:baseline;gap:.5rem;margin-bottom:.25rem}
.gauge-track{position:relative;height:1.35rem;background:var(--panel);
  border:1px solid var(--line);border-radius:2px;overflow:hidden}
.gauge-fill{position:absolute;inset:0 auto 0 0;transition:width .3s ease}
.gauge-fill.g-ok{background:color-mix(in srgb,var(--good) 28%,transparent)}
.gauge-fill.g-warn{background:color-mix(in srgb,var(--moderate) 34%,transparent)}
.gauge-fill.g-bad{background:color-mix(in srgb,var(--danger) 34%,transparent)}
.gauge-text{position:relative;display:block;line-height:1.35rem;padding:0 .5rem;
  font-family:var(--font-mono);font-size:.74rem;white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}

/* Debug console log pane. Monospace and dense — this is a terminal, not prose.
   Fixed height with its own scroll so the panel cannot push the gauges and
   node cards off the page as lines accumulate. */
.log-pane{background:var(--side-bg);border:1px solid var(--line);border-radius:2px;
  max-height:22rem;overflow:auto;padding:.4rem 0}
.lg-line{display:flex;gap:.6rem;padding:.1rem .6rem;font-family:var(--font-mono);
  font-size:.72rem;line-height:1.45;color:#C9CFCB;white-space:pre-wrap;word-break:break-word}
.lg-line:hover{background:rgba(255,255,255,.04)}
.lg-ts{color:#7E8783;flex:0 0 auto}
.lg-tag{color:#5FA8CF;flex:0 0 7.5rem;overflow:hidden;text-overflow:ellipsis}
.lg-msg{flex:1 1 auto}
.lg-warn .lg-msg{color:#E3B341}
.lg-err  .lg-msg{color:#F0776C}
.lg-info .lg-msg{color:#C9CFCB}

/* ══ Landing dashboard ══════════════════════════════════════════════════════
   The homepage, and the one screen in this app that does NOT follow the flat
   Mosaic Grid convention above.

   That is deliberate and it is scoped. Everything here is namespaced under
   .dash-* / .ds-* / .work-* / .room-*, so the drafting-table language — 2px
   corners, hairline borders, hard offset shadows — still governs the admin
   tables, the reports and the endpoint pages untouched. Only the landing
   surface is glass.

   WHY THE EXCEPTION: the flat treatment is right for dense working screens,
   where a panel is a container and should get out of the way. The landing page
   is the opposite job — it is looked at, not worked in, and the flat version of
   it read as a spreadsheet with colour. Depth, translucency and a bevelled edge
   give the eye a hierarchy to travel: the dark hero sits above the page, the
   cards float over a tinted field, the metric wells recess into the cards.

   THREE RULES THIS SECTION KEEPS:
     1. Colour still only ever means status. The glass, the bevels and the
        shadows are neutral; not one hue is decorative.
     2. Every backdrop-filter is paired with an opaque-enough background that
        the panel is still legible where the filter is unsupported or disabled.
     3. No entrance animation. This view repaints itself every 90 seconds, and
        anything that animates in would re-animate on every repaint. */

/* Dashboard-only radius scale. The base system pins --r to 2px and says so; a
   16px corner is the single loudest signal that this surface is a different
   kind of object, and it is what makes the panels read as glass rather than as
   bordered rectangles with a blur behind them. */
.dash-hero,.dash-grid,.worklist{--rd:16px;--rd-sm:11px;--rd-xs:8px}

/* ── The field behind the glass ───────────────────────────────────────────
   Glass needs something to refract or it is just a grey box. This lays four
   very wide, very low-alpha pools over the paper — cyan, brand blue and a green
   cast — so blurred panels pick up a faint tint that shifts as you move down
   the page. Cool hues only, and no violet: purple carries the hazardous band
   and must never appear as scenery.

   :has() scopes it to the dashboard; browsers without it simply get the normal
   paper background, which is a downgrade in richness and nothing else.
   Attached to .main so it scrolls with the content rather than sitting still
   underneath it. */
.main:has(.dash-hero){position:relative;isolation:isolate}
.main:has(.dash-hero)::before{
  content:'';position:absolute;inset:-10% -5% 0;z-index:-1;pointer-events:none;
  background:
    radial-gradient(52rem 34rem at 6% -2%,  rgba(15,168,214,.17), transparent 62%),
    radial-gradient(44rem 30rem at 98% 14%, rgba(28,127,176,.13), transparent 60%),
    radial-gradient(56rem 36rem at 34% 54%, rgba(0,184,92,.10),   transparent 62%),
    radial-gradient(40rem 28rem at 88% 82%, rgba(15,168,214,.09), transparent 62%);
  filter:blur(.5px)}

/* ── Hero ─────────────────────────────────────────────────────────────────
   A dark glass slab. Four layers stacked, in paint order:
     1. a base gradient, near-black but not flat black
     2. .dh-glow, a pool of the status colour behind the headline
     3. ::before, the specular sheen — a soft diagonal light streak
     4. ::after, the status rail with its own bloom

   The bevel is entirely in box-shadow: a bright inset hairline along the top
   edge (light from above), a dim inset ring for the body, and two outer
   shadows — one tight and dark for contact, one wide and soft for lift. That
   combination is what separates "a dark rectangle" from "a solid object". */
.dash-hero{
  position:relative;overflow:hidden;isolation:isolate;
  border:none;border-radius:var(--rd);padding:1.5rem 1.9rem 1.7rem;margin-bottom:2rem;
  background:
    linear-gradient(152deg,#26262B 0%,#1B1B1F 42%,#141417 100%);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.16),
    inset 0 0 0 1px rgba(255,255,255,.055),
    inset 0 -1px 0 rgba(0,0,0,.6),
    0 2px 4px rgba(10,12,16,.24),
    0 26px 60px -22px rgba(10,12,16,.72);
  --tone:var(--sky)}

/* Specular sheen. Pure light, no hue — a fixed highlight the panel appears to
   catch, which is most of what makes the surface read as glass rather than as
   matte paint. */
.dash-hero::before{
  content:'';position:absolute;inset:0;z-index:-1;pointer-events:none;
  background:linear-gradient(112deg,
    rgba(255,255,255,.10) 0%,
    rgba(255,255,255,.035) 18%,
    transparent 40%,
    transparent 100%)}

/* Status rail with a bloom. Inset from the edges and rounded, so it reads as a
   lit element sitting ON the panel rather than as a border cut into it. */
.dash-hero::after{
  content:'';position:absolute;left:0;top:12px;bottom:12px;width:3px;
  border-radius:0 3px 3px 0;background:var(--tone);
  box-shadow:0 0 14px 1px var(--tone), 0 0 34px 2px color-mix(in srgb,var(--tone) 45%,transparent)}
/* Hazard striping, but ONLY in the two states you would actually put a sign up
   about. Striping a nominal panel would make the pattern meaningless — the
   whole value of it is that it does not appear most days. */
.dash-hero.tone-alarm::after,
.dash-hero.tone-hazardous::after{
  width:7px;border-radius:0 4px 4px 0;
  background:repeating-linear-gradient(-45deg,
    var(--tone) 0 5px, rgba(0,0,0,.72) 5px 10px)}

/* The status pool. Kept tight and low-alpha on purpose: a broad wash of any
   warm colour over near-black turns to mud. The colour belongs in the rail,
   the headline and the tiles at full strength, not smeared across the panel. */
.dh-glow{position:absolute;inset:0;z-index:-1;pointer-events:none;
  background:radial-gradient(56% 118% at 3% 52%,
    color-mix(in srgb,var(--tone) 15%,transparent), transparent 70%)}

.dh-top{display:flex;justify-content:space-between;align-items:baseline;gap:1rem;flex-wrap:wrap;
  padding-bottom:1.05rem;margin-bottom:1.25rem;
  border-bottom:1px solid rgba(255,255,255,.09);
  box-shadow:0 1px 0 rgba(255,255,255,.045)}   /* engraved, not drawn */
.dh-org,.dh-clock{font-family:var(--font-mono);font-size:.67rem;letter-spacing:.15em;text-transform:uppercase}
.dh-org{color:rgba(255,255,255,.94);font-weight:600}
.dh-clock{color:rgba(255,255,255,.36);letter-spacing:.09em}

.dh-body{display:flex;gap:2.25rem;align-items:center;flex-wrap:wrap}
.dh-main{flex:1 1 340px;min-width:0}
/* ── The placard ─────────────────────────────────────────────────────────
   Glyph plate, state tag, then the sentence. Read left to right that is the
   same order a real sign gives you: symbol, severity word, detail. */
.dh-sign{display:flex;align-items:flex-start;gap:1rem;margin-bottom:.7rem}
.dh-words{min-width:0}

/* The plate. A bordered square in the state colour with an inner wash and an
   outer bloom — deliberately the shape of a placard rather than a bare icon,
   because a floating glyph reads as decoration and a framed one reads as
   signage. */
.dh-plate{flex:0 0 auto;display:grid;place-items:center;width:3.4rem;height:3.4rem;
  border-radius:12px;color:var(--tone);
  border:2px solid color-mix(in srgb,var(--tone) 78%,transparent);
  background:radial-gradient(120% 120% at 50% 0%,
    color-mix(in srgb,var(--tone) 26%,transparent),
    color-mix(in srgb,var(--tone) 9%,transparent));
  box-shadow:inset 0 1px 0 rgba(255,255,255,.22),
             0 0 22px -4px color-mix(in srgb,var(--tone) 62%,transparent)}
.dh-plate .ico{width:2.1rem;height:2.1rem;stroke-width:1.5}

/* Hard-edged, high-contrast, letterspaced: signage typography, not editorial.
   Solid fill with dark text rather than coloured text on dark, so it reads as
   a printed label stuck to the panel. */
.dh-state{display:inline-block;margin-bottom:.4rem;padding:.16rem .55rem;
  font-family:var(--font-mono);font-size:.61rem;font-weight:700;
  letter-spacing:.22em;text-transform:uppercase;
  color:#141417;background:var(--tone);border-radius:4px;
  box-shadow:0 0 16px -3px color-mix(in srgb,var(--tone) 70%,transparent)}

/* Smaller than it was: the plate and the tag now carry the urgency, so the
   sentence can go back to being a sentence. It still glows, which is what a lit
   sign does and a printed one cannot. */
.dh-head{font-size:clamp(1.5rem,2.7vw,2.15rem);line-height:1.05;letter-spacing:-.025em;
  color:var(--tone);text-wrap:balance;
  text-shadow:0 0 30px color-mix(in srgb,var(--tone) 30%,transparent)}
.dh-detail{color:rgba(255,255,255,.6);font-size:.94rem;line-height:1.5;max-width:54ch;
  padding-left:4.4rem}
@media(max-width:640px){
  .dh-plate{width:2.75rem;height:2.75rem}
  .dh-plate .ico{width:1.55rem;height:1.55rem}
  .dh-detail{padding-left:0}
}

/* Dark-background counterparts of the four semantics: the same
   green -> yellow -> red -> purple ladder, lifted in luminance so each holds
   its hue against near-black instead of sinking into it. Nothing here is
   orange, and the red keeps G and B equal so it cannot drift to pink. */
.dash-hero.tone-good{--tone:#2EE07F}
.dash-hero.tone-warn{--tone:#FFCE3D}
.dash-hero.tone-alarm{--tone:#FF2B2B}
.dash-hero.tone-hazardous{--tone:#C77DFF}
.dash-hero.tone-neutral{--tone:#5FD9FF}

/* ── Stat tiles ───────────────────────────────────────────────────────────
   Buttons, not figures. Each filters the grid below, because a count you
   cannot act on is decoration — "2 rooms over limit" should take you to those
   two rooms. A zero count is disabled rather than hidden: the tiles must not
   reflow as the fleet changes state, or the one you were reaching for moves
   out from under you on the 90-second refresh.

   Frosted chips: translucent fill, a lit top edge, and on hover a bloom in the
   tile's own status colour. */
.dash-stats{display:grid;grid-template-columns:repeat(4,minmax(90px,1fr));gap:.6rem;flex:0 1 452px}
.dash-stat{
  position:relative;display:flex;flex-direction:column;gap:.32rem;
  padding:.85rem .9rem .8rem;cursor:pointer;text-align:left;font:inherit;
  border:none;border-radius:var(--rd-sm);
  background:linear-gradient(180deg,rgba(255,255,255,.10),rgba(255,255,255,.045));
  -webkit-backdrop-filter:blur(12px) saturate(150%);backdrop-filter:blur(12px) saturate(150%);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.20),
    inset 0 0 0 1px rgba(255,255,255,.075),
    0 1px 2px rgba(0,0,0,.28);
  transition:background .18s ease,box-shadow .18s ease,transform .14s cubic-bezier(.2,.7,.3,1)}
.dash-stat:hover:not(:disabled){transform:translateY(-2px);
  background:linear-gradient(180deg,rgba(255,255,255,.17),rgba(255,255,255,.075));
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.30),
    inset 0 0 0 1px rgba(255,255,255,.13),
    0 8px 20px -8px color-mix(in srgb,currentColor 55%,transparent)}
.dash-stat:active:not(:disabled){transform:translateY(0)}
.dash-stat:disabled{cursor:default}
.dash-stat.on{
  background:linear-gradient(180deg,rgba(255,255,255,.19),rgba(255,255,255,.085));
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.32),
    inset 0 0 0 1px color-mix(in srgb,currentColor 62%,transparent),
    0 0 0 1px color-mix(in srgb,currentColor 30%,transparent),
    0 10px 26px -10px color-mix(in srgb,currentColor 60%,transparent)}
.dash-stat .n{font-family:var(--font-display);font-size:1.95rem;font-weight:700;
  line-height:1;letter-spacing:-.025em;color:currentColor;
  text-shadow:0 0 20px color-mix(in srgb,currentColor 32%,transparent)}
.dash-stat .l{font-family:var(--font-mono);font-size:.575rem;letter-spacing:.1em;
  text-transform:uppercase;color:rgba(255,255,255,.5);line-height:1.3}
.dash-stat.t-brand{color:#5FD9FF}
.dash-stat.t-alarm{color:#FF2B2B}
.dash-stat.t-warn{color:#FFCE3D}
.dash-stat.t-good{color:#2EE07F}
/* A zero in a warning colour reads as an alarm from across the room.
   Neutralise it — "0 not reporting" is good news and must never look like bad
   news. The glow goes too, or the tile still pulls the eye. */
.dash-stat:disabled{color:rgba(255,255,255,.26);
  background:rgba(255,255,255,.028);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.05)}
.dash-stat:disabled .n{text-shadow:none}
.dash-stat:disabled .l{color:rgba(255,255,255,.24)}

/* ── Sections ─────────────────────────────────────────────────────────────
   Headings NAME a state; they do not instruct. The rule running to the right
   edge sets the group's width before any card is read, so five stacked
   sections do not blur into one long scroll. */
.dash-section{margin-bottom:2.4rem}
.dash-group-head{margin-bottom:1.05rem}
.dash-h2{font-family:var(--font-display);font-size:.95rem;letter-spacing:-.01em;
  display:flex;align-items:center;gap:.65rem}
.dash-h2::after{content:'';flex:1;height:1px;
  background:linear-gradient(90deg,var(--line),transparent)}
.dash-h2 .cnt{order:-1;display:inline-grid;place-items:center;min-width:1.4rem;height:1.4rem;
  padding:0 .38rem;font-family:var(--font-mono);font-size:.68rem;font-weight:700;
  color:var(--paper);border-radius:var(--rd-xs);
  background:linear-gradient(180deg,#2C6A8C,#17475F);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.22), 0 1px 2px rgba(16,24,40,.24)}
.dash-group-head p{margin-top:.32rem;max-width:70ch;letter-spacing:.01em}

/* ── Worklist ─────────────────────────────────────────────────────────────
   The rows the reference dashboards have nowhere: not "which building has a
   problem" but "which room, what is wrong with it, and how stale is that
   claim" — the whole triage answer on one line, linked to the room.

   Fixed first column rather than a fraction, so the breach chip starts at the
   same x on every row instead of drifting with the length of each name. */
.worklist{display:flex;flex-direction:column;gap:.55rem}
.work-row{
  position:relative;display:grid;
  grid-template-columns:minmax(0,17rem) minmax(0,1fr) auto 1rem;
  gap:1.25rem;align-items:center;padding:.9rem 1.15rem .9rem 1.35rem;
  color:var(--ink);border:none;border-radius:var(--rd-sm);
  background:linear-gradient(180deg,rgba(255,255,255,.94),rgba(255,255,255,.8));
  -webkit-backdrop-filter:blur(16px) saturate(165%);backdrop-filter:blur(16px) saturate(165%);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.92),
    inset 0 0 0 1px rgba(16,24,40,.07),
    0 1px 2px rgba(16,24,40,.05),
    0 10px 22px -14px rgba(16,24,40,.32);
  transition:transform .16s cubic-bezier(.2,.7,.3,1),box-shadow .18s ease}
/* Rounded inset rail, matching the hero's. */
.work-row::before{content:'';position:absolute;left:0;top:10px;bottom:10px;width:3px;
  border-radius:0 3px 3px 0;background:var(--unhealthy-fill)}
.work-row.s-hazardous::before{background:var(--hazardous-fill)}
.work-row.s-moderate::before{background:var(--moderate-fill)}
.work-row:hover{transform:translateX(3px);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.96),
    inset 0 0 0 1px rgba(16,24,40,.09),
    0 2px 4px rgba(16,24,40,.06),
    0 16px 30px -16px rgba(16,24,40,.4)}
.work-where{display:flex;flex-direction:column;min-width:0}
.work-where b{font-family:var(--font-display);font-size:1rem;letter-spacing:-.01em}
.work-where span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.work-what{display:flex;flex-wrap:wrap;gap:.45rem}
.breach{display:inline-flex;align-items:baseline;gap:.36rem;
  font-family:var(--font-mono);font-size:.78rem;padding:.24rem .6rem;
  border-radius:999px;color:#8E0000;
  background:linear-gradient(180deg,
    color-mix(in srgb,var(--unhealthy-fill) 19%,#fff),
    color-mix(in srgb,var(--unhealthy-fill) 11%,#fff));
  box-shadow:inset 0 1px 0 rgba(255,255,255,.85),
             inset 0 0 0 1px color-mix(in srgb,var(--unhealthy-fill) 34%,transparent)}
.breach b{font-weight:700;font-size:.66rem;letter-spacing:.06em;text-transform:uppercase}
.breach i{font-style:normal;font-size:.64rem;opacity:.75;margin-left:-.15rem}
.breach .vs{font-size:.66rem;opacity:.8}
.work-go{color:var(--ink-faint);font-size:1rem;transition:transform .16s ease,color .16s ease}
.work-row:hover .work-go{color:var(--brand);transform:translateX(2px)}
@media(max-width:820px){
  .work-row{grid-template-columns:1fr auto;row-gap:.55rem}
  .work-what{grid-column:1 / -1}
}

/* ── Site cards ───────────────────────────────────────────────────────────
   Deliberately taller and denser than the cards these replaced. The old ones
   fit more per screen and told you nothing you could act on; these each carry
   a status, three live figures, a day of trend, every room as a link, and the
   outdoor reading to judge it against. Fewer cards, each worth stopping at.

   The bevel is the same recipe as the hero, inverted for a light surface: a
   near-white inset hairline along the top (light from above), a faint ink ring
   for the body edge, then contact + lift shadows. */
/* align-items:start is load-bearing. A grid stretches every card in a row to
   the tallest one, and the slack has to go somewhere — it was pooling as a slab
   of dead white above the room chips on whichever card had the fewest sections.
   Letting cards take their natural height costs a ragged bottom edge, which
   these read fine with: the bevel and drop shadow make each one an object in
   its own right, not a cell in a table that needs to line up. */
.dash-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(336px,1fr));
  gap:1.25rem;align-items:start}
.dash-site{
  position:relative;padding:0;overflow:hidden;display:flex;flex-direction:column;
  border:none;border-radius:var(--rd);
  background:linear-gradient(180deg,rgba(255,255,255,.95),rgba(255,255,255,.82));
  -webkit-backdrop-filter:blur(20px) saturate(170%);backdrop-filter:blur(20px) saturate(170%);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.95),
    inset 0 0 0 1px rgba(16,24,40,.07),
    0 1px 2px rgba(16,24,40,.05),
    0 16px 34px -18px rgba(16,24,40,.36);
  transition:transform .18s cubic-bezier(.2,.7,.3,1),box-shadow .22s ease}
.dash-site:hover{transform:translateY(-4px);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,1),
    inset 0 0 0 1px rgba(16,24,40,.09),
    0 3px 6px rgba(16,24,40,.07),
    0 30px 52px -22px rgba(16,24,40,.46),
    0 0 0 1px color-mix(in srgb,var(--rail,var(--ink-faint)) 22%,transparent)}
/* Status rail: inset and rounded like the hero's, with a bloom in its own hue
   so the grid colour-sorts peripherally before a single word is read. */
.dash-site::before{content:'';position:absolute;left:0;top:14px;bottom:14px;width:3px;z-index:2;
  border-radius:0 3px 3px 0;background:var(--rail,var(--ink-faint));
  box-shadow:0 0 12px color-mix(in srgb,var(--rail,transparent) 55%,transparent)}
.dash-site.s-good{--rail:var(--good-fill)}
.dash-site.s-moderate{--rail:var(--moderate-fill)}
.dash-site.s-unhealthy{--rail:var(--unhealthy-fill)}
.dash-site.s-hazardous{--rail:var(--hazardous-fill)}
.dash-site.s-stale,.dash-site.s-empty{--rail:#B9C0BC}
/* NO pulse animation here, unlike .ep-card.alarm on the site page. That is
   right when one card on screen is alarming; on a grid where several can be at
   once it turns the page into a strobe and makes the calm cards harder to read
   than the loud ones. The rail bloom, the tinted header and the banner are
   already unmissable. */
.dash-site.over{--rail:var(--unhealthy-fill)}

/* A breath of the status colour behind the title, fading out downward — enough
   to sort the grid at a glance, not enough to fight the figures. */
.ds-head{display:flex;justify-content:space-between;align-items:flex-start;gap:.9rem;
  padding:1.25rem 1.35rem 1.05rem;color:var(--ink);
  background:linear-gradient(180deg,var(--tint,transparent),transparent)}
.ds-head:hover h3,.ds-head:hover h3 > span{color:var(--brand)}
.dash-site.s-good{--tint:color-mix(in srgb,var(--good-fill) 26%,transparent)}
.dash-site.s-moderate{--tint:color-mix(in srgb,var(--moderate-fill) 38%,transparent)}
.dash-site.s-unhealthy{--tint:color-mix(in srgb,var(--unhealthy-fill) 17%,transparent)}
.dash-site.s-hazardous{--tint:color-mix(in srgb,var(--hazardous-fill) 22%,transparent)}
.ds-title{min-width:0}
/* Scales with the card and wraps to a SECOND LINE rather than colliding with
   the status pill beside it. `min-width:0` on the flex child is what actually
   permits the wrap — without it the title's intrinsic width wins and it shoves
   the pill off the card edge. Clamped at two lines: a third would push the
   figures out of alignment across a row of cards. */
.ds-title h3{font-size:clamp(1rem,1.05rem + .35vw,1.2rem);line-height:1.15;
  letter-spacing:-.02em;margin-bottom:.4rem;transition:color .15s linear;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
/* The glyph would be stretched by the flex cross-axis default once the text
   wraps; pin it to the first line's cap height instead. */
.ds-title h3.is-place{display:flex;align-items:flex-start}
.ds-title h3.is-place > .ico{margin-top:.12em}
.ds-title h3.is-place > span{white-space:normal;overflow:hidden;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
/* The status pill must never be the thing that gives way. */
.ds-head > .status{flex:0 0 auto}
.ds-sub{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;
  font-family:var(--font-mono);font-size:.65rem;letter-spacing:.06em;text-transform:uppercase;
  color:var(--ink-faint)}
.ds-off{color:var(--moderate);font-weight:600}
/* Open/Closed is inferred from operating hours, and in this deployment every
   site is still on the seeded Mon–Fri 8–5 placeholder. `.guess` marks that with
   a dashed edge so a guess never renders identically to a fact. */
.open-chip,.closed-chip{padding:.1rem .5rem;border-radius:999px;border:1px solid transparent;
  font-weight:600;letter-spacing:.07em}
.open-chip{background:color-mix(in srgb,var(--good-fill) 24%,transparent);color:var(--good);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.7)}
.closed-chip{background:rgba(58,58,56,.075);color:var(--ink-soft);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.7)}
.open-chip.guess,.closed-chip.guess{border-style:dashed;border-color:currentColor;opacity:.85}

.ds-alarm{display:flex;align-items:center;gap:.5rem;
  font-family:var(--font-mono);font-size:.63rem;letter-spacing:.1em;text-transform:uppercase;
  font-weight:700;color:#fff;padding:.5rem 1.35rem;
  background:linear-gradient(180deg,#FF2020,var(--unhealthy));
  box-shadow:inset 0 1px 0 rgba(255,255,255,.28), inset 0 -1px 0 rgba(0,0,0,.16)}
.ds-alarm::before{content:'';width:6px;height:6px;border-radius:999px;background:#fff;flex:0 0 auto;
  box-shadow:0 0 8px rgba(255,255,255,.9)}

/* Three live figures. Recessed wells, so the numbers sit IN the card rather
   than on it — the inner shadow at the top of each cell is the whole effect. */
.dash-metrics{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;
  background:rgba(16,24,40,.07);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.8)}
.dash-metric{position:relative;padding:.85rem .45rem .8rem 1.15rem;
  background:linear-gradient(180deg,rgba(250,251,252,.9),rgba(255,255,255,.72));
  box-shadow:inset 0 2px 3px -2px rgba(16,24,40,.14)}
.dash-metric:nth-child(n+2){padding-left:.85rem}
.dm-v{display:block;font-family:var(--font-display);font-size:1.6rem;font-weight:700;
  line-height:1;letter-spacing:-.03em;color:var(--ink-faint);white-space:nowrap}
.dm-v i{font-style:normal;font-family:var(--font-mono);font-size:.6rem;font-weight:500;
  margin-left:.2rem;letter-spacing:0;opacity:.7}
.dm-l{margin-top:.45rem;font-family:var(--font-mono);font-size:.59rem;
  letter-spacing:.09em;text-transform:uppercase;color:var(--ink-soft)}
.dm-l em{font-style:normal;opacity:.6;margin-left:.25rem}
.dash-metric.b-good .dm-v{color:var(--good)}
.dash-metric.b-unhealthy .dm-v{color:var(--unhealthy)}
.dash-metric.b-hazardous .dm-v{color:var(--hazardous)}
/* Moderate is the one band that cannot carry its hue in the numeral. Green, red
   and purple all darken to legible text while staying recognisably themselves;
   yellow does not — darkened far enough to read on white it stops being yellow
   and turns olive-brown. So the well carries the colour and the numeral only
   has to be readable. */
.dash-metric.b-moderate{background:linear-gradient(180deg,
  color-mix(in srgb,var(--moderate-fill) 34%,#fff),
  color-mix(in srgb,var(--moderate-fill) 22%,#fff))}
.dash-metric.b-moderate .dm-v{color:#6B4E00}
.dash-metric.b-moderate .dm-l{color:#7A5C00}
/* Temperature has no BANDS entry (comfort, not a health category) so it lands
   in b-none and stays ink — correct, not an oversight: colouring it would imply
   a health band that does not exist. */
.dash-metric.b-none .dm-v{color:var(--ink)}

.ds-spark{position:relative;padding:.4rem 0 0}
.ds-spark.empty{padding:1.15rem 1.35rem}
.dash-spark{display:block;width:100%;height:48px}
.dash-spark path.line{fill:none;stroke:var(--brand);stroke-width:1.9;
  vector-effect:non-scaling-stroke;stroke-linecap:round;stroke-linejoin:round}
.dash-spark path.fill{fill:var(--brand);opacity:.16;stroke:none}
.dash-spark circle{fill:var(--brand)}
/* The trend takes the site's current status colour, so a card that is amber at
   the top is amber all the way down rather than telling two colour stories. */
.dash-spark.s-good path.line{stroke:var(--good-fill)} .dash-spark.s-good path.fill{fill:var(--good-fill)} .dash-spark.s-good circle{fill:var(--good-fill)}
.dash-spark.s-moderate path.line{stroke:var(--moderate-fill)} .dash-spark.s-moderate path.fill{fill:var(--moderate-fill)} .dash-spark.s-moderate circle{fill:var(--moderate-fill)}
.dash-spark.s-unhealthy path.line{stroke:var(--unhealthy-fill)} .dash-spark.s-unhealthy path.fill{fill:var(--unhealthy-fill)} .dash-spark.s-unhealthy circle{fill:var(--unhealthy-fill)}
.dash-spark.s-hazardous path.line{stroke:var(--hazardous-fill)} .dash-spark.s-hazardous path.fill{fill:var(--hazardous-fill)} .dash-spark.s-hazardous circle{fill:var(--hazardous-fill)}
.dash-spark.s-stale path.line{stroke:var(--ink-faint)} .dash-spark.s-stale path.fill{fill:var(--ink-faint)} .dash-spark.s-stale circle{fill:var(--ink-faint)}
.ds-spark-l{display:block;padding:.2rem 1.35rem .4rem;font-family:var(--font-mono);
  font-size:.58rem;letter-spacing:.09em;text-transform:uppercase;color:var(--ink-faint)}

/* ── Room chips: the drill-down ───────────────────────────────────────────
   The single biggest departure from the reference dashboards, where reaching a
   room costs two navigations (open the building, then pick the room). Every
   room is on the homepage, colour-coded, one click from its own page — so the
   bad room is findable without reading any of the names.

   Pills, and the status dot is lit: a small point of saturated colour with a
   halo is legible at a glance in a way a flat square is not. */
.room-strip{display:flex;flex-wrap:wrap;gap:.42rem;padding:.9rem 1.35rem}
.room-strip.empty{padding:.9rem 1.35rem 1.15rem}
.room-chip{display:inline-flex;align-items:center;gap:.42rem;max-width:100%;
  padding:.3rem .68rem;border:none;border-radius:999px;
  background:linear-gradient(180deg,rgba(255,255,255,.96),rgba(246,248,249,.86));
  color:var(--ink);font-size:.78rem;line-height:1.3;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.95),
             inset 0 0 0 1px rgba(16,24,40,.11),
             0 1px 1px rgba(16,24,40,.05);
  transition:transform .14s cubic-bezier(.2,.7,.3,1),box-shadow .16s ease,color .16s linear}
.room-chip:hover{transform:translateY(-1px);color:var(--brand);
  box-shadow:inset 0 1px 0 rgba(255,255,255,1),
             inset 0 0 0 1px color-mix(in srgb,var(--brand) 45%,transparent),
             0 4px 10px -4px rgba(16,24,40,.3)}
.rc-dot{width:9px;height:9px;flex:0 0 auto;border-radius:999px;background:var(--good-fill);
  box-shadow:0 0 7px color-mix(in srgb,var(--good-fill) 85%,transparent)}
.room-chip.s-moderate .rc-dot{background:var(--moderate-fill);box-shadow:0 0 7px color-mix(in srgb,var(--moderate-fill) 85%,transparent)}
.room-chip.s-unhealthy .rc-dot{background:var(--unhealthy-fill);box-shadow:0 0 7px color-mix(in srgb,var(--unhealthy-fill) 85%,transparent)}
.room-chip.s-hazardous .rc-dot{background:var(--hazardous-fill);box-shadow:0 0 7px color-mix(in srgb,var(--hazardous-fill) 85%,transparent)}
/* Offline is a hollow, unlit marker: "no data" and "data that happens to be
   grey" must not look alike. */
.room-chip.s-stale{color:var(--ink-faint);
  background:transparent;
  box-shadow:inset 0 0 0 1px rgba(16,24,40,.12)}
.room-chip.s-stale .rc-dot{background:transparent;box-shadow:inset 0 0 0 1.5px var(--ink-faint)}
.room-chip.over{color:#fff;font-weight:600;
  background:linear-gradient(180deg,#FF2020,var(--unhealthy));
  box-shadow:inset 0 1px 0 rgba(255,255,255,.3),
             0 2px 8px -2px color-mix(in srgb,var(--unhealthy-fill) 70%,transparent)}
.room-chip.over .rc-dot{background:#fff;box-shadow:0 0 7px rgba(255,255,255,.95)}
.room-chip.over:hover{color:#fff;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.42),
             0 6px 16px -4px color-mix(in srgb,var(--unhealthy-fill) 85%,transparent)}

/* A site with no endpoints. No metric row and no sparkline: a rank of
   em-dashes under "PM2.5 / CO2 / TEMP" reads as broken instrumentation rather
   than as an empty building, which is the opposite of the truth. */
.ds-blank{padding:1.2rem 1.35rem;display:flex;flex-direction:column;align-items:flex-start;gap:.8rem}
.ds-blank p{max-width:34ch}

/* Why a card is amber when both figures on it are green. worstStatus() weighs
   VOC and NOx Index too, and neither fits in the three-metric row — without
   this line the badge looks like it disagrees with its own card. */
.ds-why{padding:.65rem 1.35rem;font-size:.78rem;line-height:1.4;color:var(--ink-soft);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.7), inset 0 -1px 0 rgba(16,24,40,.06)}
.ds-why.b-moderate{background:color-mix(in srgb,var(--moderate-fill) 22%,#fff)}
.ds-why.b-unhealthy{background:color-mix(in srgb,var(--unhealthy-fill) 11%,#fff)}
.ds-why.b-hazardous{background:color-mix(in srgb,var(--hazardous-fill) 13%,#fff)}

.ds-outdoor{display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;
  padding:.8rem 1.35rem;
  background:linear-gradient(180deg,
    color-mix(in srgb,var(--sky) 9%,#fff),
    color-mix(in srgb,var(--brand) 13%,#fff));
  box-shadow:inset 0 1px 0 rgba(255,255,255,.85), inset 0 2px 3px -2px rgba(16,24,40,.1);
  font-family:var(--font-mono);letter-spacing:.03em;color:#2B5468}
.aqi-badge{font-family:var(--font-display);font-weight:700;font-size:.92rem;line-height:1;
  padding:.3rem .55rem;border-radius:var(--rd-xs);color:#fff;
  background:linear-gradient(180deg,
    color-mix(in srgb,var(--good-fill) 88%,#fff), var(--good-fill));
  box-shadow:inset 0 1px 0 rgba(255,255,255,.4), 0 2px 6px -2px color-mix(in srgb,var(--good-fill) 70%,transparent)}
/* Yellow needs dark text; every other band is dark enough for white. */
.aqi-badge.a-moderate{color:#4A3600;
  background:linear-gradient(180deg,color-mix(in srgb,var(--moderate-fill) 88%,#fff),var(--moderate-fill));
  box-shadow:inset 0 1px 0 rgba(255,255,255,.6), 0 2px 6px -2px color-mix(in srgb,var(--moderate-fill) 70%,transparent)}
.aqi-badge.a-usg,.aqi-badge.a-unhealthy{
  background:linear-gradient(180deg,#FF2020,var(--unhealthy));
  box-shadow:inset 0 1px 0 rgba(255,255,255,.3), 0 2px 6px -2px color-mix(in srgb,var(--unhealthy-fill) 70%,transparent)}
.aqi-badge.a-very_unhealthy,.aqi-badge.a-hazardous{
  background:linear-gradient(180deg,var(--hazardous-fill),var(--hazardous));
  box-shadow:inset 0 1px 0 rgba(255,255,255,.3), 0 2px 6px -2px color-mix(in srgb,var(--hazardous-fill) 70%,transparent)}

/* Distinct from .status.stale ("Offline"): nothing here has failed. */
.status.empty{color:var(--ink-faint)}
.status.empty .dot{background:transparent;box-shadow:inset 0 0 0 1.5px currentColor}

/* Sites with no paired station still get the row, quieted. Two reasons: it is
   the section whose absence caused the height mismatch in the first place, and
   a missing station is a real configuration gap worth seeing — a site without
   coordinates silently loses outdoor AQI and its whole outdoor history, which
   is exactly the kind of thing that goes unnoticed for months. */
.ds-outdoor.none{background:linear-gradient(180deg,rgba(248,249,250,.6),rgba(243,245,246,.8));
  color:var(--ink-faint)}
.ds-outdoor.none > .ico{color:var(--ink-faint);opacity:.5}

/* flex-basis:100% pins the concentration to its own final line instead of
   letting it wrap only when the line above happens to be long. It is the one
   number here directly comparable to the indoor PM2.5 figures further up the
   card, so where it sits must not depend on how long the station's dominant
   pollutant happens to be spelled. */
.od-pm{flex-basis:100%;margin-top:.15rem;color:#2B5468}
.od-pm b{font-weight:700;color:var(--brand)}

/* Letterhead on the monthly report — the one document that leaves the building.
   ⚠️ Uses the LIGHT mark and the light wordmark dots: this prints on white, and
   the dark-background hues are lifted specifically to survive near-black. On
   paper they read chalky. */
.report-brand{display:flex;align-items:center;gap:.4rem;justify-content:flex-end;
  margin-bottom:.45rem;font-family:var(--font-display);font-weight:700;font-size:1.12rem;
  color:var(--ink)}
.report-brand svg{width:20px;height:20px;display:block}

/* ⚠️ THE SWEEP'S viewBox IS A TIGHT BOUNDING BOX OF THE STROKED PATH — its
   round cap lands exactly on the edge, so the glyph has NO side bearing of its
   own and every pixel of clearance has to come from this margin. The global
   .16em is enough at the sidebar's 1.32rem (3.4px a side) and is not at
   letterhead size, where it fell to 1.5px and the sweep's upper tip rested
   against the I of IAQ — the mark read as "Live ЛAQ". Hence the larger side
   margin here, and only here: the sidebar and the sign-in card are already
   calibrated at their own sizes.

   The box is also restored to the path's true 1.394 aspect ratio (.82/.588).
   preserveAspectRatio="none" means the box IS the shape, so the global
   .86 x .58 was stretching the curve 6% wide — which is what tipped an
   already-tight mark into looking bent.

   top:0 rather than the global -.02em: .wm centres its items, and with
   .report-meta's line-height:1.5 that already lands the sweep's box inside the
   cap band (0.6px under the cap line, 1.8px over the baseline). The global
   nudge was calibrated against a different line-height and only pushes it out.
   The right margin is the larger of the two on purpose — "Live" ends on a
   round e, which carries its own optical space; "IAQ" opens on a flat stem,
   which carries none. */
.report-brand .wm-sep{width:.82em;height:.588em;margin:0 .26em 0 .2em;top:0}

.dash-foot{padding-top:.6rem;border-top:1px solid var(--line-faint)}

@media(max-width:640px){
  .dash-hero{padding:1.25rem 1.15rem 1.35rem}
  .dash-stats{grid-template-columns:repeat(2,1fr);flex-basis:100%}
  .dash-grid{grid-template-columns:1fr}
}

/* Blur is the most expensive thing on this page and the least necessary.
   Anyone who has asked for reduced motion is also the population most likely to
   be on a machine where a dozen backdrop-filters cost real frames, so drop them
   and keep the bevels, which cost nothing. */
@media(prefers-reduced-motion:reduce){
  .dash-site,.work-row,.dash-stat{-webkit-backdrop-filter:none;backdrop-filter:none;transition:none}
  .dash-site:hover,.work-row:hover,.dash-stat:hover:not(:disabled),.room-chip:hover{transform:none}
}

/* The dashboard is a triage screen, not a document — printing it makes no
   sense, and the interactive filters would print as dead buttons. */
@media print{ .dash-stats,.room-strip{display:none !important} }

/* ── Icon sizing ──────────────────────────────────────────────────────────
   One rule for every glyph. They are drawn on a shared 16x16 grid at a shared
   stroke weight, so the only thing a call site ever sets is the box — and
   because they all inherit currentColor, an icon is automatically the right
   band colour wherever it lands. */
.ico{width:1em;height:1em;flex:0 0 auto;display:block;overflow:visible}

/* The glyph rides the label line. It is deliberately much bigger than the
   0.59rem text it sits beside — it is the fast read, the text is the
   confirmation — and it is the only thing in the cell allowed to take the
   band's full-chroma colour. */
.dm-l{display:flex;align-items:center;gap:.42rem}
.dm-l .ico{width:1.35rem;height:1.35rem}
.dash-metric.b-good .dm-l .ico{color:var(--good-fill)}
.dash-metric.b-unhealthy .dm-l .ico{color:var(--unhealthy-fill)}
.dash-metric.b-hazardous .dm-l .ico{color:var(--hazardous-fill)}
/* Yellow-on-yellow would vanish into its own well, so this one glyph takes the
   darker text token instead of the fill. */
.dash-metric.b-moderate .dm-l .ico{color:#B08200}
/* Temperature has no BANDS entry — comfort, not a health category — so there is
   no band colour to take. It stays neutral rather than borrowing a hue it has
   not earned: a blue thermometer sitting beside a green PM2.5 would read as a
   second "this is fine" signal for a reading nothing has judged. */
.dash-metric.b-none .dm-l .ico{color:var(--ink-faint)}

.breach .ico{width:1.18rem;height:1.18rem;opacity:.9;align-self:center}

/* Stat tiles: the glyph sits in the corner as a watermark, not as a label.
   At full strength four icons across the hero would compete with the four
   numbers, which are the actual content. */
.dash-stat{position:relative}
.ds-ico{position:absolute;top:.7rem;right:.72rem;opacity:.42;
  transition:opacity .18s ease,transform .18s ease}
.ds-ico .ico{width:1.62rem;height:1.62rem}
.dash-stat:hover:not(:disabled) .ds-ico{opacity:.72;transform:scale(1.06)}
.dash-stat.on .ds-ico{opacity:.8}
.dash-stat:disabled .ds-ico{opacity:.16}

.ds-outdoor > .ico{width:1.5rem;height:1.5rem;color:var(--brand);opacity:.95}

.dh-clock{display:inline-flex;align-items:center;gap:.4rem}
.dh-clock .ico{width:1rem;height:1rem;opacity:.65}

.work-when{display:inline-flex;align-items:center;gap:.35rem;white-space:nowrap}
.work-when .ico{width:1.05rem;height:1.05rem;opacity:.65}

/* ── Density toolbar ──────────────────────────────────────────────────────
   Explicit, sticky, remembered. Deliberately a control and not a hover
   behaviour — see the comment on DENSITY_KEY in app.js for why hover-to-expand
   was rejected. */
.dash-toolbar{display:flex;justify-content:space-between;align-items:center;gap:1rem;
  flex-wrap:wrap;margin-bottom:1.5rem;padding-bottom:.7rem;
  border-bottom:1px solid var(--line-faint)}
.density{display:inline-flex;padding:2px;border-radius:9px;
  background:rgba(16,24,40,.06);box-shadow:inset 0 1px 2px rgba(16,24,40,.09)}
.density button{font:inherit;font-family:var(--font-mono);font-size:.64rem;font-weight:600;
  letter-spacing:.09em;text-transform:uppercase;cursor:pointer;
  padding:.4rem .85rem;border:none;border-radius:7px;background:transparent;color:var(--ink-soft);
  transition:background .16s ease,color .16s ease,box-shadow .16s ease}
.density button:hover{color:var(--ink)}
/* The eye. Sits INSIDE the trough alongside the two segments so it reads as
   part of the control rather than as a loose decoration next to it, and is held
   at the muted ink weight — it labels the control, it is not a third option and
   must never look pressable. */
.density-eye{display:inline-flex;align-items:center;padding:0 .5rem 0 .55rem;color:var(--ink-faint)}
.density-eye svg{display:block}
/* The selected segment lifts out of the trough — the same recessed-track,
   raised-thumb idiom as the metric wells, so it reads as switched rather than
   merely highlighted. */
.density button.on{background:var(--panel);color:var(--brand);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.9), 0 1px 2px rgba(16,24,40,.18)}

/* ── Compact rows ─────────────────────────────────────────────────────────
   Fixed column widths, not fractions. A dense list is read down a column, and
   a figure whose x-position drifts with the length of the site's name gives
   back everything density was bought for. */
.crow-list{display:flex;flex-direction:column;gap:.4rem}
.crow{position:relative;display:grid;align-items:center;gap:1rem;
  grid-template-columns:2.1rem minmax(11rem,24rem) auto minmax(4.6rem,1fr) 7rem 6.6rem 5.5rem 2.6rem 1rem;
  padding:.65rem 1rem .65rem 1.3rem;border-radius:10px;
  background:linear-gradient(180deg,rgba(255,255,255,.94),rgba(255,255,255,.8));
  -webkit-backdrop-filter:blur(14px) saturate(160%);backdrop-filter:blur(14px) saturate(160%);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.9),
             inset 0 0 0 1px rgba(16,24,40,.07),
             0 1px 2px rgba(16,24,40,.045);
  transition:transform .14s cubic-bezier(.2,.7,.3,1),box-shadow .16s ease}
.crow:hover{transform:translateX(3px);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.96),
             inset 0 0 0 1px rgba(16,24,40,.1),
             0 8px 18px -10px rgba(16,24,40,.4)}
/* Same rail and same hues as the detailed card, so switching density never
   changes what a colour means. */
.crow::before{content:'';position:absolute;left:0;top:8px;bottom:8px;width:3px;
  border-radius:0 3px 3px 0;background:var(--rail,#B9C0BC);
  box-shadow:0 0 10px color-mix(in srgb,var(--rail,transparent) 50%,transparent)}
.crow.s-good{--rail:var(--good-fill)}
.crow.s-moderate{--rail:var(--moderate-fill)}
.crow.s-unhealthy{--rail:var(--unhealthy-fill)}
.crow.s-hazardous{--rail:var(--hazardous-fill)}
.crow.over{--rail:var(--unhealthy-fill);background:linear-gradient(180deg,
  color-mix(in srgb,var(--unhealthy-fill) 8%,#fff),
  color-mix(in srgb,var(--unhealthy-fill) 4%,#fff))}

.crow-name{display:flex;flex-direction:column;min-width:0;color:var(--ink);line-height:1.25}
/* The site name is the row's anchor — it is what someone scans the list FOR,
   and at 0.98rem it sat only fractionally above the 0.7rem meta line beneath
   it, so the two read as one block of small text. Widening the gap between the
   two sizes is what makes the name findable at a glance. */
.crow-name b{font-family:var(--font-display);font-size:clamp(1.02rem,.72rem + .82vw,1.32rem);
  letter-spacing:-.025em;line-height:1.18}
.crow-name b.is-place{align-items:flex-start}
.crow-name b.is-place > .ico{margin-top:.1em}
/* Wraps to two lines rather than truncating. On a dense list an ellipsis is
   worse than a taller row: "Spokane Urgent Care Val…" and "Spokane Urgent Care
   Nor…" are the same string to a reader scanning quickly. */
.crow-name b.is-place > span{white-space:normal;overflow:hidden;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
.crow-name span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:.66rem;
  letter-spacing:.02em;opacity:.85}
.crow:hover .crow-name b{color:var(--brand)}

/* Room dots keep the one-click drill-down that the cards' chips provide. Capped
   at eight plus a "+N", so a large site cannot push the metric columns out of
   alignment for every other row. */
.crow-dots{display:flex;align-items:center;gap:.3rem;flex-wrap:nowrap}
.rdot{width:10px;height:10px;border-radius:999px;background:var(--good-fill);
  box-shadow:0 0 6px color-mix(in srgb,var(--good-fill) 80%,transparent);
  transition:transform .12s ease}
.rdot:hover{transform:scale(1.45)}
.rdot.s-moderate{background:var(--moderate-fill);box-shadow:0 0 6px color-mix(in srgb,var(--moderate-fill) 80%,transparent)}
.rdot.s-unhealthy{background:var(--unhealthy-fill);box-shadow:0 0 6px color-mix(in srgb,var(--unhealthy-fill) 80%,transparent)}
.rdot.s-hazardous{background:var(--hazardous-fill);box-shadow:0 0 6px color-mix(in srgb,var(--hazardous-fill) 80%,transparent)}
/* Hollow and unlit, exactly as in the detailed view: "no data" and "data that
   happens to be grey" must not look alike at any density. */
.rdot.s-stale{background:transparent;box-shadow:inset 0 0 0 1.5px var(--ink-faint)}
.rdot.over{box-shadow:0 0 0 2px #fff, 0 0 0 3.5px var(--unhealthy-fill)}
.rdot-more{margin-left:.15rem}

.crow-flag{display:flex;justify-content:flex-start;padding-left:.35rem}
.crow-over{font-family:var(--font-mono);font-size:.6rem;font-weight:700;letter-spacing:.08em;
  text-transform:uppercase;color:#fff;padding:.2rem .45rem;border-radius:999px;
  background:linear-gradient(180deg,#FF2020,var(--unhealthy));
  box-shadow:inset 0 1px 0 rgba(255,255,255,.3)}

.crow-m{display:flex;align-items:baseline;gap:.35rem;font-family:var(--font-mono);
  white-space:nowrap;color:var(--ink-faint)}
.crow-m .ico{width:1.05rem;height:1.05rem;align-self:center;color:var(--ink-faint)}
.crow-m b{font-family:var(--font-display);font-size:1.02rem;font-weight:700;letter-spacing:-.02em}
.crow-m i{font-style:normal;font-size:.6rem;opacity:.7}
.crow-m.b-good b,.crow-m.b-good .ico{color:var(--good)}
.crow-m.b-good .ico{color:var(--good-fill)}
.crow-m.b-unhealthy b{color:var(--unhealthy)} .crow-m.b-unhealthy .ico{color:var(--unhealthy-fill)}
.crow-m.b-hazardous b{color:var(--hazardous)} .crow-m.b-hazardous .ico{color:var(--hazardous-fill)}
.crow-m.b-moderate b{color:#8A6500} .crow-m.b-moderate .ico{color:#B08200}
.crow-m.b-none b{color:var(--ink)}

.crow-spark{display:block;min-width:0}
.crow-spark .dash-spark{height:26px;width:100%}
.crow-aqi{display:flex;justify-content:flex-end}
.crow-aqi .aqi-badge{font-size:.76rem;padding:.22rem .4rem}
.crow-go{color:var(--ink-faint);text-align:right;transition:color .14s linear,transform .14s ease}
.crow:hover .crow-go{color:var(--brand);transform:translateX(2px)}

/* Below this the eight columns stop being scannable and start being a squeeze.
   Drop the ornamental ones first — trend and outdoor are context, the name,
   the breach flag and the two live figures are the answer. */
@media(max-width:1180px){
  .crow{grid-template-columns:2.1rem minmax(11rem,24rem) auto minmax(4.6rem,1fr) 7rem 6.6rem 2.6rem 1rem}
  .crow-spark{display:none}
}
@media(max-width:900px){
  .crow{grid-template-columns:minmax(0,1fr) auto auto 1rem;row-gap:.45rem}
  .crow-m,.crow-aqi{display:none}
}
@media(prefers-reduced-motion:reduce){
  .crow{-webkit-backdrop-filter:none;backdrop-filter:none;transition:none}
  .crow:hover{transform:none}
  .rdot:hover{transform:none}
}

/* ── Place vs device ──────────────────────────────────────────────────────
   Two object types run through this whole screen — sites (places, containers)
   and endpoints (devices, contents) — and they were rendering at the same
   weight. Worse, the hierarchy inverted between views: on a card the big bold
   name is the site, in the worklist the big bold name is the endpoint and the
   site is the muted line beneath it.

   Three signals now separate them, and they always agree:
     glyph   building vs sensor
     shape   squared containers vs fully rounded pills and dots
     face    display for places, body for devices

   The shape rule is the one that works peripherally: if it has a corner it is
   somewhere you can stand in; if it is a pill or a dot it is a thing bolted to
   a wall. */
.is-place,.is-device{display:inline-flex;align-items:center;gap:.42rem;min-width:0}
.is-place > span,.is-device > span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.is-place > .ico,.is-device > .ico{flex:0 0 auto}

/* Places take the brand blue: it is the app's structural colour, used for
   navigation and containers, and it is NOT one of the four status hues — so a
   site glyph can never be mistaken for a reading. */
.is-place > .ico{color:var(--brand);opacity:.85}
/* Devices stay neutral. Their status is already carried by the rail, the pill
   and the dot beside them; colouring the glyph too would be a fourth copy of
   the same fact competing with the figures that are not redundant. */
.is-device > .ico{color:var(--ink-faint)}

/* Card title: the glyph sits with the name, sized to the display type. */
.ds-title h3.is-place > .ico{width:1.15rem;height:1.15rem}
.ds-head:hover h3.is-place > span{color:var(--brand)}

.crow-name b.is-place > .ico{width:1.3rem;height:1.3rem}
.crow:hover .crow-name b.is-place > span{color:var(--brand)}

.work-where b.is-device > .ico{width:1.05rem;height:1.05rem}
.work-where .is-place > .ico{width:.85rem;height:.85rem;opacity:.7}

/* ── Whole-card / whole-row drill-down ────────────────────────────────────
   The header anchor was the only click target, so most of a 360px card was
   dead. It is now a "stretched link": the existing <a> grows an invisible
   ::after covering its whole positioned ancestor.

   ⚠️ WHY NOT JUST WRAP THE CARD IN AN <a>: it already contains other links —
   one per room chip, plus the outdoor strip and the provision button. Nesting
   anchors is invalid HTML and browsers silently un-nest it, which would break
   the room drill-down that is the whole point of the chips.

   The stretched overlay sits at z-index 1; anything that must stay
   independently clickable is lifted to 2. Order matters: the overlay is
   declared first so a later rule cannot accidentally sink a chip beneath it. */
.dash-site,.crow{cursor:pointer}
.ds-head::after,
/* ⚠️ THIS IS A STRETCHED LINK: it projects the site anchor over the WHOLE row
   so the entire panel is one click target. Anything else that must be
   clickable in this row therefore has to be lifted ABOVE it — see .crow-wx.
   Adding an interactive control to .crow without doing that produces a control
   that renders, hovers, and cannot be clicked at all. */
.crow-name::after{content:'';position:absolute;inset:0;z-index:1}

/* Above the overlay: their own destinations, not the card's. */
.room-strip a,
.ds-outdoor,
.ds-blank a,
.crow-dots a,
.crow-go{position:relative;z-index:2}

/* The status rails are decoration painted over the card; without this they
   punch a dead strip down the left edge of the click target. */
.dash-site::before,.crow::before,.dash-hero::before,.dash-hero::after{pointer-events:none}

/* Keyboard parity: the stretched link is still just a link, so it focuses
   normally — but the ring would draw around the collapsed <a> rather than the
   card. Move it to the card so what is highlighted is what will be activated. */
.ds-head:focus-visible,.crow-name:focus-visible{outline:none}
.dash-site:has(.ds-head:focus-visible),
.crow:has(.crow-name:focus-visible){outline:2px solid var(--accent-text);outline-offset:2px}

/* ── Endpoint cards read as DEVICES ───────────────────────────────────────
   Same rule as the dashboard: places are square-cornered containers, devices
   are rounded. These cards sit in a grid directly under a site's own heading,
   so without a shape difference the two object types were indistinguishable
   at a glance — which is exactly the confusion the dashboard fixed. */
.ep-card.is-device-card{border-radius:12px;position:relative}

/* ── Device health dot ─────────────────────────────────────────────────────
   IS THE NODE TALKING — not "is the air good". Painted from the ADMIN ACCENT
   palette, never the status ladder, for the reason given at the top of this
   file: green there means "this air is fine", and a green dot on a card that
   also carries an air-quality pill would answer a question nobody asked.

   The two can legitimately disagree. A node that dropped off an hour ago still
   shows its last reading, so "offline" and "Good" appear together and are BOTH
   correct — with ladder colours that reads as a contradiction, with these it
   reads as two facts.

   --sec-nodes is already the hardware hue in this system. --sec-alarms is the
   one vivid colour that can never be mistaken for a reading, because the EPA
   ladder dropped its orange rung. Grey stays the neutral "not a peer" value.
   Cyan/orange also survives red-green colour blindness, which green/red does
   not.

   Square, at the system's 2px radius — a circle would be the only round shape
   on the page. */
.ep-health{position:absolute;top:.9rem;right:.9rem;width:10px;height:10px;border-radius:var(--r)}
.ep-health.up{background:var(--sec-nodes)}
.ep-health.down{background:var(--sec-alarms)}
/* Hollow, not filled: "never reported" is an absence, and a solid grey dot
   reads as a third live state rather than as nothing having happened yet. */
.ep-health.never{background:transparent;box-shadow:inset 0 0 0 2px var(--sec-console)}

/* Legend. Required, not optional: cyan and orange are the right colours here
   for the reasons above, but unlike red/green they are not self-explanatory, so
   the palette has to come with its key. Each card's dot also carries a title
   attribute, so the meaning is available at the indicator itself as well. */
.ep-legend{display:flex;flex-wrap:wrap;gap:1.1rem;align-items:center;margin:0 0 1rem;
  font-family:var(--font-mono);font-size:.7rem;letter-spacing:.04em;text-transform:uppercase;
  color:var(--ink-faint)}
.ep-legend-item{display:inline-flex;align-items:center;gap:.4rem}
/* The dot is absolutely positioned on a card; in the legend it sits inline. */
.ep-legend .ep-health{position:static;width:9px;height:9px;flex:none}
.ep-legend b{font-family:var(--font-body);font-weight:700;color:var(--ink)}
/* A zero count is still worth showing — "0 offline" is the reassuring case and
   hiding it would make the legend's length change as the fleet changes. Dimmed
   rather than removed. */
.ep-legend-item.is-zero{opacity:.45}
.ep-card.is-device-card .name.is-device{display:flex;align-items:center;gap:.45rem}
.ep-card.is-device-card .name.is-device > .ico{width:1.15rem;height:1.15rem;
  flex:0 0 auto;color:var(--ink-faint)}
.ep-card.is-device-card .loc.is-place{display:flex;align-items:center;gap:.4rem}
.ep-card.is-device-card .loc.is-place > .ico{width:.9rem;height:.9rem;
  flex:0 0 auto;color:var(--brand);opacity:.8}

/* Page titles carry the same distinction, so you can tell at a glance whether
   you are looking at a building or a sensor. */
h1.page.is-place,h1.page.is-device{display:flex;align-items:center;gap:.6rem}
h1.page.is-place > .ico,h1.page.is-device > .ico{width:1.7rem;height:1.7rem;flex:0 0 auto}
h1.page.is-place > .ico{color:var(--brand);opacity:.9}
h1.page.is-device > .ico{color:var(--ink-faint)}

/* ── deployed-hardware model ────────────────────────────────────────────── */
/* ⚠️ DELIBERATELY QUIETER THAN THE STATUS PILL. These sit side by side on every
   endpoint card, and status is the thing being scanned for — a model chip with
   equal weight would compete with it for the eye on a screen whose whole job is
   triage. Neutral fill, no colour, smaller glyph. */
.card-chips { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
.model-chip {
  display: inline-flex; align-items: center; gap: .3rem;
  padding: .12rem .45rem; border-radius: 999px;
  font-size: .72rem; font-weight: 600; letter-spacing: .01em;
  color: var(--muted); background: var(--chip-bg, rgba(127, 134, 130, .12));
  border: 1px solid rgba(127, 134, 130, .22);
  white-space: nowrap;
}
.model-chip svg { width: 11px; height: 11px; flex: none; opacity: .8; }
/* Unspecified is styled as ABSENCE, not as an error — nobody has said yet is a
   normal state during provisioning, and an alarm colour here would cry wolf. */
.model-chip.unknown { opacity: .62; font-style: italic; font-weight: 500; }
.model-chip.superseded { border-style: dashed; }

.hw-card { margin: 1rem 0 0; padding: .85rem 1rem; }
.hw-head { display: flex; align-items: center; gap: .45rem; margin-bottom: .55rem; }
.hw-head svg { width: 15px; height: 15px; flex: none; opacity: .75; }
.hw-rows {
  display: grid; grid-template-columns: max-content 1fr;
  gap: .28rem .9rem; margin: 0;
}
.hw-rows dt { color: var(--muted); font-size: .78rem; }
.hw-rows dd { margin: 0; font-size: .85rem; }
.hw-note { margin: .6rem 0 0; }
.hw-card.unknown { display: flex; gap: .6rem; align-items: flex-start; }
.hw-card.unknown svg { width: 16px; height: 16px; flex: none; opacity: .6; margin-top: .15rem; }
.hw-card.unknown p { margin: .2rem 0 0; }

@media (max-width: 520px) {
  .hw-rows { grid-template-columns: 1fr; gap: .1rem; }
  .hw-rows dd { margin-bottom: .45rem; }
}

/* The Aria mark inline beside a model name. Sized in em so it tracks whatever
   type it sits next to.

   ⚠️ LARGER THAN A NORMAL UI GLYPH, because the FRAMED cut spends most of its
   box on the frame: 9.5% padding plus a 5.2% rule, with the letter shrunk to
   0.74 inside that. The A ends up about half the width of the icon, so sizing
   it like the other 1em glyphs left an 8 px letter that read as a smudge. */
.model-chip.aria svg { width: 1.45em; height: 1.45em; opacity: 1; margin: -.25em 0; }
.hw-model { display: flex; align-items: center; gap: .45rem; }
.hw-model svg { width: 1.7em; height: 1.7em; flex: none; margin: -.3em 0; }

/* Endorsement line under the sidebar wordmark.
   ⚠️ Sized and coloured to ATTRIBUTE, not to compete — LiveIAQ is the portal
   the viewer signed in to, Aria Sense is the hardware behind the numbers, and
   equal weight would read as a co-brand. Block-level so it drops to its own
   line under the wordmark regardless of which brand variant is showing. */
/* ⚠️ flex-basis, not just display:block. .brandmark is a FLEX container, so a
   block child is still laid out as a flex item and sits BESIDE the wordmark —
   which is where this first landed, overflowing the sidebar. Forcing a full-row
   basis (with wrap on the parent) is what actually drops it to its own line. */
.brandmark { flex-wrap: wrap; }
.brand-endorse {
  display: block;
  flex: 0 0 100%;
  margin-top: .18rem;
  font-size: .58rem;
  font-weight: 600;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, .38);
  white-space: nowrap;
}
.brandmark.mark-only .brand-endorse { margin-top: .3rem; }

/* ── Login log ───────────────────────────────────────────────────────────────
   The sign-in audit trail (Admin -> Login log). Its job is triage, so the
   styling is built around one question per row: is this person fine, or do I
   need to do something for them.

   ⚠️ Status is never carried by colour ALONE. Every state also has a word in the
   cell ("Failed", "Locked — too many attempts"), because the reader is often
   reading it aloud to the person on the phone, and because red/green alone
   fails for the ~8% of men with a colour-vision deficiency. The colour is
   redundant emphasis, never the message. */

/* The verdict column — the single next action for that member. */
.verdict{display:inline-block;font-size:.78rem;line-height:1.35}
.verdict.ok{color:var(--good)}
.verdict.warn{color:var(--moderate)}
.verdict.bad{color:var(--danger);font-weight:600}
.verdict.muted{color:var(--ink-faint)}

/* Pass/fail on an individual event. Pill-shaped so a column of them scans as a
   column rather than as prose. */
.outcome{display:inline-block;padding:.12rem .45rem;border-radius:999px;
  font-family:var(--font-mono);font-size:.64rem;letter-spacing:.06em;text-transform:uppercase;font-weight:700}
.outcome.pass{background:rgba(43,122,74,.14);color:var(--good)}
.outcome.fail{background:rgba(178,58,46,.14);color:var(--danger)}

/* An attempt against an address that has no account. This is the tag that turns
   "my login is broken" into "you are typing the wrong address", so it is marked
   inline next to the address rather than buried in the reason column. */
.tag-unknown{display:inline-block;margin-left:.4rem;padding:.1rem .4rem;border-radius:var(--r);
  background:rgba(178,58,46,.1);border:1px solid rgba(178,58,46,.25);color:var(--danger);
  font-family:var(--font-mono);font-size:.6rem;letter-spacing:.05em;text-transform:uppercase}

/* Failure counts, so a non-zero number reads as a number worth looking at. */
.rtable td.num.bad-num{color:var(--danger);font-weight:700}

/* The member currently filtering the event list below. */
.rtable tr.row-focus td{background:rgba(93,200,236,.08)}
.rtable tr.row-focus td:first-child{box-shadow:inset 3px 0 0 var(--brand)}

.rtable td.nowrap{white-space:nowrap}

/* ══ Weather drawer ═══════════════════════════════════════════════════════
   Opens OVER the screen you were on, because wind is context for the reading
   you are already looking at. Right-hand sheet rather than a modal box: it can
   be tall (24 hours plus a 7-day) without becoming a scrolling window inside a
   scrolling window, and it leaves the site's own chart visible beside it.

   ⚠️ THE PANEL IS DARK AND THE APP IS LIGHT, DELIBERATELY. An overlay that
   matches the page it covers reads as part of it, and then nothing tells you
   why the page stopped responding to clicks. The contrast IS the affordance. */
#wx-drawer{position:fixed;inset:0;z-index:80}
.wx-scrim{position:absolute;inset:0;background:rgba(16,20,18,.55);
  -webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);
  animation:wx-fade .16s ease}
/* ⚠️ A 3/4-SCREEN CENTRED SHEET, NOT A 30rem EDGE DRAWER. The edge drawer was
   too narrow for what this now carries: NWS writes multi-paragraph narratives
   per period, and at 30rem the forecast became a column of two-word lines with
   the 24-hour strip scrolling sideways underneath. Width is what makes a
   forecast readable. Centred rather than right-anchored because it is now the
   subject, not a side panel. */
.wx-panel{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
  width:min(76vw,64rem);height:min(86vh,54rem);border-radius:12px;overflow:hidden;
  display:flex;flex-direction:column;background:#1B1F1D;color:#F2F5F3;
  box-shadow:0 30px 80px -30px rgba(0,0,0,.85);animation:wx-pop .2s cubic-bezier(.2,.7,.3,1)}
@keyframes wx-fade{from{opacity:0}to{opacity:1}}
@keyframes wx-pop{from{transform:translate(-50%,-48%) scale(.985);opacity:.5}
  to{transform:translate(-50%,-50%) scale(1);opacity:1}}
.wx-head{display:flex;align-items:flex-start;justify-content:space-between;gap:1rem;
  padding:1.1rem 1.2rem .9rem;border-bottom:1px solid rgba(255,255,255,.12)}
.wx-eyebrow{margin:0;font-family:var(--font-mono);font-size:.64rem;letter-spacing:.12em;
  text-transform:uppercase;color:rgba(255,255,255,.5)}
.wx-title{margin:.15rem 0 0;font-family:var(--font-display);font-size:1.25rem;line-height:1.15}
.wx-close{background:none;border:0;color:rgba(255,255,255,.6);font-size:1.7rem;line-height:1;
  cursor:pointer;padding:0 .2rem}
.wx-close:hover{color:#fff}
.wx-body{flex:1;overflow-y:auto;padding:1.1rem 1.4rem 2rem}
/* Days sit two-up once there is room; the narrative is what needs the width. */
@media (min-width:900px){ .wx-days{grid-template-columns:1fr 1fr} }
.wx-loading,.wx-empty{color:rgba(255,255,255,.6);font-size:.9rem;padding:1rem 0}

/* Alerts lead. Red edge, because an NWS alert is the one thing on this panel
   that may require an action today. */
/* ⚠️ THE THREE NWS LEVELS GET THREE TREATMENTS. Same ladder as the fire chips:
   red is act-now, amber is be-ready, and the lowest rung is deliberately quiet
   so that a routine advisory never looks like a warning at a glance. */
.wx-alert{border-left:3px solid;border-radius:6px;padding:.75rem .9rem;margin-bottom:.9rem}
.wx-alert.k-warning{border-color:var(--unhealthy-fill);background:rgba(230,0,0,.14)}
.wx-alert.k-watch{border-color:var(--moderate-fill);background:rgba(255,196,46,.14)}
/* ⚠️ TWO COLOURS, NOT THREE: red for warnings, amber for BOTH watches and
   advisories. That is the operational split — a warning means act today, and
   everything below it means pay attention — and it matches how NWS products are
   actually triaged. The two amber levels stay distinguishable by their pill and
   their caution line, not by a third hue nobody can rank at a glance. Advisory
   is a shade softer so the ordering still reads. */
.wx-alert.k-advisory{border-color:var(--moderate);background:rgba(255,196,46,.09)}
.wxa-kind{font-family:var(--font-mono);font-size:.58rem;font-weight:700;letter-spacing:.1em;
  text-transform:uppercase;white-space:nowrap;padding:.15rem .4rem;border-radius:2px}
.k-warning .wxa-kind{background:var(--unhealthy-fill);color:#fff}
.k-watch .wxa-kind{background:var(--moderate-fill);color:#241c00}
.k-advisory .wxa-kind{background:var(--moderate);color:#241c00}
.wxa-caution{margin:.35rem 0 0;font-size:.8rem;line-height:1.45;font-weight:600;
  color:rgba(255,255,255,.92)}
.wxa-when{display:flex;flex-wrap:wrap;gap:.15rem .8rem;margin-top:.25rem;
  font-family:var(--font-mono);font-size:.62rem;color:rgba(255,255,255,.55)}
.wxa-area{margin:.3rem 0 0;font-size:.72rem;color:rgba(255,255,255,.55)}
.wxa-do{margin-top:.55rem;padding-top:.5rem;border-top:1px solid rgba(255,255,255,.16)}
.wxa-do b{display:block;font-family:var(--font-mono);font-size:.6rem;letter-spacing:.1em;
  text-transform:uppercase;color:rgba(255,255,255,.7)}
.wxa-do p{margin:.25rem 0 0;font-size:.8rem;line-height:1.5;color:rgba(255,255,255,.9)}
.wx-noalert{padding:.65rem .8rem;margin-bottom:.9rem;border-radius:6px;font-size:.82rem;
  color:rgba(255,255,255,.6);background:rgba(255,255,255,.05);
  border-left:3px solid rgba(255,255,255,.2)}
.wxa-top{display:flex;align-items:baseline;justify-content:space-between;gap:.6rem}
.wxa-event{font-family:var(--font-display);font-weight:700;font-size:1rem}
.wxa-ends{font-family:var(--font-mono);font-size:.66rem;color:rgba(255,255,255,.6);white-space:nowrap}
.wxa-head{margin:.3rem 0 0;font-size:.84rem;color:rgba(255,255,255,.92)}
.wxa-desc{margin:.4rem 0 0;font-size:.78rem;line-height:1.5;color:rgba(255,255,255,.72);
  white-space:pre-line}
.wxa-src{margin:.35rem 0 0;font-family:var(--font-mono);font-size:.62rem;letter-spacing:.06em;
  text-transform:uppercase;color:rgba(255,255,255,.45)}

.wx-now{display:flex;align-items:center;gap:1.1rem;flex-wrap:wrap;
  padding:.85rem 0 1rem;border-bottom:1px solid rgba(255,255,255,.1)}
.wxn-main{display:flex;flex-direction:column}
.wxn-temp{font-family:var(--font-display);font-size:2.4rem;font-weight:700;line-height:1}
.wxn-short{font-size:.82rem;color:rgba(255,255,255,.7)}
.wxn-wind,.wxn-rh{display:flex;align-items:center;gap:.5rem}
.wxn-wind b,.wxn-rh b{display:block;font-family:var(--font-display);font-size:1rem}
.wxn-wind .tiny,.wxn-rh .tiny{color:rgba(255,255,255,.5)}

/* The arrow is a rotated glyph rather than an icon set: one character, no
   sprite, and it can be rotated to any of the 16 compass points exactly. */
.wx-arrow{display:inline-block;font-size:1rem;line-height:1;color:var(--sky);
  transform-origin:50% 50%}
.wx-arrow.big{font-size:1.7rem}
.wx-arrow-na{color:rgba(255,255,255,.35)}

.wx-smoke{margin:.9rem 0;padding:.65rem .8rem;border-radius:6px;font-size:.82rem;line-height:1.45;
  background:rgba(255,196,46,.14);border-left:3px solid var(--moderate-fill);color:rgba(255,255,255,.9)}
.wx-shifts{margin:.9rem 0;padding:.7rem .85rem;border-radius:6px;
  background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12)}
.wxs-title{font-family:var(--font-display);font-weight:700;font-size:.95rem}
.wx-shifts .tiny{color:rgba(255,255,255,.55)}
.wx-shifts ul{margin:.5rem 0 0;padding-left:1.1rem;font-size:.82rem;line-height:1.6}

.wx-sec{margin:1.2rem 0 .5rem;font-family:var(--font-mono);font-size:.64rem;letter-spacing:.12em;
  text-transform:uppercase;color:rgba(255,255,255,.5)}
.wx-hours{display:flex;gap:.3rem;overflow-x:auto;padding-bottom:.4rem;
  scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.25) transparent}
.wx-hcell{flex:0 0 auto;display:flex;flex-direction:column;align-items:center;gap:.15rem;
  min-width:3rem;padding:.45rem .3rem;border-radius:6px;background:rgba(255,255,255,.05)}
.wx-hcell.is-smoky{background:rgba(255,196,46,.16)}
.wxh-t{font-family:var(--font-mono);font-size:.62rem;color:rgba(255,255,255,.55)}
.wxh-temp{font-family:var(--font-display);font-weight:700;font-size:.92rem}
.wxh-wind{font-family:var(--font-mono);font-size:.66rem;color:rgba(255,255,255,.75)}
.wxh-dir{font-family:var(--font-mono);font-size:.58rem;color:rgba(255,255,255,.45)}

.wx-days{display:grid;gap:.4rem;grid-template-columns:1fr}
.wx-day{display:grid;grid-template-columns:6.5rem 3rem 1fr;align-items:center;gap:.6rem;
  padding:.55rem .7rem;border-radius:6px;background:rgba(255,255,255,.05)}
.wx-day.is-smoky{background:rgba(255,196,46,.14)}
.wxd-name{font-family:var(--font-display);font-weight:700;font-size:.85rem}
.wxd-temp{font-family:var(--font-display);font-weight:700;font-size:1.05rem;text-align:right}
.wxd-wind{display:flex;align-items:center;gap:.35rem;font-family:var(--font-mono);font-size:.66rem;
  color:rgba(255,255,255,.7);grid-column:3}
.wxd-short{grid-column:1/-1;font-size:.76rem;color:rgba(255,255,255,.62)}
/* The forecaster's own narrative. Full width, generous leading — this is the
   thing being read, not a caption under the numbers. */
.wxd-detail{grid-column:1/-1;margin:.35rem 0 0;font-size:.8rem;line-height:1.55;
  color:rgba(255,255,255,.82)}
.wx-week{margin-top:.9rem}
.wx-week summary{cursor:pointer;font-family:var(--font-mono);font-size:.66rem;letter-spacing:.1em;
  text-transform:uppercase;color:rgba(255,255,255,.55);margin-bottom:.5rem}
.wx-src{display:flex;flex-wrap:wrap;align-items:center;gap:.6rem;
  margin-top:1.2rem;padding-top:.7rem;border-top:1px solid rgba(255,255,255,.1);
  font-family:var(--font-mono);font-size:.62rem;letter-spacing:.06em;color:rgba(255,255,255,.42)}
.wx-out{margin-left:auto;color:var(--sky);text-decoration:none;white-space:nowrap}
.wx-out:hover{color:#fff}

/* The Wx affordance on a dashboard row. Quiet until hovered — it sits beside
   the arrow that opens the site, and must not compete with it. */
/* A peer of the metric glyphs, not a button that wandered in: same 1.05rem
   icon, same --ink-faint resting colour, no chrome until you reach for it. */
/* ⚠️ z-index 2 IS LOAD-BEARING, NOT POLISH. The row's stretched link sits at
   z-index 1 and covers every pixel of the row including this button, so
   without lifting above it the glyph was unclickable — every click fell to the
   site link and simply opened the site. That was the reported bug, and no
   amount of handler rebinding or delegation could have fixed it, because the
   event never reached the button in the first place. */
/* ⚠️ COLOURED AT REST, NOT ONLY ON HOVER. In --ink-faint it read as part of the
   site name's own block — grey text beside dark text — and nothing said it was
   a separate control. The weather hue is what separates them, and it is the
   same hue the panel it opens uses for its wind arrows and its button, so the
   colour is a promise about where the click goes. */
.crow-wx{position:relative;z-index:2;
  display:flex;flex-direction:column;align-items:center;gap:.05rem;padding:.2rem .15rem;
  background:none;border:0;border-radius:3px;cursor:pointer;color:var(--sky-ink);
  transition:color .12s ease,background .12s ease}
.crow-wx .ico{width:1.05rem;height:1.05rem;display:block;color:var(--sky)}
/* ⚠️ THE WORD IS PART OF THE TARGET, NOT DECORATION. A bare 1rem glyph is a
   ~17px hit area sitting one grid gap from an anchor that navigates away — the
   miss lands on the site link, which is exactly the failure that got reported.
   The label roughly triples the target and says what it does. */
.crow-wx .wxb-label{font-family:var(--font-mono);font-size:.58rem;font-weight:700;
  letter-spacing:.06em;line-height:1}
.crow-wx:hover{color:var(--sky-ink);background:rgba(15,168,214,.14)}
.crow-wx:focus-visible{outline:2px solid var(--sky);outline-offset:1px}
@media (max-width:900px){
  .wx-panel{width:100vw;height:100vh;border-radius:0;top:0;left:0;transform:none}
  @keyframes wx-pop{from{opacity:.4}to{opacity:1}}
}


/* Site-page control: same action, page-level affordance. */
/* ⚠️ TEAL, NOT GREEN, AND THIS IS A DELIBERATE OVERRIDE OF THE REQUEST.
   Green in this app is load-bearing: it is the bottom rung of the air-quality
   ladder, and it appears a few centimetres above this button in the building
   status. A green control on a site page reads as a claim that the site is
   GOOD — the one thing a button must never accidentally assert. --sky is
   already the weather colour here: the wind arrows in the panel and the row
   glyph's hover both use it, so this makes the whole feature one hue instead of
   introducing a fourth. It is still unmistakably a cool "weather" green-blue,
   which is what the request was reaching for. */
.btn.wx-open{display:inline-flex;align-items:center;gap:.5rem;margin-bottom:1.25rem;
  color:var(--sky);border-color:var(--sky);background:rgba(15,168,214,.08)}
.btn.wx-open:hover{color:#fff;background:var(--sky);border-color:var(--sky)}
.btn.wx-open .ico{width:1.05rem;height:1.05rem}
/* Fire chips are links now — keep them looking like chips, not like text links. */
a.cond-fire{text-decoration:none;color:inherit;transition:border-color .12s ease,transform .12s ease}
a.cond-fire:hover{transform:translateY(-1px);border-color:var(--brand)}
.cond-fire .cf-ext{margin-left:.3rem;font-size:.78em;opacity:.55}
a.cond-fire:hover .cf-ext{opacity:1}


/* ── Forecast graphics ───────────────────────────────────────────────────
   Added because the panel had become a wall of prose. The glance is now
   carried by shape and colour — a sky glyph, a temperature line, a precip
   bar — and the forecaster's narrative sits behind a disclosure for the
   follow-up question. Text is the backup, not the interface. */
.wxh-sky .ico{width:1rem;height:1rem;display:block;color:rgba(255,255,255,.72)}
.wx-hcell.is-smoky .wxh-sky .ico{color:var(--moderate-fill)}
.wxh-pop{display:flex;align-items:flex-end;height:14px}
.wxh-pop i{display:block;width:4px;border-radius:1px;background:var(--sky);min-height:1px;opacity:.85}
.wx-trace-wrap{position:relative;color:var(--sky);margin:.1rem 0 .35rem}
.wx-trace{width:100%;height:26px;display:block}
.wx-trace-ends{display:flex;justify-content:space-between;font-family:var(--font-mono);
  font-size:.58rem;color:rgba(255,255,255,.45);margin-top:-.15rem}
.wxd-sky{display:inline-flex;vertical-align:-.2em;margin-right:.35rem}
.wxd-sky .ico{width:1rem;height:1rem;color:rgba(255,255,255,.72)}
.wx-day.is-smoky .wxd-sky .ico{color:var(--moderate-fill)}
.wxd-more{grid-column:1/-1;margin-top:.3rem}
.wxd-more summary{cursor:pointer;font-family:var(--font-mono);font-size:.58rem;letter-spacing:.08em;
  text-transform:uppercase;color:rgba(255,255,255,.5)}
.wxd-more summary:hover{color:rgba(255,255,255,.8)}

/* Marker popup: the live reading is the headline, everything else is context. */
.smk-pop{min-width:12rem}
.smk-pop b{font-family:var(--font-display);font-size:.98rem}
.smk-nudge{display:block;margin-top:.15rem;font-size:.62rem;color:var(--ink-faint)}
.smk-read{display:flex;gap:.9rem;margin:.45rem 0 .25rem}
.smk-v{display:flex;flex-direction:column;font-family:var(--font-display);font-weight:700;
  font-size:1.25rem;line-height:1.05;color:var(--ink)}
.smk-v i{font-family:var(--font-mono);font-size:.58rem;font-weight:400;font-style:normal;
  letter-spacing:.05em;text-transform:uppercase;color:var(--ink-faint);margin-top:.15rem}
.smk-v.b-good{color:var(--good)} .smk-v.b-moderate{color:var(--moderate)}
.smk-v.b-unhealthy{color:var(--unhealthy)} .smk-v.b-hazardous{color:var(--hazardous)}
.smk-v.b-none{color:var(--ink-faint)}
.smk-when,.smk-out{font-family:var(--font-mono);font-size:.62rem;color:var(--ink-faint)}
.smk-go{display:inline-block;margin-top:.5rem;font-family:var(--font-mono);font-size:.66rem;
  letter-spacing:.06em;text-transform:uppercase;color:var(--brand);text-decoration:none}
.smk-go:hover{text-decoration:underline}

/* ── Interactive map canvas ──────────────────────────────────────────────
   ⚠️ THE HEIGHT IS LOAD-BEARING, NOT STYLING. Leaflet measures its container
   on init; a div with no height initialises to 0x0 and renders nothing but the
   legend beneath it — which is exactly what shipped, because the rule that was
   supposed to carry this was written into a regex replacement that silently
   did not match and was never verified. An empty map is indistinguishable from
   a broken one, so this rule is the difference between a feature and a blank.
   ⚠️ AND IT MUST STAY BELOW THE APP'S OVERLAYS. Leaflet paints its panes at
   z-index 400-700 and its controls above that; the weather drawer sits at 80,
   so an unconstrained map on the page underneath would punch through it. */
.smoke-canvas{position:relative;z-index:0;width:100%;
  height:clamp(24rem,52vh,38rem);border-radius:6px;overflow:hidden;background:#20262a}
/* ⚠️ DO NOT OVERRIDE LEAFLET'S PANE z-index VALUES. An earlier version set
   .leaflet-pane{z-index:auto} here to keep the map under the app's drawer, and
   that is exactly the wrong lever: Leaflet stacks tiles at 200, overlays (where
   circleMarkers live) at 400 and markers at 600, and flattening them all to
   auto leaves the marker layer at the mercy of DOM order.
   It was never needed. `position:relative; z-index:0` on the canvas above
   creates a STACKING CONTEXT, so every one of Leaflet's internal z-indexes is
   contained inside it and none of them can rise above the canvas's own 0 —
   which is already below the weather drawer at 80. One property does the job
   the three overrides were trying to do, without touching the library. */
.leaflet-container{font-family:inherit;background:#20262a}

/* Count badge on a shared-address marker. */
.smk-count{display:flex;align-items:center;justify-content:center;
  font-family:var(--font-mono);font-size:.66rem;font-weight:700;color:#121815;
  pointer-events:none}
.smk-site{padding-top:.4rem;margin-top:.4rem;border-top:1px solid var(--line-faint)}
.smk-pop > b + .smk-site{border-top:0;margin-top:.2rem;padding-top:.2rem}
.smk-name{font-family:var(--font-display);font-weight:700;font-size:.85rem;color:var(--ink)}
