/* ── Tokens ─────────────────────────────────────────────────────────────
   Light and dark are both selected sets, not an automatic flip. Dark values are
   declared under two scopes: the media query covers the OS setting, the
   [data-theme] scope covers the in-app toggle and must win both ways.

   The two series hues carry the app's whole identity: terracotta for the fire
   side (boiler, pellets) and teal for water. That pairing is not a taste call —
   the mock's original coral/olive scored ΔE 1.1 under deuteranopia, i.e. the two
   dial rings would have been the same colour for a red-green colourblind reader.
   Both sets below pass all six checks of the dataviz validator against their own
   page colour:
     dark   #dd6d45 / #1aa0b9  on #0d0d0c  — CVD ΔE 16.4, normal ΔE 26.1
     light  #c85a33 / #0d8ea3  on #f1f1ee  — CVD ΔE 16.2, normal ΔE 25.2
   Re-run before changing either: node scripts/validate_palette.js "<a>,<b>"
   --mode <mode> --surface <page>. */

:root {
  color-scheme: light;

  --page:      #f1f1ee;
  --surface:   #fcfcfb;
  --surface-2: #f5f5f1;
  --ink:       #0b0b0b;
  --ink-2:     #52514e;
  /* 4.70:1 on --page and 5.19:1 on --surface. The old #898781 was 3.17:1, which
     this redesign leans on far harder than the previous one did (eyebrows, tab
     labels, the dial's own labels), so it had to come down to a passing step. */
  --muted:     #6d6b66;
  --grid:      #e1e0d9;
  --axis:      #c3c2b7;
  --border:    rgba(11, 11, 11, 0.09);

  --series:      #c85a33;  /* heat — boiler, pellets, the trend line */
  --series-2:    #0d8ea3;  /* water — the hot-water ring */
  /* The accent as TEXT, and the label that sits on it when it fills a button.
     --series itself is a *mark* colour: validated at the 3:1 non-text threshold,
     it only reaches 4.12:1 as 14px type, so link buttons and pending values need
     their own darker step. Measured: #b3491f is 5.26:1 on --surface and 4.77:1 on
     --page as text, and 5.40:1 against white as a button fill. */
  --series-ink:  #b3491f;
  --on-series:   #ffffff;
  --series-wash: rgba(200, 90, 51, 0.10);
  --ghost:       rgba(11, 11, 11, 0.05);
  --shadow:      0 1px 2px rgba(11, 11, 11, 0.04), 0 10px 28px -14px rgba(11, 11, 11, 0.12);

  /* Status palette — reserved, never reused for a series. `offline` maps to
     --warning rather than a third orange, because the old --serious step had
     drifted to within a hair of the terracotta series colour. */
  --good:      #0ca30c;
  --warning:   #fab219;
  --critical:  #d03b3b;
  /* The critical step as TEXT. --critical is tuned as a fill (white on it is
     4.8:1) and only reaches 4.25:1 as the 12px fault line in the header, so that
     one use gets its own step: 4.66:1 light, 5.35:1 dark. */
  --critical-ink: #c93232;

  /* Luminosity is a dark-mode affordance. On a near-white page a glow reads as a
     printing error, so both are off here and switched on in the dark scopes. */
  --dial-glow:   none;
  --figure-glow: none;

  --radius:    18px;
  --radius-sm: 12px;
  --gap:       12px;
  /* The tab bar's own height, before its safe-area padding. The toast sits above
     it, so one number owns both rather than two that can drift apart. */
  --tabbar-h:  56px;
  --font: system-ui, -apple-system, "Segoe UI", sans-serif;
}

