﻿/* ============================================================
   W8Sense — design tokens & styles
   ============================================================ */

:root {
  /* Accent (overridden by Tweaks) */
  --accent: #0E9F9C;
  --accent-2: color-mix(in oklab, var(--accent) 75%, white);
  --accent-soft: color-mix(in oklab, var(--accent) 14%, white);
  --accent-text: white;

  /* Neutrals — cool */
  --bg: #F7F8FA;
  --surface: #FFFFFF;
  --surface-2: #FBFCFD;
  --soft: #F1F4F7;
  --line: #E5E9EE;
  --line-strong: #D4DAE2;
  --text: #0F1620;
  --text-2: #3A4654;
  --muted: #6A7585;
  --muted-2: #9098A3;

  /* Status */
  --ok: #16A34A;
  --ok-bg: #DCFCE7;
  --warn: #D97706;
  --warn-bg: #FEF3C7;
  --crit: #DC2626;
  --crit-bg: #FEE2E2;
  --info: #2563EB;
  --info-bg: #DBEAFE;

  /* Chart event markers.
     Deliberately OUTSIDE the status palette: SensorDetail rebinds --ok/--warn/--crit per sensor
     type on its root element, and the reading line is drawn in the sensor's status colour, so an
     event marker painted from that same palette would read as a status rather than an annotation. */
  --ev-in: #2563EB;     /* material in  — fill / delivery / load */
  --ev-out: #7C3AED;    /* material out — empty / draw / collect */
  --ev-alert: #DC2626;  /* threshold breach */
  --ev-mark: #64748B;   /* everything else — no direction claimed */
  /* A reading worth marking — the day's high or low on a probe. Magenta because it is the only
     hue family left: teal is the series line, blue and violet are the two --ev directions, red is
     --ev-alert, slate is --ev-mark, and the whole green/amber/cyan arc belongs to --ok/--warn/--crit,
     which the appearance settings rebind per sensor type and which are therefore unpredictable
     here. It shares a chart with the band-crossing marks, so it also had to clear those. */
  --ev-sample: #C026D3;

  /* Spacing scale */
  --r1: 6px;
  --r2: 10px;
  --r3: 14px;
  --r4: 20px;
  --r-pill: 999px;

  /* Shadows */
  --shadow-1: 0 1px 2px rgba(15, 22, 32, 0.05), 0 1px 3px rgba(15, 22, 32, 0.04);
  --shadow-2: 0 4px 12px rgba(15, 22, 32, 0.06), 0 2px 4px rgba(15, 22, 32, 0.04);
  --shadow-3: 0 12px 32px rgba(15, 22, 32, 0.12);

  /* The estate's ONE focus ring, defined once so "one ring" is enforceable rather than a
     convention three rules agree to keep. Two layers, each doing a different job:

       1px solid  — the actual indicator. With the accent border underneath it, focus paints a 2px
                    perimeter at 3.25:1 against --surface in light and 5.4:1 in dark. It is a
                    THICKNESS change as well as a hue change, so it does not depend on the viewer
                    telling teal from grey.
       4px @18%   — the soft halo the estate already had. Measured against white it is ~1.2:1, so
                    it is decoration and cannot be the indicator; the solid layer exists precisely
                    because the halo alone was one.

     box-shadow never affects layout, so nothing reflows when a control takes focus. It IS thrown
     away wholesale in forced-colors mode — see the @media (forced-colors: active) block at the end
     of the Selects section, which is what stops that mode from having no focus indicator at all. */
  --focus-ring:      0 0 0 1px var(--accent), 0 0 0 4px color-mix(in oklab, var(--accent) 18%, transparent);
  --focus-ring-crit: 0 0 0 1px var(--crit),   0 0 0 4px color-mix(in oklab, var(--crit)   18%, transparent);

  /* Type */
  --font-sans: "Inter", system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;

  /* Density */
  --pad-card: 18px;
  --pad-screen: 28px;
  --gap-grid: 16px;
  --sidebar-w: 248px;
  --topbar-h: 60px;

  /* Tells the browser to use its light defaults for native UI (autofill, scrollbars,
     date pickers, select dropdowns). Overridden in dark mode below. */
  color-scheme: light;

  /* Select chevron — light theme, drawn in --muted (#6A7585).
     Why two data-URIs (one per theme) instead of ONE mask-image tinted with currentColor:
     a mask applies to the ELEMENT's own rendering, so masking a <select> would clip the label
     text it is supposed to sit beside — and a <select> does not render ::before/::after, so a
     tinted mask would need a wrapper <span> around all 14 selects in the app. Two variables is
     one extra line of CSS and zero markup. A single hard-coded stroke would vanish in one of
     the two themes, which is the failure this pair exists to avoid. */
  --select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none' stroke='%236A7585' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m2.5 4.75 3.5 3.5 3.5-3.5'/%3E%3C/svg%3E");
}

:root[data-density="compact"] {
  --pad-card: 12px;
  --pad-screen: 20px;
  --gap-grid: 12px;
  --topbar-h: 52px;
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: #0B0F14;
  --surface: #131922;
  --surface-2: #181F2A;
  --soft: #1B2330;
  --line: rgba(255, 255, 255, 0.12);
  --line-strong: rgba(255, 255, 255, 0.22);
  --text: #ECF1F7;
  --text-2: #BCC6D3;
  --muted: #8492A4;
  --muted-2: #5F6C7E;
  --accent-soft: color-mix(in oklab, var(--accent) 22%, #131922);
  --ok-bg: color-mix(in oklab, var(--ok) 18%, #131922);
  --warn-bg: color-mix(in oklab, var(--warn) 18%, #131922);
  --crit-bg: color-mix(in oklab, var(--crit) 18%, #131922);
  --info-bg: color-mix(in oklab, var(--info) 18%, #131922);
  --ev-in: #60A5FA;
  --ev-out: #A78BFA;
  --ev-alert: #F87171;
  --ev-mark: #94A3B8;
  --ev-sample: #E879F9;
  --shadow-1: 0 1px 2px rgba(0,0,0,0.4);
  --shadow-2: 0 4px 12px rgba(0,0,0,0.4);
  --shadow-3: 0 12px 32px rgba(0,0,0,0.5);
  /* Select chevron — dark theme, drawn in dark --muted (#8492A4). Same geometry, different ink. */
  --select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none' stroke='%238492A4' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m2.5 4.75 3.5 3.5 3.5-3.5'/%3E%3C/svg%3E");
}

/* ============================================================
   Reset / base
   ============================================================ */
* { box-sizing: border-box; }
html, body, #root { height: 100%; }
body {
  margin: 0;
  font-family: var(--font-sans);
  background: var(--bg);
  color: var(--text);
  font-size: 14.5px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
h1, h2, h3, h4 { margin: 0; font-weight: 600; letter-spacing: -0.01em; }
h1 { font-size: 22px; letter-spacing: -0.02em; }
h2 { font-size: 18px; }
h3 { font-size: 15px; }
p { margin: 0; }
a { color: var(--accent); cursor: pointer; text-decoration: none; }
a.link:hover { text-decoration: underline; }
button { font-family: inherit; cursor: pointer; }
input, select, textarea { font-family: inherit; }
.num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.muted { color: var(--muted); }

/* ============================================================
   Buttons
   ============================================================ */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  font-weight: 500; font-size: 14px;
  border-radius: var(--r2); border: 1px solid transparent;
  transition: background 120ms, border-color 120ms, color 120ms, transform 80ms;
  cursor: pointer; white-space: nowrap;
}
.btn:active { transform: translateY(1px); }
.btn-sm { padding: 6px 12px; font-size: 13px; }
.btn-md { padding: 9px 14px; }
.btn-lg { padding: 12px 18px; font-size: 15px; }
.btn-primary {
  background: var(--accent); color: var(--accent-text);
  border-color: var(--accent);
  box-shadow: 0 1px 0 color-mix(in oklab, var(--accent) 70%, black) inset;
}
.btn-primary:hover { background: color-mix(in oklab, var(--accent) 88%, black); }
.btn-secondary {
  background: var(--surface); color: var(--text);
  border-color: var(--line-strong);
}
.btn-secondary:hover { background: var(--soft); }
.btn-ghost {
  background: transparent; color: var(--text-2); border-color: transparent;
}
.btn-ghost:hover { background: var(--soft); }
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; border-radius: var(--r2);
  background: transparent; border: 1px solid transparent; color: var(--text-2);
  position: relative;
}
.icon-btn:hover { background: var(--soft); color: var(--text); }

/* ============================================================
   Auth screens
   ============================================================ */
.auth-shell {
  display: grid; grid-template-columns: 1.05fr 1fr;
  min-height: 100vh; background: var(--bg);
}
@media (max-width: 900px) {
  .auth-shell { grid-template-columns: 1fr; }
  .auth-brand { display: none; }
}
.auth-brand {
  background: linear-gradient(155deg, var(--accent) 0%, color-mix(in oklab, var(--accent) 60%, #0a0f1a) 100%);
  color: white; padding: 40px;
  display: flex; flex-direction: column; justify-content: space-between;
  position: relative; overflow: hidden;
}
.auth-brand::before {
  content: ''; position: absolute; inset: 0;
  background-image:
    radial-gradient(circle at 80% 20%, rgba(255,255,255,0.16), transparent 45%),
    radial-gradient(circle at 10% 90%, rgba(0,0,0,0.18), transparent 50%);
  pointer-events: none;
}
.auth-brand::after {
  content: ''; position: absolute; inset: 0;
  background-image:
    linear-gradient(rgba(255,255,255,0.06) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255,255,255,0.06) 1px, transparent 1px);
  background-size: 48px 48px;
  background-position: -1px -1px;
  mask-image: linear-gradient(180deg, transparent, black 30%, black 70%, transparent);
  pointer-events: none;
}
.auth-brand > * { position: relative; z-index: 1; }
.auth-brand-mark { display: flex; align-items: center; gap: 12px; }
.logo { width: 40px; height: 40px; display: grid; place-items: center;
  background: rgba(255,255,255,0.1); border-radius: 10px;
}
.logo-text { font-size: 22px; font-weight: 700; letter-spacing: -0.02em; }
.logo-text span { font-weight: 400; opacity: 0.75; }
.auth-brand-copy h1 { font-size: 38px; line-height: 1.1; font-weight: 600; max-width: 460px; }
.auth-brand-copy p { margin-top: 16px; max-width: 460px; font-size: 15px; opacity: 0.85; line-height: 1.55; }
.auth-brand-meta { display: flex; justify-content: space-between; align-items: center; font-size: 13px; opacity: 0.9; }
.ticker { display: flex; align-items: center; gap: 8px; }
.ticker .dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: #6EE7B7; box-shadow: 0 0 0 4px rgba(110, 231, 183, 0.25);
  animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.4; }
}

.auth-main {
  display: grid; place-items: center; padding: 40px 24px;
}
.auth-card {
  width: 100%; max-width: 420px;
  background: var(--surface); border-radius: var(--r4);
  padding: 36px;
  box-shadow: var(--shadow-2);
  border: 1px solid var(--line);
}
@media (max-width: 900px) {
  .auth-card { box-shadow: none; border: none; padding: 24px; max-width: 460px; }
}
.auth-head { margin-bottom: 24px; }
.auth-head h2 { font-size: 24px; margin-bottom: 6px; }
.auth-head p { color: var(--muted); font-size: 14px; }
.auth-form { display: flex; flex-direction: column; gap: 14px; }
.auth-row { display: flex; justify-content: space-between; align-items: center; font-size: 13px; }
.auth-foot { text-align: center; margin-top: 12px; font-size: 14px; color: var(--muted); }
.auth-sent { text-align: center; padding: 12px 0; }
.auth-sent-icon {
  width: 56px; height: 56px; border-radius: 50%;
  background: var(--ok-bg); color: var(--ok);
  display: grid; place-items: center; margin: 0 auto 16px;
}
.auth-sent h3 { font-size: 18px; margin-bottom: 8px; }
.auth-sent p { color: var(--muted); margin-bottom: 20px; line-height: 1.6; }

.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
@media (max-width: 520px) { .form-grid { grid-template-columns: 1fr; } }

/* ============================================================
   Fields
   ============================================================ */
.field { display: flex; flex-direction: column; gap: 6px; }
.field-label { font-size: 13px; font-weight: 500; color: var(--text-2); }
.field-input {
  display: flex; align-items: center; gap: 8px;
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--r2); padding: 10px 12px;
  transition: border-color 120ms, box-shadow 120ms;
  color: var(--muted);
}
.field-input:focus-within {
  border-color: var(--accent);
  box-shadow: var(--focus-ring);
}
.field-input input {
  flex: 1; border: none; outline: none; background: transparent;
  font-size: 14.5px; color: var(--text);
  min-width: 0;
}
/* The wrapper-styled text fields declare the control's padding on the WRAPPER, but the UA still
   gives the inner <input> a residual padding of its own (Chrome 1px/2px, Firefox 1px/1px). That
   residual is why a bare <select> could never be lined up with them: the declared 10px was really
   rendering as 11px, by a different amount per browser. Zero it, so the declared padding IS the
   rendered padding and .select / .select--sm match these boxes by construction everywhere.
   Costs each of these fields 2px of height — they were never the height their own CSS claimed. */
.field-input input,
.search-input input { padding: 0; }
/* Chrome's autofill paints its own background and text colour with high specificity.
   Repaint by faking a huge inset shadow in --surface and forcing the text colour back to --text.
   The long transition delay prevents the yellow flash on focus. */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
  -webkit-text-fill-color: var(--text) !important;
  -webkit-box-shadow: 0 0 0 1000px var(--surface) inset !important;
  caret-color: var(--text);
  transition: background-color 9999s ease-in-out 0s;
}
.field-eye {
  background: transparent; border: none; padding: 2px; color: var(--muted);
  display: inline-flex;
}
.field.has-error .field-input { border-color: var(--crit); }
/* An errored field that takes focus keeps saying "error" — without this it kept the crit border
   (higher specificity) but drew the accent halo around it, which reads as two states at once. */
.field.has-error .field-input:focus-within { box-shadow: var(--focus-ring-crit); }
.field-hint { font-size: 12px; color: var(--muted); }
/* Same lock idiom as .field-readonly below, for a hint that explains a control disabled in place
   rather than a whole read-only field. The sentence next to it carries the actual reason. */
.field-hint.is-locked::before { content: "🔒 "; }
.field-error { font-size: 12px; color: var(--crit); }

/* Read-only fields — W8Gear-owned values W8Sense surfaces but cannot edit. Muted, non-interactive,
   no focus ring, with a small lock glyph on the hint. See the sensor-settings-governance skill. */
.field-readonly .field-input { background: var(--soft); border-style: dashed; }
.field-readonly .field-input:focus-within { border-color: var(--line-strong); box-shadow: none; }
.field-readonly .field-input input { color: var(--text-2); cursor: default; }
.field-readonly .field-hint::before { content: "🔒 "; }

/* ============================================================
   Selects — ONE treatment for every dropdown in the app
   ============================================================
   The control stays a NATIVE <select>. Keyboard navigation, type-ahead, the OS picker on mobile
   and screen-reader semantics all come free, and this is an operational tool people drive from
   the keyboard. The price is that the OPEN popup is drawn by the OS and cannot be styled
   cross-browser — so the CLOSED control is what we make excellent, and `color-scheme` is what
   makes the popup at least agree with the app's theme instead of flashing white over a dark UI.

   Two sizes, chosen deliberately rather than arrived at by accident:
     .select      — form size. Matches .field-input, the text control it sits beside in a .field.
     .select--sm  — filter / inline size. Matches .search-input, the text control it sits beside
                    in every .filter-bar.

   Metrics copied from .field-input: background var(--surface) · border 1px var(--line-strong) ·
   border-radius var(--r2) · padding-block 10px · padding-inline-start 12px · font-size 14.5px.
   padding-inline-END is larger (34px) only to clear the chevron; the TEXT insets align, which is
   what the eye actually reads down a column of controls.

   line-height is pinned to `normal` on purpose. The UA forces `normal` on <input>, but a <select>
   INHERITS body's 1.5 — leave it alone and the select renders ~4px taller than the text field next
   to it, which is exactly the "why does this row look sloppy" effect nobody can name. */
.select {
  appearance: none;
  -webkit-appearance: none;
  font: inherit;
  font-size: 14.5px;
  line-height: normal;
  color: var(--text);
  background-color: var(--surface);
  background-image: var(--select-chevron);
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 12px 12px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r2);
  padding: 10px 34px 10px 12px;
  cursor: pointer;
  /* A <select> is intrinsically as wide as its longest <option>. min-width:0 lets it shrink
     inside flex/grid parents, max-width:100% stops it escaping a block one, and the ellipsis
     pair keeps a long option from pushing the chevron off the end. */
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  transition: border-color 120ms, box-shadow 120ms, background-color 120ms;
  /* The root already declares color-scheme, but declaring it ON THE CONTROL is what guarantees
     the OS-drawn option list follows the app even if a subtree ever overrides it. */
  color-scheme: light;
}
:root[data-theme="dark"] .select { color-scheme: dark; }

/* Firefox paints the dropdown panel from the select's own background/colour, so naming both on
   the rows keeps the popup right in both themes there too. */
.select option,
.select optgroup { background-color: var(--surface); color: var(--text); }
.select optgroup { font-weight: 600; }

/* Hover is the least important of the three states, so it has to LOSE to the two that carry
   meaning. It does not do that by itself: `.select:hover:not(:disabled)` is (0,3,0), higher than
   `.select:focus-visible` (0,2,0) and `.select[aria-invalid="true"]` (0,2,0), so source order
   could not save them — hovering a focused select repainted its focus border in the hover mix,
   and hovering an invalid one repainted its red border. Both guards are load-bearing. */
.select:hover:not(:disabled):not(:focus-visible):not([aria-invalid="true"]) {
  border-color: color-mix(in oklab, var(--accent) 45%, var(--line-strong));
}
/* The estate's ONE focus ring — identical to .field-input:focus-within and .rule-input:focus.
   :focus-visible, not :focus, so a mouse click that opens the popup does not also paint a ring.
   Tab focus matches :focus-visible on a <select> in every engine, which is the case that has to
   work; a pointer click may not, and that matches the native control's own behaviour.
   `outline: none` is only safe because --focus-ring puts a real 2px perimeter back in its place.
   The forced-colors block below covers the one mode where that shadow is discarded. */
.select:focus { outline: none; }
.select:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: var(--focus-ring);
}
/* Disabled says so twice — a dashed border (shape) as well as the muted fill (colour) — reusing
   the .field-readonly idiom this file already uses for W8Gear-owned values. Colour alone was the
   whole signal before, and a disabled <select> is out of the tab order, so there is no focus or
   hover moment left in which to discover the state some other way. */
.select:disabled {
  background-color: var(--soft);
  color: var(--muted);
  border-style: dashed;
  cursor: not-allowed;
  opacity: 1;
}
/* Invalid — mirrors .field.has-error .field-input. aria-invalid is the hook the rule editor
   already sets on its parameter controls, so styling follows the announced state rather than a
   second, separate class. */
.field.has-error > .select,
.select[aria-invalid="true"] { border-color: var(--crit); }
.field.has-error > .select:focus-visible,
.select[aria-invalid="true"]:focus-visible {
  box-shadow: var(--focus-ring-crit);
}

/* Compact variant. Matches .search-input: padding 8px 12px, font-size 14px, same border+radius,
   so a filter bar reads as one row of equal-height controls. */
.select--sm {
  font-size: 14px;
  padding: 8px 30px 8px 12px;
  background-position: right 10px center;
  background-size: 11px 11px;
}

/* ============================================================
   Dropdown — <Dropdown>, the app's own listbox. THIS is what every picker uses now.
   ============================================================
   The .select rules above are the legacy native control and are kept only for anything not yet
   converted. Do not add a new <select>; the popup it opens is drawn by the OS, in the OS's own
   metrics, and no rule in this file can reach it.

   The CLOSED control (.dd-trigger) is metric-identical to .select, which is itself copied from
   .field-input — so a Dropdown, a text field and any surviving select still line up down a column.
   The OPEN panel is finally ours: app radius, app shadow, app spacing, both themes.

   z-index pair: the backdrop catches the outside click, the panel sits one above it. Both are above
   .card (which sets no z-index) and below the app's modal layer. */
.dd { position: relative; }

.dd-trigger {
  /* Same box as .select — see the header. */
  display: flex; align-items: center; gap: 8px;
  width: 100%;
  font: inherit;
  font-size: 14.5px;
  line-height: normal;
  text-align: left;
  color: var(--text);
  background-color: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r2);
  padding: 10px 12px;
  cursor: pointer;
  min-width: 0;
  transition: border-color 120ms, box-shadow 120ms, background-color 120ms;
}
.dd-trigger--sm { font-size: 14px; padding: 8px 12px; }

/* The value takes all the room and truncates; the caret never moves. */
.dd-value { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dd-value.is-placeholder { color: var(--muted); }

.dd-caret { display: flex; color: var(--muted); transition: transform 140ms ease; }
.dd.is-open .dd-caret { transform: rotate(180deg); }

/* Same three-state treatment, same precedence guards, as .select — hover must lose to focus and to
   invalid, and :not() is the only thing that makes it lose. */
.dd-trigger:hover:not(:disabled):not(:focus-visible):not([aria-invalid="true"]) {
  border-color: color-mix(in oklab, var(--accent) 45%, var(--line-strong));
}
.dd-trigger:focus { outline: none; }
.dd-trigger:focus-visible,
.dd.is-open .dd-trigger {
  outline: none;
  border-color: var(--accent);
  box-shadow: var(--focus-ring);
}
.dd-trigger:disabled {
  background-color: var(--soft);
  color: var(--muted);
  border-style: dashed;
  cursor: not-allowed;
  opacity: 1;
}
.field.has-error > .dd .dd-trigger,
.dd-trigger[aria-invalid="true"] { border-color: var(--crit); }
.dd-trigger[aria-invalid="true"]:focus-visible { box-shadow: var(--focus-ring-crit); }

.dd-backdrop { position: fixed; inset: 0; z-index: 60; }

.dd-panel {
  position: absolute;
  z-index: 61;
  top: calc(100% + 6px);
  left: 0;
  /* At least as wide as the control, wider if the rows need it, never wider than the viewport. */
  min-width: 100%;
  width: max-content;
  max-width: min(380px, calc(100vw - 24px));
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r2);
  box-shadow: var(--shadow-3);
  overflow: hidden;
  animation: dd-in 120ms ease-out;
}
@keyframes dd-in { from { opacity: 0; transform: translateY(-4px); } }

.dd-list { max-height: 300px; overflow-y: auto; padding: 6px; }
.dd-list:focus { outline: none; }

.dd-group {
  font-size: 11px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--muted);
  padding: 10px 10px 4px;
}
.dd-group:first-child { padding-top: 4px; }

.dd-opt {
  display: flex; align-items: flex-start; gap: 8px;
  padding: 9px 10px;
  border-radius: var(--r1);
  cursor: pointer;
  font-size: 14px;
  color: var(--text);
}
/* Hover and keyboard focus paint the SAME highlight on purpose: the row under the pointer and the
   row the arrow keys are on are the same idea, and giving them two different looks left people
   hunting for which one Enter would take. */
.dd-opt:hover,
.dd-opt:focus,
.dd-opt:focus-visible { background: var(--soft); outline: none; }
.dd-opt.is-selected { color: var(--accent); font-weight: 500; }
.dd-opt.is-selected:hover, .dd-opt.is-selected:focus { background: var(--accent-soft); }
.dd-opt.is-disabled { color: var(--muted); cursor: not-allowed; }
.dd-opt.is-disabled:hover, .dd-opt.is-disabled:focus { background: transparent; }

/* Reserved whether or not it holds a tick, so labels align down the list instead of shifting by
   14px the moment something is chosen. */
.dd-opt-tick { flex: 0 0 16px; display: flex; justify-content: center; padding-top: 1px; color: var(--accent); }
.dd-opt-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.dd-opt-label { line-height: 1.35; }
.dd-opt-desc { font-size: 12px; color: var(--muted); font-weight: 400; line-height: 1.4; }

.dd-empty { padding: 14px 10px; font-size: 13px; color: var(--muted); text-align: center; }

@media (prefers-reduced-motion: reduce) {
  .dd-panel { animation: none; }
  .dd-caret { transition: none; }
}

/* ------------------------------------------------------------
   Forced colours (Windows High Contrast) — the one mode where the ring above evaporates.

   `forced-colors: active` resolves box-shadow to none. Every focus rule in this file declares
   `outline: none` on the assumption the shadow will draw the ring, so without this block a
   keyboard user in high contrast gets NO focus indicator anywhere in the app — not on selects,
   not on text fields, not in the rule editor. Put a real outline back, in the user's own
   Highlight colour rather than our accent, which forced colours would override anyway.
   ------------------------------------------------------------ */
@media (forced-colors: active) {
  .select:focus-visible,
  .rule-input:focus,
  .field-input:focus-within,
  /* The coverage matrix adds two more focusable things: the scroll region itself (a region that only
     scrolls is unreachable from the keyboard without one) and the tick, which is a link to its rule.
     Both declare `outline: none` on the same assumption as everything above, so both belong here. */
  .cov-wrap:focus-visible,
  a.cov-tick:focus-visible,
  /* The dropdown replaced the native select, so its trigger and its rows inherit the same problem:
     both declare `outline: none` and lean on a box-shadow that forced colours discards. A row is
     focusable by design here (the roving-tabindex listbox pattern), so without this the arrow keys
     move an invisible cursor down the list. */
  .dd-trigger:focus-visible,
  .dd-opt:focus,
  .dd-opt:focus-visible {
    outline: 2px solid Highlight;
    outline-offset: 2px;
  }

  /* Forced colours repaint every background, so the matrix's three cell states cannot be told apart
     by fill here. They are already distinguished by GLYPH; give the two marked states borders in the
     system's own text colour so the tile shape survives too — solid for a tick, dashed for a mute. */
  .cov-tick { border: 1px solid CanvasText; }
  .cov-mute { border: 1px dashed CanvasText; }
  /* Same problem, same fix, for the List|Matrix switch: `.seg button.active` signals with a
     background alone plus a box-shadow, and this mode discards both — so without a border here the
     pressed rung is not merely quiet, it is unsignalled.

     BOTH buttons carry the border and only its COLOUR changes. `.seg button` sets `border: none` and
     neither button has a width, so there is no box for border-box to absorb 2px into: putting the
     border on the pressed one alone made it 4px wider than its neighbour and moved the divider
     between them by a measured 4px every time you toggled.

     THE `.seg` IN THE SELECTOR IS LOAD-BEARING. `.seg button { border: none }` is (0,1,1) and lives
     ~700 lines further down this file; a bare `.cov-view-toggle button` is (0,1,1) too, so it loses
     on source order and the border never renders at all — measured as border-width 0px / style none
     on both buttons under emulated forced colours. Qualifying with `.seg` makes it (0,2,1) and it
     wins. A media query contributes nothing to specificity, so being inside this block does not help.

     BUTTONFACE, NOT TRANSPARENT, for the resting state. Forced colours force EVERY border-color to a
     system colour and `transparent` is not exempt — measured painting as ButtonText, i.e. a
     transparent placeholder makes the unpressed button look pressed too, which is worse than the
     jitter it was there to remove. ButtonFace is the button's own face, so it reserves the 2px
     invisibly in any user theme rather than by hoping alpha survives. ButtonText rather than
     Highlight for the pressed one because Highlight is what the UA draws the focus ring in, and
     "pressed" and "focused" have to stay tellable apart. */
  .seg.cov-view-toggle button { border: 2px solid ButtonFace; }
  .seg.cov-view-toggle button[aria-pressed="true"] { border-color: ButtonText; }
  /* The chevron is a background IMAGE with a grey baked into the data-URI. Forced colours can
     override colour properties but cannot repaint an image, so it would keep that grey against
     whatever background the user chose. Drop it and hand the control back to the UA, which draws
     its own arrow in the system colours the user actually asked for. */
  .select {
    appearance: auto;
    -webkit-appearance: auto;
    background-image: none;
  }
}

.check {
  display: inline-flex; align-items: center; gap: 8px;
  color: var(--text-2); cursor: pointer; user-select: none; font-size: 13px;
}
.check input { width: 16px; height: 16px; accent-color: var(--accent); }
.check.terms { font-size: 13px; line-height: 1.5; align-items: flex-start; }
.check.terms input { margin-top: 2px; }

.pw-strength {
  display: flex; gap: 4px; align-items: center; margin-top: -6px;
}
.pw-strength .bar { flex: 1; height: 3px; background: var(--soft); border-radius: 2px; }
.pw-strength em { font-style: normal; font-size: 12px; margin-left: 8px; color: var(--muted); }
.pw-strength[data-level="1"] .bar:nth-child(-n+1),
.pw-strength[data-level="2"] .bar:nth-child(-n+2),
.pw-strength[data-level="3"] .bar:nth-child(-n+3),
.pw-strength[data-level="4"] .bar:nth-child(-n+4) {
  background: var(--accent);
}
.pw-strength[data-level="1"] .bar:nth-child(-n+1) { background: var(--crit); }
.pw-strength[data-level="2"] .bar:nth-child(-n+2) { background: var(--warn); }

/* ============================================================
   App shell
   ============================================================ */
.app-shell {
  display: grid;
  grid-template-columns: var(--sidebar-w) 1fr;
  min-height: 100vh;
}
.sidebar {
  background: var(--surface);
  border-right: 1px solid var(--line);
  display: flex; flex-direction: column;
  position: sticky; top: 0; height: 100vh;
  /* Lives outside the topbar's stacking context; raised so its links beat the scope-scrim. */
  z-index: 30;
}
.sidebar-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 16px 18px; border-bottom: 1px solid var(--line);
  height: var(--topbar-h);
}
.logo-row { display: flex; align-items: center; gap: 10px; }
.logo-mini { display: grid; place-items: center; width: 32px; height: 32px; }
.logo-text-sm { font-weight: 700; font-size: 16px; letter-spacing: -0.01em; }
.logo-text-sm span { font-weight: 400; color: var(--muted); }
.sidebar-close { display: none; }

.sidebar-nav {
  padding: 6px 10px;
  display: flex; flex-direction: column; gap: 2px;
}
.nav-item {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 12px; border-radius: var(--r2);
  color: var(--text-2); font-size: 14px; font-weight: 500;
  cursor: pointer; position: relative;
}
.nav-item:hover { background: var(--soft); color: var(--text); }
.nav-item.active {
  background: var(--accent-soft);
  color: var(--accent);
}
.nav-badge {
  margin-left: auto;
  background: var(--crit); color: white;
  font-size: 11px; font-style: normal; font-weight: 600;
  padding: 2px 6px; border-radius: var(--r-pill);
  min-width: 18px; text-align: center;
}
.sidebar-foot { padding: 14px; border-top: 1px solid var(--line); }
.user-chip {
  display: flex; align-items: center; gap: 10px;
  padding: 6px;
}
.avatar {
  width: 34px; height: 34px; border-radius: 50%;
  background: var(--accent-soft); color: var(--accent);
  display: grid; place-items: center; font-weight: 600; font-size: 13px;
  flex-shrink: 0;
}
.avatar-sm { width: 30px; height: 30px; font-size: 12px; cursor: pointer; }
.user-meta { flex: 1; min-width: 0; }
.user-name { font-weight: 500; font-size: 13.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.user-role { font-size: 12px; color: var(--muted); }

.app-main {
  display: flex; flex-direction: column; min-width: 0;
}
.topbar {
  height: var(--topbar-h);
  border-bottom: 1px solid var(--line);
  background: var(--surface);
  display: flex; align-items: center; gap: 12px;
  padding: 0 var(--pad-screen);
  position: sticky; top: 0; z-index: 20;
}
.mobile-menu { display: none; }
.breadcrumb {
  display: flex; align-items: center; gap: 6px;
  font-size: 14px; color: var(--muted);
  min-width: 0;
}
.breadcrumb a { color: var(--muted); cursor: pointer; }
.breadcrumb a:hover { color: var(--text); }
.breadcrumb span { color: var(--text); font-weight: 500; }
.topbar-actions { display: flex; align-items: center; gap: 6px; }
/* .topbar-alerts / .pulse-dot removed with the navigate-to-/alerts bell they styled. The topbar bell is
   now <NotificationMenu />, which carries its own scoped styles and shows a real unread COUNT rather than
   a dot that only ever meant "something critical is unread". */

/* Header scope switcher — tenant + account dropdowns in the top bar. The scope-bar's auto right
   margin pushes .topbar-actions to the far edge; the fixed scrim catches outside clicks to close. */
.scope-bar {
  display: flex; align-items: center; gap: 8px;
  margin-right: auto; min-width: 0;
  position: relative; z-index: 2;
}
.scope-field { position: relative; min-width: 0; }
.scope-pill {
  display: flex; align-items: center; gap: 7px;
  min-width: 180px; max-width: 380px;
  padding: 6px 12px;
  background: var(--soft);
  border: 1px solid var(--line);
  border-radius: var(--r2);
  color: var(--text); font-family: inherit; font-size: 13px;
  cursor: pointer;
  transition: background 120ms, border-color 120ms;
}
.scope-pill:hover:not(:disabled) { border-color: color-mix(in oklab, var(--accent) 35%, transparent); }
.scope-pill.open { border-color: var(--accent); background: var(--accent-soft); }
.scope-pill:disabled { cursor: default; }
.scope-pill-label {
  font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--muted-2); flex-shrink: 0;
}
.scope-value {
  flex: 1; min-width: 0;
  font-weight: 600; letter-spacing: -0.01em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.scope-pill svg { color: var(--muted); flex-shrink: 0; }
.scope-pill.open svg { color: var(--accent); }
.scope-menu {
  position: absolute; left: 0; top: calc(100% + 6px);
  min-width: 220px; max-width: 300px; max-height: 60vh; overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r2);
  box-shadow: var(--shadow-2);
  padding: 4px; z-index: 3;
}
.scope-menu-item {
  display: flex; align-items: center; gap: 8px;
  width: 100%;
  background: transparent; border: none;
  padding: 8px 10px; border-radius: var(--r1);
  font-size: 13px; color: var(--text); cursor: pointer; text-align: left;
  font-family: inherit;
}
.scope-menu-item:hover { background: var(--soft); }
.scope-menu-item.active { background: var(--accent-soft); color: var(--accent); }
.scope-menu-name { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.scope-menu-count {
  font-size: 11px; font-style: normal; color: var(--muted-2);
  font-family: var(--font-mono);
  background: var(--soft); padding: 1px 6px; border-radius: var(--r-pill);
}
.scope-menu-item.active .scope-menu-count { background: var(--surface); color: var(--accent); }
/* Outside-click scrim. It sits inside the sticky topbar's stacking context, so the topbar controls,
   the scope pills, and the sidebar are all lifted above it — the scrim then only covers the
   main content area, i.e. clicking the page (but not the chrome) closes an open dropdown. */
.scope-scrim { position: fixed; inset: 0; z-index: 1; background: transparent; }
/* Keep the rest of the top-bar chrome clickable over the scrim (first click must hit the control,
   not just dismiss the menu). Navigation-triggering controls also close the menu via LocationChanged. */
.breadcrumb, .mobile-menu, .topbar-actions { position: relative; z-index: 2; }
/* Narrow screens: drop the "Tenant"/"Account" prefixes, keep just the values. */
@media (max-width: 720px) {
  .scope-pill-label { display: none; }
  .scope-pill { min-width: 0; max-width: 150px; }
}

/* -- Mobile scope: one trigger opens a bottom-sheet modal with tenant + account (so the two pills
   don't collide in the cramped topbar). The trigger is hidden on desktop; the pills are hidden on mobile. */
.scope-mobile-trigger { display: none; }
.scope-modal-backdrop { position: fixed; inset: 0; z-index: 40; background: color-mix(in oklab, var(--text) 45%, transparent); }
.scope-sheet {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 41;
  background: var(--surface); border-top: 1px solid var(--line-strong);
  border-radius: 16px 16px 0 0; box-shadow: var(--shadow-2);
  max-height: 75vh; overflow-y: auto; padding: 8px 12px 20px;
  animation: scope-sheet-up 160ms ease-out;
}
@keyframes scope-sheet-up { from { transform: translateY(100%); } to { transform: translateY(0); } }
.scope-sheet-head { display: flex; align-items: center; justify-content: space-between; padding: 10px 4px 8px; font-weight: 600; font-size: 15px; }
.scope-sheet-section { margin-top: 6px; }
.scope-sheet-label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted-2); padding: 8px 6px 4px; }

@media (max-width: 768px) {
  /* Swap the two inline pills for the single trigger. */
  .scope-bar .scope-field { display: none; }
  .scope-mobile-trigger {
    display: flex; align-items: center; gap: 8px; min-width: 0; max-width: 100%;
    padding: 6px 12px; background: var(--soft); border: 1px solid var(--line);
    border-radius: var(--r2); color: var(--text); font-family: inherit; font-size: 13px; cursor: pointer;
    transition: border-color 120ms;
  }
  .scope-mobile-trigger:hover { border-color: color-mix(in oklab, var(--accent) 35%, transparent); }
  .scope-mobile-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.2; text-align: left; }
  .scope-mobile-tenant { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .scope-mobile-account { font-size: 11px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .scope-mobile-trigger > svg:last-child { color: var(--muted); flex-shrink: 0; margin-left: auto; }
  /* Drop the whole breadcrumb (product name + route) on mobile — the scope selector takes the space. */
  .breadcrumb { display: none; }
}

.content {
  flex: 1;
  padding: var(--pad-screen);
  min-width: 0;
}

.bottom-nav { display: none; }

/* ============================================================
   Page heads
   ============================================================ */
.page-head {
  display: flex; justify-content: space-between; align-items: flex-end;
  gap: 16px; margin-bottom: 20px;
}
.page-sub { color: var(--muted); font-size: 13px; margin-top: 4px; }
/* Wraps, because a page head's actions are not all shrinkable. .btn is white-space:nowrap and a .seg
   cannot go below its min-content, so a head carrying three controls had nowhere to put the third: it
   overflowed the screen instead of moving to a second line. The gap above is already a row gap too,
   so a wrapped line needs nothing else. This only ever engages under the 768px rule below, where
   .page-head becomes a column and this row is finally allowed to be narrower than its own content —
   above that, flex-shrink:0 still keeps it at its natural width. Shared by Dashboard (toggle +
   Export), Alerts (up to three buttons mid-confirmation) and Alert rules (toggle + two buttons). */
.page-head-actions { display: flex; gap: 8px; flex-shrink: 0; flex-wrap: wrap; }

/* ============================================================
   Dashboard
   ============================================================ */
.summary-row {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--gap-grid);
  margin-bottom: 20px;
}
.summary-tile {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r3);
  padding: 16px 18px;
}
.summary-label {
  display: flex; align-items: center; gap: 6px;
  font-size: 12.5px; color: var(--muted); font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.04em;
}
.summary-value {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-size: 32px; font-weight: 600; margin-top: 6px;
  letter-spacing: -0.02em;
}