/* Dark values, shared by the media query and the toggle scope below. */
@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme="light"])) {
    color-scheme: dark;
    --page:      #0d0d0c;
    --surface:   #151514;
    --surface-2: #1e1e1c;
    --ink:       #f7f7f5;
    --ink-2:     #b9b7b0;
    --muted:     #85837d;
    --grid:      #262624;
    --axis:      #34342f;
    --border:    rgba(255, 255, 255, 0.07);

    --series:      #dd6d45;
    --series-2:    #1aa0b9;
    /* 5.52:1 as text on --surface, so the mark colour doubles as the text step
       here. Filled, it takes a near-black label at 5.88:1 — white would be
       3.31:1, which is why --on-series flips rather than staying white. */
    --series-ink:  #dd6d45;
    --on-series:   #0d0d0c;
    --series-wash: rgba(221, 109, 69, 0.13);
    --ghost:       rgba(255, 255, 255, 0.06);
    --shadow:      0 1px 2px rgba(0, 0, 0, 0.5);

    --good: #2ec27e;
    --critical-ink: #e05a5a;

    --dial-glow:   drop-shadow(0 0 7px color-mix(in srgb, var(--ring-hue) 45%, transparent));
    --figure-glow: 0 0 30px color-mix(in srgb, var(--series) 30%, transparent);
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --page:      #0d0d0c;
  --surface:   #151514;
  --surface-2: #1e1e1c;
  --ink:       #f7f7f5;
  --ink-2:     #b9b7b0;
  --muted:     #85837d;
  --grid:      #262624;
  --axis:      #34342f;
  --border:    rgba(255, 255, 255, 0.07);

  --series:      #dd6d45;
  --series-2:    #1aa0b9;
  --series-ink:  #dd6d45;
  --on-series:   #0d0d0c;
  --series-wash: rgba(221, 109, 69, 0.13);
  --ghost:       rgba(255, 255, 255, 0.06);
  --shadow:      0 1px 2px rgba(0, 0, 0, 0.5);

  --good: #2ec27e;
  --critical-ink: #e05a5a;

  --dial-glow:   drop-shadow(0 0 7px color-mix(in srgb, var(--ring-hue) 45%, transparent));
  --figure-glow: 0 0 30px color-mix(in srgb, var(--series) 30%, transparent);
}

/* ── Base ──────────────────────────────────────────────────────────────── */

* { box-sizing: border-box; }

html, body { min-height: 100%; }

/* An app shell, not a document: the page itself is exactly the viewport and never
   scrolls. The header is the shell's first row, <main> takes the rest and is the
   only scrolling box, and the tab bar is pinned to the viewport's bottom edge.
   That is what makes "the Now screen doesn't scroll" true in an installed PWA and
   not just in a browser tab. Budgeting the height with viewport units can't do it:
   they count the whole screen including the safe-area insets, which are 0 in a tab
   and ~80px once installed, so the sums are wrong by exactly the amount that
   overflows. Here the insets are padding on real boxes — the header, the tab bar,
   and <main>'s reservation for it — so whatever is left over is what the Now view
   gets handed, and no arithmetic has to be right. */
body {
  display: flex;
  flex-direction: column;
  /* `svh`, not `dvh`: the smallest viewport, i.e. the one with every dynamic
     toolbar showing. Nothing here scrolls, so a toolbar can never retract to give
     the height back — and a shell even slightly taller than what's really visible
     pushes its own bottom edge off-screen with no way to scroll to it. */
  height: 100vh;
  height: 100svh;
  overflow: hidden;
  margin: 0;
  background: var(--page);
  color: var(--ink);
  font-family: var(--font);
  font-size: 16px;
  line-height: 1.45;
  -webkit-text-size-adjust: 100%;
  -webkit-tap-highlight-color: transparent;
}

[hidden] { display: none !important; }

h1, h2, p { margin: 0; }

ul { margin: 0; padding: 0; list-style: none; }

:where(button, input, [tabindex]):focus-visible {
  outline: 2px solid var(--series);
  outline-offset: 2px;
  border-radius: 4px;
}

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 18px;
  box-shadow: var(--shadow);
}

/* Small uppercase kicker over a hero figure or a list. */
.eyebrow,
.section-title {
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}

.eyebrow { text-align: center; }

.section-title { margin: 0 2px 10px; }

.card-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.card-title { font-size: 1rem; font-weight: 600; }

.card-sub {
  font-size: 0.8125rem;
  color: var(--muted);
  margin-top: 2px;
}

/* ── Sign in ───────────────────────────────────────────────────────────── */

/* Its own scroller, so a small screen with the keyboard up can still reach the
   button. min-height:0 lets it shrink below its content, which is what allows the
   overflow to scroll rather than escape the shell. */
.signin {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  display: grid;
  place-items: center;
  padding: 20px;
}

.signin-card { width: 100%; max-width: 380px; padding: 24px; }