.filter-bar {
  display: flex; gap: 10px; margin-bottom: 16px; flex-wrap: wrap;
}
.search-input {
  flex: 1; min-width: 220px;
  display: flex; align-items: center; gap: 8px;
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--r2); padding: 8px 12px;
  color: var(--muted);
}
.search-input input {
  flex: 1; border: none; outline: none; background: transparent;
  font-size: 14px; color: var(--text);
}
/* Appearance comes from .select/.select--sm (see "Selects"); the filter bar only says how wide.
   160px is the floor at which "All premises" still reads without truncating. */
.filter-bar .select { min-width: 160px; }

.sensor-grid {
  display: grid; gap: var(--gap-grid);
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
}
.sensor-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r3);
  padding: var(--pad-card);
  display: flex; flex-direction: column; gap: 12px;
  cursor: pointer;
  transition: transform 140ms, box-shadow 140ms, border-color 140ms;
}
.sensor-card:hover {
  transform: translateY(-1px);
  box-shadow: var(--shadow-2);
  border-color: var(--line-strong);
}
/* Account grouping on the dashboard grid */
.sensor-account-section {
  margin-bottom: 24px;
}
.sensor-account-section:last-child { margin-bottom: 0; }
.sensor-account-header {
  display: flex; align-items: baseline; gap: 12px;
  padding: 4px 0 12px;
  border-bottom: 1px solid var(--line);
  margin-bottom: 16px;
}
.sensor-account-header h2 {
  margin: 0;
  font-size: 13px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-2);
}
.sensor-account-header .muted { font-size: 12px; }

.sensor-card-head {
  display: flex; justify-content: space-between; align-items: flex-start; gap: 12px;
  /* Fixed-height band (template): single-line name (ellipsised) + status badge.
     Hard-locked like the footer so the reading band below it starts at the same y on
     every card — current and future — which keeps headline readings aligned across the grid. */
  min-height: 28px; max-height: 28px; overflow: hidden; flex-shrink: 0;
}
/* The text wrapper must allow shrinking below content width (default min-width:auto blocks it). */
.sensor-card-head > .sensor-card-head-text {
  min-width: 0; flex: 1;
}
.sensor-name {
  font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.sensor-name.s-md { font-size: 15px; }   /* default */
.sensor-name.s-sm { font-size: 13px; }   /* longer names */
.sensor-name.s-xs { font-size: 11px; }   /* very long; ellipsis kicks in beyond this */

.sensor-meta {
  color: var(--muted); margin-top: 2px; font-family: var(--font-mono);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.sensor-meta.s-md { font-size: 12px; }
.sensor-meta.s-sm { font-size: 11px; }
.sensor-meta.s-xs { font-size: 10px; }
.sensor-card-body {
  display: flex; gap: 16px; align-items: center;
}
.sensor-card-stats { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
.sensor-fill {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
}
.sensor-fill .big {
  font-family: var(--font-mono); font-weight: 600; font-size: 28px;
  letter-spacing: -0.02em;
}
.sensor-fill .big em { font-style: normal; color: var(--muted); font-size: 18px; font-weight: 500; margin-left: 1px; }
.sensor-fill .cap { font-size: 12px; color: var(--muted); font-family: var(--font-mono); }
.sensor-mini-stats {
  display: flex; gap: 14px; font-size: 12.5px; color: var(--muted-2);
  flex-wrap: wrap;
}
.sensor-mini-stats > div { display: flex; align-items: center; gap: 4px; }

/* === Bin-specific card layout ===
   - Main reading centered up top
   - Time-ago centered just below the main reading
   - Body: silo on the left, secondary readout in the middle
   - Weather strip on its own line below the body, right-justified */
.sensor-card-main {
  text-align: center;
  display: flex; justify-content: center; align-items: baseline; gap: 6px;
  margin-top: 2px;
  /* Fixed-height band (template): the headline reading. Locked so the big number sits on
     the same baseline across every tile — only the data/image band below flexes. */
  min-height: 26px; max-height: 26px; overflow: hidden; flex-shrink: 0;
}
.sensor-card-time {
  display: flex; justify-content: center; align-items: center; gap: 4px;
  font-size: 11.5px;
  margin-top: -4px;
  /* Fixed-height band (template): the "Xh ago" last-reading line. */
  min-height: 16px; max-height: 16px; overflow: hidden; flex-shrink: 0;
}
.bin-body {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 14px;
  align-items: center;
}
.bin-left  { display: flex; justify-content: center; min-width: 0; }
.bin-mid   { min-width: 0; }

.sensor-card-weather {
  display: flex; justify-content: center; align-items: center; gap: 10px;
  font-size: 11.5px; color: var(--muted);
  flex-wrap: nowrap; overflow: hidden;
  padding-top: 4px;
  border-top: 1px dashed color-mix(in oklab, var(--line) 60%, transparent);
  /* Fixed-height band (template): hard-locked like the footer so the weather strip is the
     same height — and the same distance above the footer — on every tile. Empty slots show
     "—" so cards without weather reserve the identical row. */
  min-height: 28px; max-height: 28px; flex-shrink: 0;
}
/* Fixed 22×22 slot for the icon so cards without weather still occupy the same width. */
.sensor-card-weather .weather-icon-slot {
  width: 22px; height: 22px; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
}
.sensor-card-weather img { width: 22px; height: 22px; flex-shrink: 0; }
.sensor-card-weather .weather-cond { color: var(--text-2); white-space: nowrap; }
.sensor-card-weather .weather-item { font-variant-numeric: tabular-nums; white-space: nowrap; }

/* Equal-height tile template. The card stretches to the grid row's tallest card
   (min-height: 100%). Going up from the bottom, every band is a FIXED height — footer,
   weather, reading, header — and the DATA/IMAGE band (.sensor-card-body) is the sole
   flex-grow absorber. So the weather strip always sits the same distance above the footer,
   footers line up across the row, and any height difference is taken up by the data band
   (its gauge/readout stays centred). This banding is the template for any new sensor family. */
.sensor-card { min-height: 100%; }
.sensor-card > .sensor-card-body { flex: 1 1 auto; min-height: 0; }

/* Headroom row above the days/rate grid. Same 2-column grid as the row below
   so the value lands at the AVG RATE column's left edge. */
.readout-stack-row {
  display: grid; grid-template-columns: 1fr 1fr;
  align-items: baseline; gap: 6px 12px;
  margin-bottom: 6px;
}
.readout-stack-row .muted {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; font-weight: 500;
}
.readout-stack-row strong {
  font-size: 14px; font-weight: 600; font-family: var(--font-mono);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Silhouette label overlay (editable or read-only). Positioned over the SVG. */
.silhouette-wrap { position: relative; display: inline-block; }
.silhouette-label {
  position: absolute; left: 50%;
  transform: translateX(-50%);
  text-align: center;
  font-family: var(--font-mono);
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text);
  background: transparent;
  border: 1px solid transparent;
  outline: none;
  padding: 1px 2px;
  /* Subtle outline lifts the label off whatever fill colour is behind it. */
  text-shadow: 0 0 2px var(--surface), 0 0 2px var(--surface);
}
.silhouette-label-input:hover {
  border-color: color-mix(in oklab, var(--text) 18%, transparent);
  border-radius: 3px;
}
.silhouette-label-input:focus {
  border-color: var(--accent);
  background: var(--surface);
  border-radius: 3px;
}
.silhouette-label-input::placeholder { color: var(--muted-2); font-weight: 500; }
.sensor-card-foot {
  display: flex; justify-content: space-between; align-items: center;
  padding-top: 8px; border-top: 1px solid var(--line);
  font-size: 12px; color: var(--muted);
  line-height: 1.2;
  /* Lock the footer to a single 32px line regardless of premise/type text length —
     min-height + max-height + overflow:hidden makes the strip identical across
     every family and prevents long type names (e.g. "VersionState") from wrapping. */
  min-height: 32px;
  max-height: 32px;
  overflow: hidden;
  gap: 8px;
  flex-shrink: 0;
}
.sensor-card-foot > span { display: flex; align-items: center; gap: 6px; min-width: 0; }
.sensor-card-foot > .foot-meta {
  flex: 1; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The inner em (Sensor.Type) must shrink within the flex foot-meta — without min-width:0
   on the foot-meta and the em itself, the em stays at its content width and forces the
   foot-meta to overflow before ellipsis kicks in. */
.sensor-card-foot > .foot-meta > em {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* SiloGauge */
.silo { flex-shrink: 0; }

/* Table layout */
.sensor-table-wrap {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r3);
  overflow: auto;
}
.sensor-table { width: 100%; border-collapse: collapse; font-size: 14px; }
.sensor-table th {
  text-align: left; font-weight: 500; color: var(--muted);
  padding: 12px 16px; border-bottom: 1px solid var(--line);
  font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em;
  background: var(--surface-2);
  position: sticky; top: 0;
}
.sensor-table th.num, .sensor-table td.num { text-align: right; font-family: var(--font-mono); }
.sensor-table td { padding: 12px 16px; border-bottom: 1px solid var(--line); }
.sensor-table tr { cursor: pointer; }
.sensor-table tr:hover { background: var(--soft); }
.sensor-table tr:last-child td { border-bottom: none; }
.td-primary { font-weight: 500; }
.td-sub { font-size: 12px; color: var(--muted); font-family: var(--font-mono); }

/* Threshold picker — table variant with selection */
.sensor-table-pick tr.selected { background: var(--accent-soft); }
.sensor-table-pick tr.selected:hover { background: var(--accent-soft); }
.sensor-table-pick .col-check { width: 40px; padding-right: 0; }
.sensor-table-pick .col-check input[type="checkbox"] { cursor: pointer; }
.sensor-table-pick td.empty-row {
  text-align: center; color: var(--muted); padding: 24px; cursor: default;
}
.sensor-table-pick tr:has(td.empty-row) { cursor: default; }
.sensor-table-pick tr:has(td.empty-row):hover { background: transparent; }
.badge-fill  { background: var(--warn-bg);  color: var(--warn); }
.badge-empty { background: var(--ok-bg);    color: var(--ok);   }

.selection-bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: 10px 0; flex-wrap: wrap;
}
.selection-counts { font-size: 14px; }
.selection-counts strong { color: var(--accent); }
.selection-actions { display: flex; gap: 6px; flex-wrap: wrap; }

.threshold-chip {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 11px; font-weight: 600; letter-spacing: 0.02em;
  padding: 2px 6px; border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--chip-color) 14%, transparent);
  color: var(--chip-color);
  margin-right: 4px;
  font-family: var(--font-mono);
}

.thresholds-pane > .card { margin-bottom: 12px; }
.thresholds-actions {
  display: flex; align-items: center; justify-content: space-between;
  padding: 12px 0;
}
.threshold-form {
  display: grid; gap: 10px;
}
.threshold-kind.seg-sm { font-size: 12px; }
.threshold-kind.seg-sm button { padding: 4px 8px; }

/* Badge */
.badge {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 12px; font-weight: 500;
  padding: 3px 9px; border-radius: var(--r-pill);
  white-space: nowrap;
}

/* ============================================================
   Detail screen
   ============================================================ */
.detail-head { margin-bottom: 20px; }
.back {
  background: transparent; border: none; color: var(--muted);
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 13px; padding: 0; margin-bottom: 12px;
  cursor: pointer;
}
.back:hover { color: var(--text); }
.detail-head-main {
  display: flex; justify-content: space-between; align-items: flex-end;
  gap: 16px; flex-wrap: wrap;
}
.detail-head-actions { display: flex; gap: 8px; }
.detail-eyebrow {
  font-family: var(--font-mono); font-size: 12px; color: var(--muted);
  letter-spacing: 0.04em; text-transform: uppercase;
}
.detail-head h1 { font-size: 28px; margin: 4px 0 10px; }
.detail-meta { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; font-size: 13px; }
.detail-meta .muted { display: inline-flex; align-items: center; gap: 4px; }

.detail-grid {
  display: grid; gap: var(--gap-grid);
  grid-template-columns: 1.2fr 1fr;
  grid-template-areas:
    "hero tiles"
    "chart chart"
    "alerts readings"
    "events events"
    "site site";
}
.card-hero { grid-area: hero; }
/* Bins get a days-remaining card beside the reading; the tiles drop to their own full-width row.
   Only applied via .has-days, so a sensor without the projection keeps the original two-up top row
   instead of reserving an empty column. */
.card-days { grid-area: days; }
.detail-grid.has-days {
  grid-template-areas:
    "hero days"
    "tiles tiles"
    "chart chart"
    "alerts readings"
    "events events"
    "site site";
}
/* WIDE SCREENS: reading, tiles and days all on one row. Three cards abreast only earns its keep
   with real width — below this the days card would be squeezed under ~340px and the arc stops being
   readable, so the two-up layout above takes over instead of letting them all shrink together. */
@media (min-width: 1400px) {
  .detail-grid.has-days {
    grid-template-columns: 1.25fr 1fr 0.85fr;
    grid-template-areas:
      "hero tiles days"
      "chart chart chart"
      "alerts readings readings"
      "events events events"
      "site site site";
  }
  /* The tiles used to collapse to a single column here, to fill a third of a wide row with four
     stacked tiles. There are two tiles now — "last reading" and "reading interval" — and stacking
     them both undoes the pairing the card is built around and re-lengthens the card this layout
     change was meant to shorten. They stay side by side; see .metric-grid-pair. */
}

@media (max-width: 960px) {
  .detail-grid.has-days {
    grid-template-areas: "hero" "days" "tiles" "chart" "alerts" "readings" "events" "site";
  }
}
/* Hugs its content instead of stretching to the hero's height. Without this the card keeps the row
   height it always had and the shortened contents just leave a half-empty box — the page would be
   exactly as long as before, which is the thing this card was reshaped to fix. */
.card-tiles { grid-area: tiles; align-self: start; }
/* .card-site had NO grid-area while every sibling card had one, so it was auto-placed into whatever
   implicit cell the grid happened to leave: the empty "chart" row on a sensor with no history, and a
   half-width row under "events" on one with a chart. It renders unconditionally for every family, so
   naming it in each template costs no empty row.

   LAST ROW, in every variant. It is read-only reference detail from W8Gear — where the thing is and
   what the site is called — not something an operator scans for. Putting it second pushed the chart
   and the events, which are what the page is FOR, below the fold on a laptop. */
.card-site { grid-area: site; }
/* The belt summary renders only for belt weighers, so it cannot be a named area without reserving a
   dead row on every other sensor. Spanning all columns at least makes its placement deterministic
   rather than dependent on what else happens to be on the page. */
.card-belt-summary { grid-column: 1 / -1; }
.card-chart { grid-area: chart; }
.card-alerts { grid-area: alerts; }
.card-readings { grid-area: readings; }
.card-events { grid-area: events; }

@media (max-width: 960px) {
  .detail-grid {
    grid-template-columns: 1fr;
    grid-template-areas: "hero" "tiles" "chart" "alerts" "readings" "events" "site";
  }
}

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r3);
  padding: var(--pad-card);
}
.card-head {
  display: flex; justify-content: space-between; align-items: center;
  margin-bottom: 12px; gap: 12px;
}
.card-head h3 { font-size: 14px; }
.card-sub { font-size: 12px; color: var(--muted); }

.hero-body {
  display: flex; gap: 24px; align-items: center;
  padding: 8px 0;
}
.hero-stats { flex: 1; display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.hero-big {
  font-family: var(--font-mono); font-size: 56px; font-weight: 600;
  letter-spacing: -0.03em; line-height: 1;
  margin-bottom: 8px;
}
.hero-big em { font-style: normal; font-size: 28px; color: var(--muted); margin-left: 2px; }
.hero-line {
  display: flex; justify-content: space-between; gap: 12px;
  padding: 6px 0; font-size: 13px;
  border-bottom: 1px dashed var(--line);
}
.hero-line:last-child { border-bottom: none; }
.hero-line strong { font-family: var(--font-mono); font-weight: 600; }

/* Metric tile */
.metric-grid {
  display: grid; grid-template-columns: 1fr 1fr; gap: 10px;
}
.metric-tile {
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r2);
  padding: 14px;
}
.metric-label {
  display: flex; align-items: center; gap: 6px;
  font-size: 12px; color: var(--muted); text-transform: uppercase;
  letter-spacing: 0.04em; font-weight: 500;
}
.metric-icon { display: inline-flex; color: var(--accent); }
.metric-value {
  font-family: var(--font-mono); font-weight: 600;
  font-size: 24px; letter-spacing: -0.02em; margin-top: 6px;
}
.metric-value .unit { font-size: 14px; color: var(--muted); margin-left: 3px; font-weight: 500; }
.metric-sub { font-size: 12px; color: var(--muted); margin-top: 4px; display: flex; gap: 8px; }
/* A pair that must not break apart. "How recent" and "how often" only answer the question when they
   are read together, so this opts out of every responsive rule that re-columns .metric-grid. */
.metric-grid-pair { grid-template-columns: 1fr 1fr; }
/* Account / premise under the reporting pair: caption weight, not tile weight. */
.tile-facts {
  display: grid; grid-template-columns: 1fr 1fr; gap: 4px 16px;
  margin: 10px 0 0; padding-top: 10px; border-top: 1px dashed var(--line);
}
.tile-facts > div { min-width: 0; }
.tile-facts dt {
  display: flex; align-items: center; gap: 5px;
  font-size: 11px; color: var(--muted);
  text-transform: uppercase; letter-spacing: 0.04em; font-weight: 500;
}
.tile-facts .fact-icon { display: inline-flex; color: var(--accent); }
.tile-facts dd {
  margin: 3px 0 0; font-size: 13px; font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.trend.up { color: var(--ok); }
.trend.down { color: var(--crit); }
.trend.flat { color: var(--muted); }

/* Segmented control */
.seg {
  display: inline-flex; background: var(--soft);
  border-radius: var(--r2); padding: 3px;
}
.seg button {
  background: transparent; border: none; padding: 5px 12px;
  font-size: 12.5px; font-weight: 500; color: var(--muted);
  border-radius: 7px; cursor: pointer;
}
.seg button.active {
  background: var(--surface); color: var(--text);
  box-shadow: var(--shadow-1);
}

/* Severity, as three colours rather than three words in one colour — green / orange / red, on the
   same --ok / --warn / --crit tokens a sensor card uses, so the portal has ONE severity vocabulary.

   Only the ACTIVE button colours. Painting all three at rest turns a control into a traffic light
   that is always on, and the eye then has nothing left to read the selection by. The word stays
   present in every state, because colour alone excludes the readers most affected by red vs green. */
.seg-sev button.active.is-info { background: var(--ok-bg);   color: var(--ok);   }
.seg-sev button.active.is-warn { background: var(--warn-bg); color: var(--warn); }
.seg-sev button.active.is-crit { background: var(--crit-bg); color: var(--crit); }

/* The same three on the badge. <RuleSeverityTag> is the only thing that should set these. */
.sev-tag.is-info { background: var(--ok-bg);   color: var(--ok);   }
.sev-tag.is-warn { background: var(--warn-bg); color: var(--warn); }
.sev-tag.is-crit { background: var(--crit-bg); color: var(--crit); }

/* Dashboard freshness stamp — "updated 2m ago" + inline Refresh, replacing a hard-coded
   "updated just now". Goes amber once the data outlives StaleAfter so silent staleness is visible. */
.page-sub-sep { margin: 0 6px; opacity: 0.5; }
.freshness { display: inline-flex; align-items: center; gap: 6px; }
.freshness.is-stale { color: var(--warn); font-weight: 500; }
.freshness-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--accent); animation: freshness-pulse 1.1s ease-in-out infinite;
}
@keyframes freshness-pulse { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .freshness-dot { animation: none; opacity: 0.8; }
}
.linkish {
  background: none; border: none; padding: 0 0 0 10px; cursor: pointer;
  color: var(--accent); font: inherit; font-size: inherit;
}
.linkish:hover:not(:disabled) { text-decoration: underline; }
.linkish:disabled { color: var(--muted); cursor: default; }
.empty-offline-meta { display: block; margin-top: 10px; font-size: 12px; }