.signin-title { font-size: 1.5rem; font-weight: 650; letter-spacing: -0.01em; }

.signin-lede { color: var(--ink-2); font-size: 0.9375rem; margin-top: 6px; }

.field { display: block; margin-top: 20px; }

.field-label {
  display: block;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--ink-2);
  margin-bottom: 6px;
}

.field input {
  width: 100%;
  min-height: 48px;
  padding: 0 14px;
  font: inherit;
  color: var(--ink);
  background: var(--page);
  border: 1px solid var(--axis);
  border-radius: var(--radius-sm);
}

.button {
  width: 100%;
  min-height: 48px;
  margin-top: 16px;
  font: inherit;
  font-weight: 600;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.button-primary { background: var(--series-ink); color: var(--on-series); }
.button-primary:disabled { opacity: 0.6; cursor: progress; }

/* For confirming something destructive, where the colour carries the meaning. */
.button-danger { background: var(--critical); color: #fff; }

.signin-error {
  margin-top: 14px;
  padding: 10px 12px;
  font-size: 0.875rem;
  color: var(--ink);
  background: var(--ghost);
  border-left: 3px solid var(--critical);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}

.signin-note { margin-top: 16px; font-size: 0.8125rem; color: var(--muted); }

/* ── Top bar ───────────────────────────────────────────────────────────── */

/* In flow at the top of the shell. It used to be sticky over a scrolling page;
   now nothing scrolls past it, so there is nothing to stick to. */
.topbar {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: max(10px, env(safe-area-inset-top)) 16px 10px;
  background: var(--page);
}

/* ── Account menu ───────────────────────────────────────────────────────── */

/* Takes the header's free space, so the timestamp isn't ellipsised by a
   content-sized wrapper. min-width:0 still lets a long alias truncate. */
.account-wrap { position: relative; flex: 1 1 auto; min-width: 0; }

.account {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 4px 6px 4px 4px;
  margin-left: -4px;
  font: inherit;
  color: var(--ink);
  text-align: left;
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.account:hover { background: var(--ghost); }

.account-text { display: flex; flex-direction: column; min-width: 0; }

.account-name {
  font-size: 1.0625rem;
  font-weight: 650;
  letter-spacing: -0.01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.account-meta {
  font-size: 0.75rem;
  font-weight: 400;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* A fault takes over this line — red words in the header, at no layout cost. */
.account-meta[data-fault] { color: var(--critical-ink); font-weight: 600; }

/* Sits right after the text rather than at the far edge of the stretched button. */
.account-chevron { flex: none; margin-right: auto; color: var(--muted); }

.account[aria-expanded="true"] .account-chevron { transform: rotate(180deg); }

.menu {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 10; /* over <main>, which starts immediately below the header */
  min-width: 180px;
  padding: 6px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.28);
}

/* Who you're signed in as, and which controller — reference, not actions. */
.menu-meta {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 6px 10px 10px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--grid);
}

.menu-user { font-size: 0.875rem; font-weight: 600; }

.menu-serial {
  font-size: 0.75rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.menu-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: 44px;
  padding: 0 10px;
  font: inherit;
  font-size: 0.9375rem;
  color: var(--ink);
  background: none;
  border: 0;
  border-radius: 7px;
  cursor: pointer;
  text-align: left;
}

.menu-item:hover { background: var(--ghost); }

.menu-item svg { flex: none; color: var(--muted); }

/* ── Topbar actions ─────────────────────────────────────────────────────── */

.topbar-actions { display: flex; align-items: center; gap: 6px; flex: none; }

.icon-button {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  color: var(--ink-2);
  background: none;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
}

.icon-button:hover { background: var(--ghost); color: var(--ink); }

/* Loading shows here and nowhere else — dimming the page on every poll read as
   a constant reload. */
.icon-button[data-loading] { color: var(--series); }
.icon-button[data-loading] svg { animation: spin 0.75s linear infinite; }

@keyframes spin { to { transform: rotate(360deg); } }

/* ── Header power button ────────────────────────────────────────────────── */

/* Circular, icon-only. That is only unambiguous because the running state is
   spelled out in words inside the dial and every tap opens a confirm naming the
   action — the colour is reinforcement, not the message. */
.power-button {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  padding: 0;
  color: var(--ink-2);
  background: var(--surface);
  border: 1px solid var(--axis);
  border-radius: 50%;
  cursor: pointer;
}

.power-button:hover { color: var(--ink); border-color: var(--ink-2); }

.power-button[data-on] {
  color: var(--good);
  background: color-mix(in srgb, var(--good) 13%, var(--surface));
  border-color: color-mix(in srgb, var(--good) 55%, var(--axis));
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--good) 10%, transparent);
}

/* ── Main + views ──────────────────────────────────────────────────────── */

/* The dashboard wrapper has to pass the shell's height through to <main>, or the
   Now view has nothing to distribute. min-height:0 on every link in the chain:
   without it a flex item refuses to shrink below its content, and "refuses to
   shrink" is how a fixed shell springs a leak. */
#app {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

/* The one scrolling box in the app. Views that are genuinely longer than the
   screen (Status, All readings) scroll here, between a header and a tab bar that
   stay put. */
.main {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  /* Keeps a flick at the end of a list from turning into a page bounce. */
  overscroll-behavior-y: contain;
  width: 100%;
  max-width: 560px;
  margin: 0 auto;
  /* The bottom padding is the fixed tab bar's height plus its safe-area inset, plus
     a gap. One token owns the height, so the two can't drift apart. */
  padding: 8px 16px calc(var(--tabbar-h) + env(safe-area-inset-bottom) + 12px);
  box-sizing: border-box;
}

/* Trend, Status and Targets are documents: natural height, and <main> scrolls them.
   Only Now is told to fill the box. */
.view { display: flex; flex-direction: column; gap: var(--gap); flex: 0 0 auto; }

/* Now is a composition rather than a document: dial at the top, pellets at the
   foot, and the spare height as the gap between them.
   `flex: 1 1 auto` with `min-height: 0` is doing two jobs — grow into the free
   space, and be *allowed to shrink below its content*, which is what lets the dial
   give up pixels instead of pushing the pellet count off the screen. */
#view-now {
  flex: 1 1 auto;
  min-height: 0;
  justify-content: space-between;
}

/* Past a certain height the gap stops being breathing room and becomes a chasm —
   a desktop window is twice a phone. Capped only where the cap can't collide with
   the content it's spacing. */
@media (min-height: 860px) {
  #view-now { max-height: 800px; }
}

/* Deliberately no dimming or skeleton on refetch. The previous render stays put
   at full opacity and values are swapped in place; the only loading cue is the
   refresh button. */

/* ── Severity accents ──────────────────────────────────────────────────── */

[data-severity="burning"]  { --accent: var(--good); }
[data-severity="active"]   { --accent: var(--series-2); }
[data-severity="idle"]     { --accent: var(--muted); }
[data-severity="offline"]  { --accent: var(--warning); }
[data-severity="critical"] { --accent: var(--critical); }
[data-severity="unknown"]  { --accent: var(--muted); }

/* ── The dial ──────────────────────────────────────────────────────────── */

/* Two concentric arcs sweeping 270°, open at the bottom. Sits directly on the
   page rather than in a card: the composition is the card. */
.dial {
  position: relative;
  /* Width leads while there's room for it... */
  height: min(320px, 78vw);
  width: auto;
  aspect-ratio: 1;
  /* ...but the dial is also the one element on this screen allowed to give up
     space, and shrinking is what makes the no-scroll layout hold up everywhere.
     A viewport-height sum can't do this job: `dvh` measures the whole screen
     including the safe-area insets, which are 0 in a browser tab and ~80px in an
     installed PWA — so a layout that fits in Safari would scroll once installed.
     Flexbox knows the real numbers, because the insets are padding on real boxes.
     `align-self` keeps the width tied to the aspect ratio rather than stretching
     to the column. */
  flex: 0 1 auto;
  align-self: center;
  min-height: 160px;
  /* The readout scales with the box, so a shrunk dial doesn't spill its numbers.
     Requires the dial's size to be independent of its contents, which it is. */
  container-type: size;
  /* The bottom ~7% of the box is inside the arc's opening and below the status
     line, so it is empty by construction — reclaim it. */
  margin: 2px auto -16px;
}

.dial-svg { display: block; width: 100%; height: 100%; overflow: visible; }