/* Chart header controls — holds the value/% toggle next to the 24h/7d/30d range picker.
   Wraps on narrow cards so the two segmented controls stack instead of squashing the title. */
.chart-controls {
  display: flex; align-items: center; gap: 8px;
  flex-wrap: wrap; justify-content: flex-end;
}

/* Readings table */
.readings-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.readings-table th {
  text-align: left; font-weight: 500; color: var(--muted);
  padding: 8px 12px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em;
  border-bottom: 1px solid var(--line);
}
.readings-table th.num, .readings-table td.num { text-align: right; font-family: var(--font-mono); }
.readings-table td { padding: 8px 12px; border-bottom: 1px solid var(--line); }
.readings-table tr:last-child td { border-bottom: none; }

/* ============================================================
   Alerts
   ============================================================ */
.tab-row {
  display: flex; gap: 4px; border-bottom: 1px solid var(--line);
  margin-bottom: 16px; overflow-x: auto;
}
.tab {
  background: transparent; border: none; padding: 10px 14px;
  font-size: 13.5px; font-weight: 500; color: var(--muted);
  border-bottom: 2px solid transparent; cursor: pointer;
  white-space: nowrap;
}
.tab.active { color: var(--text); border-bottom-color: var(--accent); }
.tab:hover:not(.active) { color: var(--text-2); }

.alert-list {
  list-style: none; padding: 0; margin: 0;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r3); overflow: hidden;
}
.alert-list > li {
  display: flex; gap: 14px; padding: 16px 18px;
  border-bottom: 1px solid var(--line);
  cursor: pointer; align-items: flex-start;
  transition: background 100ms;
}
.alert-list > li:hover { background: var(--soft); }
.alert-list > li:last-child { border-bottom: none; }
.alert-list > li.ack { opacity: 0.65; }
/* A row with no sensor to open (sensor-less alert, or a sensor outside the loaded fleet) must not
   advertise a click-through it cannot honour. */
.alert-list > li.no-target { cursor: default; }
.alert-list > li.no-target:hover { background: transparent; }
.alert-sev { padding-top: 4px; }
.alert-body { flex: 1; min-width: 0; }
.alert-title-row { display: flex; align-items: center; gap: 8px; }
.alert-title { font-weight: 600; font-size: 14.5px; }
.alert-ack {
  font-size: 11px; color: var(--muted); background: var(--soft);
  padding: 2px 8px; border-radius: var(--r-pill);
}
.alert-message { color: var(--text-2); font-size: 13.5px; margin-top: 4px; line-height: 1.5; }
.alert-meta { font-size: 12px; margin-top: 6px; }
.alert-sensorid { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.85em; opacity: 0.7; }
.alert-actions { display: flex; align-items: center; gap: 8px; color: var(--muted); flex-shrink: 0; }

.alert-list.compact { background: transparent; border: none; }
.alert-list.compact > li {
  padding: 10px 0; border-bottom: 1px dashed var(--line);
  gap: 10px;
}
.alert-list.compact > li:hover { background: transparent; }

/* ============================================================
   Map
   ============================================================ */
/* The fleet map fills the content area instead of standing in a fixed 560px box.
   THE HEIGHT CHAIN THIS RELIES ON: html/body/#root are height:100%; .app-shell is a grid with
   min-height:100vh; .app-main is a stretched grid item; .content is flex:1 inside it. That gives
   .content a definite height for the percentage below to resolve against. */
.map-screen {
  display: flex; flex-direction: column;
  height: 100%;
}
/* The head and legend keep their natural size; only the canvas absorbs what is left. Without this
   a long "N sensors have no location" note would be squeezed instead of the map. */
.map-screen > .page-head,
.map-screen > .map-legend { flex: 0 0 auto; }

.map-canvas {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--r3);
  overflow: hidden;
  /* flex:1 replaces the old fixed height. min-height is a FLOOR, not a preference: if that
     percentage chain is ever broken by a layout change upstream, height:100% resolves to auto and a
     flex child with no content would collapse to nothing — a map of zero pixels looks exactly like
     the "no pins" bug this page just had fixed. Degrade to a large box instead. */
  flex: 1 1 auto;
  min-height: 460px;
}
.map-canvas .leaflet-container {
  height: 100%; width: 100%;
  background: var(--surface-2);
  font-family: var(--font-sans);
}
.leaflet-popup-content-wrapper { border-radius: var(--r2); }

/* ============================================================
   Reading-cadence notice
   Warns that a sensor reports too infrequently for W8Gear to compute deltas, which silently
   removes every delta-derived event (bin fills, throughput totals) while the chart keeps drawing.
   Three states share one block so the severe case cannot drift from the warning case.
   ============================================================ */
.cadence-notice {
  display: flex; gap: 10px; align-items: flex-start;
  margin-top: 14px; padding: 12px 14px;
  border: 1px solid var(--line); border-left-width: 3px;
  border-radius: var(--r2);
  font-size: 13px; line-height: 1.5;
}
.cadence-warn   { border-left-color: var(--warn); background: color-mix(in oklab, var(--warn) 8%, var(--surface)); }
.cadence-severe { border-left-color: var(--crit); background: color-mix(in oklab, var(--crit) 8%, var(--surface)); }
.cadence-ok     { border-left-color: var(--ok);   background: color-mix(in oklab, var(--ok)   6%, var(--surface)); }
.cadence-headline { font-weight: 600; margin-bottom: 4px; }
.cadence-detail   { color: var(--muted); }
.cadence-detail strong { color: var(--text); font-weight: 600; }
.cadence-suggest {
  margin-top: 8px; padding-top: 8px; border-top: 1px dashed var(--line);
  color: var(--muted);
}
.cadence-suggest strong { color: var(--text); }
.cadence-suggest code {
  font: 500 12px 'JetBrains Mono', monospace;
  background: var(--surface-2); padding: 1px 5px; border-radius: var(--r1);
}
/* The trade-off line sits under the suggestion rather than beside it — it qualifies the advice and
   must not read as part of the instruction. */
.cadence-suggest .muted { display: block; margin-top: 6px; font-size: 12.5px; }

/* Compact read-only map on the sensor detail Site & location card.
   Resets BOTH the flex growth and the min-height floor from .map-canvas — this one sits in a card,
   not a full-height screen, and min-height would otherwise beat its 260px outright (min-height
   always wins over height) and blow the card open to 460px. */
.map-canvas-sm { flex: 0 0 auto; height: 260px; min-height: 0; border-radius: var(--r2); }
.map-coords { margin-top: 8px; font: 500 12px 'JetBrains Mono', monospace; }

/* Read-only site-info (PORTAL_INFO) list on the detail page. */
.site-info { margin: 14px 0 0; display: flex; flex-direction: column; gap: 8px; }
.site-info-row { display: flex; justify-content: space-between; gap: 16px; font-size: 13.5px; }
.site-info-row dt { color: var(--muted); }
.site-info-row dd { margin: 0; color: var(--text); text-align: right; }
.map-grid {
  position: absolute; inset: 0;
  background-image:
    linear-gradient(var(--line) 1px, transparent 1px),
    linear-gradient(90deg, var(--line) 1px, transparent 1px);
  background-size: 40px 40px;
  opacity: 0.5;
}
.map-placeholder-label {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  text-align: center; font-family: var(--font-mono); font-size: 12px;
  color: var(--muted-2); pointer-events: none; line-height: 1.6;
}
.map-pin {
  position: absolute; transform: translate(-50%, -100%);
  background: transparent; border: none; cursor: pointer;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
}
.pin-dot {
  width: 14px; height: 14px; border-radius: 50% 50% 50% 0;
  background: var(--pin-color); transform: rotate(-45deg);
  box-shadow: 0 2px 6px rgba(0,0,0,0.2), 0 0 0 3px color-mix(in oklab, var(--pin-color) 25%, transparent);
}
.pin-label {
  font-family: var(--font-mono); font-size: 11px; font-weight: 600;
  background: var(--surface); padding: 2px 6px; border-radius: var(--r-pill);
  border: 1px solid var(--line); margin-top: 4px;
}
.map-legend {
  display: flex; gap: 18px; margin-top: 12px; font-size: 13px; color: var(--muted);
  flex-wrap: wrap;
}
.map-legend span { display: flex; align-items: center; gap: 6px; }

/* ============================================================
   Settings
   ============================================================ */
.settings-layout {
  display: grid; grid-template-columns: 200px 1fr; gap: 20px;
}
.settings-nav {
  display: flex; flex-direction: column; gap: 2px;
}
/* Scope group headers — which slice of the world a settings section applies to. */
.settings-nav-group {
  display: flex; flex-direction: column; gap: 1px;
  margin: 14px 0 4px; padding: 0 12px;
}
.settings-nav-group:first-child { margin-top: 0; }
.settings-nav-group .group-label {
  font-size: 10.5px; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--muted);
}
.settings-nav-group .group-scope {
  font-size: 11px; font-weight: 500; color: var(--text-2);
}
.settings-nav a {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px; border-radius: var(--r2);
  color: var(--text-2); font-size: 14px; font-weight: 500; cursor: pointer;
  white-space: nowrap;
}
.settings-nav a:hover { background: var(--soft); color: var(--text); }
.settings-nav a.active { background: var(--accent-soft); color: var(--accent); }
/* Mobile: stack the layout and turn the side nav into a horizontal scroll strip. Placed AFTER the base
   .settings-nav* rules on purpose — a media query adds no specificity, so source order must win. */
@media (max-width: 720px) {
  .settings-layout { grid-template-columns: 1fr; }
  .settings-nav { flex-direction: row; overflow-x: auto; gap: 6px; padding-bottom: 4px; }
  .settings-nav a { flex: 0 0 auto; }
  /* Horizontal tab strip has no room for group headers; each pane restates its own scope. */
  .settings-nav-group { display: none; }
}
.settings-card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r3); padding: 24px;
}
.settings-card > h3 { font-size: 17px; margin-bottom: 6px; }
.settings-card > p.muted { margin-bottom: 20px; font-size: 13px; }
.settings-card .form-grid { margin-bottom: 16px; }
.settings-foot { display: flex; gap: 8px; padding-top: 8px; border-top: 1px solid var(--line); margin-top: 16px; padding-top: 16px; }

/* Help page — definition rows (W8Sense names them help-* because .config-row is taken by the
   sensor configure form grid; visually identical to the reference portals' config-spec rows). */
.help-lead { margin: 0 0 14px; font-size: 14.5px; }
.help-spec { display: flex; flex-direction: column; gap: 0; margin: 0; }
.help-row {
  display: grid; grid-template-columns: 160px 1fr; gap: 12px; align-items: center;
  padding: 10px 0; border-bottom: 1px solid var(--line);
}
.help-row:last-child { border-bottom: none; }
.help-row dt { font-weight: 600; font-size: 13.5px; color: var(--text); }
.help-row dd { margin: 0; font-size: 13.5px; color: var(--text-2); line-height: 1.5; }
.help-list { margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 8px; }
.help-list li { font-size: 14px; color: var(--text); line-height: 1.5; }
.help-list code, .help-row code { font-size: 12.5px; padding: 1px 5px; border-radius: var(--r1); background: var(--soft); }
@media (max-width: 560px) { .help-row { grid-template-columns: 1fr; gap: 2px; } }

/* ---------- unacknowledged-alert indicator, on the tile and the table row ----------
   Deliberately NOT status-coloured. An unacknowledged alert is a different fact from Sensor.Status —
   status is the live threshold state right now, this is history nobody has picked up — so a "Normal"
   tile can carry a bell. Tinting it by severity would make it read as a second StatusBadge, which this
   tile has a standing rule against. */
.sensor-card-bell,
.sensor-row-bell {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--crit);
  flex-shrink: 0;
}
.sensor-card-bell { margin-right: 2px; }

/* Inline with the status dot rather than below it — the cell is one line tall in every layout. */
.sensor-row-bell { margin-left: 6px; vertical-align: middle; }

/* The detail card's per-row acknowledge. The dimmed-when-acknowledged treatment already exists as
   .alert-list > li.ack, so that is reused rather than duplicated. The row is already display:flex, so
   margin-left:auto pushes the button to the trailing edge. */
.alert-list .alert-ack { margin-left: auto; flex-shrink: 0; align-self: center; }
.alert-unack-count { color: var(--crit); font-weight: 600; }
.dot-sep { opacity: 0.5; margin: 0 5px; }

/* ---------- subscriptions, in Settings → Notifications ----------
   A MATRIX: a row per alert, a column per channel. It answers "which alerts reach me by SMS?" down a
   column, which a list of per-row checkbox clusters could only answer by being read end to end. Same
   shape as W8Alert's entitlement grid on purpose — the two screens are the two halves of one decision,
   so recognising one should teach the other. */
.sub-table-wrap { overflow-x: auto; margin: 4px 0 2px; }
.sub-table {
  width: 100%; border-collapse: collapse; font-size: 14px;
  background: var(--surface); border-radius: var(--r3, 14px); overflow: hidden;
}
.sub-table th {
  text-align: left; color: var(--muted); font-weight: 600; font-size: 11px;
  letter-spacing: 0.05em; text-transform: uppercase;
  padding: 9px 10px; border-bottom: 1px solid color-mix(in srgb, var(--text) 12%, transparent);
  white-space: nowrap;
}
.sub-table td {
  padding: 10px; border-bottom: 1px solid color-mix(in srgb, var(--text) 7%, transparent);
  vertical-align: middle;
}
.sub-table tr:last-child td { border-bottom: none; }

/* The name column takes the width the channel columns do not need; the rest are as narrow as their
   header allows so the checkboxes sit close enough to scan as a grid rather than as scattered marks. */
.sub-col-name { width: auto; }
.sub-col-rule, .sub-col-ch, .sub-col-when { width: 1%; text-align: center; }
.sub-rule-cell, .sub-cell { text-align: center; white-space: nowrap; }
.sub-name-cell { min-width: 220px; }

.sub-name { font-weight: 600; font-size: 13.5px; }
.sub-desc { font-size: 12.5px; line-height: 1.45; margin-top: 2px; }