/* Each ring's hue, inherited by its track, fill, notch and centre dot. */
[data-ring="boiler"] { --ring-hue: var(--series); }
[data-ring="dhw"]    { --ring-hue: var(--series-2); }

/* Unfilled track is a light step of the fill's OWN hue, so the state reads
   across the whole ring instead of the track competing as a second series. */
.dial-track {
  fill: none;
  stroke: color-mix(in srgb, var(--ring-hue) 20%, var(--page));
  stroke-linecap: round;
}

.dial-fill {
  fill: none;
  stroke: var(--ring-hue);
  stroke-linecap: round;
  filter: var(--dial-glow);
}

/* Target marker: a page-coloured break through the ring with a hairline down the
   middle, so it reads on the pale track and the saturated fill alike. */
.dial-notch-gap { stroke: var(--page); }
.dial-notch { stroke: var(--ink-2); }

/* The readout, inside the inner ring. 17.5% inset leaves a 65% box, which is
   exactly the inner ring's clear diameter. */
.dial-centre {
  position: absolute;
  inset: 17.5%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  text-align: center;
}

.dial-read { min-width: 0; }

/* A shrunk dial has a smaller readout box, so the stack tightens with it — the
   query is on the dial itself, which is why `container-type` is set there. Below
   this the two readings would spill over the rings. */
@container (max-width: 245px) {
  .dial-centre { gap: 3px; }
  .dial-label { font-size: 0.5625rem; letter-spacing: 0.07em; }
  .dial-sub { font-size: 0.625rem; }
}

/* Last resort, at the dial's floor size: the target line goes rather than spill
   over the rings. Two readings and their labels are the part you can't drop; the
   targets have a whole tab of their own. */
@container (max-width: 200px) {
  .dial-sub { display: none; }
}

.dial-label {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}

/* The coloured mark that ties this readout to its ring. Identity rides here, so
   the number itself can stay in ink. */
.dial-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--ring-hue);
  flex: none;
}

.dial-value {
  /* cqmin, not vw: this has to track the dial it sits inside, which is now sized
     by the space available rather than by the viewport. */
  font-size: clamp(1.5rem, 13cqmin, 2.625rem);
  font-weight: 300;
  line-height: 1.1;
  letter-spacing: -0.02em;
  margin-top: 1px;
}

.dial-unit {
  font-size: 0.36em;
  font-weight: 500;
  color: var(--ink-2);
  margin-left: 0.16em;
  letter-spacing: 0;
}

.dial-sub {
  font-size: 0.6875rem;
  color: var(--muted);
  white-space: nowrap;
}

.dial-rule {
  width: 46px;
  height: 1px;
  background: var(--grid);
}

/* With only one ring fitted the divider would dangle under a single readout. */
.dial-centre:has(.dial-read[hidden]) .dial-rule { display: none; }

/* The state, in the arc's open bottom — words, at zero layout cost. A button,
   because it's also the route into the Status view. */
.dial-status {
  position: absolute;
  left: 15%;
  right: 15%;
  /* Centred in the arc's opening, which runs from the endpoints at 81% of the
     box down to its bottom edge — not below the ring, which would cost height. */
  bottom: 7%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 4px 6px;
  font: inherit;
  text-align: center;
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.dial-status:hover { background: var(--ghost); }

.dial-status-icon {
  display: grid;
  place-items: center;
  flex: none;
  color: var(--accent, var(--muted));
}

.dial-status-text {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--ink-2);
  line-height: 1.25;
  /* One line, always. A second line grows upward into the readout — "Out of
     pellets" wrapping across the hot-water temperature is how that looks. The full
     wording is on the Status tab, which this button opens. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* A shrunk dial gives the status line the full width of the box and a smaller
   type size, so it stays one line instead of ellipsising early. */
@container (max-width: 245px) {
  .dial-status { left: 2%; right: 2%; }
  .dial-status-text { font-size: 0.75rem; }
}

/* ── Pellets (the hero figure) ─────────────────────────────────────────── */

.fuel { text-align: center; }

/* The single hero figure on the screen: how many kilograms are left.
   Ink, not terracotta — a value wears a text token, and the meter right below it
   needs the colour channel free to carry severity. The glow supplies the mock's
   luminosity without putting a series hue on a numeral. */
.figure {
  /* Same reasoning as the dial: the 9.5vh term only bites on a short screen, where
     the alternative is a scrollbar. It stays the largest thing on the page either way. */
  font-size: clamp(3.25rem, min(19vw, 9.5vh), 4.5rem);
  font-weight: 300;
  line-height: 1.05;
  letter-spacing: -0.03em;
  margin-top: 6px;
  text-shadow: var(--figure-glow);
}

.figure-unit {
  font-size: 0.3em;
  font-weight: 500;
  color: var(--ink-2);
  margin-left: 0.2em;
  letter-spacing: 0;
  text-shadow: none;
}

/* ── Meter ─────────────────────────────────────────────────────────────── */

/* The fill carries severity; the track is a lighter step of the fill's OWN ramp,
   so state reads across the whole bar. Deriving the track from --meter-hue
   matters: a red fill on a fixed blue track made a nearly-empty hopper look
   mostly full, because the saturated blue read as the "filled" part. */
.meter {
  --meter-hue: var(--series);
  width: min(260px, 100%);
  height: 7px;
  margin: 14px auto 0;
  border-radius: 999px;
  background: color-mix(in srgb, var(--meter-hue) 22%, var(--page));
  overflow: hidden;
}

.meter[data-level="low"]   { --meter-hue: var(--warning); }
.meter[data-level="empty"] { --meter-hue: var(--critical); }

.meter-fill {
  height: 100%;
  border-radius: 999px;
  background: var(--meter-hue);
  transition: width 0.4s ease;
}

.meter-sub {
  font-size: 0.8125rem;
  color: var(--muted);
  margin-top: 9px;
  min-height: 1.2em;
}

/* Severity rides a background tint, never the text colour: --warning as ink is
   about 1.8:1 on the light surface. The wording ("16 h left") carries the
   meaning; the tint only reinforces it. */
.fuel-days {
  display: inline-block;
  margin-left: 2px;
  padding: 1px 8px;
  font-weight: 600;
  color: var(--ink-2);
  background: var(--surface-2);
  border-radius: 999px;
  white-space: nowrap;
}

.fuel-days[data-level="low"] {
  color: var(--ink);
  background: color-mix(in srgb, var(--warning) 26%, var(--surface-2));
}

.fuel-days[data-level="empty"] {
  color: var(--ink);
  background: color-mix(in srgb, var(--critical) 24%, var(--surface-2));
}

/* ── Stat strip ────────────────────────────────────────────────────────── */

/* Auto-columns rather than a fixed count: hidden cells (no consumption data yet)
   drop out of the grid entirely, so the remaining one or two still fill the row
   instead of leaving a gap. */
.stat-strip {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 8px;
  margin-top: 18px;
  text-align: left;
}

.stat {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
  padding: 11px 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

.stat-label {
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  /* Allowed to wrap: at three-across on a 320px screen "Last 24 h" would
     otherwise be clipped, and a cropped label is worse than a taller cell. */
}

.stat-value {
  font-size: 1.0625rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  white-space: nowrap;
}

/* Three cells across a small phone: 320px leaves ~58px of content per cell,
   which clips "14,2 kg" at the full type size. */
@media (max-width: 380px) {
  .stat { padding: 9px 9px; }
  .stat-label { font-size: 0.625rem; letter-spacing: 0.04em; }
  .stat-value { font-size: 0.9375rem; }
}

/* Short viewports: a 4-inch phone, or any phone with the browser's own chrome
   showing — 100dvh on an iPhone SE with both Safari bars is around 560px. The
   composition keeps its shape and everything gives up a few pixels instead, which
   is worth more here than a scrollbar. The dial and the hero figure carry vh terms
   of their own, so those shrink without being named. */
@media (max-height: 640px) {
  .figure { margin-top: 2px; }
  .meter-sub { margin-top: 6px; }
  .stat { padding: 7px 9px; }
}

/* ── Status view ───────────────────────────────────────────────────────── */

.status-head { display: flex; align-items: flex-start; gap: 12px; }

.status-icon {
  display: grid;
  place-items: center;
  flex: none;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  color: var(--accent, var(--muted));
  background: color-mix(in srgb, var(--accent, var(--muted)) 14%, transparent);
}

.status-words { min-width: 0; }

.status-title { font-size: 1.0625rem; font-weight: 650; letter-spacing: -0.01em; }

.status-sub { font-size: 0.875rem; color: var(--ink-2); margin-top: 2px; }

.status-official {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--grid);
  font-size: 0.75rem;
  color: var(--muted);
}

/* ── Controls ──────────────────────────────────────────────────────────── */

#controls { margin-top: 16px; }

.control-button {
  min-height: 48px;
  padding: 0 14px;
  font: inherit;
  font-weight: 600;
  color: var(--ink);
  background: var(--page);
  border: 1px solid var(--axis);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.control-button:hover:not(:disabled) { border-color: var(--critical); }

.control-button:disabled { opacity: 0.45; cursor: not-allowed; }

.control-button-wide { width: 100%; margin-top: 14px; }

.setpoint {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

/* Separator only between setpoints, not above the first one. */
.setpoint + .setpoint {
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid var(--grid);
}

.setpoint-head { display: flex; flex-direction: column; min-width: 0; }

.setpoint-label { font-size: 0.9375rem; font-weight: 600; }

.setpoint-range { font-size: 0.75rem; color: var(--muted); }

.stepper { display: flex; align-items: center; gap: 4px; flex: none; }

.stepper-button {
  width: 44px;
  height: 44px;
  font-size: 1.25rem;
  font-weight: 600;
  color: var(--ink);
  background: var(--page);
  border: 1px solid var(--axis);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.stepper-button:hover:not(:disabled) { border-color: var(--ink-2); }
.stepper-button:disabled { opacity: 0.35; cursor: not-allowed; }

.stepper-value {
  min-width: 5.2em;
  text-align: center;
  font-size: 1.0625rem;
  font-weight: 640;
  font-variant-numeric: tabular-nums;
}

/* A value written locally but not yet acknowledged by the controller. */
.stepper-value[data-pending] { color: var(--series-ink); }

.control-note { font-size: 0.8125rem; color: var(--muted); margin-top: 14px; }

.control-note[data-pending] { color: var(--series-ink); }

/* ── Tab bar ───────────────────────────────────────────────────────────── */

/* Pinned to the viewport's own bottom edge, not laid out as the shell's last row.
   As a flex item it was only ever as visible as the shell's height was accurate,
   and a shell that overshoots the visible area by a few pixels puts its last row
   past the fold — with nothing scrollable, that row is simply gone. Pinned, the bar
   is where the device says the bottom is, whatever the height maths did.
   <main> reserves its height below, so nothing ends up underneath it. */
.tabbar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 15;
  display: flex;
  padding-bottom: env(safe-area-inset-bottom);
  /* Opaque: content scrolling inside <main> passes behind it. */
  background: var(--page);
  border-top: 1px solid var(--border);
}

.tab {
  position: relative;
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  min-height: var(--tabbar-h);
  padding: 8px 2px 7px;
  font: inherit;
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--muted);
  background: none;
  border: 0;
  cursor: pointer;
}

.tab svg { flex: none; }

/* The label goes to full ink and the icon takes the accent. Terracotta on the
   light page is 3.74:1 — fine for a 22px glyph (non-text needs 3:1), short of the
   4.5:1 a 10px label needs. So the colour rides the graphic, the word stays ink,
   and the selected state still reads on three channels. */
.tab[aria-selected="true"] { color: var(--ink); }
.tab[aria-selected="true"] svg { color: var(--series); }

/* A fault is visible from any tab, not just the one that explains it. */
.tab-badge {
  position: absolute;
  top: 7px;
  left: 50%;
  margin-left: 7px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--critical);
  border: 2px solid var(--page);
}

/* ── Dialogs ───────────────────────────────────────────────────────────── */

.dialog {
  width: min(360px, calc(100vw - 32px));
  padding: 20px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
}

.dialog::backdrop { background: rgba(0, 0, 0, 0.5); }

.dialog-title { font-size: 1.125rem; font-weight: 650; }

.dialog-body { font-size: 0.9375rem; color: var(--ink-2); margin-top: 8px; }

.dialog-actions { display: flex; gap: 10px; margin-top: 20px; }

.dialog-actions .button { margin-top: 0; }

.dialog-cancel {
  color: var(--ink);
  background: var(--page);
  border-color: var(--axis);
}

.field-optional {
  font-weight: 400;
  color: var(--muted);
  text-transform: none;
  letter-spacing: normal;
}

/* ── Tiles ─────────────────────────────────────────────────────────────── */

.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--gap);
}