/* A subscribed row is tinted so the ones you are actually on stand out of a long list at a glance. */
.sub-table tr.is-subscribed { background: color-mix(in srgb, var(--accent, #0C6F79) 5%, transparent); }

/* Rule column: read-only by design. A tick, not a checkbox — whether a rule emits this is a fact about
   the tenant, and a control here would invite somebody to try switching an alert on from a column that
   cannot do it. */
.sub-rule-yes { color: var(--ok, green); display: inline-flex; }
.sub-rule-no { opacity: 0.55; }

/* The checkbox alone, no label — the column header names the channel, so repeating it in every cell
   would triple the row height for no information. */
.check-bare { display: inline-flex; align-items: center; justify-content: center; gap: 0; }
.check-bare span { display: none; }

/* An empty cell would read as "unticked". The dash says the box is not on offer at all, and its title
   says WHICH of the two bounds refused it — the remedies differ (one is permanent, one is a phone
   call). */
.sub-cell-off { opacity: 0.4; cursor: help; }

.sub-notoffered { font-size: 12px; }
.sub-noconsent { font-size: 12px; font-style: italic; }
.sub-withheld { margin-top: 2px; }

/* ---------- narrowing: conditions + an active window (W8Alert ADR 0011) ----------
   The When column is a SUMMARY that opens an editor, not a control in itself. A cell that fitted the
   whole thing would need six inputs at 60px wide; a cell that says "2 conditions - 06:00-12:00" answers
   the scanning question ("which of these did I narrow?") in the space a column actually has. */
.sub-when-cell { text-align: center; white-space: nowrap; }
.sub-when-btn {
  font: inherit; font-size: 12.5px; color: var(--muted); cursor: pointer;
  background: none; border: 1px dashed color-mix(in srgb, var(--text) 22%, transparent);
  border-radius: var(--r-pill, 999px); padding: 3px 10px; white-space: nowrap;
}
.sub-when-btn:hover { color: var(--text); border-color: var(--accent); }

/* Solid once open, so the row it belongs to is obvious when the editor pushes the grid apart. */
.sub-when-btn.is-open {
  border-style: solid; border-color: var(--accent);
  color: var(--accent-text, var(--accent)); background: var(--accent-soft, transparent);
}

.sub-editor-row > td { padding: 0; background: color-mix(in srgb, var(--text) 3%, transparent); }
.sub-editor {
  display: flex; flex-wrap: wrap; gap: 22px 34px; align-items: flex-start;
  padding: 16px 14px 14px;
  border-left: 3px solid var(--accent);
}
.sub-editor-block { display: flex; flex-direction: column; gap: 8px; min-width: 260px; }
.sub-editor-head {
  font-size: 11px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase;
  color: var(--muted);
}
.sub-editor-note { font-size: 12px; margin: 0 0 8px; max-width: 46ch; line-height: 1.5; }
.sub-editor-note:last-child { margin-bottom: 0; }
.sub-editor-note strong { color: var(--text); }
.sub-editor-note a { color: var(--accent); }

/* "+ Add a condition" sits under a stack of inputs, so it needs to read as a control rather than as
   another line of hint text. btn-ghost gives it a border on hover; this keeps it legible at rest. */
.sub-cond-add { margin-top: 2px; }

/* One condition per line. The value box takes the slack so a long threshold does not push the delete
   button off the row on a narrow screen. */
.sub-cond { display: flex; gap: 6px; align-items: center; flex-wrap: nowrap; }
.sub-cond-var, .sub-cond-op, .sub-cond-val {
  font: inherit; font-size: 13px; padding: 5px 8px;
  border: 1px solid var(--line); border-radius: var(--r1, 8px);
  background: var(--surface); color: var(--text); min-width: 0;
}
.sub-cond-var { flex: 1 1 130px; }
.sub-cond-op { flex: 0 1 140px; }
.sub-cond-val { flex: 1 1 90px; }
.sub-cond-del { flex: 0 0 auto; color: var(--muted); }
.sub-cond-del:hover { color: var(--crit, #b3261e); }
.sub-cond-add { align-self: flex-start; }

.sub-window { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.sub-window-time {
  font: inherit; font-size: 13px; padding: 5px 8px;
  border: 1px solid var(--line); border-radius: var(--r1, 8px);
  background: var(--surface); color: var(--text);
}

/* Toggles rather than checkboxes: seven of them read as a week at a glance, which is the question
   ("weekdays?") people actually ask of this control. */
.sub-days { display: flex; gap: 4px; flex-wrap: wrap; }
.sub-day {
  font: inherit; font-size: 12px; cursor: pointer; padding: 4px 9px;
  border: 1px solid var(--line); border-radius: var(--r-pill, 999px);
  background: var(--surface); color: var(--muted); min-width: 42px;
}
.sub-day:hover { border-color: var(--accent); color: var(--text); }
.sub-day.is-on {
  background: var(--accent); border-color: var(--accent);
  color: var(--accent-text, #fff); font-weight: 600;
}

/* The hours-a-week line. Sits directly under the day buttons because it is a consequence OF them, and
   is deliberately not styled as a warning: a weekday-only window is a legitimate choice, and dressing
   the arithmetic up as an error would teach people to dismiss it. */
.sub-coverage { margin-top: 4px; }
.sub-coverage strong { color: var(--text); font-variant-numeric: tabular-nums; }

.sub-editor-actions { display: flex; gap: 8px; align-items: center; align-self: flex-end; }

@media (max-width: 560px) {
  .sub-name-cell { min-width: 160px; }
  .sub-editor { gap: 16px; padding: 14px 12px; }
  .sub-editor-block { min-width: 0; width: 100%; }
}

/* ---------- the condition reference (/help/conditions) ----------
   One block per alert condition. There are 33 of them on one page, so the block has to carry its own
   boundary: without a rule between them the eye loses which answer belongs to which condition halfway
   down a family, and this is a page people read by scanning for one name. */
.condition-block { padding: 14px 0; border-top: 1px solid var(--line); }
.condition-block:first-of-type { border-top: none; padding-top: 4px; }
.condition-head {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-bottom: 8px;
}
.condition-name { font-weight: 600; font-size: 15px; }
.condition-key {
  font-family: var(--font-mono); font-size: 12px;
  background: var(--soft); color: var(--muted);
  padding: 2px 7px; border-radius: var(--r1);
}
/* The prose here runs to whole paragraphs, and the shared .help-row centres its label against the
   value — which leaves "Comes from" floating beside the middle of a six-line answer. Top-aligned is
   the only thing that reads as a definition list once the values get long. */
.condition-block .help-row { align-items: start; }
.condition-block .help-row dt { padding-top: 1px; }
.condition-params { padding-left: 16px; gap: 5px; }
.condition-params li { font-size: 13px; color: var(--text-2); }

/* ---------- the wizard's inline condition guide ----------
   Quieter than the accent-bordered function-key panel directly above it: that panel is the ACTION
   (a key to go and check in W8Alert), this is reference the reader consults and moves on from. Giving
   them the same weight made the step read as two competing conclusions. */
.guide-panel {
  border: 1px solid var(--line); border-radius: var(--r2);
  background: var(--surface-2); padding: 14px; margin-top: 12px;
}
.guide-head {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  font-size: 13px; font-weight: 600; color: var(--text);
  margin-bottom: 6px;
}
.guide-head :where(svg) { color: var(--muted); }
.guide-trigger { margin-left: auto; }
/* Long prose against a fixed label column: top-aligned, or the label floats beside the middle of a
   six-line answer. Narrower label than the help page's 160px — the wizard column is tighter. */
.guide-spec .help-row { grid-template-columns: 132px 1fr; align-items: start; padding: 8px 0; }
.guide-spec .help-row dt { font-size: 12.5px; padding-top: 1px; }
.guide-spec .help-row dd { font-size: 12.5px; }
.guide-params { padding-left: 15px; gap: 4px; }
.guide-params li { font-size: 12.5px; color: var(--text-2); }
@media (max-width: 560px) { .guide-spec .help-row { grid-template-columns: 1fr; gap: 2px; } }

.settings-subhead {
  font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--muted); font-weight: 600;
  margin: 24px 0 10px;
}
.pw-form { display: flex; flex-direction: column; gap: 14px; max-width: 460px; }
.row-between { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }

/* Toggle */
.toggle-list { display: flex; flex-direction: column; }
.toggle-row {
  display: flex; justify-content: space-between; align-items: center;
  padding: 14px 0; border-bottom: 1px solid var(--line);
  gap: 12px;
}
.toggle-row:last-child { border-bottom: none; }
.toggle-label { font-weight: 500; font-size: 14px; }
.toggle-desc { font-size: 12.5px; margin-top: 2px; }
.toggle {
  width: 40px; height: 22px; border-radius: var(--r-pill);
  background: var(--line-strong); border: none; position: relative;
  cursor: pointer; transition: background 160ms; flex-shrink: 0;
}
.toggle.on { background: var(--accent); }
.toggle-knob {
  position: absolute; top: 2px; left: 2px;
  width: 18px; height: 18px; border-radius: 50%;
  background: white; transition: transform 160ms;
  box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
.toggle.on .toggle-knob { transform: translateX(18px); }

/* Team */
.team-list { display: flex; flex-direction: column; }
.team-row {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 0; border-bottom: 1px solid var(--line);
}
.team-row:last-child { border-bottom: none; }
.team-meta { flex: 1; min-width: 0; }
.team-name { font-weight: 500; font-size: 14px; }
.team-email { font-size: 12.5px; }
.role-pill {
  font-size: 11.5px; font-weight: 500; padding: 3px 10px;
  border-radius: var(--r-pill);
  background: var(--soft); color: var(--text-2);
}
.role-pill.role-admin { background: var(--accent-soft); color: var(--accent); }
.role-pill.role-engineer { background: var(--info-bg); color: var(--info); }
.role-pill.role-viewer { background: var(--soft); color: var(--muted); }

/* Empty states */
.empty { padding: 60px; text-align: center; color: var(--muted); }
.empty-small { padding: 16px; text-align: center; color: var(--muted); font-size: 13px; }

/* W8Gear-offline empty-state — visually distinct from a genuine "no matches" so users read it as
   a back-end reachability problem, not a filter result. */
.empty-offline {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 10px; padding: 56px 24px;
}
.empty-offline-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 48px; height: 48px; border-radius: 50%;
  background: var(--warn-bg); color: var(--warn);
}
.empty-offline strong { font-size: 15px; color: var(--text); }
.empty-offline p { margin: 0; max-width: 380px; font-size: 13px; color: var(--muted); }

/* Loading state — centered spinner + message. Used by the dashboard while sensors fetch. */
.dashboard-loading {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 14px; padding: 80px 20px; color: var(--muted);
}
.dashboard-loading p { margin: 0; font-size: 14px; }
.spinner {
  width: 36px; height: 36px; border-radius: 50%;
  border: 3px solid color-mix(in oklab, var(--accent) 18%, transparent);
  border-top-color: var(--accent);
  animation: w8sense-spin 0.8s linear infinite;
}
@keyframes w8sense-spin {
  to { transform: rotate(360deg); }
}

/* Sparkline */
.sparkline { display: block; }
.linechart { display: block; overflow: visible; }
/* The chart SVG scales non-uniformly (see MetricLineChart's preserveAspectRatio note), which would
   otherwise draw a vertical rule at (cardWidth/_width)px and a horizontal one at 1px — mismatched
   weights on the same grid. non-scaling-stroke pins every stroke to its CSS width regardless. */
.linechart line, .linechart path { vector-effect: non-scaling-stroke; }

/* ============================================================
   MetricLineChart — value-aware line chart with event markers
   Belt throughput/speed overlay, reading history, and point-in-time event overlays.
   The hover crosshair is pure CSS (`.mc-hit:hover + .mc-hover`, an adjacent sibling on a
   transparent hit slice) — no JS, so it still works in the installed PWA offline.
   ============================================================ */
.metric-chart { display: flex; flex-direction: column; gap: 10px; }
.metric-svg { display: block; width: 100%; overflow: visible; font-family: var(--font-mono); }

.metric-chart-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; padding: 0 2px;
}
.metric-legend { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px; }
.metric-legend-item {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; color: var(--muted); white-space: nowrap;
}
.metric-legend-note { color: var(--muted-2); font-size: 11px; font-style: italic; }
.metric-legend-count {
  font-family: var(--font-mono); font-style: normal; font-size: 11px; font-weight: 600;
  color: var(--text-2); background: var(--soft);
  padding: 1px 6px; border-radius: var(--r-pill);
}
.metric-swatch { width: 14px; height: 3px; border-radius: 2px; display: inline-block; }
/* Dashed swatch for the secondary series — masked so it keeps its inline background colour. */
.metric-swatch.dashed {
  -webkit-mask-image: repeating-linear-gradient(90deg, #000 0 5px, transparent 5px 8px);
          mask-image: repeating-linear-gradient(90deg, #000 0 5px, transparent 5px 8px);
}
.metric-ev-swatch { width: 12px; height: 12px; display: block; overflow: visible; }

.metric-stats { display: flex; flex-wrap: wrap; gap: 4px 16px; align-items: baseline; }
.metric-stat { display: inline-flex; align-items: baseline; gap: 6px; white-space: nowrap; }
.metric-stat em {
  font-style: normal; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--muted-2);
}
.metric-stat b { font-family: var(--font-mono); font-size: 12px; font-weight: 600; color: var(--text-2); }
.metric-stat.is-latest b { color: var(--text); }

/* --- SVG internals --- */
.mc-grid { stroke: var(--line); stroke-width: 1; stroke-dasharray: 1 5; }
.mc-axis { stroke: var(--line-strong); stroke-width: 1; }
.mc-xtick { stroke: var(--line-strong); stroke-width: 1; opacity: 0.7; }
.mc-daybreak { stroke: var(--line); stroke-width: 1; opacity: 0.55; }
/* Inter with tabular figures, not JetBrains Mono. Monospace digits read as a terminal readout, and
   because this SVG scales non-uniformly on X, a monospace face is exactly the one whose stretch is
   most obvious — every glyph is the same width, so the distortion repeats identically across the
   axis. tabular-nums keeps the column alignment that motivated the mono face in the first place. */
.mc-tick { font: 500 11px var(--font-sans); font-variant-numeric: tabular-nums; fill: var(--muted); }
.mc-tick-sub { font-size: 10px; fill: var(--muted-2); }
.mc-unit { font: 600 10px var(--font-sans); fill: var(--muted-2); letter-spacing: 0.04em; }

.mc-line { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.mc-line-secondary { stroke-width: 1.75; stroke-dasharray: 5 4; opacity: 0.9; }

.mc-extreme { fill: var(--surface); stroke-width: 1.75; cursor: help; }
/* No tooltip of their own — let the hover crosshair underneath keep working through them. */
.mc-last-halo { opacity: 0.18; pointer-events: none; }
.mc-last { stroke: var(--surface); stroke-width: 2; pointer-events: none; }
.mc-last-label { font: 600 11px var(--font-mono); pointer-events: none; }

/* Event markers — discrete points in time, never joined by a path. The stem drops to the baseline
   so the eye can read WHEN it happened off the X axis without a connecting line implying a trend. */
.mc-event { cursor: help; }
/* The stem is decoration only — pointer-events off so it can't block the crosshair for the whole
   column it crosses. The glyph itself stays hoverable and carries the <title>. */
.mc-event-stem {
  stroke: var(--line-strong); stroke-width: 1; stroke-dasharray: 2 3; opacity: 0.45;
  pointer-events: none;
}
.mc-event-mark { stroke: var(--surface); stroke-width: 1.5; stroke-linejoin: round; }
.mc-event:hover .mc-event-stem { opacity: 1; }
.mc-event:hover .mc-event-mark { stroke: var(--text); }

/* Hover crosshair + readout */
.mc-hit { fill: transparent; pointer-events: all; }
.mc-hover { opacity: 0; pointer-events: none; transition: opacity 100ms ease; }
.mc-hit:hover + .mc-hover { opacity: 1; }
.mc-cross { stroke: var(--line-strong); stroke-width: 1; stroke-dasharray: 3 3; }
.mc-cross-dot { stroke: var(--surface); stroke-width: 2; }
.mc-tip {
  fill: var(--surface); stroke: var(--line-strong); stroke-width: 1;
  filter: drop-shadow(0 2px 6px rgba(15, 22, 32, 0.16));
}
.mc-tip-time { font: 500 10.5px var(--font-mono); fill: var(--muted); }
.mc-tip-val { font: 600 12px var(--font-mono); }

/* Honest empty state — what a range with no readings now shows, instead of the page quietly
   widening the window and mislabelling a month of history as "24h". */
.metric-empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px; padding: 40px 16px; text-align: center;
  border: 1px dashed var(--line); border-radius: var(--r2);
  background: var(--surface-2);
}
.metric-empty-title { font-size: 13px; font-weight: 500; color: var(--text-2); }
.metric-empty-hint { font-size: 12px; color: var(--muted); }

@media (max-width: 620px) {
  .metric-chart-head { flex-direction: column; gap: 8px; }
  .metric-stats { gap: 4px 12px; }
}
@media (prefers-reduced-motion: reduce) {
  .mc-hover { transition: none; }
}

/* Scrim */
.scrim {
  position: fixed; inset: 0; background: rgba(0,0,0,0.4); z-index: 50;
  display: none;
}

/* ============================================================
   Responsive (mobile/tablet)
   ============================================================ */
@media (max-width: 980px) {
  .summary-row { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 768px) {
  .app-shell { grid-template-columns: 1fr; }
  .sidebar {
    position: fixed; top: 0; left: 0; height: 100vh; width: 280px;
    transform: translateX(-100%); transition: transform 220ms;
    z-index: 60; box-shadow: var(--shadow-3);
  }
  .sidebar.open { transform: translateX(0); }
  .sidebar-close { display: inline-flex; }
  .mobile-menu { display: inline-flex; }
  .scrim { display: block; }
  .bottom-nav {
    display: flex; justify-content: space-around;
    background: var(--surface); border-top: 1px solid var(--line);
    padding: 4px 4px calc(env(safe-area-inset-bottom, 0) + 4px);
    position: sticky; bottom: 0; z-index: 30;
  }
  .bnav-item {
    display: flex; flex-direction: column; align-items: center; gap: 2px;
    padding: 8px 12px; color: var(--muted); font-size: 11px; font-weight: 500;
    border-radius: var(--r2); cursor: pointer; position: relative;
    flex: 1; max-width: 100px;
  }
  .bnav-item.active { color: var(--accent); }
  .bnav-badge {
    position: absolute; top: 4px; right: 22px;
    background: var(--crit); color: white;
    font-size: 10px; font-style: normal; font-weight: 600;
    padding: 1px 5px; border-radius: var(--r-pill); min-width: 16px; text-align: center;
  }
  .content { padding: 16px; padding-bottom: 80px; }
  .page-head { flex-direction: column; align-items: stretch; }
  .page-head-actions { justify-content: stretch; }
  .page-head-actions .btn { flex: 1; }
  .detail-head h1 { font-size: 22px; }
  .hero-body { flex-direction: column; align-items: stretch; }
  .hero-big { font-size: 44px; }
  h1 { font-size: 20px; }
}
@media (max-width: 480px) {
  .summary-row { grid-template-columns: 1fr 1fr; gap: 8px; }
  .summary-tile { padding: 12px; }
  .summary-value { font-size: 24px; }
  /* Filter bar on phones: search input gets a full row, the rest of the controls
     (Account / Premise / Status / Type selects, Fill/Empty segment) wrap to half-width
     pairs so each control has enough room to render its label text in full. */
  .filter-bar { gap: 8px; }
  .filter-bar .search-input { flex: 1 1 100%; min-width: 0; }
  .filter-bar .select { flex: 1 1 calc(50% - 4px); min-width: 0; max-width: 100%; }
  .filter-bar .seg { flex: 1 1 100%; }
  .sensor-grid { grid-template-columns: 1fr; }
  .metric-grid { grid-template-columns: 1fr 1fr; }
  .sensor-card-body { flex-direction: column; }
  .sensor-card-body .silo, .sensor-card-body .silhouette-gauge { align-self: center; }
  /* Tighten table padding on phones so the remaining columns breathe. */
  .sensor-table th, .sensor-table td { padding: 10px 8px; }
}

/* ----- Responsive column visibility utility ----- *
 * Tag a <th>/<td> with hide-phone  → hidden at ≤480px (still visible on tablet/desktop).
 * Tag with hide-tablet → hidden at ≤768px (so also hidden on phone).
 * No class → always visible.
 * Used by SensorTable to drop low-value columns on small screens.
 */
@media (max-width: 768px) {
  .hide-tablet { display: none !important; }
}
@media (max-width: 480px) {
  .hide-phone { display: none !important; }
}

/* ============================================================
   v2 — Multi-family sensor support
   ============================================================ */

/* Sidebar section headings — the two scope groups (Tenant workspace / Global) */
.sidebar-section {
  display: flex; align-items: center; gap: 6px;
  font-size: 10.5px; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--muted);
  padding: 16px 14px 6px;
  margin-top: 4px;
}
.sidebar-section svg { color: var(--muted-2); }
/* The very first section (Tenant workspace) sits under the logo — trim its top padding. */
.sidebar-section--first { padding-top: 10px; margin-top: 0; }
/* Configuration sub-header inside a nav group — subordinate to the section heading above it. */
.sidebar-subsection {
  display: flex; align-items: center; gap: 6px;
  font-size: 9.5px; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--muted-2);
  padding: 8px 14px 3px 16px;
}
.sidebar-subsection svg { color: var(--muted-2); opacity: 0.8; }
/* Global block sits below the tenant workspace as a secondary group — divide them with a rule. */
.sidebar-nav--global { border-top: 1px solid var(--line); margin-top: 4px; }

/* Scroll container holding the stacked nav groups; the head + foot stay pinned. */
.sidebar-scroll {
  flex: 1; min-height: 0; overflow-y: auto;
  display: flex; flex-direction: column;
}
.sidebar-scroll::-webkit-scrollbar { width: 6px; }
.sidebar-scroll::-webkit-scrollbar-thumb { background: var(--line); border-radius: 3px; }

/* Breadcrumb anchor styling */
.breadcrumb a {
  cursor: pointer;
  text-decoration: none;
}
.breadcrumb a:hover { color: var(--text); }

/* Sensor card adapts to family */
.sensor-card[data-family="state"] .sensor-card-body,
.sensor-card[data-family="map"] .sensor-card-body,
.sensor-card[data-family="counter"] .sensor-card-body,
.sensor-card[data-family="numeric"] .sensor-card-body {
  align-items: center;
}
/* The detail hero's gauge slot. Named for the class the markup ACTUALLY carries — this rule spent
   its life as `.sensor-gauge-wrap`, which no component has ever rendered, so the hero gauge was an
   unstyled block: not centred when .hero-body stacks under 768px, and with nothing stopping
   .hero-stats (flex: 1) from squeezing it. Two names for one thing is how a rule goes dead without
   anything visibly breaking. */
.hero-gauge-wrap {
  flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  min-width: 80px;
}

/* Card readout */
.readout-row {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
}
.readout-big {
  font-family: var(--font-mono); font-weight: 600; font-size: 24px;
  letter-spacing: -0.02em; line-height: 1;
}
.readout-big em {
  font-style: normal; color: var(--muted); font-size: 14px;
  font-weight: 500; margin-left: 2px;
}
.readout-cap {
  font-size: 12px; font-family: var(--font-mono);
}
.readout-grid {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: 6px 12px;
  font-size: 12px;
}
.readout-grid-3 {
  grid-template-columns: 1fr 1fr 1fr;
  margin-top: 4px;
}
/* VALUE UNDER ITS HEADING, FULL WIDTH — for families with only a couple of metrics.
   Label-left/value-right splits the column in half, and half of an already narrow column could
   not hold a value like "-50.0–1000.0 kPa": it was ellipsised to "-50.0-1..." on the pressure and
   dial tiles, which is worse than useless — a truncated number reads as a real one. Giving the
   value the whole width fixes it without shrinking the type. Deliberately NO nowrap/ellipsis
   here: if a value is still too long it wraps to a second line, because a wrapped number is
   readable and a clipped one is not. */
.readout-col { display: flex; flex-direction: column; gap: 8px; }
.readout-col > div { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.readout-col .muted {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; font-weight: 500;
}
.readout-col strong {
  font-size: 14px; font-weight: 600; line-height: 1.25;
  overflow-wrap: anywhere;
}
.readout-col strong.num { font-family: var(--font-mono); }
.readout-grid > div { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.readout-grid .muted {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; font-weight: 500;
}
.readout-grid strong {
  font-size: 13px; font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.readout-grid strong.num { font-family: var(--font-mono); }

.foot-meta {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 12px; color: var(--muted);
}
.foot-meta em { font-style: normal; font-family: var(--font-mono); font-size: 11px; flex-shrink: 0; }

/* SVG gauges base */
.silhouette-gauge, .dial-gauge { display: block; }

/* Numeric readout */
.numeric-readout {
  display: flex; flex-direction: column; gap: 8px;
  padding: 4px;
  width: 100% !important;
}
.numeric-big {
  font-family: var(--font-mono); font-weight: 700;
  letter-spacing: -0.02em; line-height: 1;
}
.numeric-big .num-unit {
  font-size: 0.5em; color: var(--muted); font-weight: 500;
  margin-left: 4px;
}
.numeric-bar { display: flex; flex-direction: column; gap: 4px; }
.numeric-bar-track {
  position: relative; height: 8px; background: var(--soft);
  border-radius: 4px; overflow: visible;
}
.numeric-bar-fill {
  height: 100%; border-radius: 4px;
  transition: width 200ms;
}
.numeric-bar-tick {
  position: absolute; top: -2px; bottom: -2px;
  width: 2px;
}
.numeric-bar-meta {
  display: flex; justify-content: space-between;
  font-size: 11px; color: var(--muted); font-family: var(--font-mono);
}

/* Belt weigher gauge (conveyor) + readout */
.belt-gauge { display: block; }
.belt-flow {
  stroke-dasharray: 5 7;
  animation: belt-flow-scroll var(--belt-flow-dur, 2s) linear infinite;
}
.belt-flow.paused { animation: none; stroke-dasharray: 2 6; stroke-opacity: 0.3 !important; }
@keyframes belt-flow-scroll { to { stroke-dashoffset: -24; } }
@media (prefers-reduced-motion: reduce) { .belt-flow { animation: none; } }

/* Belt card readout: Feed/Load/Speed stack full-width (label auto, value right-aligned) so values
   never truncate; Shift/Batch share the 2-col grid below, values left-aligned under their labels. */
.belt-readout .readout-stack-row { grid-template-columns: auto 1fr; gap: 6px 10px; margin-bottom: 5px; }
.belt-readout .readout-stack-row strong { overflow: visible; text-overflow: clip; text-align: right; white-space: nowrap; }
.belt-readout .readout-grid { gap: 4px 10px; }
.belt-readout .readout-grid strong { overflow: visible; text-overflow: clip; text-align: left; white-space: nowrap; }

/* Belt left column: conveyor image, with the period Total above the run-state indicator beneath it. */
.belt-left { flex-direction: column; gap: 3px; }
.belt-total { font-size: 11px; color: var(--muted); font-family: var(--font-mono); white-space: nowrap; }
.belt-run { font-size: 11.5px; font-weight: 600; white-space: nowrap; letter-spacing: 0.02em; }
/* Belt gauge at text sizes (lg/xl): stack the SVG image and the run caption, centred.
   At md the wrapper carries only .belt-gauge-wrap (plain block) so the dashboard card is unchanged. */
.belt-gauge-wrap-text { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.belt-run-hero { font-size: 13px; }

/* ===== Uniform family images + under-image sub-indicator ===== */
.weighbridge-gauge, .flow-meter-gauge, .state-gauge, .map-pin-gauge { display: block; }

/* Left cell as a column (image on top, sub-indicator beneath) for the non-belt families. */
.card-left { flex-direction: column; gap: 3px; align-items: center; }
.sensor-card-main .map-coords { font-size: 16px; }

/* Flow meter: rotor spins + flow dashes scroll at the current rate. */
.flow-rotor { transform-box: fill-box; transform-origin: center; animation: gauge-spin var(--rotor-dur, 2s) linear infinite; }
.flow-dash  { stroke-dasharray: 5 7; animation: belt-flow-scroll var(--flow-dur, 2s) linear infinite; }
.flow-rotor.paused, .flow-dash.paused { animation: none; }
.flow-dash.paused { stroke-opacity: 0.25 !important; }

/* Pump impeller spins while active; gate panels slide; LED pulses while running. */
.state-rotor { transform-box: fill-box; transform-origin: center; animation: gauge-spin var(--rotor-dur, 1.2s) linear infinite; }
.state-rotor.paused { animation: none; }
.state-panel { transition: x 300ms ease; }
.state-led-pulse { animation: led-pulse 1.6s ease-in-out infinite; }

/* GPS ping ring. */
.map-ping { transform-box: fill-box; transform-origin: center; animation: map-ping 2.4s ease-out infinite; }

@keyframes gauge-spin { to { transform: rotate(360deg); } }
@keyframes led-pulse  { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
@keyframes map-ping   { 0% { opacity: 0.7; transform: scale(0.5); } 100% { opacity: 0; transform: scale(1.7); } }

@media (prefers-reduced-motion: reduce) {
  .flow-rotor, .flow-dash, .state-rotor, .map-ping { animation: none; }
}

/* ===== Demo mode ===== */
/* Sidebar badge shown above the user chip while demo mode is on. */
.demo-chip {
  display: flex; align-items: center; gap: 8px; cursor: pointer;
  margin-bottom: 8px; padding: 7px 10px; border-radius: 8px;
  background: color-mix(in oklab, var(--warn) 14%, var(--surface));
  border: 1px solid color-mix(in oklab, var(--warn) 45%, transparent);
  color: var(--text); font-size: 12.5px; user-select: none;
}
.demo-chip:hover { background: color-mix(in oklab, var(--warn) 22%, var(--surface)); }
.demo-chip-dot {
  width: 8px; height: 8px; border-radius: 50%; background: var(--warn); flex-shrink: 0;
  animation: led-pulse 1.8s ease-in-out infinite;
}
.demo-chip-text { font-weight: 600; flex: 1; }
.demo-chip-badge {
  font-size: 9.5px; font-weight: 700; letter-spacing: 0.06em;
  padding: 1px 6px; border-radius: 999px;
  background: var(--warn); color: #1a1206;
}

/* Settings → Demo pane. */
.demo-toggle-row {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 14px 16px; border: 1px solid var(--line); border-radius: 10px;
  background: var(--surface); margin-top: 12px;
}
.demo-toggle-text { display: flex; flex-direction: column; gap: 2px; }
.demo-toggle-text strong { font-size: 14px; }
.demo-toggle-text .muted { font-size: 12px; }
.demo-note { font-size: 11.5px; margin-top: 10px; display: flex; align-items: center; gap: 5px; }

/* Generic on/off switch. */
.switch {
  position: relative; width: 44px; height: 24px; flex-shrink: 0;
  border-radius: 999px; border: none; cursor: pointer; padding: 0;
  background: var(--soft); transition: background 160ms ease;
}
.switch.on { background: var(--ok); }
.switch:disabled { opacity: 0.6; cursor: default; }
.switch-knob {
  position: absolute; top: 2px; left: 2px; width: 20px; height: 20px;
  border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,0.3);
  transition: transform 160ms ease;
}
.switch.on .switch-knob { transform: translateX(20px); }

/* Belt period-summary table on the detail page */
.belt-summary-wrap { overflow-x: auto; }
.belt-summary-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.belt-summary-table th, .belt-summary-table td {
  text-align: left; padding: 6px 10px; border-bottom: 1px solid var(--soft); white-space: nowrap;
}
.belt-summary-table th {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--muted); font-weight: 500;
}
.belt-summary-table td.num, .belt-summary-table th.num { text-align: right; font-family: var(--font-mono); }
.belt-summary-table tbody tr:last-child td { border-bottom: none; }

/* Counter tile */
.counter-tile {
  display: flex; flex-direction: column; gap: 10px; padding: 4px;
}
.counter-big {
  font-family: var(--font-mono); font-weight: 700;
  letter-spacing: -0.02em; line-height: 1;
}
.counter-big .num-unit {
  font-size: 0.5em; color: var(--muted); font-weight: 500; margin-left: 4px;
}
.counter-rate {
  display: flex; align-items: baseline; gap: 8px;
  padding: 8px 12px;
  background: var(--soft);
  border-radius: var(--r2);
}
.rate-label {
  font-size: 10.5px; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted);
}
.rate-value {
  font-family: var(--font-mono); font-weight: 600; font-size: 16px;
  color: var(--text);
}
.rate-value em { font-style: normal; color: var(--muted); font-size: 11px; margin-left: 2px; }

/* State pill */
.state-pill {
  display: flex; flex-direction: column; gap: 6px; padding: 4px;
}
.state-big {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--font-mono); font-weight: 700;
  letter-spacing: 0.04em;
  border: 2px solid currentColor;
  padding: 8px 14px;
  border-radius: var(--r-pill);
  align-self: flex-start;
}
.state-led {
  width: 8px; height: 8px; border-radius: 50%;
  box-shadow: 0 0 0 3px color-mix(in oklab, currentColor 18%, transparent);
}
.state-meta {
  font-size: 12px; color: var(--muted);
}

/* Map pin tile */
.map-pin-tile {
  display: flex; align-items: center; gap: 12px; padding: 4px;
}
.map-pin-meta {
  display: flex; flex-direction: column; gap: 2px;
  font-size: 12px;
}
.map-pin-meta .num { font-family: var(--font-mono); font-weight: 600; }

/* Unconfigured tile */
.unconfigured-tile {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 12px 16px;
  background: var(--soft);
  color: var(--muted);
  border-radius: var(--r2);
  font-size: 13px;
  border: 1px dashed var(--line-strong);
}

/* Threshold list (detail page) */
.threshold-list {
  list-style: none; padding: 0; margin: 0;
  display: flex; flex-direction: column; gap: 6px;
}
.threshold-list > li {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 0;
  border-bottom: 1px dashed var(--line);
}
.threshold-list > li:last-child { border-bottom: none; }
.threshold-bar {
  width: 4px; height: 18px; border-radius: 2px; flex-shrink: 0;
}
.threshold-label {
  flex: 1;
  font-size: 13px; color: var(--text-2);
}
.threshold-value {
  font-weight: 600; font-size: 13.5px;
}

/* ============================================================
   Sensor Configuration screen
   ============================================================ */
.configure-grid {
  display: grid; gap: var(--gap-grid);
  grid-template-columns: 1fr 1fr;   /* exactly two panels per row */
  align-items: start;
}
.config-preview {
  background: linear-gradient(180deg, var(--surface), var(--surface-2));
}
.config-preview-body {
  display: flex; gap: 20px; align-items: center;
  padding: 16px 0;
}
/* Each form card is a query container: its two-field rows fold to a single field when the card is
   narrow, so inputs never spill past the card border. */
.config-form { container-type: inline-size; }
.config-row > * { min-width: 0; }
@container (max-width: 400px) { .config-row { grid-template-columns: 1fr; } }
@media (max-width: 900px) {
  .configure-grid { grid-template-columns: 1fr; }
}
.config-form .card-head { margin-bottom: 16px; }
.config-row {
  display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
  margin-bottom: 12px;
}
@media (max-width: 520px) { .config-row { grid-template-columns: 1fr; } }

/* Segmented control taking full width inside a field */
.seg.seg-block {
  display: grid;
  grid-template-columns: 1fr 1fr;
  padding: 3px;
  width: 100%;
}
.seg.seg-block button { padding: 8px 12px; font-size: 13px; }

/* Threshold edit form */
.threshold-form {
  display: flex; flex-direction: column; gap: 8px;
}
.threshold-field {
  display: grid;
  grid-template-columns: 6px 1fr auto;
  align-items: center; gap: 14px;
  padding: 10px 0;
  border-bottom: 1px dashed var(--line);
}
.threshold-field:last-child { border-bottom: none; }
.threshold-color {
  width: 6px; height: 22px; border-radius: 3px;
}
.threshold-field .threshold-label {
  font-size: 14px; color: var(--text);
}
.threshold-input {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--r2); padding: 6px 10px;
  width: 160px;
}
.threshold-input input {
  border: none; outline: none; background: transparent;
  font-family: var(--font-mono); font-size: 14px; color: var(--text);
  width: 100%; text-align: right;
}
.threshold-input input::placeholder { color: var(--muted-2); }

/* Sidebar height handling — keep tree scrollable on small screens */
@media (max-width: 768px) {
  .sidebar { width: 300px; }
  .config-preview { position: static; }
}

/* ============================================================
   Events card
   ============================================================ */
.events-head-actions {
  display: flex; align-items: center; gap: 10px;
}
.event-chip-row {
  display: flex; flex-wrap: wrap; gap: 6px;
  margin-bottom: 12px;
}
.event-chip {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--soft);
  border: 1px solid transparent;
  color: var(--text-2);
  padding: 5px 10px;
  border-radius: var(--r-pill);
  font-size: 12.5px; font-weight: 500;
  cursor: pointer;
  transition: background 100ms, border-color 100ms;
}
.event-chip:hover:not(:disabled) {
  background: var(--surface);
  border-color: var(--line-strong);
}
.event-chip.active {
  background: var(--accent-soft);
  color: var(--accent);
  border-color: color-mix(in oklab, var(--accent) 35%, transparent);
}
.event-chip:disabled { opacity: 0.45; cursor: not-allowed; }
.event-chip em {
  font-style: normal;
  background: var(--surface);
  color: var(--muted);
  padding: 1px 6px;
  border-radius: var(--r-pill);
  font-size: 11px;
}
.event-chip.active em { background: var(--surface); color: var(--accent); }

.event-table-wrap {
  border: 1px solid var(--line);
  border-radius: var(--r2);
  overflow: hidden;
}
.event-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}
.event-table th {
  background: var(--surface-2);
  border-bottom: 1px solid var(--line);
  padding: 9px 14px;
  text-align: left;
  font-weight: 500; font-size: 11px;
  text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--muted);
  white-space: nowrap;
}
.event-table th.num, .event-table td.num {
  text-align: right; font-family: var(--font-mono);
}
.event-table th.sortable { cursor: pointer; user-select: none; }
.event-table th.sortable:hover { color: var(--text); }
.event-table td {
  padding: 9px 14px;
  border-bottom: 1px solid var(--line);
}
.event-table tbody tr:last-child td { border-bottom: none; }
.event-table tbody tr:hover { background: var(--soft); }
.event-type-cell { display: inline-flex; align-items: center; gap: 6px; color: var(--text-2); }
.event-type-cell svg { color: var(--muted); }
.event-table-foot {
  padding: 10px 14px;
  border-top: 1px solid var(--line);
  font-size: 12px;
  color: var(--muted);
  background: var(--surface-2);
  text-align: center;
}

@media (max-width: 600px) {
  .events-head-actions { flex-wrap: wrap; }
  .event-table { font-size: 12px; }
  .event-table th, .event-table td { padding: 7px 8px; }
}

/* Event type list (configuration page) */
.event-type-list { display: flex; flex-direction: column; }
.event-type-row {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 0;
  border-bottom: 1px solid var(--line);
}
.event-type-row:last-child { border-bottom: none; }
.event-type-icon {
  width: 32px; height: 32px; border-radius: 8px;
  background: var(--accent-soft); color: var(--accent);
  display: grid; place-items: center;
  flex-shrink: 0;
}
.event-type-meta { flex: 1; min-width: 0; }
.event-type-name { font-weight: 500; font-size: 14px; }
.event-type-sub { font-size: 12px; color: var(--muted); margin-top: 2px; }

/* Custom date range row inside events card */
.custom-range-row {
  display: flex; align-items: flex-end; gap: 12px;
  padding: 12px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r2);
  margin-bottom: 12px;
  flex-wrap: wrap;
}
.range-field { flex: 0 0 180px; }
.range-field .field-input { padding: 8px 10px; }
.range-field input[type="date"] {
  border: none; outline: none; background: transparent;
  font-size: 14px; color: var(--text); width: 100%;
  font-family: var(--font-mono);
}
.range-hint {
  flex: 1; min-width: 200px;
  font-size: 12.5px; color: var(--muted); padding-bottom: 10px;
}

/* Threshold → alerts annotation */
.threshold-note {
  display: flex; align-items: center; gap: 8px;
  font-size: 12.5px; color: var(--accent);
  background: var(--accent-soft);
  padding: 8px 12px;
  border-radius: var(--r2);
  margin-bottom: 12px;
  line-height: 1.4;
}
.threshold-note svg { flex-shrink: 0; }

/* A stored days threshold on a sensor that has no days projection. Warn-toned rather than
   critical: nothing is broken, a setting is simply being ignored — and it sits under its own
   field rather than at the top of the form so it is obvious WHICH band it refers to. */
.threshold-orphan {
  font-size: 12px; color: var(--warn);
  padding: 6px 12px 10px 26px;
  line-height: 1.4;
  margin-top: -6px;
}

/* ============================================================
   Appearance pane
   ============================================================ */
.scope-row {
  display: flex; align-items: center; gap: 12px;
  margin-bottom: 20px;
  flex-wrap: wrap;
}
.scope-label { font-size: 13px; font-weight: 500; }
/* 300px fitted two targets; the thresholds pane now offers three (Me / Tenant / Organisation) and
   the labels are words, not initials. Appearance still renders two and simply sits narrower. */
.scope-toggle { max-width: 420px; flex: 0 1 420px; }
.scope-hint { font-size: 12px; color: var(--muted); }

/* Shown only while the Organisation save target is selected — the one scope whose effect reaches
   past the tenants the operator can see, so it is stated on the page rather than in a tooltip. */
.scope-warning {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; line-height: 1.4;
  color: var(--warn);
}
.scope-warning svg { flex: none; }

.color-form {
  display: flex; flex-direction: column; gap: 4px;
  margin-bottom: 16px;
}
.color-row {
  display: flex; justify-content: space-between; align-items: center;
  gap: 16px;
  padding: 14px 0;
  border-bottom: 1px solid var(--line);
}
.color-row:last-child { border-bottom: none; }
.color-row-label {
  display: flex; align-items: center; gap: 10px;
  flex: 1; min-width: 0;
}
.color-row-label strong { font-size: 14px; }
.color-row-label .muted { font-size: 12.5px; }
.color-row-input {
  display: flex; align-items: center; gap: 10px;
}
.color-row-input input[type="color"] {
  width: 36px; height: 36px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r2);
  background: transparent;
  cursor: pointer;
  padding: 2px;
}
.color-row-input code {
  font-family: var(--font-mono); font-size: 12.5px;
  background: var(--soft); padding: 2px 8px;
  border-radius: var(--r-pill); color: var(--text-2);
}
.scope-inherit { font-size: 11px; }

.palette-row {
  display: flex; gap: 10px; flex-wrap: wrap;
  margin-bottom: 16px;
}
.palette-card {
  display: flex; flex-direction: column; gap: 6px;
  align-items: flex-start;
  padding: 10px 12px;
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--r2); cursor: pointer;
  transition: border-color 100ms;
}
.palette-card:hover { border-color: var(--accent); }
.palette-swatches { display: flex; gap: 4px; }
.palette-swatches span {
  width: 24px; height: 24px; border-radius: 6px;
  border: 1px solid color-mix(in oklab, currentColor 12%, transparent);
}
.palette-name {
  font-size: 12px; font-weight: 500; color: var(--text-2);
}

.color-preview {
  display: flex; gap: 16px; flex-wrap: wrap;
  padding: 16px;
  background: var(--surface-2);
  border-radius: var(--r3);
  border: 1px solid var(--line);
  margin-bottom: 8px;
}
.color-preview-tile {
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  flex: 1; min-width: 120px;
  padding: 12px;
  background: var(--surface);
  border-radius: var(--r2);
}

.confirm-text {
  font-size: 14px; font-weight: 500; color: var(--text-2);
  align-self: center;
  padding: 0 8px;
}
.alerts-tab-row {
  display: flex; justify-content: space-between; align-items: center;
  gap: 12px; flex-wrap: wrap;
}
.alerts-tabs {
  display: flex; gap: 4px;
  overflow-x: auto; flex: 1; min-width: 200px;
}
.alerts-range { flex-shrink: 0; }
.save-flash {
  color: var(--ok);
  font-size: 13px;
  align-self: center;
  font-weight: 500;
}

/* Type selector + per-type override pills */
.type-select-wrap {
  display: flex; align-items: center; gap: 8px;
  flex: 1 1 280px;
}
/* Layout only — the select's look comes from .select (see "Selects"). */
.type-select-wrap .select { flex: 1 1 auto; min-width: 0; }