.tile {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px;
}

.tile-label {
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}

.tile-value {
  font-size: 1.625rem;
  font-weight: 500;
  letter-spacing: -0.02em;
  margin-top: 3px;
}

.tile-unit {
  font-size: 0.5em;
  font-weight: 500;
  color: var(--ink-2);
  margin-left: 0.2em;
  letter-spacing: 0;
}

.tile-sub { font-size: 0.75rem; color: var(--muted); margin-top: 2px; }

/* ── Chart ─────────────────────────────────────────────────────────────── */

.chart-card { padding-bottom: 12px; }

.chart-host { position: relative; margin-top: 10px; }

.chart { display: block; overflow: visible; touch-action: pan-y; }

.chart-grid  { stroke: var(--grid); stroke-width: 1; }
.chart-tick  { fill: var(--muted); font-size: 10px; font-variant-numeric: tabular-nums; }
.chart-line  { fill: none; stroke: var(--series); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.chart-area  { fill: var(--series-wash); stroke: none; }

/* 2px surface ring keeps the marker legible where it crosses the line. */
.chart-end,
.chart-hoverdot { fill: var(--series); stroke: var(--surface); stroke-width: 2; }

.chart-endlabel { fill: var(--ink-2); font-size: 11px; font-weight: 600; }

.chart-crosshair { stroke: var(--axis); stroke-width: 1; pointer-events: none; }

/* Chrome applies :focus-visible on click for tabindex-only elements, so this
   ring shows on mouse too — offset it far enough that it reads as a focus ring
   rather than a border around the plot. */
.chart-hit { cursor: crosshair; }
.chart-hit:focus-visible { outline-offset: 4px; }

.chart-empty {
  padding: 28px 4px;
  font-size: 0.875rem;
  color: var(--muted);
  text-align: center;
}

.chart-tooltip {
  position: absolute;
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 6px 9px;
  pointer-events: none;
  white-space: nowrap;
  font-size: 0.8125rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
}

.chart-tooltip-time { color: var(--muted); font-size: 0.75rem; }

/* ── Tables (the WCAG-clean twin of every chart) ────────────────────────── */

.link-button {
  padding: 6px 2px;
  font: inherit;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--series-ink);
  background: none;
  border: 0;
  cursor: pointer;
}