.type-override-list {
  display: flex; flex-wrap: wrap; gap: 8px;
  margin-bottom: 16px;
}
.type-override-pill {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--r-pill);
  padding: 5px 10px 5px 12px;
  font-size: 12.5px; cursor: pointer;
  color: var(--text-2);
}
.type-override-pill:hover { border-color: var(--accent); color: var(--text); }
.type-override-pill strong { font-weight: 600; font-size: 13px; }
.scope-tag {
  font-style: normal; font-size: 10.5px; font-weight: 600;
  padding: 1px 6px; border-radius: var(--r-pill);
  text-transform: uppercase; letter-spacing: 0.04em;
}
.scope-tag.user { background: var(--accent-soft); color: var(--accent); }
.scope-tag.tenant { background: var(--info-bg); color: var(--info); }

/* Dashboard layout toggle */
.layout-toggle button {
  display: inline-flex; align-items: center; gap: 4px;
}

/* Two-section appearance card */
.appearance-block {
  border: 1px solid var(--line);
  border-radius: var(--r3);
  padding: 16px 18px;
  margin-bottom: 14px;
  background: var(--surface-2);
}
.appearance-block-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px;
  padding-bottom: 12px;
  margin-bottom: 12px;
  border-bottom: 1px solid var(--line);
}
.appearance-block-head h4 {
  font-size: 14px; font-weight: 600; margin: 0;
}
.appearance-block-head .muted { font-size: 12.5px; }
.resolution-note { font-size: 12.5px; }

/* Theme/accent section in Appearance pane */
.appearance-section { margin-bottom: 12px; }
.appearance-section:last-of-type { margin-bottom: 0; }
.appearance-row {
  display: flex; align-items: center; gap: 12px;
  flex-wrap: wrap;
}
.theme-seg {
  display: inline-flex;
  background: var(--soft);
  border-radius: var(--r2);
  padding: 3px;
  gap: 2px;
}
.theme-seg button {
  background: transparent; border: none; padding: 6px 14px;
  border-radius: 7px; cursor: pointer;
  font-size: 13px; font-weight: 500;
  color: var(--muted);
  display: inline-flex; align-items: center; gap: 6px;
}
.theme-seg button.active {
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-1);
}

/* ============================================================
   Alerts — pane + shared button variants
   ============================================================ */
/* Destructive button variants. Introduced for the (now deleted) alert-authoring screens; kept as
   generic parts of the button system, not as authoring leftovers. */
.btn-danger {
  background: var(--crit); color: white; border-color: var(--crit);
}
.btn-danger:hover { background: color-mix(in oklab, var(--crit) 88%, black); }
.btn-ghost.danger { color: var(--crit); }
.btn-ghost.danger:hover { background: var(--crit-bg); }

/* The "Always on" pill on the in-app channel row (NotificationsPane). */
.chan-fixed { font-size: 12px; padding: 2px 8px; border-radius: 999px; background: var(--soft); }

/* NotificationsPane additions */
.channel-note { margin-top: -8px; margin-bottom: 14px; font-size: 12px; }
.quiet-hours-row { padding: 12px 0 4px; }
.settings-actions { display: flex; align-items: center; justify-content: flex-end; gap: 12px; margin-top: 18px; }

/* ============================================================
   Alert rules — the cascade list and the catalogue-driven editor
   Everything here extends a pattern that already exists (.scope-tag from the threshold pane,
   .threshold-note from the sensor configure screen, .config-row/.configure-grid from the same).
   ============================================================ */

/* .scope-tag.user + .scope-tag.tenant already exist for threshold scopes; a rule scope has four
   rungs, coloured so precedence reads left-to-right in the list: broad and cool at the top,
   specific and accented at the bottom. */
.scope-tag.account { background: var(--soft);        color: var(--text-2); }
.scope-tag.premise { background: var(--warn-bg);     color: var(--warn); }
.scope-tag.sensor  { background: var(--accent-soft); color: var(--accent); }
/* "two rules tied" is a fault, not a rung — it must not borrow a scope colour and read as one. */
.scope-tag.is-ambiguous { background: var(--crit-bg); color: var(--crit); }
/* The rung stays a label; the premise/sensor NAME beside it is somebody's proper noun and reads as
   one, not as more uppercase tracking. */
.scope-tag .scope-target { text-transform: none; letter-spacing: 0; font-weight: 500; font-size: 11.5px; }

/* Severity/state variants of the existing inline note, so a 403 or a paused rule can be explained
   in the same shape the threshold note already uses rather than a new banner design. */
.threshold-note.is-warn  { color: var(--warn);   background: var(--warn-bg); }
.threshold-note.is-crit  { color: var(--crit);   background: var(--crit-bg); }
.threshold-note.is-muted { color: var(--text-2); background: var(--soft); }
.threshold-note.is-block { align-items: flex-start; }
.threshold-note.is-block svg { margin-top: 2px; }
.threshold-note strong { font-weight: 600; }
/* Explanatory prose under a failure or a condition — sized once here rather than per element. */
.rule-note { font-size: 13px; line-height: 1.55; }
.rule-note-gap { margin-top: 12px; }

/* Role names are literals an admin has to type into W8Auth exactly — same chip everywhere. */
.threshold-note code, .rule-line code, .rule-note code {
  font-family: var(--font-mono); font-size: 11.5px;
  background: color-mix(in oklab, currentColor 10%, transparent);
  padding: 1px 5px; border-radius: var(--r1);
  word-break: break-word;
}

/* One precedence rung of the list, headed by what that rung means. */
.rule-group { margin-bottom: 20px; }
.rule-group:last-child { margin-bottom: 0; }
.rule-group-head {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  margin-bottom: 8px; padding: 0 2px;
}
/* 15px, so the rung heading out-ranks the 14.5px rule names it heads — at 14px a group head weighed
   less than its own contents and the page read as one flat list with sentences in it. */
.rule-group-head h2 { font-size: 15px; font-weight: 600; }
.rule-group-head .muted { font-size: 12.5px; }

/* The cascade annotations under a rule row — what it currently covers, and what beats it. */
.rule-lines { display: flex; flex-direction: column; gap: 3px; margin-top: 6px; }
.rule-line {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  font-size: 12.5px; color: var(--text-2); line-height: 1.45;
}
.rule-line svg { flex-shrink: 0; color: var(--muted); }
.rule-line.is-beaten, .rule-line.is-beaten svg { color: var(--warn); }
.rule-line.is-muted, .rule-line.is-muted svg { color: var(--muted); }

/* The rule NAME is a button, because the row cannot be one: it already contains Pause. Styled as the
   title it replaces so the list looks unchanged, but it is tabbable and takes the browser's focus
   ring. */
.alert-title.link-title {
  background: none; border: none; padding: 0;
  font: inherit; font-weight: 600; font-size: 14.5px;
  color: var(--text); text-align: left; cursor: pointer;
}
.alert-title.link-title:hover { color: var(--accent); text-decoration: underline; }

/* What the rule watches, in W8Gear's words rather than its keys. */
.rule-condition { color: var(--text-2); font-weight: 500; }
.rule-cascade-note { font-size: 12px; color: var(--muted); }

/* Empty list: the state and the way out of it, not just the state. */
.empty-action { margin-top: 16px; }

/* The inspector's sensor picker is a labelled field, not a bare select. */
.rule-inspect-field { max-width: 420px; margin-bottom: 14px; }

/* Per-sensor cascade inspector: the winner, then the rules it beat. */
.rule-cascade-list { display: flex; flex-direction: column; }
.rule-cascade-row { padding: 12px 0; border-bottom: 1px solid var(--line); }
.rule-cascade-row:last-child { border-bottom: none; }
.rule-cascade-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.rule-cascade-name { font-weight: 600; font-size: 13.5px; }
.rule-cascade-loser {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  font-size: 12.5px; color: var(--muted);
  padding-left: 20px; margin-top: 5px;
}
.rule-cascade-loser svg { flex-shrink: 0; }

/* Parameter rows, rendered one per entry in the schema the server served. */
.rule-params { display: flex; flex-direction: column; }
.rule-param {
  display: grid; grid-template-columns: minmax(150px, 210px) minmax(0, 1fr);
  gap: 6px 16px; align-items: start;
  padding: 12px 0; border-bottom: 1px dashed var(--line);
}
.rule-param:last-child { border-bottom: none; }
/* The label is the parameter said in words; the key underneath is what W8Gear's own validation
   messages and logs will call it, so both are on screen. */
.rule-param-name {
  display: block; font-size: 13px; font-weight: 600;
  color: var(--text); word-break: break-word; cursor: pointer;
}
.rule-param-key {
  display: block; margin-top: 2px;
  font-family: var(--font-mono); font-size: 11px; color: var(--muted);
  word-break: break-word;
}
.rule-param-req { color: var(--crit); margin-left: 3px; }
.rule-param-unit { color: var(--text-2); }
.rule-param-state { font-size: 12.5px; color: var(--text-2); }
.rule-param-control { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.rule-param-control .rule-input { flex: 1 1 130px; }
/* Sizing hooks, shared by the <input>s and the <select>s in a parameter row. */
.rule-param-control .rule-ctl-grow  { flex: 1 1 160px; }
.rule-param-control .rule-ctl-fixed { flex: 0 0 auto; }
/* The neighbour rule again: everything else in a parameter row is a mono 13.5px .rule-input
   (values here are literals W8Gear echoes back), so the select matches ITS row rather than the
   filter bar's. Size and padding still come from .select--sm, so the heights stay identical. */
.rule-param-control .select--sm { font-family: var(--font-mono); font-size: 13.5px; }
.rule-param-hint {
  grid-column: 2; display: flex; align-items: flex-start; gap: 5px;
  font-size: 12px; color: var(--muted); line-height: 1.45;
}
.rule-param-hint svg { flex-shrink: 0; margin-top: 2px; }
.rule-param-hint.is-error { color: var(--crit); }

/* Each editor card is already a query container (.config-form), so the parameter rows fold on the
   CARD's width, not the window's — the two-column editor grid is still two columns at 780px, where a
   viewport breakpoint would leave a 150px label column inside a 370px card. */
@container (max-width: 400px) {
  .rule-param { grid-template-columns: 1fr; }
  .rule-param-hint { grid-column: 1; }
}

/* Text/number inputs in a parameter row. 12px inline padding so the .select--sm dropdowns that
   share the row line up on the same text inset. */
.rule-input {
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--r2); padding: 8px 12px;
  font-size: 13.5px; color: var(--text); font-family: var(--font-mono);
  min-width: 0;
}
/* Same focus treatment as .field-input:focus-within — a border-colour change alone is a
   colour-only, low-contrast focus indicator, which for a keyboard-driven form is no indicator. */
.rule-input:focus {
  outline: none; border-color: var(--accent);
  box-shadow: var(--focus-ring);
}
.rule-input:disabled { background: var(--soft); color: var(--muted); cursor: not-allowed; }

/* Validation summary — sentences, not red borders. */
.rule-problems { margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 6px; }
.rule-problems li { font-size: 13px; color: var(--crit); line-height: 1.45; }

/* A card that must span the full editor grid (delete confirmation, failure banners). */
.rule-span-2 { grid-column: 1 / -1; }
@media (max-width: 900px) { .rule-span-2 { grid-column: auto; } }

.rule-editor-foot {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin-top: 4px;
}
.rule-editor-foot .spacer { flex: 1 1 auto; }

/* Segmented choices sized by CLASS rather than an inline style, so the container queries below can
   actually win — an inline grid-template-columns outranks every stylesheet rule. */
.seg.seg-block.rule-seg-3 { grid-template-columns: repeat(3, 1fr); }
.seg.seg-block.rule-seg-4 { grid-template-columns: repeat(4, 1fr); margin-bottom: 12px; }
.seg.seg-block.rule-seg-4 button { display: inline-flex; align-items: center; justify-content: center; gap: 4px; }
.seg.seg-block button:disabled { opacity: 0.55; cursor: not-allowed; }
.rule-toggle-inline { border-bottom: none; padding: 6px 0; }

/* .site-info was built for short read-only values ("Site", "Location"). The condition facts are
   sentences, so they left-align and wrap instead of being right-aligned against a two-word term. */
.site-info.rule-facts .site-info-row { align-items: baseline; }
.site-info.rule-facts dt { flex: 0 0 auto; }
.site-info.rule-facts dd { flex: 1 1 auto; text-align: left; line-height: 1.45; }

/* Phone-width editor. Four scope rungs will not sit on one line inside a ~320px card, and a squeezed
   row that clips "Premise" is worse than two rows. Driven by the CARD's width for the same reason
   the parameter rows are. */
@container (max-width: 400px) {
  .seg.seg-block.rule-seg-4 { grid-template-columns: repeat(2, 1fr); }
  .seg.seg-block.rule-seg-3 { grid-template-columns: 1fr; }
  /* The rule cards carry long explanatory subtitles; side by side with the heading they squash to a
     word per line, so let the card head stack once the card is narrow. */
  .card-head { flex-direction: column; align-items: flex-start; gap: 4px; }
  .site-info.rule-facts .site-info-row { flex-direction: column; gap: 2px; }
}

/* ============================================================
   Alert coverage matrix — the second view on /alerts/rules
   Table chrome, scrollport, header cells and count strip are all the estate's existing ones
   (.sensor-table-wrap, .sensor-table, .map-legend); what is genuinely new is a sticky FIRST column,
   a three-state cell, and an "unresolved row" treatment, none of which existed before.
   ============================================================ */

/* A general utility, introduced here because the matrix is the first screen whose meaning depends on
   text nobody sees. Every cell state has a spoken equivalent — a glyph alone announces as nothing,
   and an empty <td> announces as "blank", which is exactly the ambiguity this view exists to remove.
   clip-path rather than the old clip:rect() hack: the text stays in the accessibility tree and stays
   selectable, and the 1px box keeps prose nobody sees from resizing the cell it lives in.

   THE PRICE OF position:absolute — and it must be paid by whoever contains it. Each span is laid out
   against its nearest POSITIONED ancestor, and on a grid this wide its static position is far past
   the right edge of the page. With no positioned ancestor that role falls to the initial containing
   block: the spans then escape the scrollport that was supposed to clip them and widen the DOCUMENT
   instead. So any scrollport holding .sr-only has to be position:relative — see .cov-wrap. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* The wrap is .sensor-table-wrap's — surface, border, radius, overflow:auto — plus the three things a
   matrix needs it to promise: it never widens the page (.content is min-width:0, so the clamp holds),
   it can be reached by keyboard, and it is a scrollport its own contents cannot escape.

   position:relative is that third promise. overflow:auto clips only descendants whose containing
   block is this box, so without it every .sr-only span above resolves against the initial containing
   block, is laid out at a static position past the right edge, and adds itself to the DOCUMENT's
   scrollable width — measured +494px at a 320px viewport, i.e. the whole page scrolled sideways
   whenever the grid did. It is the cheaper of the two one-line fixes and the only one that keeps the
   text: hiding the spans would fix the scroll by deleting the thing they exist for.

   scroll-padding-left is the sticky column's width, and it is what stops that column parking on top
   of whatever was just scrolled into view. Focus scrolls by the MINIMUM needed, so reaching a tick
   from the right (Shift-Tab) aligns it flush with the scrollport's left edge — exactly where the
   sensor column is pinned. Measured 13 of 18 ticks obscured without it, 0 with it (WCAG 2.2
   SC 2.4.11). Keep it equal to .cov-row-head's min-width; the phone value is in the 640px block at
   the end of this section. */
.cov-wrap {
  max-width: 100%;
  position: relative;
  scroll-padding-left: 180px;
}
.cov-wrap:focus-visible { outline: none; box-shadow: var(--focus-ring); }

.sensor-table.coverage-table {
  /* Collapsed borders belong to the TABLE, not to the cell, so a position:sticky cell can leave its
     own borders behind as it moves. Separate borders with zero spacing render identically and stick
     everywhere. */
  border-collapse: separate;
  border-spacing: 0;
  /* Let the grid be as wide as its columns need and scroll, rather than squeezing every function into
     the viewport until the headings are one letter per line. */
  width: auto;
  min-width: 100%;
}
/* Rows are not clickable here — only the ticks inside them are — so the pointer .sensor-table sets
   would be a promise this view does not keep. The row HIGHLIGHT stays: on a grid this wide, seeing
   the row you are on is the only way to keep your place while scrolling sideways. */
.coverage-table tr { cursor: default; }

.coverage-table thead th {
  vertical-align: bottom;
  min-width: 96px; max-width: 160px;
  z-index: 2;
}
.coverage-table thead th.cov-corner { z-index: 3; left: 0; }
.cov-col-name { display: block; }
/* The raw key, when the registry does not declare the function — said once, under a heading that is
   already the key itself. It cannot ALSO be a lighter grey: --muted-2 on --surface-2 measures 2.84:1
   in light and 3.10:1 in dark, and the heading it sits under is --muted at 4.55:1, which is already
   the AA floor for this surface. There is no room below that, so the key takes the same ink as the
   heading and stays subordinate by size, family and case instead of by fading out. */
.cov-col-key {
  display: block; margin-top: 3px;
  font-family: var(--font-mono); font-size: 10px;
  text-transform: none; letter-spacing: 0;
  color: var(--muted);
}

/* The sensor column stays put while the functions scroll past it. This is the only sticky-left in the
   app, so its stacking order is stated rather than inherited: corner above header row above sensor
   column above the cells. Each sticky cell needs an opaque background or the grid slides under it. */
.coverage-table .cov-corner,
.coverage-table .cov-row-head {
  position: sticky;
  left: 0;
  border-right: 1px solid var(--line);
}
/* A row header is a th, so .sensor-table th has already made it an uppercase 12px column heading
   stuck to the top. Undo all three: it is a sensor's name, and it sticks sideways, not upward. */
.coverage-table tbody th.cov-row-head {
  top: auto;
  z-index: 1;
  background: var(--surface);
  text-transform: none; letter-spacing: 0;
  font-size: 13.5px; font-weight: 500; color: var(--text);
  padding: 10px 14px;
  min-width: 180px; max-width: 260px;
}
.coverage-table tbody tr:hover th.cov-row-head { background: var(--soft); }
.coverage-table tbody th.cov-row-head .td-primary,
.coverage-table tbody th.cov-row-head .td-sub { display: block; }

/* Mutes that could not be joined to a function key. Flagged on the row because the row is the finest
   granularity the exclusion payload supports — see the component's comment.

   --warn straight onto --warn-bg is 2.86:1 in light and 4.39:1 in dark; at 10.5px neither is AA, and
   a badge saying coverage is missing is the last thing that should be hard to read. Mixing toward
   --text darkens the ink on the light amber and lightens it on the dark one, so a single declaration
   clears both themes and neither needs a per-theme override. */
.cov-unplaced {
  display: inline-block; margin-top: 4px;
  font-size: 10.5px; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.04em;
  color: color-mix(in oklab, var(--warn) 70%, var(--text));
  background: var(--warn-bg);
  padding: 1px 6px; border-radius: var(--r-pill);
}

.coverage-table td.cov-cell { text-align: center; padding: 8px 10px; }

/* Three states, and each carries a SHAPE as well as a colour: --accent is tenant-overridable and a
   status hue is not something every reader can separate, so the tile geometry has to do the work on
   its own. Tick = check glyph in a solid tile. Mute = cloud-off glyph in a dashed one. Blank = no
   tile at all. */
.cov-mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px;
  border-radius: var(--r1);
  border: 1px solid transparent;
  vertical-align: middle;
}
.cov-tick { color: var(--ok); background: var(--ok-bg); }
a.cov-tick { text-decoration: none; }
a.cov-tick:hover { border-color: var(--ok); }
a.cov-tick:focus-visible { outline: none; box-shadow: var(--focus-ring); }
.cov-mute {
  color: var(--muted); background: var(--soft);
  border: 1px dashed var(--line-strong);
}
/* The second mark in a cell that is BOTH covered and muted — a broader rule excused while a narrower
   one still wins. Smaller, because the tick is the headline and the mute is the footnote. */
.cov-mark-sm { width: 18px; height: 18px; margin-left: 4px; }
/* Legend only: the absence, drawn so "nothing here" is something the key can point at. Its edge IS
   the swatch — there is no fill to fall back on — so it has to carry the 3:1 a meaningful graphic
   needs on its own. --line is the table's hairline and vanishes against --bg at 1.15:1; --muted is
   4.40:1 light / 6.07:1 dark and is the ink of the sentence beside it. */
.cov-blank { border: 1px dashed var(--muted); background: transparent; }

/* A row nobody answered for. It must not read as a row of empty cells, so it takes one spanning cell
   with the estate's existing "this is not a real value" vocabulary — soft fill, dashed edge, muted
   prose (compare .unconfigured-tile and .metric-empty). */
.coverage-table td.cov-unresolved {
  text-align: left;
  padding: 10px 14px;
  background: var(--surface-2);
  border-bottom: 1px dashed var(--line-strong);
  color: var(--muted);
  font-size: 12.5px; line-height: 1.45;
}
.coverage-table td.cov-unresolved svg { vertical-align: -2px; margin-right: 6px; flex-shrink: 0; }
.coverage-table tbody tr.is-unresolved th.cov-row-head { color: var(--muted); }

.coverage-table tbody tr:last-child th,
.coverage-table tbody tr:last-child td { border-bottom: none; }

/* Both strips sit on --bg, where --muted measures 4.40:1 — just under AA, and these are the only two
   places the marks are named and the only place the grid admits what it left out, so neither can be
   the line that is hard to read. Nudged toward --text until they clear (5.30:1 light, 6.97:1 dark);
   the same mix in both, because they are one voice. The pairing is estate-wide — .page-sub is --muted
   on --bg too — so this is deliberately a local fix and not a change to the token. */
.cov-legend {
  margin-bottom: 4px;
  color: color-mix(in oklab, var(--muted) 88%, var(--text));
}
.cov-legend .cov-mark { width: 22px; height: 22px; }
/* The count strip lives OUTSIDE the scrollport: inside it, the one sentence explaining what the grid
   left out would scroll away with the columns it is explaining. */
.cov-foot {
  margin-top: 10px;
  font-size: 12.5px; line-height: 1.55;
  color: color-mix(in oklab, var(--muted) 88%, var(--text));
}

/* Phone. A wide grid cannot be reflowed into cards without losing the comparison that IS the view, so
   it stays a table and scrolls — with the sensor name pinned, which is what makes sideways scrolling
   survivable at all. What goes is the premise sub-line: it disambiguates two bins with one name, and
   at 380px the name itself is the thing that must fit. */
@media (max-width: 640px) {
  .coverage-table { font-size: 13px; }
  .coverage-table thead th { padding: 8px 10px; font-size: 11px; min-width: 84px; }
  .coverage-table tbody th.cov-row-head {
    min-width: 128px; max-width: 150px;
    padding: 8px 10px; font-size: 13px;
  }
  /* Follows the narrower sensor column above — see .cov-wrap's scroll-padding-left. Left at 180 it
     would over-scroll every tick by 52px on the width where horizontal room is scarcest. */
  .cov-wrap { scroll-padding-left: 128px; }
  .coverage-table tbody th.cov-row-head .td-sub { display: none; }
  .coverage-table td.cov-cell { padding: 6px 4px; }
  .cov-mark { width: 22px; height: 22px; }
  .cov-mark-sm { width: 16px; height: 16px; margin-left: 2px; }
}

/* ============================================================
   Rule wizard — the guided /alerts/rules/new + edit flow
   ============================================================
   Built for someone who has never met an "alert function" before. Three rules govern the layout:

   1. ONE DECISION PER STEP. The old editor put scope, condition, twelve schema-driven parameters,
      severity, suppression and delete on one page and let the reader work out the order. The steps
      below are in causal order — where, then what, then when, then what it is called — because each
      one narrows the next: the scope can pin the sensor type, the sensor type filters the conditions,
      the condition declares the parameters.

   2. THE SUMMARY IS ALWAYS ON SCREEN. It is not the last step; it is a rail (desktop) or a docked
      strip (phone) that states, in one sentence, what will exist if Create is pressed. A wizard whose
      consequences are only visible on the final step is a wizard people click through.

   3. NOTHING IS HIDDEN BEHIND A COMPLETED STEP. Every step already visited stays clickable, and on an
      EXISTING rule every step is open from the start — an operator who came to change one number must
      not be walked through four screens to reach it. */

/* 340px, not 300px: the summary rail's job is to show the alert function key, and at 300px the
   longest key in the estate had ~162px of value column for ~227px of text, so it wrapped mid-key.
   With the narrower label column (see .wz-fact) this leaves ~236px — every key on one line, which
   is the whole point of showing it. The 40px comes out of a main column that is minmax(0, 1fr). */
.wizard {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 340px;
  gap: var(--gap-grid);
  align-items: start;
}
@media (max-width: 1080px) { .wizard { grid-template-columns: minmax(0, 1fr); } }

/* ---------- the step rail across the top ---------- */
.wz-steps {
  display: flex; align-items: stretch; gap: 4px;
  margin-bottom: var(--gap-grid);
  overflow-x: auto;
  /* A wizard whose steps scroll sideways on a phone is fine; one that scrolls the PAGE sideways is
     not, so the track clips itself rather than pushing the layout. */
  scrollbar-width: thin;
}
.wz-step {
  flex: 1 1 0; min-width: 132px;
  display: flex; align-items: center; gap: 9px;
  padding: 10px 12px;
  background: transparent; border: none; border-radius: var(--r2);
  text-align: left; cursor: pointer;
  color: var(--muted); font: inherit; font-size: 13px;
  border-top: 3px solid var(--line);
  transition: color 120ms, border-color 120ms, background-color 120ms;
}
.wz-step:hover:not(:disabled) { background: var(--soft); }
.wz-step:disabled { cursor: not-allowed; opacity: 0.6; }
.wz-step.is-current { color: var(--text); border-top-color: var(--accent); background: var(--soft); }
.wz-step.is-done { color: var(--text-2); border-top-color: var(--ok); }

/* The number becomes a tick once the step is satisfied — the one place in the wizard where a glance
   answers "what is left". */
.wz-step-n {
  flex: 0 0 22px; height: 22px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: var(--line); color: var(--text-2);
  font-size: 12px; font-weight: 600;
}
.wz-step.is-current .wz-step-n { background: var(--accent); color: #fff; }
.wz-step.is-done .wz-step-n { background: var(--ok-bg); color: var(--ok); }
.wz-step-text { min-width: 0; }
.wz-step-title { display: block; font-weight: 600; line-height: 1.3; }
.wz-step-sub { display: block; font-size: 11.5px; color: var(--muted); line-height: 1.3; }
@media (max-width: 700px) { .wz-step-sub { display: none; } }

/* ---------- the step body ---------- */
.wz-body { display: flex; flex-direction: column; gap: var(--gap-grid); min-width: 0; }
.wz-head { margin-bottom: 16px; }
.wz-head h2 { font-size: 18px; margin: 0 0 4px; }
.wz-head p { margin: 0; font-size: 13.5px; color: var(--text-2); line-height: 1.5; }

.wz-fields { display: flex; flex-direction: column; gap: 18px; }

.wz-foot {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding-top: 4px;
}
.wz-foot .spacer { flex: 1 1 auto; }

/* ---------- the always-on summary ---------- */
.wz-summary { position: sticky; top: 16px; display: flex; flex-direction: column; gap: 12px; }
@media (max-width: 1080px) { .wz-summary { position: static; } }

.wz-sentence {
  font-size: 14px; line-height: 1.55; color: var(--text);
  padding: 12px; border-radius: var(--r2); background: var(--soft);
}
.wz-sentence .is-blank { color: var(--muted); font-style: italic; }
.wz-sentence strong { font-weight: 600; }

.wz-facts { display: flex; flex-direction: column; gap: 0; margin: 0; }
/* The label column is FIXED, not auto, because each fact is its own grid — auto would size every
   row independently and the values would no longer line up. 60px is the longest label ("All-clear")
   plus a hair, where 92px left ~30px of dead space in front of every value. Alert function keys are
   the longest thing this rail ever shows (w8gear.weighment.dwell-exceeded, 31 chars), and that space
   was coming straight out of their line. */
.wz-fact {
  display: grid; grid-template-columns: 60px minmax(0, 1fr); gap: 8px;
  padding: 8px 0; border-bottom: 1px solid var(--line);
}
.wz-fact:last-child { border-bottom: none; }
.wz-fact dt { font-size: 12px; color: var(--muted); }
.wz-fact dd { margin: 0; font-size: 13px; color: var(--text); min-width: 0; overflow-wrap: anywhere; }

/* ---------- the function-key panel: the whole point of asks 1 and the Help page ---------- */
.fn-panel {
  border: 1px solid var(--accent);
  background: var(--accent-soft);
  border-radius: var(--r2);
  padding: 14px;
}
.fn-panel-head {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  font-size: 13px; font-weight: 600; color: var(--accent);
  margin-bottom: 8px;
}
.fn-key-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.fn-key {
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
  font-size: 14px; font-weight: 600;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--line-strong); border-radius: var(--r1);
  padding: 5px 9px;
  overflow-wrap: anywhere;
}
.fn-key.is-clear { font-weight: 500; }
.fn-note { margin: 10px 0 0; font-size: 12.5px; line-height: 1.5; color: var(--text-2); }
.fn-note:first-of-type { margin-top: 8px; }
.fn-panel-empty {
  border: 1px dashed var(--line-strong);
  background: transparent;
  border-radius: var(--r2);
  padding: 14px;
  font-size: 13px; color: var(--muted); line-height: 1.5;
}

/* ---------- review step ---------- */
.wz-review { display: flex; flex-direction: column; gap: 14px; }
.wz-review-group { border: 1px solid var(--line); border-radius: var(--r2); padding: 14px; }
.wz-review-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  margin-bottom: 10px;
}
.wz-review-head h3 { font-size: 14px; margin: 0; }
.wz-review-edit {
  background: none; border: none; padding: 0; cursor: pointer;
  font: inherit; font-size: 12.5px; color: var(--accent); text-decoration: underline;
}

/* A Dropdown inside a .filter-bar. The bar is a flex row and <Dropdown> renders a wrapper div, so
   the sizing that used to sit on `.filter-bar .select` moves out one level onto that wrapper —
   without it every filter collapses to its content width and the row reads as ragged. */
.filter-dd { flex: 0 1 auto; min-width: 160px; }
.type-select-wrap .dd { flex: 1 1 auto; min-width: 0; }

/* ============================================================
   Radzen chart theming
   ------------------------------------------------------------
   Radzen's chart and gauge CSS is entirely variable-driven, so rather than ship
   one of its component skins (which would fight this app's own design system) we
   load only default-base.css and remap its variables onto our tokens. Charts then
   follow light/dark with everything else, from one stylesheet, with no JS.

   THE SERIES PALETTE IS VALIDATED, NOT CHOSEN BY EYE. These four ran through the
   dataviz six-checks validator against BOTH surfaces (#FFFFFF light, #131922 dark)
   and pass every check; worst adjacent pair is 7C3AED<->0E9F9C at OKLab dE 20.9
   under deuteranopia, 30.2 unsimulated. The obvious-looking blue+purple pairing
   this replaced measured dE 0.4 under deuteranopia — indistinguishable — and 12.4
   even with full colour vision, which is below the readable floor. Do not add or
   reorder these without re-running the validator: adjacency is what is checked.

   Deliberately NOT reused here: --ok / --warn / --crit. Status colours are reserved
   for state and must never also mean "series 3", or a red line stops meaning alarm.
   ============================================================ */
:root {
  --rz-series-1: #0E9F9C;   /* teal — the app accent, so a single-series chart matches the UI */
  --rz-series-2: #7C3AED;   /* violet */
  --rz-series-3: #C2710C;   /* amber, deliberately NOT --warn #D97706 */
  --rz-series-4: #2563EB;   /* blue */

  /* Chrome mapped to our tokens so the chart recedes and the data carries the page. */
  --rz-base-background-color: var(--surface);
  --rz-text-color: var(--text);
  --rz-text-secondary-color: var(--muted);
  --rz-text-tertiary-color: var(--muted-2);
  --rz-chart-axis-color: var(--line-strong);
  --rz-base-300: var(--line);
  --rz-gauge-arc-scale-color: var(--line);
  --rz-gauge-arc-value-color: var(--accent);
  --rz-gauge-pointer-color: var(--text);
  --rz-chart-tooltip-background: var(--surface);
  --rz-chart-tooltip-color: var(--text);
}

/* Dark takes the SAME validated series hues: the palette was checked against the
   dark surface too, so this is a deliberate re-validation result, not an oversight
   or an automatic flip. Only the chrome moves, and it moves because the tokens do. */
:root[data-theme="dark"] {
  --rz-base-background-color: var(--surface);
  --rz-text-color: var(--text);
  --rz-text-secondary-color: var(--muted);
  --rz-text-tertiary-color: var(--muted-2);
}

/* Radzen sets its own font stack; charts should wear the app's. */
.rz-chart, .rz-gauge { font-family: var(--font-sans); }

/* Days-remaining gauge (see DaysRemainingGauge.razor) */
.hero-line-gauge { margin-top: 10px; }
.hero-gauge-note { font-size: 11px; text-align: center; margin-top: -4px; }
/* The card is the gauge's panel: let the gauge take the height that is left under the heading and
   centre in it, rather than sitting at natural size against the top edge with dead space beneath. */
.card-days { display: flex; flex-direction: column; }
.card-days .days-gauge,
.card-days .gauge-empty { flex: 1 1 auto; display: flex; flex-direction: column; justify-content: center; }
.days-gauge { display: block; }
.days-gauge-face { text-align: center; line-height: 1.15; }
.days-gauge-value { font: 700 44px/1 var(--font-mono); font-variant-numeric: tabular-nums; }
.days-gauge-value em { font: 500 18px var(--font-sans); font-style: normal; margin-left: 2px; }
.days-gauge-caption { font: 500 12px var(--font-sans); color: var(--muted); margin-top: 2px; }
.days-gauge-band {
  display: flex; align-items: center; justify-content: center; gap: 6px;
  font: 500 12px var(--font-sans); color: var(--text-2, var(--text)); margin-top: 4px;
}
.days-gauge-band .dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
/* No projection: deliberately plain — an empty state must not look like a reading of zero. */
.gauge-empty { display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 22px 0; }
.gauge-empty-dash { font: 600 30px var(--font-mono); color: var(--muted-2); line-height: 1; }
.gauge-empty-label { font: 500 12px var(--font-sans); color: var(--muted); }

/* Chart empty state (SensorTrendChart). Deliberately NOT a chart frame with no line in it — an
   empty axis pair reads as "the sensor reported zero", which is a measurement claim. */
.chart-empty { padding: 40px 16px; text-align: center; }
.chart-empty-title { font: 500 13px var(--font-sans); color: var(--muted); }
.chart-empty-hint { font: 400 12px var(--font-sans); color: var(--muted-2); margin-top: 4px; }

/* The W8Alert registration verdict inside the function-key panel. Same three colours as every other
   status in the app; is-ok is the only place in the wizard that goes green, because it is the only
   claim on the page that something downstream will actually work. */
.fn-verdict {
  display: flex; align-items: flex-start; gap: 8px;
  margin-top: 10px; padding: 10px;
  border-radius: var(--r1);
  font-size: 12.5px; line-height: 1.5;
}
.fn-verdict svg { flex-shrink: 0; margin-top: 2px; }
.fn-verdict strong { display: block; font-weight: 600; }
.fn-verdict.is-ok    { background: var(--ok-bg);   color: var(--ok);   }
.fn-verdict.is-crit  { background: var(--crit-bg); color: var(--crit); }
.fn-verdict.is-muted { background: var(--soft);    color: var(--text-2); }

/* Days-gauge legend: the bands with their numbers, so a colour is a threshold and not a mood. */
.days-gauge-legend { margin: 8px 0 0; display: grid; gap: 2px; }
.days-gauge-legend-row {
  display: grid; grid-template-columns: 74px 1fr; align-items: center; gap: 8px;
  padding: 3px 6px; border-radius: var(--r1, 6px);
  font: 500 11.5px var(--font-sans); color: var(--muted);
}
/* The active band is marked by weight and a surface tint, not by colour alone — the row colours are
   already the thing being explained, so they cannot also carry "you are here". */
.days-gauge-legend-row.is-active { background: var(--soft, rgba(127,127,127,.10)); color: var(--text); font-weight: 600; }
.days-gauge-legend-row dt { display: flex; align-items: center; gap: 6px; margin: 0; }
.days-gauge-legend-row dd { margin: 0; }
.days-gauge-legend-row .dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
.days-gauge-legend-range { font-variant-numeric: tabular-nums; }

/* Trend chart hover tooltip */
.trend-tip { text-align: left; }
.trend-tip-value { font: 700 15px var(--font-mono); font-variant-numeric: tabular-nums; }
.trend-tip-value em { font: 500 11px var(--font-sans); font-style: normal; margin-left: 3px; opacity: .75; }
.trend-tip-when { font: 500 11px var(--font-sans); opacity: .75; margin-top: 2px; }

/* Event marks legend under the trend chart */
.trend-marks-legend {
  display: flex; flex-wrap: wrap; gap: 14px; justify-content: center;
  font: 500 11.5px var(--font-sans); color: var(--muted); margin-top: 4px;
}
.trend-marks-legend .mk { font-size: 12px; margin-right: 3px; }

/* Says why a download produced nothing, next to the button that produced it. The adapter returns
   empty rather than substituting the chart's much smaller series, so this is the only signal. */
.download-note { font-size: 12px; align-self: center; max-width: 220px; line-height: 1.3; }

/* Custom event range: the Apply button and its validation line. The dates are a draft until
   applied, so the hint has to say which state you are in — pending, invalid, or showing. */
.range-apply { align-self: end; margin-bottom: 10px; white-space: nowrap; }
.range-invalid { color: var(--crit); }

/* ---------- the alerts board (mirrors W8Alert's, deliberately) ----------
   Delivery chips and the acknowledgement badge share one shape because they sit in adjacent columns
   and are read together — but they never share a colour scale: delivery is a fact about a message,
   acknowledgement is a fact about a person. */
.ack-chips { display: flex; flex-wrap: wrap; gap: 4px; }
.ack-badge {
  display: inline-block; font-size: 11.5px; font-weight: 600;
  padding: 2px 8px; border-radius: var(--r-pill, 999px); white-space: nowrap;
  background: var(--soft); color: var(--muted);
}
.ack-badge.ok   { background: var(--ok-bg);   color: var(--ok); }
.ack-badge.warn { background: var(--warn-bg); color: var(--warn); }
.ack-badge.crit { background: var(--crit-bg); color: var(--crit); }

/* WHY NOBODY GOT IT — the sentence that replaced a blank cell.
   A sentence rather than a chip, because it is not one channel's outcome: it is the absence of every
   channel, and squeezing "outside your notification window" into a pill would either truncate it or
   make it look like a fifth delivery. Muted at rest since most of these are a setting working exactly
   as asked; a dispatch BLOCK is warmer, because something was created and then stopped. */
.delivery-none {
  display: inline-block; font-size: 12px; line-height: 1.4; max-width: 34ch;
  color: var(--muted);
}
.delivery-none.is-block { color: var(--warn); }

/* The message line under each title is secondary; keep it from crowding the row on narrow screens. */
.alerts-table .td-primary { min-width: 240px; }
.alerts-table .td-sub { font-size: 12.5px; }

/* The bulk-acknowledge control shares the filter row but is not a filter, so a spacer pushes it to the
   far end rather than letting it read as a fifth tab. */
.filter-bar-spacer { flex: 1 1 auto; min-width: 12px; }
.bulk-confirm-text { font-size: 12.5px; align-self: center; }