/* No max-height: a capped box gives these tables their own cramped inner
   scrollbar. They're behind a toggle, so let them grow and let the page scroll. */
.table-wrap { margin-top: 10px; }

table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }

caption {
  text-align: left;
  font-size: 0.8125rem;
  color: var(--muted);
  padding-bottom: 6px;
}

th, td { text-align: left; padding: 7px 8px; border-bottom: 1px solid var(--grid); }

th {
  background: var(--surface);
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--muted);
}

/* Row headers are labels, not column headings — they read as body text. */
th.row-label {
  font-size: 0.875rem;
  font-weight: 400;
  color: var(--ink);
}

/* A column of clock times: left-aligned, but digits must still line up. */
th.row-time { font-variant-numeric: tabular-nums; }

td.num, th.num {
  text-align: right;
  /* Columns of numbers align vertically, so tabular figures belong here. */
  font-variant-numeric: tabular-nums;
}

tbody tr:last-child :where(td) { border-bottom: 0; }

.table-group td {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  background: var(--ghost);
}

/* ── Misc ──────────────────────────────────────────────────────────────── */

.foot { font-size: 0.75rem; color: var(--muted); text-align: center; margin-top: 4px; }

.toast {
  position: fixed;
  left: 50%;
  /* Above the tab bar, not behind it. */
  bottom: calc(var(--tabbar-h) + 16px + env(safe-area-inset-bottom));
  transform: translateX(-50%);
  z-index: 20;
  max-width: calc(100% - 32px);
  padding: 10px 16px;
  font-size: 0.875rem;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 3px solid var(--warning);
  border-radius: var(--radius-sm);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.22);
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}
