*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* ── Design tokens ─────────────────────────────────────────────────────────────
   Two colour families live here. DATA colours (the temperature bands, and the
   humidity scale in colourScale.ts) encode readings and are never used for
   chrome. CHROME colours (surfaces, ink, hairlines, one accent) style the app
   itself and never encode data. */
:root {
  color-scheme: light; /* native controls + scrollbars follow the theme */

  /* data: temperature bands (matches colourScale.ts anchors) */
  --cold:        #3b82f6;
  --comfortable: #22c55e;
  --warm:        #f59e0b;
  --hot:         #f97316;
  --danger:      #ef4444;
  --pending:     #94a3b8;

  /* chrome: surfaces + ink */
  --bg:       #f4f6f8;
  --card-bg:  #ffffff;
  --text:     #101828;
  --text-2:   #475467;
  --muted:    #667085;
  --faint:    #98a2b3;
  --line:     #e4e7ec;
  --line-soft:#eef2f6;

  /* chrome: the one interactive accent (selected chips, links, focus) */
  --accent:        #1d4ed8;
  --accent-soft:   #eff6ff;
  --accent-border: #93c5fd;

  /* status tints (host battery, sensor badges, window advice, errors) */
  --good-bg: #ecfdf3;  --good-ink: #027a48;
  --warn-bg: #fffaeb;  --warn-ink: #b54708;
  --bad-bg:  #fef3f2;  --bad-ink:  #b42318;
  --bad-border: #fecdca;

  /* chrome: translucent bars, table heads, floor-plan linework, misc */
  --glass:     rgba(255, 255, 255, .88);
  --thead-bg:  #f9fafb;
  --plan-wall: #1e293b;
  --plan-detail: #64748b;
  --plan-detail-soft: #94a3b8;
  --dial-bg:   rgba(255, 255, 255, .8);
  --dial-ink:  #0369a1;
  --ring:      rgba(16, 24, 40, .08);
  --chart-line: #2a78d6;     /* matches chart.ts SERIES_TEMP per theme */
  --chart-forecast: #eb6834; /* matches chart.ts SERIES_FORECAST per theme */

  /* living weather cards (weatherAnim.ts) — tuned to read over the light tint */
  --wx-rain:   rgba(64, 110, 170, .5);
  --wx-snow:   rgba(120, 140, 175, .9);
  --wx-fog:    rgba(228, 233, 240, .72);
  --wx-cloud-c:rgba(255, 255, 255, .78);
  --wx-flash:  rgba(255, 243, 198, .72);
  --wx-streak: rgba(110, 135, 170, .34);
  --wx-glow:   rgba(255, 212, 138, .5);
  /* Night variants (Met Office codes 0/2/9/13/16/19/22/25/28). "Barely": a
     cooler, smaller glow and a few still points — a night card in LIGHT theme
     must not become a dark tile in a row of light ones. */
  --wx-night:  rgba(112, 146, 210, .55);
  --wx-star:   rgba(72, 94, 140, .62);

  /* Ink that sits ON a filled accent / alert surface. A TOKEN, not a
     per-component dark override: "auto" (the default) DELETES [data-theme], so
     `:root[data-theme="dark"] .btn { color: ... }` never fires for anyone who
     has not used the theme toggle - which is most people on a dark phone. Both
     palette blocks below set these, so both routes into dark are covered. */
  --on-accent: #ffffff;
  --on-alert:  #ffffff;
  /* The empty part of a 24h track / evening rail. Its own token because in dark
     `--line-soft` sits 1 step from `--card-bg` and the rail simply vanishes,
     leaving the blocks floating with nothing to read them against (UI §10 asks
     these two to lean on fills in dark). */
  --track-bg: #eef2f6;
  --track-grid: #e4e7ec;   /* the 6/12/18 marks cut into that track */

  --radius:    12px;
  --radius-sm: 8px;
  --shadow:    0 1px 2px rgba(16, 24, 40, .05);
  --shadow-pop: 0 4px 6px -2px rgba(16, 24, 40, .05), 0 12px 24px -4px rgba(16, 24, 40, .14);
}

/* Dark palette — same token names, re-stepped for a dark slate surface. Two
   routes in: an explicit choice saved from the mobile More tab (data-theme) or
   the system preference when no choice is stored. Keep both blocks identical. */
:root[data-theme="dark"] {
  color-scheme: dark;
  --bg:       #0c1116;
  --card-bg:  #151b23;
  --text:     #e7ecf2;
  --text-2:   #b3bcc8;
  --muted:    #8a94a3;
  --faint:    #667180;
  --line:     #232b36;
  --line-soft:#1a212b;
  --accent:        #8ab4ff;
  --accent-soft:   #14243f;
  --accent-border: #2d4a7a;
  --on-accent: #0c1116;
  --on-alert:  #1a0e0d;
  --track-bg: #232b36;
  --track-grid: #151b23;
  --good-bg: #10281c;  --good-ink: #5ad48f;
  --warn-bg: #2c2110;  --warn-ink: #f5c065;
  --bad-bg:  #2c1514;  --bad-ink:  #f08a80;
  --bad-border: #5c2320;
  --glass:     rgba(18, 24, 32, .88);
  --thead-bg:  #1a212b;
  --plan-wall: #cbd5e1;
  --plan-detail: #8a94a3;
  --plan-detail-soft: #667180;
  --dial-bg:   rgba(21, 27, 35, .8);
  --dial-ink:  #7dd3fc;
  --ring:      rgba(255, 255, 255, .12);
  --chart-line: #3987e5;
  --chart-forecast: #d95926;

  /* living weather cards — brightened to read over the dark tint */
  --wx-rain:   rgba(150, 190, 240, .62);
  --wx-snow:   rgba(222, 233, 250, .88);
  --wx-fog:    rgba(120, 132, 150, .46);
  --wx-cloud-c:rgba(200, 216, 238, .16);
  --wx-flash:  rgba(190, 220, 255, .5);
  --wx-streak: rgba(165, 195, 235, .4);
  --wx-glow:   rgba(255, 204, 128, .4);
  --wx-night:  rgba(150, 185, 245, .34);
  --wx-star:   rgba(236, 244, 255, .78);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --bg:       #0c1116;
    --card-bg:  #151b23;
    --text:     #e7ecf2;
    --text-2:   #b3bcc8;
    --muted:    #8a94a3;
    --faint:    #667180;
    --line:     #232b36;
    --line-soft:#1a212b;
    --accent:        #8ab4ff;
    --accent-soft:   #14243f;
    --accent-border: #2d4a7a;
    --on-accent: #0c1116;
    --on-alert:  #1a0e0d;
    --track-bg: #232b36;
    --track-grid: #151b23;
    --good-bg: #10281c;  --good-ink: #5ad48f;
    --warn-bg: #2c2110;  --warn-ink: #f5c065;
    --bad-bg:  #2c1514;  --bad-ink:  #f08a80;
    --bad-border: #5c2320;
    --glass:     rgba(18, 24, 32, .88);
    --thead-bg:  #1a212b;
    --plan-wall: #cbd5e1;
    --plan-detail: #8a94a3;
    --plan-detail-soft: #667180;
    --dial-bg:   rgba(21, 27, 35, .8);
    --dial-ink:  #7dd3fc;
    --ring:      rgba(255, 255, 255, .12);
    --chart-line: #3987e5;
    --chart-forecast: #d95926;

  /* living weather cards — brightened to read over the dark tint */
  --wx-rain:   rgba(150, 190, 240, .62);
  --wx-snow:   rgba(222, 233, 250, .88);
  --wx-fog:    rgba(120, 132, 150, .46);
  --wx-cloud-c:rgba(200, 216, 238, .16);
  --wx-flash:  rgba(190, 220, 255, .5);
  --wx-streak: rgba(165, 195, 235, .4);
  --wx-glow:   rgba(255, 204, 128, .4);
  --wx-night:  rgba(150, 185, 245, .34);
  --wx-star:   rgba(236, 244, 255, .78);
  }
}

body {
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  background: var(--bg);
  color: var(--text);
  min-height: 100dvh;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--accent); }

/* Visible keyboard focus everywhere, without hover/click rings. */
:is(button, input, select, a, [tabindex]):focus-visible {
  outline: 2px solid var(--accent-border);
  outline-offset: 1px;
}
:is(button, input, select):focus { outline: none; }
:is(button, input, select):focus-visible {
  outline: 2px solid var(--accent-border);
  outline-offset: 1px;
}

/* ── Shared control language ───────────────────────────────────────────────────
   Every chip, toggle and small control draws from one base so the app reads as
   one system: white surface, hairline border, quiet ink; selection is the soft
   accent fill (never a solid colour block). */
:is(.range-selector__btn, .mode-toggle__btn, .overlay-toggle__btn,
    .rate-rooms__btn, .zone-toggle__btn) {
  color: var(--muted);
  background: var(--card-bg);
  border: 1px solid var(--line);
  cursor: pointer;
  transition: color .15s ease, border-color .15s ease, background .15s ease;
}
:is(.range-selector__btn, .mode-toggle__btn, .overlay-toggle__btn,
    .rate-rooms__btn, .zone-toggle__btn):hover {
  color: var(--text);
  border-color: var(--faint);
}
:is(.range-selector__btn, .mode-toggle__btn, .overlay-toggle__btn,
    .rate-rooms__btn).is-active,
:is(.range-selector__btn, .mode-toggle__btn, .overlay-toggle__btn,
    .rate-rooms__btn).is-on {
  color: var(--accent);
  background: var(--accent-soft);
  border-color: var(--accent-border);
  font-weight: 600;
}

/* ── Header ── */
.header {
  position: sticky;
  top: 0;
  background: var(--glass);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  padding: 12px 16px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-bottom: 1px solid var(--line);
  z-index: 10;
}

.header__title { font-size: 1rem; font-weight: 650; letter-spacing: -.01em; }
.header__updated {
  font-size: .74rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  text-decoration: none;
}

/* Server-laptop battery chip — colour-coded so a low host is obvious. */
.host-battery {
  font-size: .74rem;
  font-weight: 600;
  padding: 3px 9px;
  border-radius: 999px;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.host-battery:empty { display: none; }
.host-battery--ok { background: var(--good-bg); color: var(--good-ink); }
.host-battery--low { background: var(--warn-bg); color: var(--warn-ink); }
.host-battery--critical { background: var(--bad-bg); color: var(--bad-ink); }

/* Server-laptop battery glyph — mobile strip + dashboard header. A stroked
   shell with a currentColor fill that tracks the level and (on mains) a bolt;
   the treatment class recolours the whole glyph, so it reads in either theme.
   Neutral on mains, amber discharging, red + pulse when critically low. */
.host-batt {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: .72rem;
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--text-2);
  text-decoration: none;
  white-space: nowrap;
  position: relative;
  -webkit-tap-highlight-color: transparent;
}
/* 35px of chip, 44px of reach — the header strip's height is fixed by the
   reading beside it, so the target grows through the pseudo, not the box. */
.host-batt::after { content: ""; position: absolute; inset: -5px -4px; }
.host-batt[hidden] { display: none; }
.host-batt__glyph { width: 25px; height: 13px; flex: none; display: block; }
.host-batt__shell { fill: none; stroke: currentColor; stroke-width: 1.1; opacity: .6; }
.host-batt__cap { fill: currentColor; opacity: .6; }
.host-batt__fill { fill: currentColor; }
.host-batt__bolt { fill: currentColor; }
.host-batt--mains { color: var(--text-2); }
.host-batt--mains .host-batt__fill { opacity: .38; } /* dim, so the bolt reads over it */
.host-batt--discharging { color: var(--warn-ink); }
.host-batt--critical { color: var(--bad-ink); animation: hostBattPulse 1.5s ease-in-out infinite; }
@keyframes hostBattPulse { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }
@media (prefers-reduced-motion: reduce) { .host-batt--critical { animation: none; } }
/* In the slim strip, grow the tap target vertically without enlarging the strip. */
.m-strip .host-batt { padding: 11px 3px; margin: -11px -1px -11px 0; }
.host-batt:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }

/* ── Room icons (shared atom: Home card, room-screen header, the rename sheet's
   picker; the floor plan draws the same symbols in SVG user units as
   .fp-room__icon). One <svg class="ri"><use href="#ri-…"> per instance,
   referencing the sprite roomIcons.ts injects. Geometry lives on the symbol so
   it survives being cloned into the <use> shadow tree, where no stylesheet can
   reach — which leaves exactly one thing for CSS to say, and it is the one that
   inherits: `color`. Every surface therefore gets the icon in the same ink as
   the text it accompanies, in both themes, for free. */
.ri {
  display: block;
  flex: none;
  width: 1.15em;
  height: 1.15em;
}

/* ── Sensor battery indicator (shared: sensor table, mobile status card, home
   card, floor-plan popover). A stroked shell + a proportional currentColor fill
   (or a "?" for the unknown sentinel); the level tint rides the wrapper so both
   themes come from the status tokens. Full is deliberately MUTED — healthy
   shouldn't shout on a detail surface — low is amber, critical red + a slow pulse
   so it stays unmissable and reads the same as the host battery + plan badge.
   The floor plan draws its own SVG-namespace variant (.fp-room__batt). */
.batt {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: .74rem;
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  color: var(--text-2);
}
.batt__glyph { width: 22px; height: 12px; flex: none; display: block; }
.batt__shell { fill: none; stroke: currentColor; stroke-width: 1.1; opacity: .55; }
.batt__cap { fill: currentColor; opacity: .55; }
.batt__fill { fill: currentColor; }
.batt__q { fill: currentColor; font-size: 9px; font-weight: 700; text-anchor: middle; }
.batt--full { color: var(--text-2); }
.batt--low { color: var(--warn-ink); }
.batt--crit { color: var(--bad-ink); }
.batt--unknown { color: var(--faint); }
.batt--crit .batt__glyph { animation: battPulse 1.5s ease-in-out infinite; }
@keyframes battPulse { 0%, 100% { opacity: 1; } 50% { opacity: .4; } }
@media (prefers-reduced-motion: reduce) { .batt--crit .batt__glyph { animation: none; } }

/* ── Layout ── */
/* The /events data-cleaning page's container (the app itself is the .m-* mobile
   shell); capped and centred so it reads on a wide screen too. */
.main {
  padding: 20px 16px;
  max-width: 1140px;
  margin: 0 auto;
}

.section { margin-bottom: 28px; }

.section__title {
  font-size: .72rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--faint);
  margin-bottom: 10px;
}

/* Per-floor "open/close the windows" hint, under each floor heading. */
.window-hint {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 8px;
  margin: -2px 0 10px;
  font-size: .78rem;
}
.window-hint:empty { display: none; }
.window-hint__action { font-weight: 650; padding: 2px 9px; border-radius: 999px; }
.window-hint--open .window-hint__action,
.window-hint--open-muggy .window-hint__action { background: var(--good-bg); color: var(--good-ink); }
.window-hint--close .window-hint__action { background: var(--warn-bg); color: var(--warn-ink); }
.window-hint__detail { color: var(--muted); font-variant-numeric: tabular-nums; }
.window-hint__muggy { font-weight: 600; color: var(--warn-ink); }
.window-hint__flip { font-weight: 600; font-variant-numeric: tabular-nums; }
.window-hint--open .window-hint__flip,
.window-hint--open-muggy .window-hint__flip { color: var(--warn-ink); }
.window-hint--close .window-hint__flip { color: var(--good-ink); }

/* ── Card (used by the /auth pages) ── */
.card {
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 16px;
  box-shadow: var(--shadow);
  border-left: 4px solid var(--pending);
}

.card__label {
  font-size: .7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--muted);
  margin-bottom: 4px;
}

/* ── Outdoor banner ─────────────────────────────────────────────────────────
   A white card washed with the live temperature colour (set by ui.ts as
   --banner-c): tinted field, solid accent rail, and the big reading inked in a
   deepened step of the same hue. Data colour as accent, chrome stays calm. */
.outdoor-banner {
  --banner-c: var(--pending); /* overridden inline by JS with the temp colour */
  background: linear-gradient(
    135deg,
    color-mix(in srgb, var(--banner-c) 16%, var(--card-bg)),
    color-mix(in srgb, var(--banner-c) 6%, var(--card-bg))
  );
  color: var(--text);
  border: 1px solid color-mix(in srgb, var(--banner-c) 34%, var(--card-bg));
  border-left: 4px solid var(--banner-c);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 16px 20px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  transition: background .4s ease, border-color .4s ease;
  position: relative;      /* host the living-weather sky layer */
  overflow: hidden;
  isolation: isolate;      /* keep the z-index:-1 sky behind the reading */
}

/* The main column absorbs the squeeze (its description line wraps); the meta
   column keeps its natural width, so on a phone the two stay side by side
   instead of the meta dropping below the temperature. */
.outdoor-banner__main { display: flex; flex-direction: column; flex: 1 1 0; min-width: 0; }
.outdoor-banner__label {
  font-size: .7rem;
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--muted);
}
.outdoor-banner__temp {
  font-size: 2.9rem;
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap; /* never break between the number and "air" */
  color: color-mix(in srgb, var(--banner-c) 72%, var(--text));
}
.outdoor-banner__air { font-size: .85rem; font-weight: 600; color: var(--muted); letter-spacing: 0; }
.outdoor-banner__ground { font-size: .95rem; font-weight: 600; color: var(--text-2); margin-top: 2px; font-variant-numeric: tabular-nums; }
.outdoor-banner__desc { font-size: .82rem; color: var(--text-2); }

/* Short meta (humidity / updated / compare) stays right-aligned beside the main,
   so it uses the empty space next to the temperature rather than adding height. */
.outdoor-banner__meta {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 4px;
  text-align: right;
  flex: 0 0 auto;
  margin-left: auto;
}
.outdoor-banner__humidity { font-size: .82rem; font-weight: 600; white-space: nowrap; }

/* Phone: tighten the banner so main + meta always share the row. */
@media (max-width: 480px) {
  .outdoor-banner { padding: 14px 16px; }
  .outdoor-banner__temp { font-size: 2.35rem; }
  .outdoor-banner__ground { font-size: .88rem; }
  .outdoor-banner__desc { font-size: .78rem; }
  .outdoor-banner__humidity { font-size: .76rem; }
}
.outdoor-banner__time { font-size: .72rem; color: var(--muted); font-variant-numeric: tabular-nums; }
/* The long source line gets its own full-width row at the bottom, left-aligned —
   keeps the meta column tidy instead of stretching it to fit. */
.outdoor-banner__source { width: 100%; font-size: .72rem; color: var(--muted); text-align: left; }
.outdoor-banner__link { font-size: .72rem; color: inherit; opacity: .9; text-decoration: underline; }
.outdoor-banner__link:hover { opacity: 1; }
/* Same slot, now a control: with an issue time the source line states the
   forecast's age and opens the sheet that explains every number on the card.
   A real 44px target (the row is short, so the reach comes from padding, pulled
   back out with a negative margin so the banner's own rhythm barely changes).
   The pull-back is exactly the banner's own 8px flex gap and not a pixel more:
   at -11px the button's box climbed 3px INTO the reading above it, which
   `layout-audit` reported as a sibling overlap on every route carrying the
   banner. A regression tool with a permanent false line is a tool nobody reads,
   and the 6px of height this gives back is not worth that. */
.outdoor-banner__source--btn {
  display: flex;
  align-items: center;
  gap: 4px;
  background: none;
  border: 0;
  padding: 11px 0;
  margin: -8px 0;
  min-height: 44px;
  font: inherit;
  font-size: .72rem;
  color: var(--muted);
  cursor: pointer;
  text-align: left;
}
.outdoor-banner__chev { font-size: .95rem; line-height: 1; opacity: .8; }
/* Over six hours old: aged, never hidden — a stale Met Office run is still the
   best thing the app has (design §10). */
.outdoor-banner__stale { color: var(--warn-ink); font-weight: 600; }
/* No current reading (a stale forecast cache has no row for this hour). Quiet,
   never an error — the chart under it is still good. */
.outdoor-banner--empty .outdoor-banner__temp { color: var(--muted); }

/* ── Weather glyphs (wxGlyphs.ts) ─────────────────────────────────────────────
   Single-weight currentColor line symbols, so one drawing takes the ink of
   whatever names it and is correct in both themes with no per-surface palette.
   A null kind renders NO glyph at all — an absent code has no opinion, and a
   default icon would be indistinguishable from a right one. */
.wxg {
  width: 22px;
  height: 22px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ── The Watnall card (design §4.1) ───────────────────────────────────────────
   The first MEASUREMENT this app has ever shown, directly under the forecast
   banner and deliberately NOT inside it: a measurement 10 km away must be
   labelled as one, not slotted where a reader expects the forecast for here. */
.wx-station {
  padding: 12px 14px 13px;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.wx-station__head {
  display: flex;
  align-items: center;
  gap: 8px;
}
.wx-station__name { font-size: .86rem; font-weight: 700; }
/* The tag is the whole point of the card: this row is measured, not forecast. */
.wx-station__tag {
  padding: 1px 8px;
  border-radius: 999px;
  background: var(--good-bg);
  color: var(--good-ink);
  font-size: .64rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .06em;
}
.wx-station__at {
  margin-left: auto;
  font-size: .72rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.wx-station__main {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin-top: 8px;
}
/* The glyph is baseline-aligned with the reading by hand: an inline-flex box on
   a baseline row otherwise hangs its bottom edge on the text baseline. */
.wx-station__glyph {
  align-self: center;
  display: inline-flex;
  color: var(--muted);
}
.wx-station__temp {
  font-size: 1.5rem;
  font-weight: 700;
  letter-spacing: -.01em;
  font-variant-numeric: tabular-nums;
  color: color-mix(in srgb, var(--room-c, var(--pending)) 70%, var(--text));
}
.wx-station__cond { font-size: .84rem; font-weight: 600; color: var(--text-2); }
.wx-station__facts {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 22px;
  margin-top: 10px;
}
.wx-fact { display: flex; align-items: baseline; gap: 6px; }
.wx-fact__label { font-size: .7rem; color: var(--muted); }
.wx-fact__value { font-size: .8rem; font-weight: 600; font-variant-numeric: tabular-nums; }
.wx-station__note {
  margin-top: 10px;
  padding-top: 9px;
  border-top: 1px solid var(--line-soft);
  font-size: .72rem;
  line-height: 1.5;
  color: var(--muted);
}

/* ── The week ahead (design §3) ───────────────────────────────────────────────
   Seven rows, every one the same object: the Met Office's own daily forecast.
   A read-out, not a control — nothing here is tappable, because a day tap has
   nowhere honest to go. The grid keeps the day column, the glyph, the rail and
   both temperature columns aligned down the card whatever the row says. */
.wx-week {
  padding: 4px 14px 12px;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.wx-week__row {
  display: grid;
  grid-template-columns: 3.6rem 30px minmax(0, 1fr) 3.4rem 2.2rem;
  align-items: center;
  gap: 10px;
  min-height: 40px;
  border-bottom: 1px solid var(--line-soft);
}
.wx-week__row:last-of-type { border-bottom: 0; }
.wx-week__day {
  font-size: .78rem;
  font-weight: 650;
  color: var(--text-2);
  white-space: nowrap;
}
/* The glyph cell holds the condition and, when the day's chance of lightning
   clears 30%, the bolt — one slot, so a stormy row is never taller. */
.wx-week__glyph {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  color: var(--muted);
}
.wx-week__glyph .wxg { width: 20px; height: 20px; }
.wx-week__glyph .wx-mark { width: 9px; height: 9px; }
/* The rail: a pale track between the day's BOUNDS with the forecast itself as a
   solid temperature-gradient core, so the uncertainty is in the picture rather
   than in a second pair of numbers. */
.wx-week__bar {
  position: relative;
  height: 8px;
  border-radius: 999px;
  background: var(--line-soft);
  overflow: hidden;
}
.wx-week__bounds {
  position: absolute;
  top: 0;
  bottom: 0;
  border-radius: 999px;
  background: color-mix(in srgb, var(--faint) 46%, transparent);
}
.wx-week__core {
  position: absolute;
  top: 0;
  bottom: 0;
  border-radius: 999px;
  min-width: 4px;
}
.wx-week__temps {
  display: flex;
  justify-content: flex-end;
  gap: 6px;
  font-size: .8rem;
  font-variant-numeric: tabular-nums;
}
.wx-week__lo { color: var(--muted); }
.wx-week__hi { font-weight: 650; }
.wx-week__rain {
  text-align: right;
  font-size: .74rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}
.wx-week__note {
  margin-top: 10px;
  font-size: .72rem;
  line-height: 1.5;
  color: var(--muted);
}

/* ── Forecast accuracy (#accuracy, design §11) ────────────────────────────────
   The screen only observations make possible: what the forecast above has
   actually been worth. Every colour here is a chrome token — a miss is not a
   temperature and must never be painted like one. */

/* The door, under the outdoor chart on Trends. A card rather than a chip: it
   asks a question, and the answer to "what is this?" needs a second line. */
/* One door idiom, two families: `.acc-door` opens the accuracy screen off
   Trends, `.wh-door` opens the heating replay off the Plan tab and `#heating`
   (floor-plan design §4.1). Same geometry, on purpose — a screen that opens a
   screen looks the same wherever it is, and the sub-line is where the two
   differ. One rule rather than two so they cannot drift apart. */
.acc-door,
.wh-door {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 44px;
  padding: 13px 14px;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  color: var(--text);
  text-decoration: none;
}
.acc-door__text,
.wh-door__text { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.acc-door__title,
.wh-door__title { font-size: .86rem; font-weight: 650; }
.acc-door__sub,
.wh-door__sub { font-size: .74rem; line-height: 1.45; color: var(--muted); }
.acc-door__chev,
.wh-door__chev { flex: 0 0 auto; margin-left: auto; color: var(--faint); font-size: 1.1rem; }
.acc-door:focus-visible,
.wh-door:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.acc-blocks { display: flex; flex-direction: column; gap: 14px; }
.acc-card {
  padding: 14px;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.acc-card__title { font-size: .88rem; font-weight: 700; margin-bottom: 10px; }

/* The number that answers the block's question. The caption sits on its
   baseline, so it reads as the rest of a sentence rather than a label. */
.acc-stat {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 2px 10px;
}
.acc-stat__value {
  font-size: 2rem;
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
}
.acc-stat__caption { min-width: 0; font-size: .82rem; font-weight: 600; color: var(--text-2); }
/* The band's sentence is a whole claim ("…it aims for 80% — so the band is
   honest"), so it takes its own full-width lines under the number instead of
   being squeezed into the column beside it. */
.acc-stat--wrap { display: block; }
.acc-stat--wrap .acc-stat__value { display: block; margin-bottom: 2px; }
.acc-stat__sentence { font-size: .84rem; line-height: 1.55; color: var(--text-2); }

.acc-card__line { margin-top: 10px; font-size: .82rem; line-height: 1.5; color: var(--text-2); }
.acc-card__line--quiet { color: var(--muted); }
.acc-card__foot {
  margin-top: 11px;
  padding-top: 9px;
  border-top: 1px solid var(--line-soft);
  font-size: .72rem;
  line-height: 1.5;
  color: var(--muted);
}

/* The bias, inside the temperature card but visibly its own block: it is a
   different KIND of number from the miss above it — signed, and mostly the 64 m
   of hill between the house and the station — and reading it as a second error
   is precisely the misreading this screen exists to prevent. */
.acc-bias {
  display: flex;
  gap: 12px;
  margin-top: 12px;
  padding-top: 11px;
  border-top: 1px solid var(--line-soft);
}
.acc-bias__value {
  flex: 0 0 auto;
  font-size: 1.05rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text-2);
}
.acc-bias__text { min-width: 0; }
.acc-bias__lead { font-size: .8rem; font-weight: 600; line-height: 1.45; }
.acc-bias__note { margin-top: 5px; font-size: .72rem; line-height: 1.5; color: var(--muted); }

/* Three tallies and never a percentage: a hit rate computed from these would be
   a score, which a station 10 km away cannot support. */
.acc-tally__row {
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 9px 0;
  border-bottom: 1px solid var(--line-soft);
}
.acc-tally__row:last-child { border-bottom: 0; }
.acc-tally__label { min-width: 0; font-size: .82rem; color: var(--text-2); }
.acc-tally__value {
  flex: 0 0 auto;
  margin-left: auto;   /* <dd> ships a 40px indent — this replaces it */
  font-size: 1rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* Day by day: the mean made auditable. The rail is neutral grey on purpose — a
   forecast miss is a magnitude, not a temperature, and the data palette would
   claim a meaning we have not defined. */
.acc-day {
  display: grid;
  grid-template-columns: 4.2rem minmax(0, 1fr) 2.8rem 3.2rem;
  align-items: center;
  gap: 10px;
  min-height: 34px;
  border-bottom: 1px solid var(--line-soft);
}
.acc-day:last-child { border-bottom: 0; }
.acc-day--head { min-height: 22px; border-bottom: 0; }
/* The header row borrows the grid for its column labels only — its rail cell
   must stay empty, or it draws a day-shaped bar with no day behind it. */
.acc-day--head .acc-day__bar { background: none; }
.acc-day--head .acc-day__mae,
.acc-day--head .acc-day__band {
  font-size: .62rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--faint);
}
.acc-day__label {
  font-size: .76rem;
  color: var(--text-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.acc-day__bar { height: 6px; border-radius: 999px; background: var(--line-soft); overflow: hidden; }
.acc-day__fill {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: color-mix(in srgb, var(--pending) 80%, transparent);
}
.acc-day__mae, .acc-day__band { text-align: right; font-size: .78rem; font-variant-numeric: tabular-nums; }
.acc-day__band { color: var(--muted); }

/* The designed wait. Seven pips, N filled: "not yet" with a finish line you can
   count, never an empty chart. */
.acc-collect {
  padding: 22px 16px;
  background: var(--card-bg);
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  text-align: center;
}
.acc-collect__pips { display: flex; justify-content: center; gap: 7px; margin-bottom: 14px; }
.acc-pip {
  width: 12px;
  height: 12px;
  border-radius: 999px;
  border: 1.5px solid var(--line);
}
.acc-pip--done { background: var(--accent); border-color: var(--accent); }
.acc-collect__lead { font-size: .95rem; font-weight: 700; }
.acc-collect__body {
  margin: 9px auto 0;
  max-width: 30rem;
  font-size: .78rem;
  line-height: 1.6;
  color: var(--muted);
}

.acc-foot { font-size: .72rem; line-height: 1.55; color: var(--muted); }
.acc-retry { display: flex; justify-content: center; margin-top: 14px; }

.acc-skels { display: flex; flex-direction: column; gap: 14px; }
.acc-skel {
  display: flex;
  flex-direction: column;
  gap: 11px;
  padding: 16px;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  animation: notifShimmer 1.4s ease-in-out infinite;
}
.acc-skel span { display: block; border-radius: 6px; background: var(--line-soft); }
.acc-skel__title { width: 34%; height: 12px; }
.acc-skel__big { width: 52%; height: 26px; }
.acc-skel__line { width: 88%; height: 10px; }
@media (prefers-reduced-motion: reduce) { .acc-skel { animation: none; } }

/* ── Floor plan ── */
.floorplan {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

@media (min-width: 768px) {
  .floorplan { flex-direction: row; align-items: flex-start; }
  .floorplan__panel { flex: 1; }
}

.floorplan__panel {
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 14px 16px 16px;
  box-shadow: var(--shadow);
}

.floorplan__svg { position: relative; }

.fp-svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible; /* don't clip the bay window / outer-wall stroke at edges */
  filter: drop-shadow(0 1px 2px rgba(16, 24, 40, .14));
}

/* Floor-plan chrome lane: the title row of each floor panel carries the plan's
   context dial (wind or sun, oriented to the ~SSE-up plan) at its right edge.
   The dial sits in the panel's chrome, disjoint from the room canvas by
   construction, so it can never clip a room's pill/label the way the old
   corner overlays did. */
.floorplan__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 10px;
  min-height: 34px; /* stable whether or not a dial is showing (overlay = off) */
}
.floorplan__head .section__title { margin-bottom: 0; }

/* The context dial + its label, as an inline chip in the header lane. */
.fp-overlay {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
  pointer-events: none;
}
.fp-overlay:empty { display: none; }
.fp-overlay__dial { display: block; width: 34px; height: 34px; flex: 0 0 auto; }
.fp-overlay__ring { fill: var(--dial-bg); stroke: var(--line); stroke-width: 2; }
.fp-overlay__arrow { fill: #0ea5e9; }              /* wind: airflow direction */
.fp-overlay__calm { fill: var(--faint); }          /* calm / sun-down marker */
.fp-overlay__sun { fill: var(--warm); }            /* sun: disc at the sun's bearing */
.fp-overlay__sun-ray { stroke: var(--warm); stroke-width: 2; stroke-linecap: round; }
.fp-overlay__n {
  fill: var(--muted);
  font-size: 16px;
  font-weight: 700;
  text-anchor: middle;
  dominant-baseline: central;
}
.fp-overlay__label {
  padding: 1px 7px;
  font-size: .7rem;
  font-weight: 600;
  color: var(--dial-ink);
  background: var(--dial-bg);
  border-radius: 999px;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* Humidity-scale legend: the reference gradient bar + per-band rows, shown in
   the More tab's humidity guide (#humidity-guide, buildGuide in mobile.ts). */
.humidity-legend__bar {
  height: 10px;
  border-radius: 5px;
  margin: 8px 0 3px;
  border: 1px solid var(--ring);
}
.humidity-legend__ticks {
  display: flex;
  justify-content: space-between;
  font-size: .68rem;
  color: var(--faint);
  margin-bottom: 4px;
  font-variant-numeric: tabular-nums;
}
.humidity-legend__band { margin-top: 8px; }
.humidity-legend__band strong { display: inline; } /* row layout, not the card's block */
.humidity-legend__swatch {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 3px;
  margin-right: 6px;
}
.humidity-legend__range { color: var(--muted); }
.humidity-legend__note {
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--line-soft);
  font-size: .74rem;
}

/* Outside humidity, coloured on the same scale as the room pills. */
.humidity-pill {
  padding: 2px 9px;
  border-radius: 999px;
  font-weight: 600;
}

/* Room-picker chips — the room selection shared by the mobile Trends / Predict
   graphs and the /events room filter (rate-rooms__dot carries each room's hue). */
.rate-rooms {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 8px;
}
.rate-rooms__btn {
  padding: 3px 10px;
  font-size: .72rem;
  border-radius: 999px;
}
.rate-grid__empty { font-size: .8rem; color: var(--muted); }

/* off / wind / sun selector above the floor plan. */
.overlay-toggle-bar { margin-bottom: 10px; }
.overlay-toggle { display: inline-flex; gap: 4px; }
.overlay-toggle__btn {
  padding: 4px 14px;
  font-size: .74rem;
  border-radius: var(--radius-sm);
}

.fp-room { cursor: pointer; }
.fp-room:focus { outline: none; }
.fp-room:hover .fp-room__fill { stroke: #101828; stroke-width: 1.1; }
.fp-room:focus-visible .fp-room__fill { stroke: #101828; stroke-width: 1.3; }

.fp-room__fill {
  fill: #cbd5e1; /* slate grey default — unassigned; JS overrides per reading */
  stroke: #94a3b8; /* thin interior partition walls */
  stroke-width: 0.4;
  stroke-linejoin: round;
  transition: fill .3s ease;
}

/* Brief green outline pulse when a room gets a fresh reading (reassurance). */
.fp-room--flash .fp-room__fill { animation: fpFlash 0.9s ease-out; }
@keyframes fpFlash {
  0% { stroke: #16a34a; stroke-width: 2.4; }
  100% { stroke: #94a3b8; stroke-width: 0.4; }
}

/* Thick exterior wall drawn over the room fills for an architectural read. */
.fp-wall-outer {
  fill: none;
  stroke: var(--plan-wall);
  stroke-width: 1.8;
  stroke-linejoin: round;
  stroke-linecap: round;
  pointer-events: none;
}

/* Stair treads in the hall / landing. */
.fp-stairs {
  fill: none;
  stroke: var(--plan-detail-soft);
  stroke-width: 0.5;
  stroke-linecap: round;
  pointer-events: none;
}

/* Window openings: a gap punched through the wall, plus a glass line. */
.fp-window-gap { fill: var(--card-bg); pointer-events: none; }
.fp-window {
  fill: none;
  stroke: var(--plan-detail);
  stroke-width: 0.6;
  stroke-linecap: round;
  pointer-events: none;
}

/* Door swings (front door + interior doors). */
.fp-door {
  fill: none;
  stroke: var(--plan-detail);
  stroke-width: 0.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  pointer-events: none;
}

.fp-room__label,
.fp-room__temp,
.fp-room__humidity,
.fp-room__time {
  text-anchor: middle;
  pointer-events: none; /* clicks fall through to the polygon */
}

.fp-room__label {
  font-size: 3px;
  font-weight: 600;
  fill: #1e293b;
  text-transform: uppercase;
  letter-spacing: .02em;
}

/* The white halo (paint-order stroke) keeps text legible over any temperature
   colour, but a thick one muddies small glyphs — keep it thin and lean on a
   darker fill instead. */
.fp-room__temp {
  font-size: 6px;
  font-weight: 700;
  fill: #0f172a;
  paint-order: stroke;
  stroke: #ffffff;
  stroke-width: 0.4;
  stroke-linejoin: round;
}

/* Unassigned / no reading: a subtle muted dash, not a haloed white bar. */
.fp-room__temp--empty { fill: #94a3b8; stroke: none; font-size: 4.5px; }

.fp-room__humidity {
  font-size: 4.5px;
  fill: #1e293b;
  paint-order: stroke;
  stroke: #ffffff;
  stroke-width: 0.28;
}

/* Dry→muggy scale pill behind the humidity value (drawn by paintHumidityBadge;
   colour comes from humidityToColour inline). */
.fp-room__humidity-bg { pointer-events: none; }

/* Bottom meta line: which sensor feeds the room + how fresh ("Yellow · 33s"). */
.fp-room__time {
  font-size: 3px;
  fill: #334155;
  paint-order: stroke;
  stroke: #ffffff;
  stroke-width: 0.22;
}

/* At-a-glance heating/cooling readout trailing the temperature: a direction
   caret + an honest rate number ("▲1.8"), inheriting the value's dark ink, white
   halo + paint-order so both read on ANY room fill at wall-dashboard distance
   (where a size-only glyph does not). Direction is the caret ORIENTATION, not a
   tint: rooms are already colour-washed by absolute temp, and a warm/cool tint
   collides with the fill exactly in the cases that matter most (orange warming
   caret on a hot room, blue cooling caret on a cold room). The caret sits a hair
   larger than the rate number so its up/down direction reads first. */
.fp-room__trend { font-size: 5px; }
.fp-room__trendnum { font-size: 4.6px; }

/* The room's icon on the plan, leading its name (floorplan.setRoomLabel places
   and sizes it in user units — the size is derived from the painted text's own
   box, so it follows the type scale above instead of duplicating it). Same fixed
   dark ink as the label: the plan's rooms are colour-washed in BOTH themes, so
   its text has never been theme-dependent and the icon must not be either. */
.fp-room__icon { color: #1e293b; opacity: .92; pointer-events: none; }

.fp-room--compact .fp-room__label { font-size: 2.3px; }
/* Second rung of the room-NAME fit ladder (floorplan.setRoomLabel): a friendly
   name ("Rosie's office") that won't fit at full size drops to the size the
   genuinely small rooms already use before anything is truncated — no new size
   is introduced into the scale. Declared AFTER the base rule so it wins at equal
   specificity, and deliberately BELOW the .fp-room--compact rule (0,2,0) so a
   room already painted small keeps its own size and skips straight to the
   ellipsis rung. */
.fp-room__label--tight { font-size: 2.3px; }
.fp-room--compact .fp-room__temp { font-size: 4px; }
.fp-room--compact .fp-room__humidity { font-size: 3.4px; }
.fp-room--compact .fp-room__time { font-size: 2.4px; }

/* Floor-plan battery meter (floorplan.paintBatteryBadge): painted ONLY for a
   degraded battery, pinned to the room's top-right corner. A card-backed shell
   keeps it legible over any temperature fill (the humidity-pill trick); the tint
   cascades via `color` → currentColor. Critical is red + pulses so it stays
   unmissable even when the room has gone quiet (stale) — the diagnosis for why. */
/* The role dot (floor-plan design §3): top-left inside the room, mirroring the
   battery badge's top-right corner. Filled = drives this call, hollow = rides
   along free, absent = no objective now — a SHAPE, never a colour, because the
   rooms are already washed by temperature and a role tint would collide with the
   fill exactly where it matters most. Fixed dark ink on a white halo, like every
   other mark on this plan: the plan is colour-washed in BOTH themes, so its ink
   has never been theme-dependent. The halo is a disc rather than a stroke so the
   hollow shape reads as hollow over any fill. */
.fp-room__role { pointer-events: none; }
.fp-room__role-halo { fill: #ffffff; opacity: .9; }
.fp-room__role-dot { fill: none; stroke: #1e293b; stroke-width: .45; }
.fp-room__role--drive .fp-room__role-dot { fill: #1e293b; }

.fp-room__batt { pointer-events: none; }
.fp-room__batt-bg { fill: var(--card-bg); opacity: .94; stroke: var(--ring); stroke-width: .3; }
.fp-room__batt-shell { fill: none; stroke: currentColor; stroke-width: .5; opacity: .7; }
.fp-room__batt-cap { fill: currentColor; opacity: .7; }
.fp-room__batt-fill { fill: currentColor; }
.fp-room__batt--low { color: var(--warn-ink); }
.fp-room__batt--crit { color: var(--bad-ink); animation: fpBattPulse 1.5s ease-in-out infinite; }
@keyframes fpBattPulse { 0%, 100% { opacity: 1; } 50% { opacity: .32; } }
@media (prefers-reduced-motion: reduce) { .fp-room__batt--crit { animation: none; } }

/* ── Room chart empty state (the class name is the retired popover's) ── */
.fp-popover__chart-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  font-size: .78rem;
  color: var(--muted);
}
/* Rate-of-change caption under the room chart, matching the dotted
   projection's colour so the number and the line read as one thing. */
.chart-rate {
  margin-top: 6px;
  font-size: .78rem;
  font-weight: 600;
  color: var(--chart-line);
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.chart-rate:empty {
  display: none;
}

/* Segmented time-range control (shared: popover + admin chart). */
.range-selector {
  display: flex;
  gap: 4px;
  margin-bottom: 8px;
}
.range-selector__btn {
  flex: 1;
  padding: 4px 0;
  font-size: .74rem;
  border-radius: var(--radius-sm);
}
#chart-range, #outdoor-range { max-width: 360px; }

/* Temp | Rate view toggle (shared: popover + admin chart). Styled like the
   range control, but only two options so it doesn't stretch full width. */
.mode-toggle {
  display: flex;
  gap: 4px;
  margin-bottom: 8px;
}
.mode-toggle__btn {
  padding: 4px 12px;
  font-size: .74rem;
  border-radius: var(--radius-sm);
}

/* Safe-zone band toggles: pill chips with a colour dot; dimmed when off. */
.zone-toggle {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 8px;
}
.zone-toggle__btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  font-size: .72rem;
  border-radius: 999px;
  opacity: .55;
}
.zone-toggle__btn.is-on {
  opacity: 1;
  color: var(--text);
  border-color: var(--zone-colour);
}
.zone-toggle__dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--zone-colour);
}

/* #room's reassign list (mobile.ts) — the class names are the retired popover's. */
.fp-popover__list { display: flex; flex-direction: column; gap: 4px; }

.fp-popover__sensor {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  width: 100%;
  text-align: left;
  background: var(--bg);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: 9px 10px;
  font-size: .85rem;
  color: var(--text);
  cursor: pointer;
  transition: border-color .15s ease;
}
.fp-popover__sensor:hover { border-color: var(--faint); }
.fp-popover__sensor small { color: var(--muted); font-size: .7rem; }
.fp-popover__sensor.is-active {
  border-color: var(--accent-border);
  background: var(--accent-soft);
  font-weight: 600;
}
.fp-popover__unassign { justify-content: center; color: var(--muted); margin-top: 2px; }

/* ── Chart ── */
.chart-container {
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 16px;
  box-shadow: var(--shadow);
  height: 260px;
}

@media (min-width: 600px) {
  .chart-container { height: 320px; }
}
@media (min-width: 1100px) {
  .chart-container { height: 380px; }
}

/* ── Footer ── */
.footer {
  padding: 20px 16px;
  text-align: center;
  border-top: 1px solid var(--line);
  margin-top: 8px;
}

/* ── Error state ── */
.error {
  background: var(--bad-bg);
  color: var(--bad-ink);
  border: 1px solid var(--bad-border);
  padding: 16px;
  border-radius: var(--radius);
  font-size: .88rem;
}

/* ── Mobile app shell (/mobile.html) ──────────────────────────────────────────
   Bottom-tab app presentation of the same data. Shares every token above.
   Layout: persistent outside strip → active view → fixed tab bar. */
.m-body { padding-bottom: calc(64px + env(safe-area-inset-bottom)); }
.m-body.m-sheet-open { overflow: hidden; }

/* Slim outside strip, always visible; tinted by the live temperature like the
   banner. Tapping it opens Trends. */
.m-strip {
  --strip-c: var(--pending);
  position: sticky;
  top: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 16px calc(10px + 0px);
  padding-top: calc(10px + env(safe-area-inset-top));
  background: linear-gradient(
    135deg,
    color-mix(in srgb, var(--strip-c) 16%, var(--card-bg)),
    color-mix(in srgb, var(--strip-c) 6%, var(--card-bg))
  );
  border-bottom: 1px solid color-mix(in srgb, var(--strip-c) 30%, var(--card-bg));
  cursor: pointer;
  /* Host the living-weather layer (wxSkyHtml) behind the reading, exactly like
     the hour cards: sticky is our positioning context, an isolated stacking
     context keeps the sky's z-index:-1 under the text, and overflow clips the
     drifting particles to the slim strip. */
  overflow: hidden;
  isolation: isolate;
}
.m-strip__label {
  font-size: .68rem;
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--muted);
}
.m-strip__temp {
  font-size: 1.15rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: color-mix(in srgb, var(--strip-c) 72%, var(--text));
}
.m-strip__updated {
  margin-left: auto;
  font-size: .7rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
/* The honest version: the FORECAST'S issue time, in the slot a reader has always
   taken for the data's age (design §4.1). Two tight lines at 10.5px fit the same
   space the single poll-time line used, and the right edge stays flush with the
   bell above/below it. */
.m-strip__updated--issue {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  line-height: 1.2;
  font-size: .655rem;
  text-align: right;
}
.m-strip__src { font-weight: 600; letter-spacing: .01em; }
.m-strip__issue { white-space: nowrap; }
/* Over six hours old — aged in warn ink, and everything keeps drawing. */
.m-strip__updated--issue.is-stale .m-strip__issue { color: var(--warn-ink); font-weight: 600; }
/* No forecast at all (a cold start, a failed read). The strip's designed
   miss-state: it SAYS so, in the same calm tone as the banner's empty state,
   rather than leaving stale numbers under a poll time. */
.m-strip__updated--miss { color: var(--faint); }
.m-strip__updated--miss .m-strip__src { font-weight: 600; }

.setup-checks {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 14px;
}
.setup-check { display: flex; align-items: center; gap: 10px; }
.setup-check__tick {
  flex: none;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 1.5px solid var(--faint);
  position: relative;
}
.setup-check.is-done .setup-check__tick {
  background: var(--good-ink);
  border-color: var(--good-ink);
}
.setup-check.is-done .setup-check__tick::after {
  content: "";
  position: absolute;
  left: 6px; top: 3px;
  width: 5px; height: 9px;
  border: solid #fff;
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
.setup-check__label { font-size: .84rem; color: var(--text-2); min-width: 0; }
.setup-check.is-done .setup-check__label { color: var(--text); }

.m-views { padding: 16px; max-width: 720px; margin: 0 auto; }
.m-view { animation: viewIn .18s ease-out; }
@keyframes viewIn {
  from { opacity: .55; transform: translateY(4px); }
  to { opacity: 1; transform: none; }
}

/* Bottom tab bar — the app's navigation. Big targets, safe-area aware. */
.m-tabs {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 40;
  display: flex;
  background: var(--glass);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border-top: 1px solid var(--line);
  padding-bottom: env(safe-area-inset-bottom);
}
.m-tabs__btn {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 8px 0 6px;
  font-size: .64rem;
  font-weight: 600;
  color: var(--faint);
  background: none;
  border: none;
  cursor: pointer;
}
.m-tabs__btn svg { width: 22px; height: 22px; fill: currentColor; }
.m-tabs__btn.is-active { color: var(--accent); }

/* Plan view: single-column floorplan panels (they're side-by-side on desktop). */
.m-plan { flex-direction: column; }

/* Trends: taller touch-friendly charts. */
.m-chart { height: 300px; }
/* Empty Predict chart: collapse the reserved height so the placeholder line
   doesn't sit alone in a tall box. */
.m-chart.m-chart--empty { height: auto; padding: 6px 0; }

/* More: link list rows. */
.m-links { display: flex; flex-direction: column; gap: 8px; }
.m-links__item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 14px 16px;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  font-size: .9rem;
  font-weight: 550;
  color: var(--text);
  text-decoration: none;
}
.m-links__item span { color: var(--faint); font-size: 1.1rem; }
.m-guide { border-left: none; }

/* "Install as an app" one-time setup steps (More tab). */
.m-setup { margin: 8px 0 0 18px; display: flex; flex-direction: column; gap: 8px; font-size: .84rem; }
.m-setup a { font-weight: 600; }
.m-setup__step { font-size: .84rem; color: var(--text-2); }
.m-setup__note {
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--line-soft);
  font-size: .74rem;
  color: var(--muted);
}

/* "Plan alerts" Web Push opt-in (More tab). A label + a native-style switch; a
   status line appears below when the browser has blocked notifications or after
   a toggle. The whole section is hidden unless push can work here (§13.5). */
.m-notify__row { display: flex; align-items: flex-start; gap: 14px; }
.m-notify__label { min-width: 0; flex: 1 1 auto; }
.m-notify__title { font-size: .9rem; font-weight: 650; color: var(--text); }
.m-notify__sub { margin: 4px 0 0; font-size: .78rem; color: var(--muted); line-height: 1.45; }
.m-notify__status {
  margin: 11px 0 0; padding-top: 10px; border-top: 1px solid var(--line-soft);
  font-size: .76rem; color: var(--text-2); line-height: 1.45;
}
.m-switch {
  flex: none; position: relative; width: 48px; height: 28px; margin-top: 1px; padding: 0;
  border-radius: 999px; border: 1px solid var(--line); background: var(--bg);
  cursor: pointer; transition: background .16s ease, border-color .16s ease;
  -webkit-tap-highlight-color: transparent;
}
/* Expand the tap target to ≥44px without enlarging the 28px visual track. */
.m-switch::after {
  content: ""; position: absolute; top: -9px; bottom: -9px; left: -8px; right: -8px;
}
.m-switch__knob {
  position: absolute; top: 50%; left: 3px; width: 22px; height: 22px; border-radius: 50%;
  transform: translateY(-50%); background: var(--card-bg); box-shadow: 0 1px 3px rgba(0,0,0,.3);
  transition: left .16s ease, background .16s ease;
}
.m-switch.is-on { background: var(--accent); border-color: var(--accent); }
.m-switch.is-on .m-switch__knob { left: 23px; background: #fff; }
.m-switch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.m-switch:disabled { opacity: .5; cursor: not-allowed; }
.m-switch.is-busy { opacity: .6; cursor: progress; }

/* ── Notifications: bell + badge, centre (#alerts), settings (#notifications) ──
   Vocabulary under the m- namespace (design §8.3). Glyph/urgency tints and the
   unread accent all come from the shared status/accent tokens, so both themes
   are covered with no hard-coded colours. */

/* Bell in the persistent outside strip (top-right, beside the battery). */
.m-bell {
  flex: none;
  position: relative;
  width: 34px; height: 34px;
  margin: -6px -4px -6px 0;      /* grow the tap target without adding strip height */
  display: flex; align-items: center; justify-content: center;
  background: none; border: none; cursor: pointer;
  color: var(--text-2);
  -webkit-tap-highlight-color: transparent;
}
/* The negative margin above buys strip height, not reach: the button's own box
   is still 34px. This is the reach. */
.m-bell::after { content: ""; position: absolute; inset: -5px; border-radius: 999px; }
.m-bell__glyph {
  width: 21px; height: 21px;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}
.m-bell--active { color: var(--text); }
.m-bell__badge {
  position: absolute; top: 1px; right: -1px;
  min-width: 16px; height: 16px; padding: 0 4px;
  display: flex; align-items: center; justify-content: center;
  font-size: .6rem; font-weight: 700; line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--card-bg); background: var(--bad-ink);
  border: 1.5px solid var(--strip-c, var(--card-bg));
  border-radius: 999px;
}
.m-bell__badge[hidden] { display: none; }
.m-bell:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 8px; }

/* Trailing unread count on the More-tab "Notifications" row. */
.m-links__trail { display: inline-flex; align-items: center; gap: 8px; }
.m-links__item .m-pillcount {
  min-width: 18px; height: 18px; padding: 0 5px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: .68rem; font-weight: 700; line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--card-bg); background: var(--bad-ink); border-radius: 999px;
}
.m-links__item .m-pillcount[hidden] { display: none; }

/* Header action (the settings gear in the #alerts header). */
.m-room-screen__action {
  margin-left: auto; flex: 0 0 auto;
  width: 36px; height: 36px;
  display: flex; align-items: center; justify-content: center;
  color: var(--text-2); text-decoration: none;
  border: 1px solid var(--line); border-radius: 999px; background: var(--bg);
}
.m-room-screen__action svg { width: 19px; height: 19px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linejoin: round; }
.m-room-screen__action:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Centre: inbox toolbar (mark-all-read / clear). */
.m-notif-actions {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; margin-bottom: 14px;
}
.m-notif-actions[hidden] { display: none; } /* beat the class over the UA [hidden] rule */
.m-notif-act {
  min-height: 40px; padding: 8px 14px;
  font-size: .82rem; font-weight: 600; color: var(--accent);
  background: var(--card-bg); border: 1px solid var(--line);
  border-radius: 999px; cursor: pointer;
}
.m-notif-act:disabled { opacity: .45; cursor: default; }
.m-notif-act--danger { color: var(--bad-ink); }
.m-notif-act--danger.is-armed { background: var(--bad-bg); border-color: var(--bad-border); }
.m-notif-act:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Centre: day groups + rows. */
.m-notif-group + .m-notif-group { margin-top: 18px; }
.m-notif-day {
  font-size: .68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .07em;
  color: var(--faint); margin: 0 2px 8px;
}
.m-notif-item {
  --notif-tint: var(--accent);
  display: flex; align-items: flex-start; gap: 12px; width: 100%;
  text-align: left; padding: 13px 14px; margin-bottom: 8px;
  background: var(--card-bg); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow); cursor: pointer;
}
.m-notif-item:last-child { margin-bottom: 0; }
.m-notif-item--crit { --notif-tint: var(--bad-ink); }
.m-notif-item--high { --notif-tint: var(--warn-ink); }
.m-notif-item--normal { --notif-tint: var(--accent); }
.m-notif-item--low { --notif-tint: var(--muted); }
.m-notif-item--unread {
  border-color: color-mix(in srgb, var(--notif-tint) 40%, var(--line));
  background: color-mix(in srgb, var(--notif-tint) 7%, var(--card-bg));
}
.m-notif-item__icon {
  flex: none; width: 34px; height: 34px; border-radius: 9px;
  display: flex; align-items: center; justify-content: center;
  color: var(--notif-tint);
  background: color-mix(in srgb, var(--notif-tint) 13%, var(--card-bg));
}
.m-notif-item__glyph {
  width: 19px; height: 19px; fill: none; stroke: currentColor; stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round;
}
.m-notif-item__main { flex: 1 1 auto; min-width: 0; }
.m-notif-item__title {
  display: block; font-size: .88rem; font-weight: 650; color: var(--text);
  line-height: 1.35;
}
.m-notif-item__body {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  margin-top: 2px; font-size: .8rem; color: var(--muted); line-height: 1.4;
}
.m-notif-item__side {
  flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 7px;
  padding-top: 1px;
}
.m-notif-item__time {
  font-size: .68rem; color: var(--faint); font-variant-numeric: tabular-nums; white-space: nowrap;
}
.m-notif-item__dot { width: 8px; height: 8px; border-radius: 50%; background: var(--notif-tint); }

/* Centre: empty / loading / error states. */
.m-notif-empty {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  padding: 48px 24px; gap: 6px;
}
.m-notif-empty__glyph {
  width: 56px; height: 56px; margin-bottom: 8px;
  display: flex; align-items: center; justify-content: center;
  color: var(--faint);
  border-radius: 50%; background: var(--card-bg); border: 1px solid var(--line);
}
.m-notif-empty__glyph svg { width: 28px; height: 28px; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
.m-notif-empty__lead { font-size: 1rem; font-weight: 650; color: var(--text); }
.m-notif-empty__body { font-size: .82rem; color: var(--muted); line-height: 1.5; max-width: 300px; }

.m-notif-skel {
  display: flex; align-items: center; gap: 12px; padding: 13px 14px; margin-bottom: 8px;
  background: var(--card-bg); border: 1px solid var(--line); border-radius: var(--radius);
}
.m-notif-skel__icon { flex: none; width: 34px; height: 34px; border-radius: 9px; background: var(--line-soft); }
.m-notif-skel__lines { flex: 1 1 auto; display: flex; flex-direction: column; gap: 7px; }
.m-notif-skel__lines span { height: 10px; border-radius: 5px; background: var(--line-soft); }
.m-notif-skel__lines span:first-child { width: 62%; }
.m-notif-skel__lines span:last-child { width: 90%; }
.m-notif-skel { animation: notifShimmer 1.4s ease-in-out infinite; }
@keyframes notifShimmer { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }
@media (prefers-reduced-motion: reduce) { .m-notif-skel { animation: none; } }

.m-notif-error {
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  padding: 40px 24px; text-align: center;
}
.m-notif-error p { font-size: .86rem; color: var(--muted); }

/* Settings: quiet hours + per-trigger list. */
.m-quiet__row { display: flex; align-items: center; gap: 12px; }
.m-quiet__field {
  flex: 1 1 auto; display: flex; flex-direction: column; gap: 4px;
  font-size: .7rem; font-weight: 600; color: var(--muted);
}
.m-quiet__field small { text-transform: uppercase; letter-spacing: .05em; }
.m-quiet__field input {
  height: 46px; padding: 0 12px; font-size: .95rem; font-weight: 650; color: var(--text);
  font-variant-numeric: tabular-nums;
  background: var(--bg); border: 1px solid var(--line); border-radius: 10px;
}
.m-quiet__to { color: var(--faint); font-size: 1.1rem; align-self: flex-end; padding-bottom: 12px; }
.m-quiet__note {
  margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--line-soft);
  font-size: .76rem; color: var(--muted); line-height: 1.45;
}

.m-triglist { padding: 4px 0; }
.m-trigger { padding: 14px 16px; }
.m-trigger + .m-trigger { border-top: 1px solid var(--line-soft); }
.m-trigger__head { display: flex; align-items: flex-start; gap: 14px; }
.m-trigger__label { flex: 1 1 auto; min-width: 0; }
.m-trigger__title {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  font-size: .88rem; font-weight: 650; color: var(--text);
}
.m-trigger__tag {
  font-size: .6rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
  color: var(--good-ink); background: var(--good-bg);
  padding: 2px 7px; border-radius: 999px;
}
.m-trigger__sub { margin-top: 3px; font-size: .77rem; color: var(--muted); line-height: 1.45; }
.m-trigger__detail {
  margin-top: 12px; padding-top: 12px; border-top: 1px dashed var(--line);
  display: flex; flex-direction: column; gap: 12px;
}
.m-trigger__detail.is-off { opacity: .45; pointer-events: none; }
.m-trigger__foot { font-size: .74rem; color: var(--muted); line-height: 1.45; }

.m-thresh { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.m-thresh__label { font-size: .82rem; color: var(--text-2); min-width: 0; }
.m-thresh__stepper { flex: none; }
.m-thresh--switch { min-height: 32px; }

.m-notif-saveerr {
  margin-top: 10px; padding: 10px 12px;
  font-size: .78rem; color: var(--bad-ink);
  background: var(--bad-bg); border: 1px solid var(--bad-border); border-radius: 10px;
}
.m-notif-saveerr[hidden] { display: none; }

/* ── Home outlook: today/tonight stat tiles + scrollable hour strip ── */
.m-stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
  margin-bottom: 10px;
}
.m-stat {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 12px;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.m-stat__label {
  font-size: .64rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--muted);
}
.m-stat__value {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 1.02rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.m-hours {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  padding-bottom: 4px;
  -webkit-overflow-scrolling: touch;
}
.m-hour {
  --room-c: var(--pending);
  scroll-snap-align: start;
  flex: 0 0 auto;
  min-width: 58px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: 8px 6px;
  position: relative;      /* host the living-weather sky layer */
  overflow: hidden;
  isolation: isolate;      /* keep the z-index:-1 sky inside this card */
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--room-c) 14%, var(--card-bg)),
    color-mix(in srgb, var(--room-c) 5%, var(--card-bg))
  );
  border: 1px solid color-mix(in srgb, var(--room-c) 28%, var(--card-bg));
  border-radius: var(--radius-sm);
}
.m-hour__time { font-size: .64rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.m-hour__temp {
  font-size: .95rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: color-mix(in srgb, var(--room-c) 72%, var(--text));
}
.m-hour__dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: 1px solid var(--ring);
}
/* The card's meta row: the humidity dot, and a storm mark beside it when the
   hour carries one. In the FLOW, centred — not a corner badge, which would sit
   ~4px from the time label at every width on a 58px tile. */
.m-hour__meta {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  min-height: 10px;
}
/* Storm marks (bolt / hail pellets). A SHAPE, never a colour meaning: the tile
   is already washed by temperature and already carries the humidity dot, so the
   mark takes --warn-ink (defined in all three theme blocks) and nothing else.
   Level 2 only — a 30-70% lightning probability is not an assertion, and this
   mark never animates. */
.wx-mark {
  width: 10px;
  height: 10px;
  flex: 0 0 auto;
  fill: var(--warn-ink);
  stroke: none;
}
.wx-mark--hail { opacity: .9; }
/* Mobile hour cards open the in-app hour-detail sheet, so they are <button>s
   (they used to be <a>s to the Met Office — the selector must stay element-
   agnostic). A button inherits neither font nor colour, hence the resets. Keep
   the card's own look, and add a discoverable tap affordance: a subtle press
   dip and a focus ring — not button chrome. */
.m-hour--link {
  font: inherit;
  color: inherit;
  text-align: center;
  text-decoration: none;
  -webkit-appearance: none;
  appearance: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: filter .15s ease, transform .15s ease;
}
.m-hour--link:active { filter: brightness(.96); transform: scale(.97); }
.m-hour--link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ── Room cards (Home) ── */
.m-rooms {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}
.m-rooms__empty { font-size: .82rem; color: var(--muted); }
.m-rooms__empty a { color: var(--accent); }

/* Card = one big touch target. The temperature colour rides the left rail and
   the reading ink (same recipe as the outdoor banner) — never the whole card. */
.m-room {
  --room-c: var(--pending); /* set inline per reading */
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  min-height: 76px;
  padding: 10px 12px;
  text-align: left;
  background: linear-gradient(
    135deg,
    color-mix(in srgb, var(--room-c) 13%, var(--card-bg)),
    color-mix(in srgb, var(--room-c) 5%, var(--card-bg))
  );
  border: 1px solid color-mix(in srgb, var(--room-c) 30%, var(--card-bg));
  border-left: 4px solid var(--room-c);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  cursor: pointer;
  transition: background .3s ease, border-color .3s ease;
}
.m-room:active { filter: brightness(.97); }
.m-room__name {
  display: flex;
  /* flex-START, not centre: a two-line name ("Mum and Dad's Room") would
     otherwise float its icon into the gap between the lines. Aligned to the top,
     the icon sits on the FIRST line at any width — and for a one-line name the
     two boxes are the same height, so nothing moves. */
  align-items: flex-start;
  gap: 5px;
  /* Pinned, not `normal`: the icon beside the name is sized in em against this
     exact line box, which is how an icon is added without the card growing. */
  line-height: 1.25;
  /* A user CAN type 20 characters with no space in them, and the icon takes ~19px
     of a ~131px card: without this the word's min-content width would push the
     name straight out through the card's side. `anywhere` (not `break-word`) is
     the one that also shrinks the intrinsic minimum. */
  overflow-wrap: anywhere;
  font-size: .68rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--muted);
}
/* The room's own icon, leading its name — currentColor, so it is the same ink
   as the label rather than a second colour competing with the temperature. */
.m-room__icon { width: 1.25em; height: 1.25em; }
.m-room__temp {
  font-size: 1.55rem;
  font-weight: 700;
  letter-spacing: -.01em;
  font-variant-numeric: tabular-nums;
  color: color-mix(in srgb, var(--room-c) 72%, var(--text));
}
.m-room__temp--empty { color: var(--faint); font-weight: 500; }
/* At-a-glance heating/cooling readout on its own line under the value
   (utils.trendArrow): a direction caret + an honest rate number ("▲ 1.8°/h").
   Direction = tint (warm/cool, the data palette — distinct from the card's
   absolute-temp wash because it's a bounded, shaped tag, never a fill), rate =
   the digits. The fast tier gets an emphasised tinted pill so "moving quickly"
   pops across the grid without reading the number; the slow tier is plain tinted
   text. Empty for a steady/absent trend → :empty hides it, so it takes no space
   and a settled house stays quiet. */
.m-room__trend {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  margin-top: 1px;
  font-size: .82rem;
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.m-room__trend:empty { display: none; }
.m-room__trend-car { font-size: 1.05em; line-height: 1; }
.m-room__trend--up { color: var(--hot); }
.m-room__trend--down { color: var(--cold); }
.m-room__trend--fast {
  font-weight: 800;
  padding: 2px 8px;
  border-radius: 999px;
}
.m-room__trend--fast.m-room__trend--up { background: color-mix(in srgb, var(--hot) 17%, transparent); }
.m-room__trend--fast.m-room__trend--down { background: color-mix(in srgb, var(--cold) 20%, transparent); }
.m-room__meta { display: flex; align-items: center; gap: 8px; min-height: 18px; }
.m-room__hum {
  font-size: .7rem;
  font-weight: 600;
  padding: 1px 7px;
  border-radius: 999px;
  font-variant-numeric: tabular-nums;
}
.m-room__age { font-size: .7rem; color: var(--muted); font-variant-numeric: tabular-nums; }
/* On the home card the battery is a compact glyph-only chip; a tint backing (the
   same warn/bad vocabulary as the humidity pill) lifts a degraded one so critical
   is unmissable without a worded label crowding the two-column meta row. */
.m-room__meta .batt { padding: 2px 6px; border-radius: 999px; }
.m-room__meta .batt--low { background: var(--warn-bg); }
.m-room__meta .batt--crit { background: var(--bad-bg); }

/* Home per-floor header: floor label + a live windows-state pill, so the floor
   plan reflects the current house state at a glance (§11.1). Preserves the
   10px gap the bare .section__title used to have below it. */
.m-floorhead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 10px;
}
.m-floorhead .section__title { margin-bottom: 0; }
.m-winstate {
  flex: 0 0 auto;
  font-size: .66rem;
  font-weight: 700;
  letter-spacing: .02em;
  padding: 2px 8px;
  border-radius: 999px;
}
.m-winstate:empty { display: none; }
.m-winstate--open {
  background: var(--accent-soft);
  color: var(--accent);
  border: 1px solid var(--accent-border);
}
.m-winstate--closed {
  background: var(--bg);
  color: var(--muted);
  border: 1px solid var(--line);
}

/* ── Trends (mobile): card headers, segmented bars, options sheet ── */
.m-cardhead {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
}
.m-cardhead .section__title { margin-bottom: 0; }
.m-tune {
  position: relative;
  padding: 4px 12px;
  font-size: .74rem;
  font-weight: 600;
  color: var(--muted);
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
}
/* A 24px pill beside a section heading, and a 44px target: the reach comes from
   an invisible ::after rather than from padding, because padding here would push
   the heading row apart on every screen that carries one (.m-rename-btn idiom). */
.m-tune::after { content: ""; position: absolute; inset: -10px -6px; border-radius: 999px; }
.m-tune:hover { color: var(--text); border-color: var(--faint); }

/* Segmented bar: one joined pill, equal cells — reskins the shared range /
   mode / overlay controls into an app control wherever they appear inside the
   mobile shell (their behaviour is untouched; the desktop keeps its chips). */
.m-seg .range-selector,
.m-seg .mode-toggle,
.mode-toggle.m-seg--inline,
.m-views .overlay-toggle {
  display: flex;
  gap: 0;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 3px;
}
.m-seg .range-selector { margin: 8px 0 0; }
.m-seg .mode-toggle { margin: 8px 0 0; display: inline-flex; }
.mode-toggle.m-seg--inline { margin-bottom: 0; }
.m-views .overlay-toggle { display: inline-flex; }
.m-seg .range-selector__btn,
.m-seg .mode-toggle__btn,
.mode-toggle.m-seg--inline .mode-toggle__btn,
.m-views .overlay-toggle__btn {
  flex: 1;
  border: none;
  background: none;
  border-radius: 7px;
  padding: 7px 12px;
}
.m-seg .range-selector__btn.is-active,
.m-seg .mode-toggle__btn.is-active,
.mode-toggle.m-seg--inline .mode-toggle__btn.is-active,
.m-views .overlay-toggle__btn.is-active {
  background: var(--accent-soft);
  color: var(--accent);
}

/* Window advice reads bigger under a thumb than a mouse. */
.m-views .window-hint { font-size: .82rem; gap: 8px; }
.m-views .window-hint__action { padding: 3px 12px; }

.m-compare-note {
  margin-top: 8px;
  font-size: .74rem;
  color: var(--muted);
}
.m-compare-note:empty { display: none; }

/* Room chips double as the combined chart's legend: each carries its line's
   colour dot, and the on-state borrows the zone-chip recipe (line colour on
   the border, not the accent). */
.m-views .rate-rooms__btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 5px 12px;
  font-size: .76rem;
}
.rate-rooms__dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--room-line, var(--faint));
}
.m-views .rate-rooms__btn.is-on {
  opacity: 1;
  color: var(--text);
  background: var(--card-bg);
  border-color: var(--room-line, var(--accent-border));
  font-weight: 600;
}
.m-room-chartbox { margin-top: 8px; }
.m-room-chartempty {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
}

/* Bottom sheet (chart options). */
.m-sheet-backdrop {
  position: fixed;
  inset: 0;
  z-index: 70;
  background: rgba(16, 24, 40, .45);
}
.m-sheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 71;
  background: var(--card-bg);
  border-top: 1px solid var(--line);
  border-radius: 16px 16px 0 0;
  box-shadow: var(--shadow-pop);
  padding: 8px 16px calc(16px + env(safe-area-inset-bottom));
  max-height: 85dvh;
  overflow-y: auto;
  animation: sheetUp .22s ease-out;
}
/* Every sheet is shown/hidden by the `hidden` ATTRIBUTE, and a variant class that
   sets `display:` (e.g. .hour-sheet's flex column) outranks the UA's
   `[hidden]{display:none}` — this codebase's most repeated landmine, and one that
   fails LOUDLY here: a closed .m-sheet is `position:fixed; bottom:0; z-index:71`,
   so it paints a card-coloured strip across the bottom of EVERY screen, over the
   tab bar's icons and over any full-screen sub-view. Asserted once on the shared
   base at (0,2,0) so it beats any single variant class, present or future. */
.m-sheet[hidden],
.m-sheet-backdrop[hidden] { display: none; }
@keyframes sheetUp {
  from { transform: translateY(24px); opacity: .6; }
  to { transform: none; opacity: 1; }
}
.m-sheet__grip {
  width: 36px;
  height: 4px;
  border-radius: 2px;
  background: var(--line);
  margin: 0 auto 10px;
}
.m-sheet__header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
}
/* Sheets whose header carries a title AND a two-or-three-line lead: centring
   the close button against the whole block drifts it down beside the lead. It
   belongs on the title's line, which is also where a thumb reaches for it. */
.m-sheet__header--tall { align-items: flex-start; }

/* A second (or third) section heading inside a bottom sheet — the air that
   separates it from the block above. Was .m-opts__gap, named for the forecast-
   model option rows that the Met Office migration deleted. */
.m-sheet__gap { margin-top: 16px; }

/* ── "Where these numbers come from" (options sheet) ─────────────────────────
   Up to five rows that are the complete provenance of every weather number in
   the app, in the slot the four-model picker used to occupy (and now this
   sheet's lead content). Read-only, so it is a
   definition list in look: a fixed-width term column, the value and its
   qualification stacked beside it. The term column is sized for the longest
   label ("Cloud cover") and never wraps; the body column carries min-width:0 so
   a long note wraps INSIDE the row instead of stretching the sheet. */
.m-prov { display: flex; flex-direction: column; gap: 2px; }
.m-prov__row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 8px 2px;
  border-bottom: 1px solid var(--line-soft);
}
.m-prov__row:last-child { border-bottom: 0; }
.m-prov__label {
  /* Sized for the longest term ("Cloud cover") at this face — measured, because
     a wrapped two-line uppercase label beside a one-line value reads as broken
     alignment rather than as a term. */
  flex: 0 0 92px;
  font-size: .72rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--muted);
}
.m-prov__body { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 auto; }
.m-prov__value { font-size: .86rem; font-weight: 600; }
.m-prov__note { font-size: .74rem; color: var(--muted); line-height: 1.4; }
/* Nothing to describe yet (the forecast hasn't loaded): one calm sentence on the
   row grid's own rhythm, never an empty box. */
.m-prov__empty {
  margin: 0;
  padding: 8px 2px;
  font-size: .78rem;
  color: var(--muted);
  line-height: 1.45;
}
.m-prov__link {
  display: inline-block;
  margin-top: 10px;
  font-size: .8rem;
  font-weight: 600;
  color: var(--accent);
  text-decoration: none;
  padding: 6px 0;
  min-height: 32px;
}

/* ── Hour-detail sheet (Home outlook strip → one forecast hour) ──────────────
   A bottom sheet in the shared .m-sheet vocabulary: hero (the hour's own living
   sky behind its temperature), the interpretation, the ensemble-distribution
   block, then label/value rows — with the Met Office action pinned at the foot.

   STRUCTURE: header · scrolling body · pinned foot. Keeping that action above
   the fold was a deliberate goal, and the sheet's content is not fixed (an
   8-row hour with an expanded ensemble block runs ~400px past a phone's
   height). A capped sheet with ONE scrolling child holds the footer at a
   constant 20px above the bottom edge in every variant — expanded and
   collapsed — rather than deferring the fold problem to
   the next row we add. `position:sticky` was the alternative and is worse here:
   inside a padded scrollport it leaves a band of content showing through the
   padding. */
.hour-sheet {
  /* max-height, NOT height: a short hour still hugs its content and shows the
     backdrop above it — only an overflowing one is capped and scrolls. */
  max-height: 92dvh;
  padding-bottom: calc(20px + env(safe-area-inset-bottom));
  /* This `display:` is why .m-sheet[hidden] exists — on its own it outranks the
     UA's [hidden] rule and leaves the closed sheet painting over the tab bar. */
  display: flex;
  flex-direction: column;
  overflow: hidden;             /* overrides .m-sheet's overflow-y:auto — the BODY scrolls */
}
/* Flex children default to shrink:1, so pin everything that is not the body —
   otherwise a longer header would compress instead of the body scrolling. */
.hour-sheet .m-sheet__grip,
.hour-sheet .m-sheet__header,
.hour-sheet .hd-foot { flex: 0 0 auto; }
.hd-scroll {
  flex: 1 1 auto;
  min-height: 0;                /* the rule that actually lets a flex child scroll */
  min-width: 0;
  overflow-y: auto;
  overscroll-behavior: contain; /* a fling at the end doesn't drag the page behind */
  -webkit-overflow-scrolling: touch;
}

/* ── The ± 1 hour pager (spec §9) ────────────────────────────────────────────
   `‹ 11:00 ›` — two 44px targets hugging the hour, in the header row where the
   title already was. The header is also where the SWIPE lives, so it declares
   the horizontal axis as the app's: without `touch-action: pan-y` an Android
   drag from near the edge is the system's back gesture, and the vertical half
   stays the browser's because nothing here wants it. */
.hour-sheet .m-sheet__header { touch-action: pan-y; }
.hd-pager { display: flex; align-items: center; gap: 2px; }
/* A FIXED stack width, and the reason it cannot be intrinsic: the hour is
   tabular and always five characters, but the line under it runs from "now" to
   "in about an hour" — sized to its content, every step would nudge both
   arrows sideways and a repeat tap would land on a different button. 100px is
   MEASURED: the longest "when" is 94px in the app's face at .8rem, the hour is
   52px. (Laying the when line out at zero width and letting it overflow hugs
   the hour tighter and was tried — at 94px it reaches within a pixel of the
   left chevron's glyph, and its box would be invisible to the geometry audit.
   A real box that fits is worth the 24px of air either side of the hour.) */
.hd-pager__stack { flex: 0 0 100px; min-width: 0; text-align: center; }
.hd-pager__hour { font-variant-numeric: tabular-nums; }
.hd-pager__stack .m-sheet__reading {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.hd-pager__btn {
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: 12px;
  background: none;
  color: var(--accent);
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
}
/* Disabled, not hidden: the end of the hourly product is an edge of the data,
   and an arrow that vanished would read as a control that comes and goes. */
.hd-pager__btn:disabled { color: var(--faint); cursor: default; }
.hd-pager__btn:active:not(:disabled) { background: var(--line-soft); }
.hd-pager__btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }

.hd-hero {
  --room-c: var(--pending); /* set inline from the hour's temperature */
  position: relative;       /* host the living-weather sky layer */
  overflow: hidden;
  isolation: isolate;       /* keep the z-index:-1 sky inside this card */
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-height: 92px;
  justify-content: center;
  padding: 14px 18px;
  border-radius: var(--radius);
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--room-c) 18%, var(--card-bg)),
    color-mix(in srgb, var(--room-c) 6%, var(--card-bg))
  );
  border: 1px solid color-mix(in srgb, var(--room-c) 30%, var(--card-bg));
}
.hd-hero__temp {
  font-size: 2.2rem;
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
  color: color-mix(in srgb, var(--room-c) 72%, var(--text));
}
.hd-hero__cond { font-size: .92rem; font-weight: 600; color: var(--text-2); }
/* The storm chip, riding the condition line at the LOUDER of the two weights
   this sheet gives the same fact (the accented first row carries the numbers).
   --warn-ink, defined in all three theme blocks; a shape and a word, never a
   new colour meaning. */
.hd-hero__chip {
  display: inline-block;
  margin-left: 8px;
  padding: 1px 9px;
  border-radius: 999px;
  background: var(--warn-bg);
  color: var(--warn-ink);
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .02em;
  vertical-align: 1px;
}
/* Level 1 is a published chance, not an assertion — a quieter chip for it. */
.hd-hero__chip--risk {
  background: transparent;
  border: 1px solid color-mix(in srgb, var(--warn-ink) 45%, transparent);
}
/* The Met Office's 8-in-10 range for this hour, under the condition as context
   rather than as a reading. Absent — and costing no height — whenever the
   payload has no distribution for it (an hour past the fan, an old snapshot). */
.hd-hero__spread {
  margin-top: 3px;
  font-size: .74rem;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}
/* The density ribbon: p5→p95, ink following the percentile staircase, p50
   ticked. A distribution you can see in one glance — and not a sample, which is
   what the 18-dot beeswarm it replaced always was. */
.hd-rib {
  display: block;
  width: 100%;
  height: auto;
  margin-top: 8px;
  /* The gradient's stops are `currentColor`, which is the only way a <stop> can
     follow the theme — a presentation attribute cannot resolve var(). */
  color: var(--chart-line);
}
.hd-rib__mid { stroke: var(--text); stroke-width: 1.4; }
.hd-rib__lab {
  font-size: 9.5px;
  fill: var(--muted);
  font-variant-numeric: tabular-nums;
}
/* The legend for the ribbon: what its ends, its shading and its mark mean. One
   step quieter than the 8-in-10 line above (that line is a fact about tonight;
   this is how to read the drawing), and it sits INSIDE the hero so it is
   attached to the bar rather than floating under the card. It wraps to two or
   three lines at 390px — hence the explicit line-height, and no max-width: the
   hero's own padding is the measure. */
.hd-rib__cap {
  margin-top: 4px;
  font-size: .72rem;
  line-height: 1.4;
  color: var(--muted);
  text-wrap: pretty;
}
/* The interpretation: what this hour is, and how it sits against its stretch of
   day. The lead line carries the weight; the animation legend is the quieter
   aside beneath it. */
.hd-story {
  margin: 12px 0 0;
  font-size: .9rem;
  line-height: 1.45;
  color: var(--text);
}
.hd-anim {
  margin: 5px 0 0;
  font-size: .78rem;
  line-height: 1.4;
  color: var(--muted);
}
.hd-rows { margin-top: 10px; }
.hd-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  padding: 9px 0;
  border-bottom: 1px solid var(--line-soft);
}
.hd-row:last-child { border-bottom: none; }
.hd-row__label {
  flex: 1 1 auto;
  min-width: 0;
  font-size: .86rem;
  color: var(--text-2);
}
/* Value column: the number on top, its qualifier underneath, both flush right
   so the numbers line up down the sheet. min-width:0 lets a long qualifier wrap
   instead of pushing the label out of the row. */
.hd-row__value {
  flex: 0 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
  text-align: right;
}
.hd-row__num {
  font-size: .92rem;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.hd-row__note { font-size: .76rem; line-height: 1.3; color: var(--muted); }
.hd-row__dot { margin-right: 6px; vertical-align: middle; }
.hd-row--accent .hd-row__num { color: var(--accent); }
.hd-row--accent .hd-row__note { color: var(--accent); opacity: .85; }
/* The storm row is the hero chip's fact at the quieter weight, so it takes the
   chip's ink rather than the accent — one fact, one colour. */
.hd-row--storm .hd-row__num,
.hd-row--storm .hd-row__note { color: var(--warn-ink); }

.hd-extra:not(:empty) { margin-top: 14px; }

/* ── Rain distribution block (hour sheet → "Rain in this hour" / "…in the three
   hours from here") ──
   A disclosure whose collapsed head already carries the headline (the chance of
   any rain over the window the reader has asked for — one hour by default on
   this sheet, spec §9); expanding adds the window control, the exceedance
   ladder, the point caveat and the explorer door. Collapsed by CLASS, never
   [hidden] — a class `display:` beats the UA's [hidden] rule. */
.ens {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--card-bg);
  overflow: hidden;
}
.ens__head {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: 48px;
  padding: 10px 12px 10px 14px;
  font: inherit;
  text-align: left;
  color: var(--text);
  background: none;
  border: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.ens__head:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }
.ens__label {
  flex: 1 1 auto;
  min-width: 0;          /* long label wraps rather than pushing the value out */
  font-size: .84rem;
  font-weight: 600;
  color: var(--text-2);
}
.ens__value {
  flex: 0 0 auto;
  font-size: .86rem;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
/* The app's one disclosure chevron idiom (see .m-disclosure__chev): a "›"
   rotated a quarter turn, which has a reliable optical centre — unlike "⌄",
   whose deep descender box hangs it off the row's centre line. */
.ens__chev {
  flex: 0 0 auto;
  font-size: 1.15rem;
  line-height: 1;
  color: var(--faint);
  transform: rotate(90deg);
  transition: transform .18s ease;
}
.ens--open .ens__chev { transform: rotate(-90deg); }
.ens__body { display: none; padding: 0 12px 12px; }
.ens--open .ens__body { display: block; }

/* The 3-hour exceedance ladder: one bar per rung the product answers for this
   hour, descending by construction (a higher threshold can only be less likely).
   A rung with no figure is OMITTED upstream, never drawn as an empty bar.

   EVERY ROW IS A 44px TARGET DRAWN AT 22 (design §3.3). The bar must not grow —
   an 8px track is the right weight for seven of them — so the height comes from
   an invisible ::after, the idiom .rp-speed uses. The 12px gap is what makes
   that honest: at -11px the hit areas meet in the middle of the gap and only
   overlap each OTHER there, so every pixel of a drawn row belongs to that row
   and an ambiguous tap only ever happens in the dead air between two. The 12px
   of top padding does the same job at the top of the list, keeping the first
   row's target off the disclosure header's bottom edge. */
.hd-lad {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 12px 0 2px;
}
.hd-lad__row {
  display: grid;
  grid-template-columns: 8.2rem minmax(0, 1fr) 2.4rem;
  align-items: center;
  gap: 10px;
  min-height: 22px;
}
/* A selectable rung is a real <button>: keyboard, aria-pressed, and none of the
   UA's chrome. */
.hd-lad__row--tap {
  position: relative;
  width: 100%;
  margin: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  background: none;
  border: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.hd-lad__row--tap::after { content: ""; position: absolute; inset: -11px 0; }
.hd-lad__row--tap:focus-visible { outline: 2px solid var(--accent); outline-offset: 5px; }
/* Selected: the label takes body ink and weight, the bar and its figure take the
   accent. Three cues rather than one, because a single 8px bar changing colour
   is not enough to find the row the block below is about. */
.hd-lad__row--sel .hd-lad__label { color: var(--text); font-weight: 700; }
.hd-lad__row--sel .hd-lad__fill { background: var(--accent); }
.hd-lad__row--sel .hd-lad__pct { color: var(--accent); }
.hd-lad__label { font-size: .76rem; color: var(--text-2); }
.hd-lad__track {
  height: 8px;
  border-radius: 999px;
  background: var(--line-soft);
  overflow: hidden;
}
.hd-lad__fill {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--chart-line);
}
.hd-lad__pct {
  text-align: right;
  font-size: .78rem;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}
/* ── The revealed rung (design §3.3, LAH decision 13) ────────────────────────
   Seven rungs cannot each carry three lines, so one is tapped and one block is
   revealed — and the provenance sentence moved in with it, which is what makes
   the untapped state seven bars and an invitation rather than three stacked
   grey paragraphs.

   TOGGLED BY CLASS, NEVER [hidden]: every rung's lines are in the DOM and the
   selected one is switched on, so the tap is a class flip with no re-render and
   no stored payload to go stale. (A class `display:` would in any case beat the
   UA's [hidden] rule — this codebase's most repeated landmine.) The hint and
   the block are the two halves of one switch, so they can never both show. */
.hd-lad__hint {
  margin: 12px 0 0;
  font-size: .72rem;
  line-height: 1.45;
  color: var(--muted);
}
/* NOTHING PUBLISHED FOR THIS HOUR AT THIS WINDOW (spec §9) — the ladder's
   rung-less state. The control row above it stays: keying that row on this
   hour's answers rather than on the payload's dose block is what left an hour
   paged into with no door back to three hours. A statement, not an invitation,
   so it takes the body's ink rather than the hint's grey. */
.hd-lad__none {
  margin: 12px 0 0;
  font-size: .8rem;
  line-height: 1.45;
  color: var(--text-2);
}
.hd-sel { display: none; }
.ens--picked .hd-lad__hint { display: none; }
.ens--picked .hd-sel { display: block; }
.hd-sel__eq { display: none; }
.hd-sel__eq.is-on { display: block; }
/* ── Hour-by-hour rain strip (the hour sheet AND #dry) ───────────────────────
   Three COLUMNS are the three hours; the rows are the hour, its chance and its
   millimetres — so a reader compares 52 · 58 · 55 across, which is the shape of
   the question. `minmax(0, 1fr)` keeps a wide cell ("0.9 mm over 3 hours", which
   only the resolution tail can produce) wrapping inside its column instead of
   pushing the grid sideways. Tabular figures throughout: these are numbers read
   against each other. A cell with no figure prints an em dash and goes quiet —
   the columns are a time axis, so one is never dropped. */
.hbh__head {
  margin: 0;
  font-size: .68rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--muted);
}
.hbh__grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  align-items: baseline;
  gap: 2px 8px;
  margin-top: 5px;
  text-align: center;
}
.hbh__t { font-size: .72rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.hbh__pct {
  font-size: 1.02rem;
  font-weight: 700;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.hbh__mm { font-size: .72rem; color: var(--text-2); font-variant-numeric: tabular-nums; }
.hbh__pct--gap, .hbh__mm--gap { font-weight: 600; color: var(--faint); }
.hbh__cap { margin: 6px 0 0; font-size: .72rem; line-height: 1.45; color: var(--muted); }
/* In the sheet it sits between the bars and the provenance line. Its own
   caption and that provenance line are the same size and ink, so without a
   deliberate beat between them they read as one five-line grey paragraph
   belonging to nothing: the strip gets a clear gap on both sides, and the
   sentence under the grid stays attached to the grid it explains. */
.ens__body .hbh { margin-top: 12px; }
.ens__body .hbh + .ens__cap { margin-top: 15px; }

.ens__cap {
  margin: 8px 0 0;
  font-size: .72rem;
  line-height: 1.45;
  color: var(--muted);
}
.ens__cta {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  margin-top: 12px;
  padding: 0 14px;
  font-size: .84rem;
  font-weight: 650;
  color: var(--accent);
  background: var(--accent-soft);
  border: 1px solid var(--accent-border);
  border-radius: 10px;
  text-decoration: none;
}
.ens__cta:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Footer action — external, so it opens the Met Office's own page.
   The ::before is the scroll affordance: a short fade sitting just above the
   divider, so a row cut by the fold reads as "there is more below" rather than
   as a clipped row. It is invisible when the body doesn't overflow (card-bg
   fading into card-bg), so it needs no state. */
.hd-foot {
  position: relative;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--line);
}
.hd-foot::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: 22px;
  pointer-events: none;
  background: linear-gradient(to bottom, transparent, var(--card-bg));
}
.hd-action {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 46px;
  padding: 0 14px;
  border-radius: 12px;
  background: var(--accent-soft);
  border: 1px solid var(--accent-border);
  color: var(--accent);
  font-size: .88rem;
  font-weight: 650;
  text-decoration: none;
  text-align: center;
}
.hd-action:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.hd-action__ext { font-size: .8em; }

.m-compare__date {
  font-size: .82rem;
  padding: 6px 10px;
  color: var(--text);
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}

/* ── Trends room-graph actions: outside-temp toggle + compare entry ── */
.m-trend-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 10px;
  margin-top: 10px;
}
/* Compare sits at the trailing edge; the overlay toggles group at the start. */
.m-trend-actions .m-chip-action { margin-left: auto; }
/* Outside-temp overlay toggle: a pill carrying a dashed grey swatch that mirrors
   the muted outdoor line it turns on, so the control looks like its result. */
.m-chip-toggle {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 13px;
  font-size: .76rem;
  font-weight: 600;
  color: var(--muted);
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
}
/* The class sets display:inline-flex, so [hidden] must be re-asserted or it
   loses to it — the Range chip hides itself on a payload with no distribution
   (a rollback, an old snapshot), and a chip that toggles nothing is worse than
   no chip. This repo's most repeated CSS defect; see hidden-audit. */
.m-chip-toggle[hidden] { display: none; }
/* A chip that can't act in the current view (Predict in Rate): the app's
   write-disable dim, never [hidden] — hiding would reflow the actions row on
   every Temp/Rate tap. Its pressed look stays (it is the stored preference that
   comes back) and the caption under the chart says why it is dimmed. */
.m-chip-toggle:disabled { opacity: .5; cursor: default; }
/* …but a PRESSED disabled chip dims only its contents: its pressed border
   (--faint) stays at full strength, so pressed-but-disabled still reads apart
   from an off chip (--line) in dark, where .5 of both borders all but met
   (T2 review). The text and swatch take the same .5 as before. */
.m-chip-toggle.is-on:disabled {
  opacity: 1;
  color: color-mix(in srgb, var(--text) 50%, transparent);
}
.m-chip-toggle.is-on:disabled > * { opacity: .5; }
.m-chip-toggle__dash {
  width: 16px;
  border-top: 2px dashed var(--faint);
}
/* Wind swatch: the same arrow the lane draws, so the control looks like its
   result. Stroked (never filled) and sized to match the dash swatches' width. */
.m-chip-toggle__wind {
  width: 16px;
  height: 12px;
  flex: 0 0 auto;
  fill: none;
  stroke: var(--faint);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.m-chip-toggle.is-on .m-chip-toggle__wind { stroke: var(--muted); }
/* Predict swatch: the forward line is dashed in the accent, so the control looks
   like its result and reads apart from the grey "outside" swatch. */
.m-chip-toggle__dash--pred { border-top-style: dashed; }
/* Range swatch: a filled band, because that is exactly what the toggle draws —
   a stacked pair matching the band's faint outer / dominant inner reading. */
.m-chip-toggle__band {
  width: 16px;
  height: 12px;
  flex: 0 0 auto;
  border-radius: 2px;
  background:
    linear-gradient(var(--faint), var(--faint)) center/100% 6px no-repeat,
    color-mix(in srgb, var(--faint) 34%, transparent);
  opacity: .55;
}
.m-chip-toggle.is-on .m-chip-toggle__band {
  opacity: 1;
  background:
    linear-gradient(var(--chart-line), var(--chart-line)) center/100% 6px no-repeat,
    color-mix(in srgb, var(--chart-line) 30%, transparent);
}

/* The Outside chart's own actions row (Range toggle + explorer door). Its own
   class rather than .m-trend-actions: that node MIGRATES into the full-screen
   room chart's control rail, and these two belong to the weather card. The row
   is permanent now — the explorer door is always there, and the chip inside
   hides itself when the payload carries no distribution to draw. */
.m-outdoor-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 10px;
  margin-top: 10px;
}
.m-outdoor-actions[hidden] { display: none; }
.m-outdoor-actions .m-chip-action { margin-left: auto; text-decoration: none; }
.m-chip-toggle.is-on { color: var(--text); border-color: var(--faint); }
.m-chip-toggle.is-on .m-chip-toggle__dash { border-top-color: var(--muted); }
.m-chip-toggle.is-on .m-chip-toggle__dash--pred { border-top-color: var(--accent); }
.m-chip-action {
  padding: 6px 4px;
  font-size: .78rem;
  font-weight: 600;
  color: var(--accent);
  background: none;
  border: none;
  cursor: pointer;
  white-space: nowrap;
}
.m-chip-action:hover { text-decoration: underline; }

/* Quiet square icon button for a card header (the chart's full-screen entry).
   36px of visible chrome inside a 44px touch target — the extra reach is taken
   with negative margin so it doesn't inflate the header's height. */
.m-iconbtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin: -4px -8px -4px 0;
  flex: 0 0 auto;
  color: var(--text-2);
  background: none;
  border: none;
  cursor: pointer;
}
.m-iconbtn svg {
  width: 20px;
  height: 20px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.m-iconbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: -6px; border-radius: 10px; }

/* ── Full-screen room chart (#chart) ──────────────────────────────────────────
   A .m-room-screen variant that does NOT scroll: a fixed header, a plot that
   takes every pixel left over, and a control block that keeps its own size. The
   inherited .m-room-screen rules already give it the notch-safe header and the
   slide-in. */
.m-chart-screen {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
/* An author `display:flex` outranks the UA [hidden] rule, so restore it
   explicitly — otherwise this fixed, inset:0 view covers the whole app forever. */
.m-chart-screen[hidden] { display: none; }
.m-chart-screen .m-room-screen__header { flex: 0 0 auto; }
.m-chart-screen__body {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-height: 0;
  padding: 12px;
  padding-left: max(12px, env(safe-area-inset-left));
  padding-right: max(12px, env(safe-area-inset-right));
  padding-bottom: max(12px, env(safe-area-inset-bottom));
}
/* The plot column: the canvas grows, its caption stays its own height. */
.m-chart-screen__plot {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-height: 0;
  min-width: 0;
}
.m-chart-screen__canvas {
  flex: 1 1 auto;
  min-height: 0;
  position: relative;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 10px 8px 4px;
}
.m-chart-screen__plot .m-compare-note { margin-top: 0; }
/* Never let a long room list starve the plot: the controls cap out and scroll. */
.m-chart-screen__controls {
  flex: 0 1 auto;
  max-height: 45%;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
.m-chart-screen__slot { display: flex; flex-direction: column; gap: 10px; }
/* The migrated control nodes carry the inline card's own margins; here the
   column gap owns the rhythm, so strip them (including the one the range
   selector sets on its inner element). */
.m-chart-screen__slot > * { margin: 0; }
.m-chart-screen__slot .m-seg .range-selector,
.m-chart-screen__slot .m-cmp-chip { margin: 0; }
/* Guidance, not a caption — the glyph keeps it from reading as a second line of
   chart legend directly under the real one. */
.m-chart-screen__rotate {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 10px;
  font-size: .72rem;
  color: var(--faint);
}
.m-chart-screen__rotate::before {
  content: "";
  width: 11px;
  height: 16px;
  flex: 0 0 auto;
  border: 1.5px solid currentColor;
  border-radius: 2px;
  transform: rotate(28deg);
}
/* Landscape is where a time series wins on a phone: the controls become a right
   rail so the plot keeps the full height, and the rotate prompt has done its job
   and retires. */
@media (orientation: landscape) {
  .m-chart-screen__body { flex-direction: row; align-items: stretch; }
  .m-chart-screen__controls {
    flex: 0 0 40%;
    max-width: 260px;
    max-height: none; /* the rail has the full height; only portrait needs a cap */
  }
  .m-chart-screen__rotate { display: none; }
  .m-chart-screen__slot { gap: 8px; }
  /* Vertical space is the scarce resource here, so the header collapses to a
     single line. */
  .m-chart-screen .m-room-screen__header { padding-top: calc(6px + env(safe-area-inset-top)); padding-bottom: 6px; }
  .m-chart-screen .m-room-screen__header > div {
    display: flex;
    align-items: baseline;
    gap: 8px;
    min-width: 0;
  }
  .m-chart-screen .m-sheet__room { font-size: .9rem; }
  .m-chart-screen .m-sheet__reading {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

/* Active-compare chip: the dismissible "which day am I comparing" indicator. The
   dashed dot mirrors the ghost overlay line, doubling as its legend. */
#m-compare-chip:empty { display: none; }
.m-cmp-chip {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 10px;
  padding: 6px 8px 6px 12px;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: .78rem;
}
.m-cmp-chip__dot {
  flex: 0 0 auto;
  width: 16px;
  border-top: 2px dashed var(--faint);
}
.m-cmp-chip__text { color: var(--text-2); min-width: 0; }
.m-cmp-chip__text strong { color: var(--text); font-weight: 650; }
.m-cmp-chip__pm {
  margin-left: auto;
  flex: 0 0 auto;
  padding: 3px 10px;
  font-size: .72rem;
  font-weight: 600;
  color: var(--muted);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
}
.m-cmp-chip__pm.is-on {
  color: var(--accent);
  border-color: var(--accent-border);
  background: var(--accent-soft);
}
.m-cmp-chip__x {
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  font-size: .72rem;
  color: var(--muted);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 50%;
  cursor: pointer;
}
.m-cmp-chip__x:hover { color: var(--text); }

/* ── Compare-a-day sub-view (#compare) ── */
.cmp-active {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 16px;
  padding: 9px 12px;
  font-size: .8rem;
  color: var(--text-2);
  background: var(--accent-soft);
  border: 1px solid var(--accent-border);
  border-radius: var(--radius);
}
.cmp-active strong { color: var(--text); }
.cmp-active__stop {
  margin-left: auto;
  padding: 4px 12px;
  font-size: .74rem;
  font-weight: 600;
  color: var(--accent);
  background: var(--card-bg);
  border: 1px solid var(--accent-border);
  border-radius: 999px;
  cursor: pointer;
}
/* Today reference strip: the anchor the whole list is compared against. A flat
   inset band (no card shadow, no chevron, not tappable) so it never reads as a
   pickable day. */
.cmp-today {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 16px;
  padding: 10px 14px;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.cmp-today__label {
  flex: 0 0 auto;
  font-size: .7rem;
  font-weight: 700;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--muted);
}
.cmp-today__stat {
  font-size: .9rem;
  font-weight: 650;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.cmp-today__sep { margin: 0 7px; color: var(--faint); font-weight: 400; }
.cmp-today__forecast {
  margin-left: auto;
  flex: 0 0 auto;
  padding: 2px 8px;
  font-size: .66rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--warn-ink);
  background: var(--warn-bg);
  border-radius: 999px;
}

.cmp-block { margin-bottom: 22px; }
.cmp-block__title {
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 4px;
}
.cmp-block__lead {
  margin: 0 0 10px;
  font-size: .8rem;
  color: var(--muted);
}
.cmp-list { display: flex; flex-direction: column; gap: 8px; }

/* One candidate day: star · main (day, range, reasons) · chevron. */
.cmp-card {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  width: 100%;
  padding: 12px 12px 12px 10px;
  text-align: left;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  cursor: pointer;
}
.cmp-card.is-active { border-color: var(--accent-border); background: var(--accent-soft); }
.cmp-card__star {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  margin-top: -1px;
  font-size: 1.1rem;
  line-height: 1;
  color: var(--faint);
  border-radius: 50%;
}
.cmp-card__star.is-on,
.cmp-card__star[aria-pressed="true"] { color: var(--warn-ink); }
.cmp-card__main { flex: 1 1 auto; min-width: 0; }
.cmp-card__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}
.cmp-card__day { font-size: .92rem; font-weight: 650; color: var(--text); }
.cmp-card__range {
  flex: 0 0 auto;
  font-size: .82rem;
  font-weight: 600;
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
}
.cmp-card__sub {
  display: block;
  margin-top: 2px;
  font-size: .76rem;
  color: var(--muted);
}
.cmp-card__note {
  display: block;
  margin-top: 3px;
  font-size: .78rem;
  color: var(--text-2);
  font-style: italic;
}
.cmp-reasons {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.cmp-reason {
  position: relative;
  padding-left: 14px;
  font-size: .76rem;
  color: var(--text-2);
  line-height: 1.35;
}
.cmp-reason::before {
  content: "";
  position: absolute;
  left: 3px;
  top: .5em;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--accent-border);
}
.cmp-card__chev {
  flex: 0 0 auto;
  align-self: center;
  color: var(--faint);
  font-size: 1.1rem;
}
.cmp-msg {
  padding: 14px;
  font-size: .82rem;
  color: var(--muted);
  background: var(--card-bg);
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  text-align: center;
}
.cmp-msg--err { color: var(--bad-ink); border-color: var(--bad-ink); }
.cmp-retry,
.cmp-pick { font-family: inherit; }
.cmp-retry {
  margin-left: 6px;
  padding: 3px 10px;
  font-size: .74rem;
  font-weight: 600;
  color: var(--accent);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
}
.cmp-pick { width: 100%; box-sizing: border-box; }

/* ── Compare preview bottom sheet ── */
.cmp-preview__header { align-items: flex-start; margin-bottom: 8px; }
.cmp-preview__title { min-width: 0; } /* let the title ellipsize, not shove the ✕ */
/* Close: top-right, where a native app puts it — an explicit exit, not the star. */
.cmp-close {
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  font-size: 1.05rem;
  line-height: 1;
  color: var(--muted);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 50%;
  cursor: pointer;
}
.cmp-close:hover { color: var(--text); }

/* Day stepper: its own row so ‹ Previous / Next › never crowd the close button. */
.cmp-step {
  display: flex;
  gap: 8px;
  margin-bottom: 14px;
}
.cmp-step__btn {
  flex: 1 1 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 40px;
  padding: 0 12px;
  font-family: inherit;
  font-size: .8rem;
  font-weight: 600;
  color: var(--text-2);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
}
.cmp-step__btn:hover { border-color: var(--faint); }
.cmp-step__btn:disabled { opacity: .4; cursor: default; }
.cmp-step__btn:disabled:hover { border-color: var(--line); }
.cmp-step__chev { font-size: 1.1rem; line-height: 1; }

/* Favourite cluster: a labelled Save toggle (unmistakably "keep this day") plus
   the note field once saved. Sits with the actions, never in the exit corner. */
.cmp-fav {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 18px;
}
.cmp-fav__toggle {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 0 16px;
  font-family: inherit;
  font-size: .85rem;
  font-weight: 600;
  color: var(--text-2);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
}
.cmp-fav__toggle.is-on {
  color: var(--warn-ink);
  border-color: var(--warn-ink);
  background: var(--warn-bg);
}
.cmp-fav__star { font-size: 1.15rem; line-height: 1; }

/* Mini-chart legend: which line is the compared day, which is today. */
.cmp-legend {
  display: flex;
  gap: 16px;
  margin-bottom: 8px;
  font-size: .74rem;
  color: var(--muted);
}
.cmp-legend__item { display: inline-flex; align-items: center; gap: 6px; }
.cmp-legend__swatch {
  width: 18px;
  height: 0;
  flex: 0 0 auto;
}
.cmp-legend__swatch--subject { border-top: 2px solid var(--accent); }
.cmp-legend__swatch--today { border-top: 2px dashed var(--faint); }

.cmp-preview__stat {
  font-size: .82rem;
  font-weight: 600;
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
  margin-bottom: 10px;
}
.cmp-preview__chart {
  position: relative;
  height: 170px;
  margin-bottom: 12px;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 8px;
}
.cmp-preview__desc {
  margin: 0 0 14px;
  font-size: .86rem;
  line-height: 1.45;
  color: var(--text);
}
.cmp-preview__subhead {
  margin: 0 0 6px;
  font-size: .74rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
}
.cmp-preview__none { margin: 0; font-size: .82rem; color: var(--muted); }
.cmp-events { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 5px; }
.cmp-events__item {
  position: relative;
  padding-left: 16px;
  font-size: .82rem;
  color: var(--text-2);
}
.cmp-events__item::before {
  content: "";
  position: absolute;
  left: 4px;
  top: .5em;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--accent-border);
}
.cmp-note__input {
  width: 100%;
  box-sizing: border-box;
  padding: 9px 11px;
  font-size: .86rem;
  font-family: inherit;
  color: var(--text);
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}
.cmp-compare {
  width: 100%;
  margin-top: 18px;
  padding: 13px;
  font-size: .92rem;
  font-weight: 650;
  color: var(--card-bg); /* dark ink on the light-blue accent in dark theme */
  background: var(--accent);
  border: none;
  border-radius: var(--radius);
  cursor: pointer;
}
.cmp-compare:active { filter: brightness(.94); }
/* Back to the date picker: a quiet secondary action under Compare. */
.cmp-pick-other {
  display: block;
  width: 100%;
  margin-top: 10px;
  padding: 8px;
  font-family: inherit;
  font-size: .82rem;
  font-weight: 600;
  color: var(--muted);
  background: none;
  border: none;
  cursor: pointer;
}
.cmp-pick-other:hover { color: var(--text-2); text-decoration: underline; }

/* ── Sensor status screen: one card per sensor (the app's admin-table). ── */
.m-status { display: flex; flex-direction: column; gap: 10px; }
.m-sensor {
  padding: 12px 14px;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.m-sensor__top {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
}
.m-sensor__name { font-size: .9rem; font-weight: 650; min-width: 0; }
.m-sensor__name small { font-weight: 500; color: var(--muted); font-size: .7rem; margin-left: 4px; }
/* Trailing cluster: the ✎ rename button + the status badge, kept together on the
   right. The pencil carries a full 44px tap target but negative vertical margin
   cancels its footprint so the compact card row doesn't grow. */
.m-sensor__topright { display: inline-flex; align-items: center; gap: 6px; flex: none; }
.m-sensor__rename {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; margin: -12px -8px -12px 0;
  padding: 0; border: 0; background: none; cursor: pointer;
  color: var(--muted); font-size: 1.1rem; line-height: 1;
  border-radius: 999px; -webkit-tap-highlight-color: transparent;
}
.m-sensor__rename:hover { color: var(--text-2); }
.m-sensor__rename:active { background: var(--accent-soft); color: var(--accent); }
.m-sensor__rename:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* Edit mode: the name row becomes a full-width field over a Cancel/Save pair. */
.m-sensor__edit { display: flex; flex-direction: column; gap: 10px; }
.m-sensor__input {
  width: 100%; box-sizing: border-box; min-height: 44px;
  padding: 9px 12px; font: inherit; font-size: 1rem; font-weight: 600;
  color: var(--text); background: var(--bg);
  border: 1px solid var(--line); border-radius: 10px;
}
.m-sensor__input:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.m-sensor__editrow { display: flex; gap: 8px; }
.m-sensor__editbtn {
  flex: 1; min-height: 44px; padding: 0 14px;
  font-size: .88rem; font-weight: 650; border-radius: 10px; cursor: pointer;
  border: 1px solid var(--line); background: var(--card-bg); color: var(--text);
}
.m-sensor__editbtn--ghost { background: transparent; color: var(--text-2); }
.m-sensor__editbtn--save { background: var(--accent); color: var(--card-bg); border-color: var(--accent); }
.m-sensor__editbtn:active { filter: brightness(.96); }
.m-sensor__editbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.m-sensor__mid {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
  margin-top: 4px;
  font-variant-numeric: tabular-nums;
}
.m-sensor__reading { font-size: 1.05rem; font-weight: 700; }
.m-sensor__stats { display: inline-flex; align-items: center; gap: 12px; }
.m-sensor__signal { font-size: .76rem; color: var(--text-2); }
.m-sensor__meta { margin-top: 3px; font-size: .74rem; color: var(--muted); font-variant-numeric: tabular-nums; }

/* ── Host battery-history + host-power panels (mobile #status + desktop /admin) ── */
.hp-section[hidden] { display: none; }
.hp-caption {
  font-size: .8rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  text-align: right;
  min-width: 0;
}
/* Range segmented control — the app's joined-pill vocabulary. */
.hp-range {
  display: inline-flex;
  gap: 0;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 3px;
  margin: 0 0 12px;
}
.hp-range__btn {
  border: none;
  background: none;
  border-radius: 7px;
  padding: 8px 16px;
  min-height: 38px;
  font-size: .8rem;
  font-weight: 600;
  color: var(--text-2);
  cursor: pointer;
}
.hp-range__btn.is-active { background: var(--accent-soft); color: var(--accent); }
.hp-range__btn:focus-visible { outline: 2px solid var(--accent-border); outline-offset: 1px; }

.hp-chartwrap {
  position: relative;
  height: 220px;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 12px;
}
.hp-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  margin: 0;
  color: var(--muted);
  font-size: .85rem;
  text-align: center;
}
.hp-empty--inline { height: auto; padding: 6px 0; }
/* A flex display would otherwise beat the UA [hidden] rule and keep the
   loading/empty message on screen behind the chart — re-hide it explicitly. */
.hp-empty[hidden] { display: none; }

/* Host-power card: hero estimate, per-process table, honesty small print. */
.hp-card { border-left: 1px solid var(--line); }
.hp-est { display: flex; flex-direction: column; gap: 2px; margin-bottom: 14px; }
.hp-est__val {
  font-size: 1.5rem;
  font-weight: 700;
  letter-spacing: -.01em;
  font-variant-numeric: tabular-nums;
}
.hp-est__note { font-size: .76rem; color: var(--muted); }

.hp-proc {
  width: 100%;
  border-collapse: collapse;
  font-size: .82rem;
  font-variant-numeric: tabular-nums;
}
.hp-proc th {
  text-align: left;
  font-size: .66rem;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--faint);
  font-weight: 600;
  padding: 0 0 6px;
  border-bottom: 1px solid var(--line);
}
.hp-proc td { padding: 8px 0; border-bottom: 1px solid var(--line-soft); }
.hp-proc tr:last-child td { border-bottom: none; }
.hp-proc th.hp-proc__num,
.hp-proc td.hp-proc__num { text-align: right; white-space: nowrap; padding-left: 10px; }
.hp-proc__role { font-weight: 600; }
.hp-proc__role small { display: block; font-weight: 400; color: var(--muted); font-size: .68rem; }
.hp-proc__row--minor td { color: var(--muted); }
.hp-proc__row--minor .hp-proc__role { font-weight: 500; color: var(--text-2); }

.hp-method { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line-soft); }
.hp-method__line { margin: 0; font-size: .75rem; color: var(--text-2); line-height: 1.5; }
.hp-method__line b { color: var(--text); font-weight: 650; }
.hp-method__caveats { margin: 8px 0 0; padding-left: 16px; display: flex; flex-direction: column; gap: 5px; }
.hp-method__caveats li { font-size: .72rem; color: var(--muted); line-height: 1.45; }

/* ── "Is my data safe" cards: DB health · Backup · Git ──────────────────────────
   Colocated with the host battery/power panels on mobile #status + desktop
   /admin. One coherent family: a header verdict badge, a status-coloured card
   edge, and the shared ok/watch/action dot vocabulary. */
.db-badge {
  font-size: .66rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  padding: 3px 9px;
  border-radius: 999px;
  white-space: nowrap;
}
.db-badge--ok { background: var(--good-bg); color: var(--good-ink); }
.db-badge--watch { background: var(--warn-bg); color: var(--warn-ink); }
.db-badge--action { background: var(--bad-bg); color: var(--bad-ink); }

/* Status-coloured left edge, echoing .card's accent idiom (which .db-card
   overrides to a neutral default until a status modifier lands). */
.db-card { border-left: 4px solid var(--line); }
.db-card--ok { border-left-color: var(--good-ink); }
.db-card--watch { border-left-color: var(--warn-ink); }
.db-card--action { border-left-color: var(--bad-ink); }

/* Shared status dot. */
.db-dot--ok, .db-finding--ok .db-finding__dot, .db-lead__dot--ok { background: var(--good-ink); }
.db-dot--watch, .db-finding--watch .db-finding__dot, .db-lead__dot--watch { background: var(--warn-ink); }
.db-dot--action, .db-finding--action .db-finding__dot, .db-lead__dot--action { background: var(--bad-ink); }

/* Summary block — the scaling nudge (DB card). */
.db-summary { list-style: none; margin: 0 0 14px; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.db-finding { display: flex; gap: 9px; align-items: flex-start; }
.db-finding__dot { flex: none; width: 8px; height: 8px; border-radius: 50%; margin-top: 6px; }
.db-finding__body { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.db-finding__what {
  align-self: flex-start;
  font-size: .68rem;
  font-weight: 700;
  padding: 1px 6px;
  border-radius: 5px;
  background: var(--line-soft);
  color: var(--text);
}
.db-finding__note { font-size: .78rem; line-height: 1.45; color: var(--text-2); }
.db-allclear { margin: 0 0 14px; font-size: .82rem; font-weight: 600; color: var(--good-ink); }

/* Compact per-table growth readout (DB card). */
.db-tbl {
  width: 100%;
  border-collapse: collapse;
  font-size: .8rem;
  font-variant-numeric: tabular-nums;
}
.db-tbl th {
  text-align: left;
  font-size: .64rem;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--faint);
  font-weight: 600;
  padding: 0 0 6px;
  border-bottom: 1px solid var(--line);
}
.db-tbl td { padding: 7px 0; border-bottom: 1px solid var(--line-soft); }
.db-tbl tr:last-child td { border-bottom: none; }
.db-tbl__name { font-weight: 600; word-break: break-word; }
.db-tbl__num { text-align: right; white-space: nowrap; padding-left: 12px; }
.db-tbl__num--soft { color: var(--muted); }
.db-tbl__flag { width: 16px; padding-left: 10px; text-align: center; }
.db-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; vertical-align: middle; }

/* Size projection line (DB card). */
.db-proj { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line-soft); }
.db-proj__line {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 5px 7px;
  font-size: .84rem;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}
.db-proj__now { color: var(--accent); }
.db-proj__lbl {
  font-size: .64rem;
  font-weight: 500;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: .04em;
}
.db-proj__arrow { color: var(--faint); font-weight: 400; }
.db-proj__note { margin: 7px 0 0; font-size: .72rem; line-height: 1.45; color: var(--muted); }

/* File metadata footer (DB card). */
.db-meta { margin: 12px 0 0; font-size: .7rem; color: var(--faint); line-height: 1.5; font-variant-numeric: tabular-nums; }

/* Lead line — the Backup + Git cards' single-glance verdict. */
.db-lead { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 9px; }
.db-lead__dot { flex: none; width: 9px; height: 9px; border-radius: 50%; }
.db-lead__main { font-size: 1rem; font-weight: 700; letter-spacing: -.01em; }
.db-lead__aside {
  margin-left: auto;
  font-size: .76rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.db-lead__sub {
  margin: 7px 0 0;
  font-size: .78rem;
  line-height: 1.45;
  color: var(--text-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.db-lead__sub--soft { color: var(--muted); font-variant-numeric: tabular-nums; }
.db-note { margin: 8px 0 0; font-size: .76rem; line-height: 1.45; color: var(--text-2); }

/* Access-tokens card (tokensCard.ts): two token rows + a renew footer, sharing
   the db-lead vocabulary. The sub-line WRAPS (unlike db-lead__sub) because an
   action-state token carries its "re-authorize" nudge here and must stay
   legible in full. */
.tk-row { padding: 11px 0; border-top: 1px solid var(--line-soft); }
.tk-row:first-of-type { padding-top: 2px; border-top: none; }
.tk-sub {
  margin: 5px 0 0 18px; /* aligns under the lead text, past the dot + gap */
  font-size: .78rem;
  line-height: 1.45;
  color: var(--text-2);
}
.tk-foot {
  display: inline-block;
  margin-top: 12px;
  font-size: .8rem;
  font-weight: 600;
  color: var(--accent);
  text-decoration: none;
}
.tk-foot:hover { text-decoration: underline; }
.tk-foot:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }

/* ── Room screen: full-screen detail, slides in over the shell ── */
.m-sheet__room { font-weight: 650; font-size: 1rem; letter-spacing: -.01em; }
.m-sheet__reading { font-size: .8rem; color: var(--muted); font-variant-numeric: tabular-nums; }

.m-room-screen {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: var(--bg);
  overflow-y: auto;
  animation: roomIn .2s ease-out;
}
/* Same guard as .m-sheet, and for the same reason: a closed sub-view is
   `position:fixed; inset:0; z-index:60`, so a variant class with its own
   `display:` would blank the whole app. .m-chart-screen already needed this
   one-off (see below); assert it on the base so the next variant is safe. */
.m-room-screen[hidden] { display: none; }
@keyframes roomIn {
  from { transform: translateX(48px); opacity: .5; }
  to { transform: none; opacity: 1; }
}
.m-room-screen__header {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 16px;
  padding-top: calc(10px + env(safe-area-inset-top));
  background: var(--glass);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--line);
}
.m-room-screen__back {
  width: 36px;
  height: 36px;
  flex: 0 0 auto;
  position: relative;
  font-size: 1.4rem;
  line-height: 1;
  color: var(--text-2);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
}
/* The circle reads better at 36px than at 44 — every sub-view header and every
   sheet's close button uses it — so the TOUCH target is grown to 44 by an
   invisible ::after, the same trick .m-rename-btn below uses. Nothing moves. */
.m-room-screen__back::after {
  content: ""; position: absolute; inset: -4px; border-radius: inherit;
}
/* The header's text column. min-width:0 so a long friendly name ellipsises
   instead of shouldering the back button out of the header. */
.m-room-screen__title { min-width: 0; display: flex; flex-direction: column; }
/* The room screen's title column TAKES the header's remaining width (room snooze
   U4's long-label sweep): content-sized, it stayed 312px while the rename button
   inside it grew to its name's max-content, so a long friendly name ran off the
   header and the whole pane scrolled sideways instead of ellipsising. */
#view-room .m-room-screen__title { flex: 1 1 auto; }

/* Tap the room's name to rename it. Visually it is still just the title (no
   button chrome until pressed) with a small pencil to say it is editable; the
   TOUCH target is grown to the full 44px by an invisible ::after that reaches
   into the header's own padding and the dead space beside the reading line —
   the alternative, padding the button itself, would push the header 20px taller
   on every room screen. */
.m-rename-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  align-self: flex-start;
  /* `max-width:100%` is safe ONLY because the column above is flexed to a
     definite width (#view-room .m-room-screen__title): inside a column sized by
     its own content, Chrome under-reports the button's max-content contribution
     and the title ellipsised at the READING line's width (113px) with 300px of
     header to spare. Without the cap the button grew to the name's max-content
     and a long name ran off the header (U4). */
  min-width: 0;
  max-width: 100%;
  margin: 0 -6px;
  padding: 2px 6px;
  background: none;
  border: none;
  border-radius: 8px;
  color: var(--text);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}
.m-rename-btn::after { content: ""; position: absolute; inset: -11px -8px -11px -8px; }
.m-rename-btn .m-sheet__room {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.m-rename-btn__pencil { flex: 0 0 auto; font-size: .78rem; color: var(--muted); }
/* The room's icon before the title. `:empty` collapses the span for a room
   without one — a flex `gap` is paid for an empty child too, and a 6px hole
   before the title is exactly the kind of seam this app is judged on. Sized
   under the title's own line box (1rem → 19.2px), so no room screen's header
   gets taller for having an icon. */
.m-rename-btn__icon { display: inline-flex; flex: 0 0 auto; color: var(--muted); }
.m-rename-btn__icon:empty { display: none; }
.m-rename-btn__glyph { width: 1.15em; height: 1.15em; }
.m-rename-btn:active { background: var(--accent-soft); }
.m-rename-btn:active .m-rename-btn__pencil { color: var(--accent); }
.m-rename-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.m-room-screen__body {
  padding: 16px 16px calc(24px + env(safe-area-inset-bottom));
  max-width: 720px;
  margin: 0 auto;
}
.m-room-screen__canvas {
  position: relative;
  height: 260px;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 12px;
  /* Vertical drags scroll the page; horizontal stays with us for the tooltip
     scrub and the block drag-select. While actively selecting we take the whole
     gesture so a drift can't scroll out from under it. */
  touch-action: pan-y;
}
.m-room-screen__canvas.is-selecting { touch-action: none; }

/* Room data-block affordances: a quiet "Block a range" pill that swaps for an
   accent instruction bar while a range is being picked (drag or tap-tap). */
.room-blocktools { margin-top: 12px; }
.room-blockbtn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 40px;
  padding: 9px 15px;
  font-size: .82rem;
  font-weight: 600;
  color: var(--text-2);
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: 999px;
  box-shadow: var(--shadow);
  cursor: pointer;
}
.room-blockbtn[hidden] { display: none; }
.room-blockbtn:hover { background: var(--bg); }
.room-blockbtn__glyph { font-size: .95rem; color: var(--muted); line-height: 1; }
.room-blockbar {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 44px;
  padding: 8px 10px 8px 14px;
  background: var(--accent-soft);
  border: 1px solid var(--accent);
  border-radius: 12px;
}
.room-blockbar[hidden] { display: none; }
.room-blockbar__text {
  flex: 1 1 auto;
  min-width: 0;
  font-size: .8rem;
  line-height: 1.35;
  color: var(--text);
}
.room-blockbar__cancel {
  flex: 0 0 auto;
  min-height: 32px;
  padding: 6px 12px;
  font-size: .8rem;
  font-weight: 600;
  color: var(--text-2);
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
}
/* Block sheet: reuses .ev-blocksheet__* / .ev-blockpreview and .plan-action. */
.m-block-hint {
  margin: 0 0 14px;
  font-size: .76rem;
  line-height: 1.5;
  color: var(--muted);
}
.m-block-remove { margin-top: 8px; }
.m-block-remove.is-armed {
  color: var(--bad-ink);
  border-color: var(--bad-ink);
}
/* Collapsible "Assigned sensor" disclosure on the room detail screen. The
   reassign list is basically never touched day-to-day, so it opens collapsed
   with the current assignment shown inline. Card-row idiom shared with
   m-setup-link; the reassign list itself reuses .fp-popover__list. */
.m-disclosure { margin: 20px 0 4px; }
.m-disclosure__summary {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: 44px;
  text-align: left;
  padding: 11px 14px;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  color: var(--text);
  font-size: .85rem;
  cursor: pointer;
}
.m-disclosure__label { font-weight: 600; white-space: nowrap; }
.m-disclosure__value {
  flex: 1;
  min-width: 0;
  text-align: right;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.m-disclosure__chev {
  flex: none;
  color: var(--faint);
  font-size: 1.1rem;
  line-height: 1;
  transition: transform .15s ease;
}
.m-disclosure.is-open .m-disclosure__chev { transform: rotate(90deg); }
.m-disclosure__panel { margin-top: 6px; }

/* ── Dry-window explorer (#dry) ───────────────────────────────────────────────
   The answer in words first, then the graph — a task screen, not a dashboard.
   Everything is now a PROBABILITY: the y axis is the Met Office's own chance of
   rain over the three hours from each departure, so every number on the screen
   is the same kind of thing as the sentence above it, the ladder in the hour
   sheet and the line on Home. (It counted ensemble members until the migration;
   the old duration picker went with them — it composed marginal hours into a
   joint answer, which is the error that rebuild existed to remove. The window
   control below is not that control back: both of its windows are PUBLISHED
   exceedances, nothing is composed.) */

/* The window control (dry-window-toggle spec §2): `Out for · 1 hour · 3 hours`.
   Its own row under the header and above the answer card — it changes the
   question that card answers, so it reads as the card's first line — inset to
   the body's 16px so it shares edges with the cards below rather than with
   nothing, and scrolling with them: a second sticky band under a header sized
   by env(safe-area-inset-top) is a fragile offset on a screen that is mostly
   one graph.
   THE [hidden] GUARD IS PART OF THE CONTROL. The row is hidden whenever the
   payload carries no hourly dose block (spec §5), and this class's own
   `display:flex` out-specifies the UA `[hidden]` rule — without the guard the
   control would stay on screen in exactly the state §5 hides it in
   (.m-notif-actions is the precedent this stylesheet was caught by). */
.dry-segbar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 14px 16px 0;
  max-width: 720px;
  margin: 0 auto;
}
.dry-segbar[hidden] { display: none; } /* beat the class over the UA [hidden] rule */
/* `flex: 0 0 auto` + nowrap: the label is two words and shrinking it to fit a
   pill is the wrong trade — the pill has the slack. */
.dry-segbar__label {
  flex: 0 0 auto;
  font-size: .78rem;
  color: var(--muted);
  white-space: nowrap;
}
.dry-segbar__seg { flex: 1 1 auto; min-width: 0; }
/* A real 44px row: this is the screen's only control (.ph-segbar's size, and
   the same argument). The active segment is TINTED, not filled — filling is
   reserved for picking between two products (.ph-segbar), and these two
   segments are two views of one question. So the resting ink stays the shared
   control language's own `--muted` (declared nowhere here, deliberately): with
   only a tint carrying the selection, .ph-segbar's darker `--text-2` left the
   unselected segment competing with it — measured in both themes, worse in
   dark, where --text-2 is the LIGHTER of the two. Weight is stepped instead,
   by a rule specific enough to beat the shared (0,4,0) tint. */
.dry-segbar .mode-toggle__btn {
  min-height: 44px;
  font-size: .88rem;
  font-weight: 650;
}
.dry-segbar .mode-toggle.m-seg--inline .mode-toggle__btn.is-active { font-weight: 700; }

/* THE SAME ROW INSIDE THE HOUR SHEET'S LADDER DISCLOSURE (spec §9), reseated.
   The disclosure body already insets its own 12px and is not a full screen, so
   the variant drops the horizontal padding and the centring width and keeps
   only the vertical rhythm: 8px of air under the head's own 10px, and the
   ladder's 12px below it — which holds this 44px target and the first rung's
   44px ::after apart with dead air to spare (.hd-lad's 12px exists for exactly
   that reason against the head, and now does the same job against this).
   Everything else — the 44px segments, the tint, the stepped weight — is
   inherited from .dry-segbar above, so the two surfaces cannot drift.
   NO [hidden] GUARD IS NEEDED HERE, unlike the screen's row: this one is
   rendered markup and is simply not emitted where the payload has no hourly
   dose block (spec §9) — nothing is drawn, so nothing can out-specify. */
.dry-segbar--sheet {
  padding: 8px 0;
  max-width: none;
}
/* One level further down the hierarchy than the screen's row, and sharing a
   card with .ens__label: quieter, so the disclosure's own heading stays the
   loudest thing in the block. */
.dry-segbar--sheet .dry-segbar__label { font-size: .76rem; }

.dry-answer {
  padding: 16px;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.dry-answer__lead {
  font-size: .74rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--muted);
}
.dry-answer__count {
  margin-top: 4px;
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}
.dry-answer__count b {
  font-size: 2.1rem;
  font-weight: 700;
  letter-spacing: -.02em;
  color: var(--text);
  margin-right: 2px;
}
/* The "no dry window" / "not forecast yet" headline: same slot, no giant number
   to shout, so it stays a calm statement rather than a broken-looking hero. */
.dry-answer__count--none {
  font-size: 1.4rem;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -.01em;
}
.dry-answer__what { margin-top: 2px; font-size: .84rem; color: var(--text-2); }
.dry-answer__next {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--line-soft);
  font-size: .82rem;
  color: var(--text-2);
}
.dry-answer__next b { color: var(--accent); font-variant-numeric: tabular-nums; }
/* The two looser readings at the pinned departure: the same question with the
   bar for what counts as rain lowered, so a lead-in says that and each reading
   gets its own row. Label + figure rather than prose, because these are numbers
   the reader compares with the hero above them — one figure column, tabular,
   right-aligned, so 70 and 88 line up under each other.
   A GRID, not the wrapping flex row this was: the labels now spell their
   millimetres, and a label wide enough to wrap under flex would put its figure
   beside the NEXT label. `minmax(0, …)` keeps the label column from blowing the
   card out at a large browser font. Deliberately NO rule above it — it is the
   same question as the sentence it follows, with the bar lowered, and a hairline
   there would cut a reading away from the number it is read against. The rules
   in this card divide its three BEATS: the answer, what that much rain is like,
   and what waiting buys. */
.dry-answer__also {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 3px 12px;
  /* 10px, the same step the "wait until" block takes below: sentence, list,
     advice each get one gap, so the card reads as three beats and not as a
     paragraph that sprouted rows. */
  margin-top: 10px;
  font-size: .78rem;
  color: var(--muted);
}
.dry-answer__also-lead { grid-column: 1 / -1; }
.dry-answer__also b {
  color: var(--text-2);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

/* ── What that much rain is like: the equivalence block (design §3.1–3.3) ─────
   Three lines, printed on TWO surfaces from one renderer (rainBlock.ts): inside
   #dry's answer card, on the same hairline idiom as the "wait until" advice
   below it — so the card reads as three beats rather than as a paragraph that
   grew — and inside the hour sheet's revealed rung, where the same hairline
   separates the block from the ladder it was tapped on. The rules below are the
   block itself and belong to neither surface; each surface's own spacing is
   scoped beside its other rules.

   THE WEIGHTS ARE THE READING ORDER. The lead is the heaviest thing in the
   block because it is the verdict (or, when there is none, the quietest,
   because a caveat is not a verdict — `--quiet` is the whole difference and it
   must stay visible in both themes). The leading reading takes body ink, the
   other takes muted: they are the same fact twice, and the ranked one is the
   one to read first. Only the amount clause is weighted inside a sentence —
   the eye lands on "1 mm, a litre on every square metre" and reads on. */
.rain-eq {
  margin-top: 11px;
  padding-top: 10px;
  border-top: 1px solid var(--line-soft);
}
.rain-eq__lead {
  font-size: .78rem;
  font-weight: 700;
  color: var(--text);
}
.rain-eq__lead--quiet { font-weight: 600; color: var(--muted); }
.rain-eq__p {
  margin-top: 3px;
  font-size: .78rem;
  line-height: 1.45;
  color: var(--muted);
}
.rain-eq__p--first { color: var(--text-2); }
.rain-eq__amt { font-weight: 600; color: var(--text-2); }

/* The strip's own card on #dry, in the row the retired duration picker freed:
   the same card skin as the answer above it, one 10px gap between them, so the
   two read as the answer and the detail behind it rather than as one card that
   grew a fourth beat. */
.dry-strip {
  margin-top: 10px;
  padding: 12px 14px;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

.dry-caption {
  margin: 20px 0 6px;
  font-size: .74rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--muted);
}
.dry-plot {
  display: block;
  width: 100%;
  height: auto;
  padding: 8px 4px;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  /* Vertical drags scroll the page; the horizontal gesture is ours for pinning. */
  touch-action: pan-y;
  -webkit-tap-highlight-color: transparent;
}
/* SVG cannot resolve var() in a presentation attribute — every colour below is a
   CSS declaration, which is what keeps the plot correct in BOTH themes. */
.dry-grid { stroke: var(--line-soft); stroke-width: 1; }
.dry-axis { stroke: var(--line); stroke-width: 1; }
.dry-ylab, .dry-xlab {
  font-size: 9.5px;
  fill: var(--muted);
  font-variant-numeric: tabular-nums;
}
/* The band between the two thresholds. color-mix per theme rather than one
   opacity for both: a 13%-alpha accent all but vanishes on the near-black dark
   surface, and this band is the design's whole point. */
.dry-band {
  fill: color-mix(in srgb, var(--accent) 16%, var(--card-bg));
  stroke: none;
}
/* The 0.5 mm reading is a SEAM inside that band, not a third peer curve: the
   wide band is drawn whole (any -> notice) and this fill OVERLAYS its lower
   half, which is why a payload with no 0.5 rung degrades to exactly the old
   screen. An alpha mix rather than a second opaque one: composited over the
   base it lands near 30% accent against the base's 16%, a division visible at a
   glance without the band becoming two unrelated colours — and it cannot
   disagree with the base in either theme, because it IS the base plus accent.
   (`--light` is the rain ladder damp / light / wet; nothing to do with the
   light theme.) */
.dry-band--light { fill: color-mix(in srgb, var(--accent) 17%, transparent); }
.dry-curve { fill: none; stroke-linejoin: round; stroke-linecap: round; }
.dry-curve--wet { stroke: var(--accent); stroke-width: 2.2; }
/* The three strokes are ordered by INK, lightest for the loosest threshold:
   dots for "any rain", dashes for the 0.5 mm seam, solid for the answer. Ink
   fraction along the path — drawn length (dash + a cap at each end) over the
   period, times opacity — is ~0.38 / ~0.62 / 1.00, so weight rises with the
   rain. Dashes read HEAVIER than dots on the phone, which is why this pair is
   the way round it is (they were swapped on 2026-09-19: dashing the outer
   "any rain" curve made the loosest reading the boldest line on the plot).

   Dots are a ZERO-length dash under the inherited round linecap: a short dash
   is fattened by half a stroke-width at each cap until the gaps close, and the
   curve reads as a second solid line pressed against its neighbour. */
.dry-curve--damp {
  stroke: var(--accent);
  stroke-width: 1.8;
  stroke-dasharray: 0 4;
  opacity: .85;
}
/* The 0.5 mm reading divides the band, it does not compete with the answer, so
   it stays thinner and fainter than the solid curve running just below it. */
.dry-curve--light {
  stroke: var(--accent);
  stroke-width: 1.6;
  stroke-dasharray: 5 3;
  opacity: .75;
}
.dry-pin { stroke: var(--text-2); stroke-width: 1; stroke-dasharray: 3 3; }
.dry-pindot { fill: var(--accent); stroke: var(--card-bg); stroke-width: 2; }
/* The storm ribbon under the axis: solid on a coded thunder hour, faded where
   thunder is only a published chance (a hatch inside a 7px block fills solid —
   measured). A different QUANTITY from the curves above, so it is never drawn as
   one, and --warn-ink keeps it out of the accent's rain vocabulary in both
   themes. */
.dry-storm { fill: var(--warn-ink); }
.dry-storm--1 { opacity: .42; }
.dry-swatch--storm {
  height: 7px;
  border-top: 0;
  border-radius: 2px;
  background: var(--warn-ink);
}
/* The "all 18 agree" marker as an axis flag UNDER the plot box — floating text
   inside the plot collides with the curve and clips at the right edge. */
.dry-flag path { fill: var(--good-ink); }
.dry-flag text { font-size: 9.5px; font-weight: 600; fill: var(--good-ink); }

.dry-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 16px;
  margin-top: 12px;
}
.dry-legend__item {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: .74rem;
  color: var(--text-2);
}
.dry-swatch { flex: 0 0 auto; width: 18px; border-top: 2px solid var(--accent); }
/* Swatches mirror the plot exactly — dots for "any rain", dashes for 0.5 mm.
   Both stay at the 2px border-top the solid swatch uses: a legend key is read
   at rest, and thinning it below 2px loses the dots entirely at 1x. */
.dry-swatch--damp { border-top-style: dotted; opacity: .85; }
.dry-swatch--light { border-top-style: dashed; opacity: .75; }
.dry-note {
  margin-top: 10px;
  font-size: .74rem;
  line-height: 1.5;
  color: var(--muted);
}
.dry-note--edge { color: var(--text-2); }

/* Designed states, not defaults: never draw a graph whose only content is
   "nothing to say". */
.dry-allclear, .dry-empty {
  margin-top: 20px;
  padding: 20px 16px;
  text-align: center;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.dry-allclear__lead, .dry-empty__lead {
  font-size: .96rem;
  font-weight: 650;
  color: var(--text);
}
.dry-allclear__body, .dry-empty__body {
  margin-top: 6px;
  font-size: .8rem;
  line-height: 1.5;
  color: var(--muted);
}

/* ── Weather-animation gallery (#animations) ─────────────────────────────────
   One specimen per sky: a full-width animated tile in the hour-card's own
   vocabulary (temperature wash + the reading over it), then what it means and
   the rule behind it. Stacked rather than gridded — a 58px hour card is too
   small to judge an animation by, and this screen exists to be judged. */
/* The intro carries the section's own top gap: .section has margin-bottom only,
   so without this the first group heading sits flush against the paragraph. */
#wxg-body > .admin-note { margin: 0 0 26px; }
.wxg-lead {
  margin: -4px 0 14px;
  font-size: .78rem;
  line-height: 1.5;
  color: var(--muted);
}
.wxg-item {
  margin-bottom: 22px;
  /* Twelve live skies is ~100 animated particles, every one carrying
     will-change — roughly three times the busiest real surface, on a screen
     whose whole premise is that the animation is cheap. Off-screen tiles skip
     rendering AND their animations; the intrinsic size keeps the scrollbar from
     jumping, and `auto` lets each tile remember its real height once seen. */
  content-visibility: auto;
  contain-intrinsic-size: auto 360px;
}
.wxg-item:last-child { margin-bottom: 0; }
/* The tile: same recipe as .hd-hero, so a specimen looks like the real thing.
   position/isolation/overflow are what make the z-index:-1 .wx-sky layer sit
   over the tint and under the reading, clipped to the card. */
.wxg-sky {
  --room-c: var(--pending);
  position: relative;
  overflow: hidden;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  /* Taller than the hour sheet's hero on purpose: a falling particle travels
     ~220px per loop, so a short box clips most of them out of frame and a
     specimen meant to be JUDGED looks emptier than the card it documents. */
  min-height: 124px;
  padding: 14px 18px;
  border-radius: var(--radius);
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--room-c) 18%, var(--card-bg)),
    color-mix(in srgb, var(--room-c) 6%, var(--card-bg))
  );
  border: 1px solid color-mix(in srgb, var(--room-c) 30%, var(--card-bg));
}
.wxg-sky__temp {
  font-size: 1.7rem;
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -.02em;
  font-variant-numeric: tabular-nums;
  color: color-mix(in srgb, var(--room-c) 72%, var(--text));
}
.wxg-sky__cond { font-size: .84rem; font-weight: 600; color: var(--text-2); }
/* Replay, on the two EPISODIC specimens only. A gust fires for ~1.4s of every
   18s and the flash for ~0.5s of every 9s — honest on a real card, useless on a
   gallery, so those two get a control instead of luck. Top-right of the tile,
   clear of the reading, 44px of touch inside 30px of visible chrome. */
.wxg-replay {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 1;                 /* above the .wx-sky layer, which sits at -1 */
  display: inline-flex;
  align-items: center;
  min-height: 30px;
  padding: 6px 12px;
  font: inherit;
  font-size: .72rem;
  font-weight: 650;
  color: var(--text-2);
  background: var(--glass);
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}
.wxg-replay::after {
  content: "";                /* the reach to 44px, without inflating the pill */
  position: absolute;
  inset: -7px;
}
.wxg-replay:active { background: var(--card-bg); }
.wxg-replay:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.wxg-item__title {
  margin-top: 12px;
  font-size: .95rem;
  font-weight: 650;
  color: var(--text);
}
.wxg-item__what {
  margin-top: 4px;
  font-size: .82rem;
  line-height: 1.5;
  color: var(--text-2);
}
/* Label/value pairs, not a table: the values wrap freely on a phone and the
   labels stay a fixed, scannable column. */
.wxg-spec {
  margin-top: 10px;
  padding: 10px 12px;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}
.wxg-spec__row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 4px 0;
}
.wxg-spec__row + .wxg-spec__row { border-top: 1px solid var(--line-soft); }
.wxg-spec dt {
  flex: 0 0 74px;
  font-size: .68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--muted);
}
.wxg-spec dd {
  flex: 1 1 auto;
  min-width: 0;          /* long rules wrap instead of pushing the label out */
  font-size: .76rem;
  line-height: 1.45;
  color: var(--text-2);
}
/* What the builder actually emitted, read back from the markup — a second line
   so it reads as evidence rather than as more prose. */
.wxg-spec__layers {
  display: block;
  margin-top: 2px;
  font-size: .72rem;
  color: var(--muted);
}

/* ── Admin / sensor-status page ── */
.admin-note { font-size: .78rem; color: var(--muted); margin-top: 10px; line-height: 1.5; }

.admin-table {
  width: 100%;
  border-collapse: collapse;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow);
  font-size: .88rem;
}
.admin-table th,
.admin-table td { padding: 10px 12px; text-align: left; border-bottom: 1px solid var(--line-soft); }
.admin-table th {
  font-size: .68rem;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--faint);
  background: var(--thead-bg);
}
.admin-table td { font-variant-numeric: tabular-nums; }
.admin-table td small { display: block; color: var(--muted); font-size: .72rem; }
.admin-table tr:last-child td { border-bottom: none; }
.admin-badge {
  font-size: .72rem;
  font-weight: 600;
  padding: 2px 8px;
  border-radius: 999px;
  white-space: nowrap;
}
.admin-badge--ok { background: var(--good-bg); color: var(--good-ink); }
.admin-badge--stale { background: var(--bad-bg); color: var(--bad-ink); }
.admin-badge--pending { background: var(--line-soft); color: var(--text-2); }

#chart-empty {
  align-items: center;
  justify-content: center;
  height: 100%;
  color: var(--muted);
  font-size: .88rem;
}

/* ── House-state controls (windows / per-room device) ─────────────────────────
   Real state, not preferences: segmented controls built by ui.ts render-into-
   slot functions, with a provenance caption ("set 21:04" / "guessed 02:10"). */
.state-control {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  row-gap: 4px;
  margin: 6px 0;
}
/* A class `display:flex` outranks the UA [hidden] rule, so hidden slots (e.g.
   the Devices page's door control on a non-AC room) would otherwise keep their
   margin box and open a phantom gap. Restore real hiding. */
.state-control[hidden] { display: none; }
/* Two per-floor windows toggles side by side (wrapping on narrow screens). */
.state-control-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0 18px;
}
.state-control__label {
  font-size: .8rem;
  font-weight: 600;
  color: var(--text-2);
}
.state-caption {
  font-size: .72rem;
  color: var(--muted);
  white-space: nowrap;
}

/* Running-device pill on room cards / meta lines. Chrome-tinted (status
   colours), not data colours — it marks a regime, not a reading. */
.device-glyph {
  display: inline-block;
  padding: 1px 7px;
  border-radius: 999px;
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .02em;
}
.device-glyph--ac { background: var(--accent-soft); color: var(--accent); border: 1px solid var(--accent-border); }
.device-glyph--heater { background: var(--warn-bg); color: var(--warn-ink); }
/* A GUESS reads as provisional: dashed edge + the trailing "?" the markup adds.
   The old treatment was italic at .68rem/700 — illegible mush — plus an opacity
   drop that just looked stale rather than uncertain. Must follow --ac/--heater
   to override their border (heater has none; the shorthand gives it one). */
.device-glyph--guessed {
  border: 1px dashed color-mix(in srgb, currentColor 45%, transparent);
  font-style: normal;
  opacity: .9;
}

/* On a Home room card the guessed tag is its own affordance — tapping it opens
   the correction prompt instead of the room (mobile.ts's delegated handler).
   The ::after is pure hit-slop: it lifts a ~20px pill to a ~42px target without
   moving the meta row, and its -4px inset stays inside the row's 8px gaps so it
   can never steal a tap from the humidity or battery chip beside it. */
.m-room .device-glyph--guessed {
  position: relative;
  padding: 2px 8px;
  opacity: 1;
}
.m-room .device-glyph--guessed::after {
  content: "";
  position: absolute;
  inset: -11px -4px;
}

/* Floor-plan room outline while its device runs: AC cold-blue, heater warm. */
.fp-room--ac .fp-room__fill { stroke: var(--accent); stroke-width: 2.5; stroke-dasharray: 6 3; }
.fp-room--heater .fp-room__fill { stroke: var(--warn-ink); stroke-width: 2.5; stroke-dasharray: 6 3; }

/* Predict tab: one row per assigned room, name + Off/AC/Heater control. */
.m-devices { margin-top: 4px; }
.m-device-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 7px 0;
  border-bottom: 1px solid var(--line-soft);
}
.m-device-row:last-child { border-bottom: none; }
.m-device-row__name { font-size: .85rem; font-weight: 600; color: var(--text); }
.m-device-row__control { margin: 0; }

/* A room's bedtime prediction time (state-control, popover/room screen). */
.bedtime-time-input {
  font: inherit;
  font-size: .82rem;
  padding: 4px 6px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--card-bg);
  color: var(--text);
}
.bedtime-time-clear {
  padding: 3px 8px;
  border-radius: 999px;
  border: 1px solid var(--line-soft);
  background: transparent;
  color: var(--muted);
  font-size: .72rem;
  cursor: pointer;
}
.bedtime-time-clear:hover, .bedtime-time-clear:active {
  color: var(--bad-ink);
  border-color: var(--bad-border);
}

/* Link out of the mobile room screen to the Devices sub-view (matches the
   More-tab link rows). */
.m-setup-link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 12px 14px;
  margin: 4px 0 14px;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  color: var(--text);
  font-size: .85rem;
  font-weight: 600;
  text-decoration: none;
}
.m-setup-link span:last-child { color: var(--faint); font-size: 1.1rem; }

/* ── Devices & rooms (deviceSettings.ts) — per-room capability config, the
   mobile #devices sub-view. ── */
.room-settings-card--loading { color: var(--muted); padding: 16px; }
/* The first floor heading follows the screen's intro paragraph directly, and an
   all-caps eyebrow welded to the end of a sentence reads as part of it. */
.room-settings-floor { margin-bottom: 22px; }
.room-settings-floor:first-child { margin-top: 18px; }
.room-settings-floor:last-child { margin-bottom: 0; }
.room-settings-floor__title {
  font-size: .72rem;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--faint);
  margin: 0 0 10px;
}
.room-settings-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 12px;
}
.room-settings {
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 12px 14px 14px;
}
/* The card's title row IS the rename affordance (a <button>, so it is reachable
   by keyboard and announces itself): the room's name, with the pencil parked at
   the right edge where the eye already ends the line. Button defaults are reset
   explicitly — a <button> inherits neither font nor colour. */
.room-settings__name {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  font-family: inherit;
  font-size: .95rem;
  font-weight: 650;
  letter-spacing: -.01em;
  text-align: left;
  color: var(--text);
  background: none;
  border: none;
  border-bottom: 1px solid var(--line-soft);
  border-radius: 0;
  margin-bottom: 4px;
  padding: 4px 0 8px;
  min-height: 36px;
  cursor: pointer;
}
.room-settings__name span:first-child {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The same row where the app carries no rename sheet: text, so it must not
   pretend to be tappable. */
div.room-settings__name { cursor: default; }
.room-settings__name:active { color: var(--accent); }
.room-settings__name:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.room-settings__controls { display: flex; flex-direction: column; }
/* The door caption is a full sentence, not a one-word provenance note — let it
   wrap onto its own line under the Open/Closed segment. */
/* ── Device-correction prompt (deviceCorrection.ts) ───────────────────────────
   "Was this guess ever right?" — reached from a guessed tag on a Home card or
   from any device toggle moved off a guessed state. It can open ON TOP of the
   House-state sheet, so it outranks the shared .m-sheet stacking (70/71). */
.devcorrect-backdrop { z-index: 72; }
.devcorrect-sheet { z-index: 73; }
/* min-width:0 so a long room name + subtitle wrap instead of shoving the ✕ off
   the flex header. */
.devcorrect__head { min-width: 0; }
.devcorrect-sheet .m-sheet__reading { white-space: normal; line-height: 1.4; }

.devcorrect__opts { display: flex; flex-direction: column; gap: 8px; }
/* Two answers, each a full-width target well over 44px. Stacked label + detail
   rather than a segmented control: these are consequences, not values, and the
   detail line has to be readable without squinting. */
.devcorrect__opt {
  display: flex;
  flex-direction: column;
  gap: 3px;
  width: 100%;
  min-height: 60px;
  padding: 12px 14px;
  text-align: left;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--text);
  cursor: pointer;
}
/* The history-repairing answer leads and carries the accent — it's the one that
   fixes the model, and the reason the prompt exists at all. */
.devcorrect__opt:first-child {
  background: var(--accent-soft);
  border-color: var(--accent-border);
}
.devcorrect__opt:active { filter: brightness(.97); }
.devcorrect__opt:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.devcorrect__opt:disabled { opacity: .55; cursor: default; }
.devcorrect__opt-label { font-size: .95rem; font-weight: 650; letter-spacing: -.01em; }
.devcorrect__opt-detail { font-size: .76rem; color: var(--muted); line-height: 1.35; }
.devcorrect__foot {
  margin: 14px 0 2px;
  font-size: .74rem;
  color: var(--muted);
  line-height: 1.45;
}

.state-caption--door {
  white-space: normal;
  flex-basis: 100%;
  line-height: 1.35;
  margin-top: 2px;
}

/* ── Device-floor review (deviceFloor.ts) — the "Review AC floor" / "Review
   heater ceiling" links on a Devices card + the mobile #devicefloor sub-view.
   Serves BOTH devices (AC floor + heater ceiling); the .acfloor-* class names
   are historical but device-neutral — the only differences between the two are
   copy/direction, handled in the TS. ── */
.room-settings__review {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  margin-top: 10px; padding: 8px 11px;
  background: var(--accent-soft); border-radius: var(--radius-sm);
  color: var(--accent); font-size: .82rem; font-weight: 600; text-decoration: none;
}
.room-settings__review span:last-child { font-size: 1.15rem; line-height: 1; }
.room-settings__review:hover { filter: brightness(1.04); }

.acfloor-loading { color: var(--muted); padding: 16px; }
.acfloor-empty {
  color: var(--text-2); font-size: .9rem; line-height: 1.5; padding: 16px;
  background: var(--card-bg); border: 1px solid var(--line); border-radius: var(--radius);
}
.acfloor-empty a { color: var(--accent); }

/* Derived-floor summary card (the header). */
.acfloor-summary {
  background: var(--card-bg); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: 16px 18px; margin-bottom: 18px;
}
.acfloor-summary__head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 12px; margin-bottom: 12px;
}
.acfloor-summary__lead { min-width: 0; }
.acfloor-summary__label {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  font-size: .72rem; text-transform: uppercase; letter-spacing: .07em;
  color: var(--faint); margin-bottom: 7px;
}
.acfloor-summary__meta {
  text-transform: none; letter-spacing: 0; font-weight: 600;
  color: var(--muted); font-size: .72rem;
}
.acfloor-summary__dial {
  font-size: 2.4rem; font-weight: 700; line-height: 1;
  color: var(--text); font-variant-numeric: tabular-nums;
}
.acfloor-summary__dial span { font-size: 1.1rem; font-weight: 600; color: var(--muted); margin-left: 2px; }
.acfloor-summary__band {
  margin-top: 5px; font-size: .78rem; color: var(--muted); font-variant-numeric: tabular-nums;
}
.acfloor-summary__mech { margin: 0; font-size: .9rem; line-height: 1.5; color: var(--text-2); }
.acfloor-summary__prior { margin: 8px 0 0; font-size: .8rem; line-height: 1.5; color: var(--warn-ink); }
.acfloor-summary__nofloor { margin: 8px 0 0; font-size: .88rem; line-height: 1.55; color: var(--text-2); }
.acfloor-summary__rate {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line-soft);
}
.acfloor-summary__rate-val {
  font-size: 1.15rem; font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums;
}
.acfloor-summary__rate-val span { font-size: .8rem; font-weight: 600; color: var(--muted); }
.acfloor-summary__rate-lbl { font-size: .8rem; color: var(--muted); }

/* Confidence chip. */
.acfloor-chip {
  flex: 0 0 auto; display: inline-block; padding: 4px 10px; border-radius: 999px;
  font-size: .7rem; font-weight: 700; white-space: nowrap;
}
.acfloor-chip--measured { background: var(--good-bg); color: var(--good-ink); }
.acfloor-chip--prior { background: var(--warn-bg); color: var(--warn-ink); }

/* Session list. */
.acfloor-sessions__head { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
.acfloor-sessions__title { margin: 0; font-size: .95rem; font-weight: 650; color: var(--text); }
.acfloor-sessions__count {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 20px; height: 20px; padding: 0 6px;
  font-size: .7rem; font-weight: 700; color: var(--accent);
  background: var(--accent-soft); border-radius: 999px;
}
.acfloor-sessions__hint { margin: 0 0 12px; font-size: .78rem; line-height: 1.5; color: var(--muted); }
.acfloor-sessions { display: flex; flex-direction: column; gap: 10px; }

.acfloor-session {
  background: var(--card-bg); border: 1px solid var(--line);
  border-radius: var(--radius-sm); padding: 12px 14px;
}
.acfloor-session--idle { opacity: .62; }
.acfloor-session--blocked { opacity: .6; border-style: dashed; }
.acfloor-session--blocked .acfloor-session__when { text-decoration: line-through; }
.acfloor-session.is-open { opacity: 1; border-color: var(--accent); border-style: solid; }
/* The whole head is the accordion toggle — reset the button chrome. */
.acfloor-session__head {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  width: 100%; margin: 0 0 10px; padding: 0; border: 0; background: none;
  font: inherit; color: inherit; text-align: left; cursor: pointer;
}
.acfloor-session__head:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
.acfloor-session__when {
  min-width: 0; font-size: .84rem; font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums;
}
.acfloor-session__right { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
.acfloor-session__flag {
  padding: 1px 8px; border-radius: 999px; font-size: .64rem; font-weight: 700;
  background: var(--warn-bg); color: var(--warn-ink); white-space: nowrap;
}
.acfloor-session__chev {
  color: var(--faint); font-size: 1.15rem; line-height: 1; transition: transform .18s ease;
}
.acfloor-session.is-open .acfloor-session__chev { transform: rotate(90deg); color: var(--accent); }
.acfloor-feeds {
  flex: 0 0 auto; display: inline-block; padding: 2px 9px; border-radius: 999px;
  font-size: .68rem; font-weight: 700; white-space: nowrap;
  background: var(--accent-soft); color: var(--accent);
}
.acfloor-feeds--none { background: var(--line-soft); color: var(--muted); }

.acfloor-session__stats { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; }
.acfloor-stat { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.acfloor-stat__v { font-size: .95rem; font-weight: 650; color: var(--text); font-variant-numeric: tabular-nums; }
.acfloor-stat__k { font-size: .64rem; color: var(--faint); line-height: 1.2; }

/* Expanded session: the day-context chart + block-in-context controls. */
.acfloor-context { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line-soft); }
.acfloor-context__caption {
  margin: 0 0 8px; font-size: .76rem; color: var(--muted); font-variant-numeric: tabular-nums;
}
.acfloor-context__key { color: var(--accent); font-weight: 600; }
.acfloor-context__glyphs { display: block; margin-top: 3px; font-size: .7rem; color: var(--faint); }
/* Break out to the card's padding edges so the chart gets full width on a phone. */
.acfloor-cchart { position: relative; height: 264px; margin: 4px -14px 0; touch-action: pan-y; }
.acfloor-context__block {
  display: flex; align-items: center; justify-content: flex-end; gap: 10px;
  flex-wrap: wrap; margin-top: 14px;
}
.acfloor-blocked { display: flex; align-items: center; gap: 8px; min-width: 0; flex: 1 1 auto; flex-wrap: wrap; }
.acfloor-blocked__badge {
  flex: 0 0 auto; padding: 1px 8px; border-radius: 999px;
  font-size: .66rem; font-weight: 700; background: var(--warn-bg); color: var(--warn-ink);
}
.acfloor-blocked__detail { font-size: .74rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.acfloor-blocked__note { font-style: italic; color: var(--text-2); }

/* Inline block form (mirrors the events delete-confirm grammar). */
.acfloor-blockform { width: 100%; }
.acfloor-blockform__note {
  display: block; width: 100%; margin-bottom: 8px; padding: 8px 10px;
  font: inherit; font-size: .85rem; color: var(--text); resize: vertical;
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm);
}
.acfloor-blockform__btns { display: flex; gap: 8px; justify-content: flex-end; }

/* Phone: 6 stats across is too tight — fold to 3×2. */
@media (max-width: 520px) {
  .acfloor-session__stats { grid-template-columns: repeat(3, 1fr); gap: 12px 8px; }
}

/* House-events (data cleaning) page. */
.ev-controls__field {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: .85rem;
  font-weight: 600;
  color: var(--text-2);
}
.ev-controls__field input,
.ev-controls__field select {
  font: inherit;
  font-weight: 400;
  padding: 4px 8px;
  border: 1px solid var(--line-soft);
  border-radius: 8px;
  background: var(--card-bg);
  color: var(--text);
}
.ev-table td { vertical-align: middle; }
.ev-source {
  display: inline-block;
  padding: 2px 9px;
  border-radius: 999px;
  font-size: .68rem;
  font-weight: 700;
  white-space: nowrap;
}
.ev-source--manual { background: var(--accent-soft); color: var(--accent); }
.ev-source--inferred { background: var(--warn-bg); color: var(--warn-ink); font-style: italic; }
.ev-source--nest { background: var(--line-soft); color: var(--text-2); }

/* ── House-events redesign: native-app chrome, marker lane, blocks, sheet ──── */

.ev-main { padding-bottom: 56px; }

/* App-style header with a back chevron (mobile) that returns into the app. */
.ev-header { gap: 10px; }
.ev-header .header__title { flex: 1 1 auto; min-width: 0; }
.ev-back {
  display: none;
  width: 34px; height: 34px; flex: 0 0 auto;
  align-items: center; justify-content: center;
  font-size: 1.6rem; line-height: 1;
  color: var(--text-2); background: var(--card-bg);
  border: 1px solid var(--line); border-radius: 9px; cursor: pointer;
}
.ev-header__home { white-space: nowrap; }

/* Sticky filter bar: date stepper · view segment · room chips (= chart legend). */
.ev-filter {
  position: sticky; top: 46px; z-index: 8;
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px;
  padding: 10px 0 12px;
  background: var(--bg);
  border-bottom: 1px solid var(--line-soft);
}
.ev-datenav { display: inline-flex; align-items: center; gap: 5px; }
.ev-datenav__step {
  width: 34px; height: 34px; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 1.15rem; color: var(--text-2);
  background: var(--card-bg); border: 1px solid var(--line);
  border-radius: 9px; cursor: pointer;
}
.ev-datenav__step:hover { border-color: var(--faint); color: var(--text); }
.ev-datenav__chip {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 128px; height: 34px; padding: 0 12px;
  font-size: .82rem; font-weight: 600; color: var(--text);
  background: var(--card-bg); border: 1px solid var(--line);
  border-radius: 9px; cursor: pointer; white-space: nowrap;
}
.ev-datenav__chip input[type=date] {
  position: absolute; inset: 0; width: 100%; height: 100%;
  margin: 0; padding: 0; border: 0; opacity: 0; cursor: pointer;
}
.ev-viewseg { flex: 0 0 auto; }
.ev-rooms { display: flex; flex-wrap: wrap; gap: 6px; width: 100%; }
.ev-room-chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 5px 11px; font-size: .76rem; border-radius: 999px;
}
.ev-room-chip.is-on {
  color: var(--text); background: var(--card-bg);
  border-color: var(--room-line, var(--accent-border)); font-weight: 600;
}
.ev-alldot { background: var(--faint); }

/* Chart + its toolbar. */
.ev-section { margin-top: 18px; }
/* position: anchor for the DOM marker lane; pan-y so a vertical swipe scrolls the
   page while a horizontal drag stays with the chart (crosshair scrub) and taps
   land cleanly instead of being stolen for a scroll gesture. */
.ev-chart { min-height: 300px; position: relative; touch-action: pan-y; }

/* Crosshair readout: the live strip under the chart. Reflects the scrubbed
   moment; its action button is the reliable DOM tap target for edit/add. Fixed
   min-height so swapping content never nudges the layout below it. */
.ev-readout {
  display: flex; align-items: center; gap: 10px;
  margin-top: 10px; min-height: 38px; padding: 7px 12px;
  background: var(--card-bg); border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  font-size: .8rem; color: var(--text-2);
  transition: border-color .15s ease, background .15s ease;
}
.ev-readout[hidden] { display: none; }
/* Idle: dissolve into the page as a plain hint until the crosshair moves. */
.ev-readout[data-state="idle"] {
  background: none; border-color: transparent; color: var(--muted);
  padding-left: 2px; padding-right: 2px;
}
.ev-readout[data-state="event"] { border-color: var(--accent-border); }
.ev-readout__body {
  flex: 1 1 auto; min-width: 0;
  display: flex; align-items: center; gap: 8px;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
/* Idle body is a full onboarding sentence, not a short event label — let it
   wrap to two lines (the strip is min-height, so it grows cleanly) instead of
   clipping half the core-gesture explanation with the active-state ellipsis. */
.ev-readout[data-state="idle"] .ev-readout__body {
  white-space: normal; overflow: visible; text-overflow: clip;
}
.ev-readout__g { flex: 0 0 auto; font-size: .9rem; line-height: 1; }
.ev-readout__time {
  flex: 0 0 auto; font-weight: 700; color: var(--text);
  font-variant-numeric: tabular-nums;
}
.ev-readout__what { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.ev-readout__what--muted { color: var(--muted); }
.ev-readout__act {
  flex: 0 0 auto;
  padding: 6px 13px; font-size: .78rem; font-weight: 600;
  color: var(--card-bg); background: var(--accent);
  border: 1px solid var(--accent); border-radius: 999px; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: filter .12s ease;
}
.ev-readout__act:hover { filter: brightness(1.06); }
.ev-readout__act:active { filter: brightness(.94); }
.ev-readout__act:focus-visible { outline: 2px solid var(--accent-border); outline-offset: 2px; }
.ev-readout__act[hidden] { display: none; }

.ev-chartfoot {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; margin-top: 8px;
}
.ev-glyphkey { margin: 0; flex: 1 1 auto; font-size: .7rem; color: var(--faint); line-height: 1.55; }
.ev-glyphkey__g { color: var(--muted); font-size: .8rem; }
.ev-blockbar {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  margin-top: 8px; padding: 8px 12px;
  background: var(--accent-soft); border: 1px solid var(--accent-border);
  border-radius: var(--radius-sm); font-size: .8rem; font-weight: 600; color: var(--text);
}
/* An author `display` beats the UA [hidden] rule — restore it explicitly. */
.ev-blockbar[hidden] { display: none; }

/* Buttons (one small family for the page). */
.ev-btn {
  padding: 7px 14px; font-size: .82rem; font-weight: 600;
  color: var(--text); background: var(--card-bg);
  border: 1px solid var(--line); border-radius: var(--radius-sm); cursor: pointer;
  transition: border-color .15s ease, background .15s ease, color .15s ease;
}
.ev-btn:hover { border-color: var(--faint); }
.ev-btn--primary { color: var(--card-bg); background: var(--accent); border-color: var(--accent); }
.ev-btn--primary:hover { filter: brightness(1.06); border-color: var(--accent); }
.ev-btn--ghost {
  padding: 5px 11px; font-size: .76rem; color: var(--muted);
  background: none; border-color: var(--line);
}
.ev-btn--ghost:hover { color: var(--text); }
.ev-btn--danger { color: var(--bad-ink); background: var(--bad-bg); border-color: var(--bad-border); }
.ev-btn--full { width: 100%; padding: 11px; font-size: .9rem; }
.ev-btnrow { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 8px; }
.ev-status { font-size: .78rem; color: var(--muted); }

/* Event cards (replace the desktop table on every width). */
.ev-list { display: flex; flex-direction: column; gap: 8px; }
.ev-card {
  display: flex; align-items: center; gap: 12px; width: 100%;
  padding: 11px 13px; text-align: left;
  background: var(--card-bg); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow); cursor: pointer;
  transition: border-color .15s ease;
}
.ev-card:hover { border-color: var(--faint); }
.ev-card__glyph {
  flex: 0 0 auto; width: 20px; text-align: center;
  font-size: 1rem; color: var(--muted); line-height: 1;
}
.ev-card__glyph.is-on { color: var(--accent); }
.ev-card__main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.ev-card__title { font-size: .88rem; font-weight: 600; color: var(--text); }
.ev-card__sub { font-size: .76rem; color: var(--muted); }
.ev-card__time {
  flex: 0 0 auto; font-size: .78rem; color: var(--text-2);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.ev-card__chev {
  flex: 0 0 auto; margin-left: -2px; color: var(--faint);
  font-size: 1.25rem; line-height: 1;
}
.ev-card:hover .ev-card__chev { color: var(--muted); }
.ev-card:focus-visible { outline: 2px solid var(--accent-border); outline-offset: 2px; }

/* Designed empty / loading states. */
.ev-empty-card {
  padding: 18px 16px; text-align: center; font-size: .82rem; color: var(--muted);
  background: var(--card-bg); border: 1px dashed var(--line); border-radius: var(--radius);
  line-height: 1.5;
}

/* Collapsible sections (excluded ranges · model internals). */
.ev-details {
  margin-top: 16px; background: var(--card-bg);
  border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden;
}
.ev-details__summary {
  display: flex; align-items: center; gap: 8px;
  padding: 13px 14px; font-size: .88rem; font-weight: 650; color: var(--text);
  cursor: pointer; list-style: none;
}
.ev-details__summary::-webkit-details-marker { display: none; }
.ev-details__summary::after {
  content: "⌄"; margin-left: auto; color: var(--muted);
  font-size: 1rem; transition: transform .15s ease;
}
.ev-details[open] .ev-details__summary::after { transform: rotate(180deg); }
.ev-details__body { padding: 2px 14px 16px; }
.ev-subhead { margin: 16px 0 6px; font-size: .8rem; font-weight: 700; color: var(--text); }
.ev-note { margin-bottom: 10px; }
.ev-count {
  display: inline-flex; min-width: 20px; height: 18px; padding: 0 6px;
  align-items: center; justify-content: center;
  font-size: .68rem; font-weight: 700; color: var(--accent);
  background: var(--accent-soft); border-radius: 999px;
}
.ev-count:empty { display: none; }
.ev-tablewrap { overflow-x: auto; }

/* Minor axis labels under "Model accuracy" (By sensor / By regime). */
.ev-subhead--minor {
  margin: 16px 0 7px;
  font-size: .66rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .07em; color: var(--muted);
}
.ev-note--tight { margin-bottom: 8px; }

/* Per-regime accuracy tiles: a cross-cutting axis, deliberately presented as
   tiles (not a second table) so it doesn't read as merged with the per-sensor
   breakdown. The forecast count leads each tile — the honesty signal — and a
   thin pool is dimmed + flagged so its MAE never reads as a verdict. */
.ev-regimes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 10px;
}
.ev-regime {
  padding: 12px 14px;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}
.ev-regime__head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
}
.ev-regime__name { font-size: .92rem; font-weight: 700; color: var(--text); }
.ev-regime__n {
  font-size: .72rem; color: var(--muted);
  white-space: nowrap; font-variant-numeric: tabular-nums;
}
.ev-regime__n strong {
  font-size: 1.15rem; font-weight: 700; color: var(--text); margin-right: 3px;
}
.ev-regime__desc { margin-top: 2px; font-size: .72rem; color: var(--text-2); }
.ev-regime__flag { color: var(--warn-ink); font-weight: 600; font-style: italic; }
/* B5: driven forecasts left out because the assumed device never ran — a quiet
   footnote to the count, so muted rather than warn-ink (it is not a model miss). */
.ev-regime__premise {
  margin-top: 2px; font-size: .72rem; color: var(--muted);
  font-variant-numeric: tabular-nums; overflow-wrap: anywhere;
}
.ev-regime__metrics {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px 10px;
  margin: 11px 0 0;
}
.ev-regime__metrics > div { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.ev-regime__metrics dt {
  font-size: .6rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .04em; color: var(--muted);
  white-space: nowrap;
}
.ev-regime__metrics dd {
  margin: 0; font-size: .84rem; font-weight: 700; color: var(--text);
  font-variant-numeric: tabular-nums;
}
.ev-regime--thin { opacity: .74; }
.ev-regime--thin .ev-regime__metrics dd { color: var(--text-2); font-weight: 600; }

/* Excluded-range cards — a <button> (whole row opens the edit-note sheet; the
   nested Remove span stops there via closest()-based dispatch). */
.ev-block {
  display: flex; align-items: center; gap: 12px; width: 100%;
  padding: 10px 12px; margin-bottom: 8px;
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm);
  font: inherit; text-align: left; cursor: pointer;
}
.ev-block:last-child { margin-bottom: 0; }
.ev-block:hover { border-color: var(--faint); }
.ev-block__glyph { flex: 0 0 auto; color: var(--muted); font-size: 1rem; }
.ev-block__main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.ev-block__room { font-size: .84rem; font-weight: 600; color: var(--text); }
.ev-block__range { font-size: .76rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.ev-block__note {
  font-size: .76rem; font-style: italic; color: var(--text-2); line-height: 1.4;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
}
/* Drill affordance — mirrors .ev-card__chev; signals the row opens the note editor. */
.ev-block__chev { flex: 0 0 auto; color: var(--faint); font-size: 1.1rem; line-height: 1; }

/* Stored-fits: blocked rows fade; the tag-source + blocked badge read clearly. */
.fits-row--blocked td { opacity: .55; }
.ev-badge--blocked {
  display: inline-block; padding: 1px 8px; border-radius: 999px;
  font-size: .66rem; font-weight: 700; background: var(--warn-bg); color: var(--warn-ink);
}
.ev-tagsrc { color: var(--muted); }

/* Bottom sheet (add / edit / block) — extends the mobile .m-sheet grammar. */
.ev-sheet__head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; margin-bottom: 12px;
}
.ev-sheet__title { font-size: 1rem; font-weight: 700; color: var(--text); }
.ev-sheet__close {
  width: 30px; height: 30px; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: .85rem; color: var(--muted);
  background: var(--bg); border: 1px solid var(--line); border-radius: 8px; cursor: pointer;
}
.ev-field { margin-bottom: 14px; }
.ev-field__label { display: block; margin-bottom: 6px; font-size: .74rem; font-weight: 600; color: var(--text-2); }
.ev-timerow { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.ev-timerow input[type=time] {
  padding: 8px 10px; font-size: .9rem; color: var(--text);
  background: var(--card-bg); border: 1px solid var(--line); border-radius: var(--radius-sm);
  font-variant-numeric: tabular-nums;
}
.ev-nudge {
  width: 36px; height: 36px; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 1.15rem; color: var(--text-2);
  background: var(--card-bg); border: 1px solid var(--line); border-radius: 8px; cursor: pointer;
}
.ev-resolved { font-size: .78rem; color: var(--muted); }
.ev-roomchips { display: flex; flex-wrap: wrap; gap: 6px; }
.ev-delrow { margin-top: 10px; }
.ev-delconfirm__q { display: block; margin-bottom: 8px; font-size: .8rem; color: var(--text-2); text-align: center; }
.ev-delconfirm__btns { display: flex; gap: 8px; }
.ev-delconfirm__btns .ev-btn { flex: 1; }
.ev-readonly { margin: 0 0 14px; font-size: .84rem; color: var(--text-2); line-height: 1.5; }
.ev-blocksheet__room { margin: 0 0 12px; font-size: .95rem; font-weight: 650; color: var(--text); }
.ev-blocksheet__range { margin: 0 0 12px; font-size: .9rem; color: var(--text); font-variant-numeric: tabular-nums; }
.ev-blocksheet__note {
  display: block; width: 100%; margin: 0 0 14px; padding: 8px 10px;
  font: inherit; font-size: .88rem; color: var(--text); resize: vertical;
  background: var(--card-bg); border: 1px solid var(--line); border-radius: var(--radius-sm);
}
.ev-blockpreview {
  margin: 4px 0 14px; padding: 10px 12px; font-size: .78rem; color: var(--text-2);
  background: var(--bg); border: 1px solid var(--line-soft); border-radius: var(--radius-sm);
  line-height: 1.5;
}
.ev-blockpreview__count { font-weight: 600; color: var(--text); }
.ev-blockpreview__list { margin: 6px 0 0; padding-left: 18px; font-variant-numeric: tabular-nums; }
.ev-blockpreview__none { color: var(--muted); }

/* Desktop: float the sheet as a centred panel rather than a full-width dock. */
@media (min-width: 640px) {
  .ev-sheet { width: 460px; margin-left: auto; margin-right: auto; bottom: 24px; border-radius: 16px; }
}

/* Toast (undo).
   Centred by auto margins inside a full-width inset, NOT by left:50% +
   translateX(-50%): with left:50% the containing block starts at the middle of
   the viewport, so the available width is 50vw and `max-width: 90vw` can never
   be reached — any message longer than ~25 characters wrapped at half width
   while looking like it had room. fit-content + margin-inline:auto centres it
   with the whole viewport available. */
.ev-toast {
  position: fixed; left: 0; right: 0;
  width: fit-content; margin-inline: auto;
  bottom: calc(22px + env(safe-area-inset-bottom)); z-index: 90;
  display: flex; align-items: center; gap: 14px;
  max-width: min(90vw, 360px); padding: 11px 18px;
  font-size: .82rem; color: #f9fafb;
  background: rgba(16, 24, 40, .93); border-radius: 999px; box-shadow: var(--shadow-pop);
}
.ev-toast[hidden] { display: none; }
/* Wide variant: the one message that legitimately runs to two lines (the
   "re-checking N past fits" refit warning). A 999px radius only reads well as a
   single short line. The explicit [hidden] rule keeps this class from ever
   out-ranking the hide above if the base selector is refactored. */
.ev-toast--wide {
  display: block;
  border-radius: 14px;
  text-align: center;
  line-height: 1.45;
  text-wrap: balance;   /* even line lengths — a 4-line ragged pill reads as broken */
}
.ev-toast--wide[hidden] { display: none; }

/* In the app shell the toast must clear the fixed tab bar (64px + inset);
   the /events page has no tab bar and keeps the base offset. */
.m-body .ev-toast { bottom: calc(76px + env(safe-area-inset-bottom)); }
.ev-toast__action {
  font-size: .82rem; font-weight: 700; color: #8ab4ff;
  background: none; border: none; padding: 0; cursor: pointer;
}

/* Mobile: reveal the back chevron, drop the redundant Dashboard link. The
   back button makes the header taller here, so the sticky filter sits lower. */
@media (max-width: 640px) {
  .ev-back { display: inline-flex; }
  .ev-header__home { display: none; }
  .ev-filter { top: 58px; }
}
/* Cited-night links in prediction captions/diagnostics → the events page. */
.pred-cite { color: inherit; text-decoration: underline dotted; text-underline-offset: 2px; }
.pred-cite:hover { color: var(--accent); }
/* "Debug this prediction ›" → the admin inspector. Reads as an action, so it
   carries the accent ink even at rest (it's a jump, not a cited source). */
.pred-debug { color: var(--accent); white-space: nowrap; }

/* "One fit owns the evidence" warning badge — the bedroom_4 smell made loud:
   an amber pill that stands out from the surrounding grey diagnostics so a
   single-analogue curve is spottable at a glance, not buried in prose. */
.pred-badge {
  display: inline-block; padding: 1px 8px; border-radius: 999px;
  font-size: .66rem; font-weight: 700; white-space: nowrap; vertical-align: baseline;
}
.pred-badge--warn { background: var(--warn-bg); color: var(--warn-ink); }

/* ── Prediction inspector (/events → Model internals) ───────────────────────
   Re-run a room's forecast at a chosen anchor and read it against reality; a
   clicked fit also jumps the page's main chart to its source night. Reuses the
   events page's chip/datenav/nudger vocabulary for consistency. */
.pi-controls { display: flex; flex-direction: column; gap: 12px; margin: 6px 0 12px; }
.pi-anchor { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 12px; }
.pi-timerow { flex: 0 0 auto; }
.pi-anchorcap { margin: 0 0 12px; font-size: .78rem; color: var(--muted); line-height: 1.5; }
.pi-chart { min-height: 300px; position: relative; }
/* Resting / no-curve placeholder inside the chart box, so the tall bordered box
   never reads as an empty broken canvas. Centred prompt with an affordance icon;
   the warn variant flags an actionable problem (bad anchor, no sensor). */
.pi-chart__hint {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 300px;
  padding: 24px 20px;
  text-align: center;
  color: var(--muted);
}
.pi-chart__hint p { margin: 0; font-size: .82rem; line-height: 1.5; max-width: 32ch; }
.pi-chart__hint-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--card-bg-2, var(--card-bg));
  font-size: 1rem;
  color: var(--faint);
}
.pi-chart__hint--warn { color: var(--warn-ink); }
.pi-chart__hint--warn .pi-chart__hint-icon {
  color: var(--warn-ink);
  border-color: var(--warn-ink);
  font-weight: 700;
}
.pi-legend { margin: 8px 0 0; font-size: .74rem; color: var(--muted); line-height: 1.5; }
.pi-legend[hidden] { display: none; }
.pi-caption { margin-top: 8px; font-size: .78rem; color: var(--text-2); line-height: 1.6; }
.pi-caption:empty { display: none; }
.pi-fallback { margin-top: 10px; }
.pi-status { margin-top: 8px; }

/* Matches table: dense, numeric, one clickable row per cited fit. */
.pi-table { margin-top: 12px; }
.pi-table td { white-space: nowrap; }
.pi-row { cursor: pointer; }
.pi-row:hover td { background: var(--line-soft); }
.pi-row.is-selected td { background: var(--accent-soft); }
/* The dominant fit — the "one fit owns the outcome" row — flagged so it can't
   be missed even before the share % is read. A left accent stripe carries the
   flag independently of the row wash (which is faint on a light card). */
.pi-row--dominant td { background: var(--warn-bg); }
.pi-row--dominant td:first-child { box-shadow: inset 3px 0 0 var(--warn-ink); }
.pi-row--dominant.is-selected td { background: var(--accent-soft); }
.pi-badge--dominant { background: var(--warn-bg); color: var(--warn-ink); }
.pi-row--dominant td .pi-badge--dominant {
  background: var(--warn-ink); color: var(--card-bg);
}
/* Room name above the segment span, so the (usually same) borrowed sensor is
   attributable without a whole column. */
.pi-cell-room { display: block; font-size: .72rem; color: var(--muted); }

/* "See in context" — scrolls up to the jumped-to main chart on demand (no auto-
   scroll, so the inspector's own highlight stays put). A quiet text affordance. */
.pi-seecontext {
  display: inline-flex; align-items: center; gap: 5px;
  margin: 12px 0 0; padding: 4px 0;
  background: none; border: none; cursor: pointer;
  font: inherit; font-size: .78rem; font-weight: 600; color: var(--accent);
}
.pi-seecontext[hidden] { display: none; }
.pi-seecontext:hover { text-decoration: underline; text-underline-offset: 2px; }
.pi-seecontext:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }

/* ── Fit-context banner (/events main chart) ────────────────────────────────
   Shown only when the chart has jumped to a clicked fit's SOURCE night. The
   anti-misread framing: this is training data, not a forecast being verified —
   the copy says so and the trust signals (r², weight, windows) sit inline.
   A calm accent info-strip, correct in light + dark. */
.ev-fitcontext {
  margin: 0 0 12px;
  padding: 11px 13px;
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: 10px;
  background: var(--accent-soft);
}
.ev-fitcontext[hidden] { display: none; }
.ev-fitcontext__head {
  font-size: .82rem; font-weight: 700; color: var(--text);
  letter-spacing: .01em;
}
.ev-fitcontext__body {
  margin: 4px 0 9px; font-size: .76rem; color: var(--text-2); line-height: 1.5;
}
.ev-fitcontext__stats {
  display: flex; flex-wrap: wrap; gap: 6px 18px;
  font-size: .76rem; color: var(--muted);
}
.ev-fitcontext__stats strong {
  color: var(--text); font-weight: 700; font-variant-numeric: tabular-nums;
}
.ev-fitcontext__arrow { color: var(--faint); }

/* The main chart clears the sticky filter bar when "See in context" scrolls it up. */
#ev-chartwrap { scroll-margin-top: 92px; }

/* Predict-tab diagnostics: one line per charted room saying where the
   evidence came from, or which failure state applies. */
.predict-diag { margin-top: 6px; }
.predict-diag__row { font-size: .74rem; color: var(--text-2); padding: 2px 0; line-height: 1.6; }
.predict-diag__row--err { color: var(--warn-ink); }
.predict-diag__phases { font-size: .7rem; color: var(--muted); padding-left: 12px; }

/* Predicted-regime cost chip — the DETERMINISTIC £ of the assumed device
   schedule (runtime × kW × Eco7-banded tariff). Styled as a solid, confident
   fact in the accent family, deliberately NOT the amber .cost-inferred (which
   flags guessed runtime). One chip per charted room; collapses when empty. */
.predict-cost { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.predict-cost:empty { display: none; margin: 0; }
.pred-cost {
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  min-width: 0;
  padding: 4px 11px;
  border: 1px solid var(--accent-border);
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--text);
  font-size: .78rem;
  line-height: 1.35;
}
.pred-cost__dot {
  width: 8px; height: 8px; border-radius: 50%;
  align-self: center; flex: none;
}
.pred-cost__room { color: var(--text-2); font-weight: 600; }
.pred-cost__device { color: var(--accent); font-weight: 600; }
.pred-cost__amount { font-weight: 700; font-variant-numeric: tabular-nums; }
.pred-cost__win { color: var(--muted); }

/* "When to cool" — two priced AC-start candidates (now vs the next Eco7 night
   rate) to a room's target, presented side by side so the human chooses. The £
   is deterministic; the reach time rides the thin AC model (hence the
   low-confidence note). Uses the card vocabulary + the accent-soft highlight
   already carrying the "selected/cheaper" meaning elsewhere. One card per
   AC-capable charted room; the wrap collapses when there are none. */
.whentocool-wrap { display: flex; flex-direction: column; gap: 10px; margin-top: 12px; }
.whentocool-wrap:empty { display: none; }
.whentocool {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--card-bg);
  padding: 10px 12px;
}
.whentocool__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 8px;
}
.whentocool__title { flex: 1 1 auto; min-width: 0; font-size: .82rem; font-weight: 700; color: var(--text); }
.whentocool__to { flex: none; font-size: .74rem; color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.whentocool__opts { display: flex; flex-direction: column; gap: 6px; }
.whentocool__opt {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--bg);
  padding: 7px 10px;
}
.whentocool__opt.is-cheaper {
  border-color: var(--accent-border);
  background: var(--accent-soft);
}
.whentocool__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}
.whentocool__when { flex: 1 1 auto; min-width: 0; font-size: .82rem; font-weight: 600; color: var(--text); }
.whentocool__sub { font-weight: 400; color: var(--muted); font-size: .76rem; }
.whentocool__tag {
  display: inline-block;
  font-size: .64rem; font-weight: 700; text-transform: uppercase; letter-spacing: .03em;
  color: var(--accent);
  background: var(--card-bg);
  border: 1px solid var(--accent-border);
  border-radius: 999px;
  padding: 1px 7px;
  vertical-align: 1px;
}
.whentocool__cost {
  flex: none;
  font-size: .86rem; font-weight: 700; color: var(--text);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.whentocool__detail { margin-top: 3px; font-size: .74rem; color: var(--text-2); }
.whentocool__note {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 9px;
  font-size: .72rem;
  color: var(--muted);
}

/* Fits preview: fitted rows read normally, rejects fade with the reason. */
.fits-row--reject td { color: var(--muted); }
.fits-row--ok td:nth-child(4), .fits-row--ok td:nth-child(5) { font-variant-numeric: tabular-nums; }

/* ── Tonight cards + prediction captions ─────────────────────────────────────── */
.tonight-cards { display: flex; flex-direction: column; gap: 8px; }
.tonight-card {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 10px;
  padding: 9px 12px;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  font-size: .85rem;
}
.tonight-card__room { font-weight: 700; color: var(--text); min-width: 90px; }
.tonight-card__line { color: var(--text-2); }
.tonight-card__conf { font-size: .75rem; color: var(--muted); }
.tonight-card__basis { flex-basis: 100%; color: var(--faint); font-size: .72rem; }
.tonight-card--stale .tonight-card__line { color: var(--faint); font-style: italic; }

/* Confidence dot: chrome status colours, not data colours. */
.conf-dot {
  display: inline-block;
  width: 8px; height: 8px;
  border-radius: 50%;
  vertical-align: baseline;
}
.conf-dot--high { background: var(--good-ink); }
.conf-dot--medium { background: var(--warn-ink); }
.conf-dot--low { background: var(--faint); }

/* What-if pills (the mobile Scenario row). Hypotheticals, so visually lighter
   than the real state controls. */
.m-scenario {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 12px;
  margin: 4px 0;
}
.whatif-group { display: inline-flex; align-items: center; gap: 3px; }
/* One scenario line (a floor's windows pills / a room's device + schedule). */
.whatif-row { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; margin: 4px 0; }
/* Wall-clock schedule inputs ("AC from 12:00 until 16:00"). */
.whatif-time {
  font: inherit;
  font-size: .72rem;
  padding: 1px 4px;
  border: 1px solid var(--line-soft);
  border-radius: 6px;
  background: var(--card-bg);
  color: var(--text);
}
.whatif-group__label { font-size: .7rem; color: var(--muted); margin-right: 2px; }
.whatif-pill {
  padding: 2px 9px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--card-bg);
  color: var(--text-2);
  font-size: .72rem;
  cursor: pointer;
}
.whatif-pill.is-on {
  background: var(--accent-soft);
  border-color: var(--accent-border);
  color: var(--accent);
  font-weight: 600;
}

/* Home-tab tonight teaser (mobile). */
.m-tonight-teaser { margin-top: 8px; font-size: .82rem; }
.m-tonight-teaser a { color: var(--accent); text-decoration: none; font-weight: 600; }

/* Home's ONE weather line (design §6.3) — storm, hail, rain and quiet on one
   precedence ladder, in the slot the dry line used to hold. It sits directly
   above the Tonight teaser — another quiet line in the same section — so it is
   deliberately built as a CONTROL rather than a second caption: a card surface,
   a real ≥44px touch target, full --text ink on the numbers and a chevron. The
   pair then reads as "a control, then a caption" instead of two stacked greys.
   Two properties carried over from the line it replaces:
     * the sentence gets the FULL width, with the destination label on its own
       line under it — sharing one row with a nowrap "Out and back dry ›" left
       the sentence a ~215px column that wrapped mid-count on most states;
     * the calm state takes `--quiet` (soft border, no shadow, muted ink) while
       the storm and rain states take the established 4px accent rail. A fixture
       that looks identical to news every day is how both stop being read.
   It is a <button> now, not an <a>: the destination follows the content (an
   hour's sheet, or #dry), so the resets below are the button chrome that a link
   never needed.
   [hidden] must beat the class's `display:flex` — a class display: winning over
   the UA's [hidden] rule is this codebase's most repeated CSS landmine. */
.m-weather-line {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-height: 46px;
  margin-top: 10px;
  padding: 9px 12px;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  color: var(--text-2);
  font: inherit;
  font-size: .8rem;
  line-height: 1.4;
  text-align: left;
  -webkit-appearance: none;
  appearance: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: filter .15s ease, transform .15s ease;
}
.m-weather-line[hidden] { display: none; }
.m-weather-line:active { filter: brightness(.97); transform: scale(.99); }
/* The calm, everyday state: the same control, one step down in weight — a soft
   border and no shadow, so it settles into the page instead of competing with
   the hour strip above it. */
.m-weather-line--quiet {
  box-shadow: none;
  border-color: var(--line-soft);
  color: var(--muted);
}
.m-weather-line--quiet .m-weather-line__text b { color: var(--text-2); }
/* A storm, or rain: the established 4px rail marks the card that is actually
   telling you something today. */
.m-weather-line--news {
  border-left: 4px solid var(--accent);
  padding-left: 11px;   /* 12 − (4 − 1): the text keeps its optical inset */
}
.m-weather-line__body {
  flex: 1 1 auto;
  min-width: 0;         /* or a long sentence pushes the chevron off the card */
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.m-weather-line__text b { color: var(--text); font-weight: 700; font-variant-numeric: tabular-nums; }
.m-weather-line__go {
  font-size: .74rem;
  font-weight: 650;
  color: var(--accent);
  white-space: nowrap;
}
.m-weather-line__chev {
  flex: 0 0 auto;
  color: var(--faint);
  font-size: 1.1rem;
  line-height: 1;
}

/* Energy tariff card (the cost-feature sign-off surface, More→Tools). */
.tariff-card { padding: 14px 16px; }
.tariff-card--loading { color: var(--muted); }
.tariff-head { margin-bottom: 6px; }
.tariff-badge {
  display: inline-block;
  padding: 2px 10px;
  border-radius: 999px;
  font-size: .72rem;
  font-weight: 600;
}
.tariff-badge--live { background: var(--good-bg); color: var(--good-ink); }
.tariff-badge--manual { background: var(--warn-bg); color: var(--warn-ink); }
.tariff-fuel {
  margin: 14px 0 6px;
  font-size: .82rem;
  font-weight: 700;
  color: var(--text-2);
  border-bottom: 1px solid var(--line);
  padding-bottom: 4px;
}
.tariff-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 4px 0;
}
.tariff-label { color: var(--text-2); font-size: .85rem; }
.tariff-value { font-weight: 600; font-variant-numeric: tabular-nums; }
.tariff-unit { color: var(--muted); font-size: .78rem; margin-left: 3px; }
.tariff-input {
  width: 5.5rem;
  padding: 3px 6px;
  border: 1px solid var(--accent-border);
  border-radius: 6px;
  background: var(--card-bg);
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.tariff-input[type="time"] { width: auto; }
.tariff-eco7 { display: inline-flex; align-items: center; gap: 6px; }
.tariff-note { margin: 6px 0 0; font-size: .74rem; color: var(--muted); }
.tariff-caveats {
  margin: 10px 0 0;
  padding: 8px 10px 8px 26px;
  background: var(--warn-bg);
  color: var(--warn-ink);
  border-radius: 8px;
  font-size: .76rem;
}
.tariff-actions { display: flex; align-items: center; gap: 12px; margin-top: 14px; }
.tariff-save {
  padding: 6px 16px;
  border: 1px solid var(--accent-border);
  border-radius: 8px;
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 600;
  cursor: pointer;
}
.tariff-save:hover { filter: brightness(1.05); }
.tariff-status { font-size: .78rem; color: var(--muted); }
.tariff-meter {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 6px 10px;
  margin-top: 8px;
  border-radius: 8px;
  font-size: .8rem;
}
.tariff-meter--ok { background: var(--good-bg); color: var(--good-ink); }
.tariff-meter--warn { background: var(--warn-bg); color: var(--warn-ink); }
.tariff-power { display: inline-flex; align-items: center; gap: 6px; }
.tariff-power select.tariff-input { width: auto; padding: 3px 6px; }

/* Energy tariff as a native mobile sub-view (#view-tariff): the shared tariff
   card is reused verbatim, so a few rows relax for the phone's narrow width and
   the action buttons grow to a comfortable touch target — scoped by id so the
   desktop /tariff page keeps its tighter layout. */
#view-tariff .tariff-row { flex-wrap: wrap; }
#view-tariff .tariff-save { padding: 10px 20px; min-height: 42px; }

/* Energy spend report (mobile Cost tab + desktop section). */
.cost-view { display: flex; flex-direction: column; gap: 14px; }
.cost-view--loading { color: var(--muted); padding: 20px 0; }
.cost-seg {
  display: inline-flex;
  align-self: center;
  border: 1px solid var(--accent-border);
  border-radius: 999px;
  overflow: hidden;
}
.cost-seg__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0 18px;
  border: none;
  background: var(--card-bg);
  color: var(--text-2);
  font-size: .82rem;
  cursor: pointer;
}
.cost-seg__btn.is-active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.cost-hero { text-align: center; padding: 6px 0 2px; }
.cost-hero__amount { font-size: 2.6rem; font-weight: 700; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.cost-hero__label { color: var(--muted); font-size: .9rem; }
.cost-hero__inferred { margin-top: 6px; color: var(--warn-ink); font-size: .8rem; }
.cost-warn {
  padding: 10px 12px;
  border-radius: 10px;
  background: var(--warn-bg);
  color: var(--warn-ink);
  font-size: .82rem;
}
.cost-warn a, .cost-note a { color: inherit; font-weight: 600; }
.cost-card { padding: 12px 16px; }
/* These are informational cards, not room/auth cards — drop the grey --pending
   left rail the base .card carries so no "no reading" stripe bleeds onto them. */
.cost-card, .tariff-card { border-left: 1px solid var(--line); }
.cost-card--muted { background: transparent; box-shadow: none; border: 1px dashed var(--line); }
.cost-card__title {
  margin: 0 0 8px;
  font-size: .8rem;
  font-weight: 700;
  color: var(--text-2);
  text-transform: uppercase;
  letter-spacing: .03em;
}
.cost-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 6px 0;
  border-top: 1px solid color-mix(in srgb, var(--text-2) 12%, transparent);
}
.cost-card__title + .cost-row, .cost-row:first-child { border-top: none; }
.cost-row__label { color: var(--text); font-size: .9rem; }
.cost-row__val { text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; }
.cost-row__kwh { display: block; font-weight: 400; font-size: .72rem; color: var(--muted); }
.cost-inferred { color: var(--warn-ink); font-weight: 500; font-size: .78rem; }
.cost-note { margin: 8px 0 0; font-size: .76rem; color: var(--muted); }
.cost-note--warn { color: var(--warn-ink); }
.cost-note--center { text-align: center; }
.cost-recon__fuel { margin-bottom: 12px; }
.cost-recon__fuelname { text-transform: capitalize; font-weight: 600; font-size: .84rem; margin-bottom: 4px; }
/* Decorative proportion bar — the numbers live in .cost-recon__meta beneath, so
   the fill is a true fraction of --share (0% shows nothing) and never clips. */
.cost-recon__bar {
  display: flex;
  height: 12px;
  border-radius: 6px;
  overflow: hidden;
  background: color-mix(in srgb, var(--muted) 30%, transparent);
}
.cost-recon__devices {
  width: var(--share);
  background: var(--accent);
}
.cost-recon__other {
  flex: 1;
  background: color-mix(in srgb, var(--muted) 55%, transparent);
}
.cost-recon__meta { font-size: .74rem; color: var(--muted); margin-top: 4px; }
.cost-recon__warn { font-size: .76rem; color: var(--warn-ink); margin-top: 4px; }
.cost-monthnav { display: flex; align-items: center; justify-content: center; gap: 14px; }
.cost-monthnav__label { font-weight: 600; min-width: 8.5rem; text-align: center; }
.cost-monthnav__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px; height: 44px;
  border: 1px solid var(--accent-border);
  border-radius: 50%;
  background: var(--card-bg);
  color: var(--accent);
  font-size: 1.1rem;
  cursor: pointer;
}
.cost-monthnav__btn:disabled { opacity: .35; cursor: default; }
.cost-card__hint { font-weight: 400; text-transform: none; letter-spacing: 0; color: var(--muted); font-size: .7rem; }
.cost-row--tappable { cursor: pointer; }
.cost-row--tappable:hover { background: var(--accent-soft); }
.cost-row--tappable .cost-row__label::before { content: "› "; color: var(--muted); }
.cost-sessions:not(:empty) { padding: 4px 0 8px 14px; border-left: 2px solid var(--accent-soft); margin: 2px 0 6px; }
.cost-session {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  padding: 4px 0; font-size: .8rem;
}
.cost-session__when { color: var(--text-2); }
.cost-session__val { text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; }

/* The banner→strip gap is owned by .cost-strip's own margin-top (works in both
   modes: mobile after #home-outlook, desktop inside the shared banner section),
   so the outlook section itself carries no extra bottom gap. */
#home-outlook { margin-bottom: 0; }

/* Home-screen burn-rate + spent-today strip. Sits as its own block between the
   outlook section and the floors; symmetric spacing so it doesn't crowd the
   floor label below or leave a chasm under the tonight teaser above. */
.cost-strip {
  display: flex;
  gap: 12px;
  margin: 14px 0 18px;
  text-decoration: none;
  color: inherit;
}
.cost-strip__item {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 10px;
  border-radius: 12px;
  background: var(--card-bg);
  border: 1px solid var(--line);
}
.cost-strip__val { font-size: 1.3rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.cost-strip__label { font-size: .72rem; color: var(--muted); }
a.cost-strip:hover .cost-strip__item { border-color: var(--accent-border); }
/* On a wide viewport (tablet / landscape) the energy-spend report and the home
   burn-rate strip are single columns — cap and centre them so they read as a
   deliberate panel, not one oversized full-bleed card. Scoped by id so the
   shared .cost-view/.cost-strip classes stay full-width on a phone. #cost-report
   has no top margin to preserve; #home-cost's strip keeps its margin-top (#1),
   so its centring uses margin-inline longhand. */
@media (min-width: 768px) {
  #cost-report .cost-view { max-width: 600px; margin: 0 auto; }
  #home-cost .cost-strip { max-width: 600px; margin-inline: auto; }
}
/* "Costing ~Xp/h" beside a running device toggle (the room-detail screen).
   Inline and centre-aligned with the "set …" caption; nowrap so it drops
   cleanly to its own line (never breaking mid-phrase) when .state-control
   wraps. */
.room-cost-rate { font-size: .82rem; font-weight: 600; color: var(--accent); white-space: nowrap; }
.room-cost-rate:empty { display: none; }
.tariff-meter__fuel { font-weight: 700; text-transform: capitalize; min-width: 4.5rem; }
.tariff-meter__detail { font-variant-numeric: tabular-nums; }
.tariff-fuel--first { margin-top: 0; }
.tariff-account { margin: 4px 0 8px; }
.tariff-account--on {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.tariff-account__state { font-weight: 600; color: var(--good-ink); font-size: .85rem; }
.tariff-field { display: flex; align-items: center; gap: 10px; margin: 8px 0; }
.tariff-field > span { min-width: 4.5rem; color: var(--text-2); font-size: .85rem; }
.tariff-input--wide { width: 100%; flex: 1; }
.tariff-link {
  border: none;
  background: none;
  color: var(--accent);
  font-weight: 600;
  cursor: pointer;
  font-size: .82rem;
}
.tariff-account code, .tariff-note code {
  font-size: .82em;
  padding: 1px 4px;
  border-radius: 4px;
  background: var(--accent-soft);
}

/* ===========================================================================
   SCENARIO PLANNER (design section 11) - the Plan tab builder, objective/budget
   bottom sheets, the full-screen plan result (two-band charts, itemized
   ledger), and the single-room what-if. New vocabulary the app did not have;
   everything else reuses the shared card / segment / pill / conf-dot grammar,
   and the sheet shells reuse .m-sheet.
   =========================================================================== */

/* -- Floor-plan builder -- */
.build-lead { font-size: .82rem; color: var(--text-2); line-height: 1.5; margin: 2px 2px 14px; }
.build-empty {
  display: none; text-align: center; color: var(--muted);
  border: 1px dashed var(--line); border-radius: var(--radius);
  padding: 16px 18px; margin: 0 2px 14px; background: var(--card-bg);
}
.build-empty b { color: var(--text); font-weight: 650; }
.build-empty__lead { font-size: .9rem; color: var(--text); font-weight: 600; margin-bottom: 4px; }
.build-empty__body { font-size: .8rem; line-height: 1.5; }
.build-empty__load { /* the obj-preset pill idiom, in its active accent */
  margin-top: 12px; min-height: 34px; padding: 6px 14px;
  border: 1px solid var(--accent-border); border-radius: 999px;
  background: var(--accent-soft); color: var(--accent);
  font-size: .78rem; font-weight: 600; cursor: pointer;
}
#view-plan.is-empty .build-empty { display: block; }
#view-plan.is-building .fp-room:not(.fp-room--targeted) .fp-room__fill { opacity: .5; }

.fp-room--targeted .fp-room__fill { stroke: var(--accent); stroke-width: 1.5; }
.fp-target__bg { fill: var(--accent-soft); stroke: var(--accent); stroke-width: .5; }
.fp-target--spill .fp-target__bg { stroke-dasharray: 1.4 1; }
.fp-target__txt { fill: var(--accent); font-size: 3.5px; font-weight: 700;
  text-anchor: middle; pointer-events: none; }
/* An objective whose interval has lapsed — reopen it to re-time (§13.4). */
.fp-room--attention .fp-room__fill { stroke: var(--warn-ink); stroke-width: 1.5; }
.fp-target--attention .fp-target__bg { fill: var(--warn-bg); stroke: var(--warn-ink); }
.fp-target--attention .fp-target__txt { fill: var(--warn-ink); }

.build-cta {
  position: fixed; left: 0; right: 0; bottom: calc(56px + env(safe-area-inset-bottom, 0px));
  z-index: 30; display: none; align-items: center; gap: 12px;
  padding: 10px 16px; background: var(--glass); backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px); border-top: 1px solid var(--line);
}
#view-plan.is-building .build-cta { display: flex; }
.build-cta__count { flex: 1 1 auto; min-width: 0; font-size: .8rem; color: var(--text-2); line-height: 1.35; }
.build-cta__count b { color: var(--text); font-weight: 700; }
.build-cta__go {
  flex: none; padding: 11px 18px; border: none; border-radius: 999px;
  background: var(--accent); color: var(--on-accent); font-size: .88rem; font-weight: 650; cursor: pointer;
}

/* Room-tap gesture chooser: set an objective vs what-if. */
.plan-gesture { display: flex; flex-direction: column; gap: 10px; }
.plan-gesture__btn {
  display: flex; align-items: center; gap: 12px; width: 100%; text-align: left; cursor: pointer;
  padding: 14px 14px; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--bg); color: var(--text);
}
.plan-gesture__btn--primary { border-color: var(--accent-border); background: var(--accent-soft); }
.plan-gesture__icon { flex: none; width: 30px; height: 30px; border-radius: 9px;
  display: flex; align-items: center; justify-content: center;
  background: var(--card-bg); color: var(--accent); font-size: 1.15rem; }
.plan-gesture__glyph { width: 17px; height: 17px; fill: none; stroke: currentColor;
  stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
.plan-gesture__what { min-width: 0; flex: 1 1 auto; }
.plan-gesture__title { font-size: .9rem; font-weight: 650; }
.plan-gesture__sub { margin-top: 1px; font-size: .74rem; color: var(--muted); line-height: 1.4; }
.plan-gesture__arrow { flex: none; color: var(--faint); font-size: 1.2rem; }

/* -- Objective editor (inside an .m-sheet) -- */
.obj-sheet__head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }
.obj-sheet__room { font-size: 1.05rem; font-weight: 700; color: var(--text); }
.obj-sheet__now { margin-left: auto; font-size: .8rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.obj-sheet__sub { font-size: .76rem; color: var(--muted); margin-bottom: 4px; line-height: 1.4; }
.obj-err { margin: 10px 0 0; font-size: .76rem; color: var(--bad-ink); font-weight: 600; line-height: 1.4; }

.obj-field { margin-top: 16px; }
.obj-field__label {
  font-size: .68rem; font-weight: 600; text-transform: uppercase; letter-spacing: .07em;
  color: var(--faint); margin-bottom: 9px;
}
.obj-band { display: flex; align-items: center; gap: 10px; }
.obj-stepper { display: flex; align-items: center; gap: 2px; }
.obj-stepper__btn {
  width: 40px; height: 44px; border: 1px solid var(--line); background: var(--card-bg);
  color: var(--accent); font-size: 1.3rem; font-weight: 600; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.obj-stepper__btn:first-child { border-radius: 10px 0 0 10px; }
.obj-stepper__btn:last-child { border-radius: 0 10px 10px 0; }
.obj-stepper__val {
  min-width: 56px; height: 44px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
  display: flex; align-items: center; justify-content: center; font-size: 1.05rem; font-weight: 700;
  color: var(--text); font-variant-numeric: tabular-nums;
}
.obj-band__dash { color: var(--faint); font-weight: 600; }

.obj-seg { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
.obj-seg__btn {
  min-height: 44px; border: 1px solid var(--line); border-radius: 10px; background: var(--card-bg);
  color: var(--text-2); font-size: .82rem; font-weight: 600; cursor: pointer; padding: 4px;
}
.obj-seg__btn.is-active { border-color: var(--accent-border); background: var(--accent-soft); color: var(--accent); }
.obj-seg__hint { margin-top: 8px; font-size: .72rem; color: var(--muted); line-height: 1.4; }
.obj-seg__hint b { color: var(--text-2); font-weight: 650; }

.obj-time { display: flex; align-items: center; gap: 8px; }
.obj-timechip {
  position: relative; flex: 1 1 0; min-height: 44px; border: 1px solid var(--accent-border);
  border-radius: 10px; background: var(--accent-soft); color: var(--accent); font-size: .95rem;
  font-weight: 650; font-variant-numeric: tabular-nums; cursor: pointer; display: flex;
  flex-direction: column; align-items: center; justify-content: center; gap: 1px;
}
.obj-timechip small { font-size: .6rem; font-weight: 600; text-transform: uppercase; letter-spacing: .05em; opacity: .8; }
.obj-timechip input { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
.obj-time__to { flex: none; color: var(--faint); font-size: .8rem; }
/* The resolved-default framing — makes "starts ~now" explicit, never silent. */
.obj-when__hint {
  margin-top: 8px; font-size: .74rem; font-weight: 600; color: var(--text-2);
  font-variant-numeric: tabular-nums;
}
.obj-presets { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 10px; }
.obj-preset {
  min-height: 34px; padding: 6px 12px; border: 1px solid var(--line); border-radius: 999px;
  background: var(--card-bg); color: var(--text-2); font-size: .76rem; font-weight: 600;
  cursor: pointer; font-variant-numeric: tabular-nums;
}
.obj-preset.is-active { border-color: var(--accent-border); background: var(--accent-soft); color: var(--accent); }

.obj-actions { display: flex; gap: 10px; margin-top: 22px; }
.obj-save {
  flex: 1 1 auto; min-height: 48px; border: none; border-radius: 12px; background: var(--accent);
  color: var(--on-accent); font-size: .95rem; font-weight: 650; cursor: pointer;
}
.obj-remove {
  flex: none; min-height: 48px; padding: 0 16px; border: 1px solid var(--line); border-radius: 12px;
  background: var(--card-bg); color: var(--bad-ink); font-size: .88rem; font-weight: 600; cursor: pointer;
}

/* -- Plan result screen -- */
.plan-hero { display: flex; align-items: flex-start; gap: 12px; padding: 4px 2px 2px; margin-bottom: 4px; }
.plan-hero__cost { font-size: 2rem; font-weight: 750; color: var(--text);
  font-variant-numeric: tabular-nums; line-height: 1.05; }
.plan-hero__cost small { font-size: .8rem; font-weight: 600; color: var(--muted);
  display: block; margin-top: 3px; letter-spacing: .01em; }
.plan-hero__side { margin-left: auto; flex: none; }
.status-chip {
  display: inline-flex; align-items: center; gap: 5px; font-size: .72rem; font-weight: 650;
  padding: 5px 10px; border-radius: 999px; border: 1px solid transparent; white-space: nowrap;
}
.status-chip--ok { background: var(--good-bg); color: var(--good-ink);
  border-color: color-mix(in srgb, var(--good-ink) 28%, transparent); }
.status-chip--conflict { background: var(--warn-bg); color: var(--warn-ink);
  border-color: color-mix(in srgb, var(--warn-ink) 30%, transparent); }
.status-chip--unstable { background: var(--bad-bg); color: var(--bad-ink); border-color: var(--bad-border); }
.status-chip--accent { background: var(--accent-soft); color: var(--accent); border-color: var(--accent-border); }
.plan-hero__rooms { margin-top: 7px; font-size: .74rem; color: var(--muted); line-height: 1.45; }
.plan-hero__rooms b { color: var(--text-2); font-weight: 650; }

.plan-block-title {
  font-size: .72rem; font-weight: 600; text-transform: uppercase; letter-spacing: .08em;
  color: var(--faint); margin: 22px 0 10px;
}

.room-result {
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--card-bg);
  box-shadow: var(--shadow); padding: 12px 14px 13px; margin-bottom: 10px;
}
.room-result__head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.room-result__head--levers { margin-top: 8px; }
.room-result__name { flex: none; font-size: .95rem; font-weight: 700; color: var(--text); }
.room-result__obj { flex: 1 1 auto; min-width: 0; font-size: .74rem; color: var(--muted);
  font-variant-numeric: tabular-nums; }
.met-badge {
  flex: none; display: inline-flex; align-items: center; gap: 5px; font-size: .7rem; font-weight: 650;
  padding: 3px 9px; border-radius: 999px; white-space: nowrap;
}
.met-badge--met { background: var(--good-bg); color: var(--good-ink); }
.met-badge--partial { background: var(--warn-bg); color: var(--warn-ink); }
.met-badge--unmet { background: var(--bad-bg); color: var(--bad-ink); }
.met-badge--caveat { background: var(--bg); color: var(--text-2); border: 1px solid var(--line); }

.band-chart { display: block; width: 100%; height: auto; margin-top: 10px; }
.band-zone { fill: var(--good-bg); }
.band-zone__edge { stroke: var(--good-ink); stroke-width: .5; stroke-dasharray: 3 2; opacity: .5; }
.band-conf { fill: color-mix(in srgb, var(--accent) 14%, transparent); stroke: none; }
.band-conf--wide { fill: color-mix(in srgb, var(--accent) 12%, transparent); }
.band-median { fill: none; stroke: var(--accent); stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
.band-ghost { fill: none; stroke: var(--faint); stroke-width: 1.4; stroke-dasharray: 2 3; opacity: .8; }
.band-onset { stroke: var(--line); stroke-width: 1.4; stroke-dasharray: 2 2.5; }
/* Pre-interval (don't-care) region: a scrim fading the lead-in toward the card, plus
   a "scored from here" edge tick where the objective interval — the only region
   graded against the band — begins. */
.band-outside { fill: var(--card-bg); opacity: .58; }
.band-scored-edge { stroke: var(--muted); stroke-width: 1; stroke-dasharray: 2 2; opacity: .85; }
.band-chart .band-scored-lbl { fill: var(--muted); font-size: 9px; font-weight: 650; font-variant-numeric: tabular-nums; }
.band-chart .band-ax { fill: var(--faint); font-size: 9.5px; font-variant-numeric: tabular-nums; }
.band-chart .band-zone-l { fill: var(--good-ink); font-size: 9px; font-weight: 650; }
.band-chart .band-note { fill: var(--accent); font-size: 9.5px; font-weight: 650; }
.band-chart .band-note--warn { fill: var(--warn-ink); font-size: 9.5px; font-weight: 650; }

.reach-lever {
  display: inline-block; flex: none; font-size: .62rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .03em; padding: 2px 7px; border-radius: 999px; vertical-align: 1px; border: 1px solid transparent;
}
.reach-lever--ac { background: var(--accent-soft); color: var(--accent); border-color: var(--accent-border); }
.reach-lever--windows { background: var(--good-bg); color: var(--good-ink);
  border-color: color-mix(in srgb, var(--good-ink) 30%, transparent); }
.reach-lever--spill { background: transparent; color: var(--accent); border: 1px dashed var(--accent-border); }
.reach-lever--recruit { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
.reach-lever--boiler { background: var(--warn-bg); color: var(--warn-ink);
  border-color: color-mix(in srgb, var(--warn-ink) 30%, transparent); }

.room-result__line { margin-top: 10px; font-size: .8rem; color: var(--text-2); line-height: 1.45; }
.room-result__line b { color: var(--text); font-weight: 650; }
.room-result__cost { margin-top: 8px; font-size: .8rem; font-weight: 600; }
.room-result__cost--free { color: var(--good-ink); }
.room-result__cost--own { color: var(--text); font-variant-numeric: tabular-nums; }

/* Timed device instruction — the plan reads as "Run the AC 12:40 → 17:00", with
   the dial + early-start explanation folded in as sub-lines. */
.reach-plan {
  display: flex; gap: 9px; align-items: flex-start; margin-top: 10px; padding: 9px 11px;
  border-radius: 9px; background: var(--accent-soft); border: 1px solid var(--accent-border);
}
.reach-plan__icon { flex: none; width: 16px; height: 16px; margin-top: 1px; fill: var(--accent); }
.reach-plan__body { min-width: 0; flex: 1 1 auto; }
.reach-plan__main { font-size: .82rem; font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; }
.reach-plan__main b { color: var(--accent); font-weight: 750; }
.reach-plan__dial { margin-top: 3px; font-size: .77rem; color: var(--text-2); }
.reach-plan__dial b { color: var(--text); font-weight: 700; }
.reach-plan__early { margin-top: 5px; font-size: .72rem; color: var(--muted); line-height: 1.45; }

.freeze-note {
  margin-top: 10px; padding: 8px 10px; border-radius: 8px; background: var(--bg);
  border: 1px solid var(--line); font-size: .72rem; color: var(--muted); line-height: 1.45;
}
.freeze-note b { color: var(--text-2); font-weight: 600; }
.conf-badge {
  display: inline-flex; align-items: center; gap: 5px; flex: none; font-size: .68rem; font-weight: 600;
  color: var(--muted); text-transform: uppercase; letter-spacing: .04em;
}
.conf-badge .conf-dot { margin: 0; }

.trv-pill {
  display: inline-flex; align-items: center; flex: none; font-size: .62rem; font-weight: 650;
  text-transform: uppercase; letter-spacing: .03em; padding: 2px 7px; border-radius: 999px;
  color: var(--text-2); background: var(--bg); border: 1px solid var(--line); vertical-align: 1px;
}
.evidence-flag {
  display: inline-flex; align-items: center; gap: 5px; flex: none; font-size: .64rem; font-weight: 600;
  color: var(--muted); padding: 2px 8px 2px 7px; border-radius: 999px; background: var(--bg);
  border: 1px dashed var(--line); vertical-align: 1px;
}
.evidence-flag svg { width: 11px; height: 11px; fill: currentColor; opacity: .85; flex: none; }

.recruit-advice {
  display: flex; gap: 9px; align-items: flex-start; margin-top: 10px; padding: 9px 11px;
  border-radius: 9px; background: var(--accent-soft); border: 1px solid var(--accent-border);
}
.recruit-advice__icon { flex: none; width: 17px; height: 17px; margin-top: 1px; fill: var(--accent); }
.recruit-advice__txt { font-size: .78rem; color: var(--text); line-height: 1.45; }
.recruit-advice__txt b { font-weight: 700; }
.conf-note {
  margin-top: 10px; padding: 9px 11px; border-radius: 9px; background: var(--warn-bg);
  border: 1px solid color-mix(in srgb, var(--warn-ink) 26%, transparent); font-size: .74rem;
  color: var(--text-2); line-height: 1.5;
}
.conf-note b { color: var(--warn-ink); font-weight: 700; display: inline-flex; align-items: center; gap: 5px; }
.conf-note .conf-dot { margin: 0; }

/* Quality as DEPTH — a position marker between the band's two edges, never a
   fill/progress bar (an empty fill reads as a fail score on an in-band room). */
.qual { margin-top: 12px; }
.qual__ends { display: flex; justify-content: space-between; font-size: .62rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .04em; color: var(--faint); }
.qual__track {
  position: relative; height: 6px; margin: 5px 0 8px; border-radius: 3px;
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--faint) 26%, var(--card-bg)),
    color-mix(in srgb, var(--accent) 55%, var(--card-bg)));
}
.qual__marker {
  position: absolute; top: 50%; width: 13px; height: 13px; border-radius: 50%;
  transform: translate(-50%, -50%); background: var(--accent); border: 2.5px solid var(--card-bg);
  box-shadow: 0 0 0 1px var(--accent-border);
}
.qual__txt { margin: 0; font-size: .75rem; color: var(--text-2); line-height: 1.4; }
.qual__txt b { color: var(--text); font-weight: 650; }

.next-tier {
  display: flex; align-items: center; gap: 11px; width: 100%; text-align: left; cursor: pointer;
  margin-top: 11px; padding: 9px 11px 9px 12px; border-radius: 9px; background: var(--accent-soft);
  border: 1px solid var(--accent-border);
}
.next-tier__amt { flex: none; font-size: .86rem; font-weight: 750; color: var(--accent); font-variant-numeric: tabular-nums; }
.next-tier__txt { min-width: 0; flex: 1 1 auto; font-size: .76rem; color: var(--text-2); line-height: 1.4; }
.next-tier__txt b { color: var(--text); font-weight: 650; }
.next-tier__arrow { flex: none; color: var(--accent); font-size: 1.1rem; line-height: 1; }

/* Additive cost-reduction offers (§8): the relax-your-edge counterfactual and the
   colder/warmer-dial cost offer. A quiet, report-only nudge — subtler than the
   tappable next-tier button, and neutral-toned so it doesn't compete with the
   green met badge. The saving/outcome lives in the leading good-ink tag. */
.cost-offer {
  margin-top: 9px; padding: 8px 11px; border-radius: 9px;
  background: var(--bg); border: 1px solid var(--line);
}
.cost-offer__row { display: flex; align-items: baseline; gap: 9px; }
.cost-offer__tag {
  flex: none; font-size: .68rem; font-weight: 700; letter-spacing: .02em; color: var(--good-ink);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.cost-offer__txt { min-width: 0; flex: 1 1 auto; font-size: .76rem; color: var(--text-2); line-height: 1.45; }
.cost-offer__fine { margin: 6px 0 0; font-size: .68rem; color: var(--muted); line-height: 1.4; }

.win-sched {
  border: 1px solid color-mix(in srgb, var(--good-ink) 26%, var(--line)); border-radius: var(--radius);
  background: var(--card-bg); box-shadow: var(--shadow); padding: 13px 14px 14px; margin-bottom: 10px;
}
.win-sched__head { display: flex; align-items: center; gap: 8px; }
.win-sched__title { font-size: .9rem; font-weight: 700; color: var(--text); }
.win-sched__head .status-chip { margin-left: auto; }
.win-sched__track { display: flex; align-items: stretch; gap: 3px; margin: 12px 0 6px; height: 30px; }
.win-seg {
  display: flex; align-items: center; justify-content: center; min-width: 0; font-size: .64rem;
  font-weight: 650; padding: 0 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; border-radius: 4px;
}
.win-seg:first-child { border-radius: 8px 4px 4px 8px; }
.win-seg:last-child { border-radius: 4px 8px 8px 4px; }
.win-seg--closed { background: color-mix(in srgb, var(--faint) 20%, var(--card-bg)); color: var(--text-2); }
.win-seg--open { background: var(--good-bg); color: var(--good-ink); }
.win-sched__scale { display: flex; justify-content: space-between; font-size: .62rem; color: var(--faint);
  font-variant-numeric: tabular-nums; }
.win-sched__note { margin-top: 9px; font-size: .78rem; color: var(--text-2); line-height: 1.5; }
.win-sched__note b { color: var(--text); font-weight: 650; }
/* Explicit open-AND-close schedule sentence for a sequenced floor. */
.win-sched__plan { margin-top: 9px; font-size: .82rem; color: var(--text-2); line-height: 1.5; }
.win-sched__plan b { color: var(--text); font-weight: 700; font-variant-numeric: tabular-nums; }
/* A floor stated closed as an assumption (not an evidence-backed forecast):
   neutral chrome, no good tint, a quiet "Assumed" tag instead of the ok-chip. */
.win-sched--assumed { border-color: var(--line); }
.win-sched--assumed .win-seg--closed { background: color-mix(in srgb, var(--faint) 16%, var(--card-bg)); }
.win-sched__tag {
  margin-left: auto; flex: none; font-size: .62rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .05em; color: var(--faint); padding: 4px 9px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--bg);
}

.win-clash {
  margin-top: 12px; padding: 10px 12px; border-radius: 10px; background: var(--warn-bg);
  border: 1px solid color-mix(in srgb, var(--warn-ink) 26%, transparent);
}
.win-clash__head { display: flex; align-items: center; gap: 7px; }
.win-clash__dot { flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--warn-ink); }
.win-clash__title { font-size: .8rem; font-weight: 700; color: var(--warn-ink); font-variant-numeric: tabular-nums; }
.win-clash__body { margin: 6px 0 0; font-size: .76rem; color: var(--text-2); line-height: 1.5; }
.win-clash__body b { color: var(--text); font-weight: 650; }
/* Passive counterfactual chip — what the OTHER window state would cost. Not a
   control: §6.2's flip needs a forced-lever request field that isn't built yet, so
   this states the trade-off without a false tappable affordance (no cursor/hover). */
.win-clash__alt {
  margin-top: 9px; width: 100%; text-align: left; border: 1px solid var(--line);
  border-radius: 8px; background: var(--card-bg); padding: 9px 11px; font-size: .74rem; font-weight: 600;
  color: var(--text-2); line-height: 1.4; font-variant-numeric: tabular-nums;
}

/* Devices — the committed switch-on schedule (design §11.5 sibling). A grouped
   list under Windows: each row is one physical run with its hours + every room it
   serves. Its reason to exist is the recruited DONOR run, whose owner is not an
   objective room and so has no card of its own. Money stays in the ledger. */
.dev-runs {
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--card-bg);
  box-shadow: var(--shadow); overflow: hidden;
}
.dev-run {
  display: flex; flex-direction: column; gap: 7px; padding: 11px 14px;
  border-bottom: 1px solid var(--line-soft);
}
.dev-run:last-child { border-bottom: 0; }
.dev-run__head { display: flex; align-items: baseline; gap: 4px 10px; flex-wrap: wrap; }
.dev-run__icon { flex: none; width: 15px; height: 15px; align-self: center; fill: var(--accent); }
.dev-run__title { flex: none; font-size: .84rem; font-weight: 700; color: var(--text); }
.dev-run__when {
  margin-left: auto; flex: none; font-size: .76rem; font-weight: 600; color: var(--text-2);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.dev-run__dur { color: var(--muted); font-weight: 500; }
/* Shared-axis on/off strip — spans the same plan window as the Windows bars above,
   so a run's accent fill lines up in time with the windows state below it. Compact
   (13px) since it carries no inline text; the run's clock times live in the head. */
.dev-run__bar {
  position: relative; height: 13px; border-radius: 5px; overflow: hidden;
  background: color-mix(in srgb, var(--faint) 16%, var(--card-bg));
}
.dev-run__fill {
  position: absolute; top: 0; bottom: 0; min-width: 2px;
  border-radius: 5px; background: var(--accent);
}
.dev-run__for { font-size: .77rem; line-height: 1.45; }
.dev-run__for--main { color: var(--text-2); }
.dev-run__for--ride { color: var(--good-ink); }
.dev-run__dial { font-size: .76rem; color: var(--text-2); }
.dev-run__dial b { color: var(--text); font-weight: 700; }
.dev-runs__caption { margin: 8px 2px 0; font-size: .72rem; color: var(--faint); line-height: 1.45; }
.dev-runs__caption b { color: var(--muted); font-weight: 650; }

.caveat-card {
  display: flex; gap: 11px; align-items: flex-start; border: 1px solid var(--line);
  border-radius: var(--radius); background: var(--card-bg); box-shadow: var(--shadow);
  padding: 12px 14px 13px; margin-bottom: 10px;
}
.caveat-card__icon {
  flex: none; width: 20px; height: 20px; border-radius: 50%; margin-top: 1px; display: flex;
  align-items: center; justify-content: center; background: var(--accent-soft); color: var(--accent);
  font: italic 800 .8rem/1 Georgia, "Times New Roman", serif;
}
.caveat-card__what { min-width: 0; }
.caveat-card__title { font-size: .84rem; font-weight: 700; color: var(--text); }
.caveat-card__body { margin: 4px 0 0; font-size: .78rem; color: var(--text-2); line-height: 1.5; }
.caveat-card__body b { color: var(--text); font-weight: 650; }

.cost-items { border: 1px solid var(--line); border-radius: var(--radius); background: var(--card-bg);
  box-shadow: var(--shadow); overflow: hidden; }
.cost-item { display: flex; align-items: baseline; gap: 10px; padding: 11px 14px; border-bottom: 1px solid var(--line-soft); }
.cost-item__what { flex: 1 1 auto; min-width: 0; }
.cost-item__lever { font-size: .82rem; font-weight: 650; color: var(--text); }
.cost-item__owner { margin-top: 2px; font-size: .72rem; color: var(--muted); line-height: 1.4; }
.cost-item__owner b { color: var(--text-2); font-weight: 600; }
.cost-item__amt { flex: none; font-size: .88rem; font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; }
.cost-item__amt--free { color: var(--good-ink); font-weight: 650; }
.cost-total { display: flex; align-items: baseline; gap: 10px; padding: 13px 14px;
  border-top: 2px solid var(--line); background: var(--thead-bg); }
.cost-total__label { flex: 1 1 auto; font-size: .82rem; font-weight: 700; color: var(--text); }
.cost-total__amt { flex: none; font-size: 1.15rem; font-weight: 750; color: var(--text); font-variant-numeric: tabular-nums; }
.cost-caption { margin-top: 8px; font-size: .73rem; color: var(--muted); line-height: 1.5; }

.plan-actions { display: flex; gap: 10px; margin-top: 20px; }
.plan-action {
  flex: 1 1 auto; padding: 14px 16px; border: none; border-radius: var(--radius); background: var(--accent);
  color: var(--on-accent); font-size: .92rem; font-weight: 650; cursor: pointer;
}
.plan-action--ghost { flex: none; padding: 14px 18px; background: var(--card-bg); color: var(--text-2); border: 1px solid var(--line); }
.plan-action__note { margin-top: 9px; font-size: .72rem; color: var(--muted); text-align: center; line-height: 1.4; }
.plan-action__note b { color: var(--text-2); font-weight: 650; }

.budget-optin { display: none; }
#view-plan.is-building .budget-optin {
  display: flex; align-items: center; gap: 10px; width: 100%; text-align: left; cursor: pointer;
  text-decoration: none; margin: 0 2px 14px; padding: 12px 14px; border: 1px dashed var(--line);
  border-radius: var(--radius); background: var(--card-bg); color: var(--text);
}
.budget-optin__icon { flex: none; width: 22px; height: 22px; border-radius: 7px; display: flex;
  align-items: center; justify-content: center; background: var(--accent-soft); color: var(--accent);
  font-size: 1.1rem; font-weight: 700; line-height: 1; }
.budget-optin__what { min-width: 0; flex: 1 1 auto; }
.budget-optin__title { font-size: .84rem; font-weight: 650; color: var(--text); }
.budget-optin__sub { margin-top: 1px; font-size: .72rem; color: var(--muted); line-height: 1.4; }
.budget-optin__opt { flex: none; font-size: .64rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .06em; color: var(--faint); }

.budget-sheet__title { font-size: 1.05rem; font-weight: 700; color: var(--text); }
.budget-sheet__sub { margin: 4px 0 16px; font-size: .78rem; color: var(--muted); line-height: 1.45; }
.budget-sheet__input { display: flex; align-items: center; justify-content: center; gap: 12px; }
.budget-sheet__val { min-width: 118px; height: 52px; padding: 0 10px; border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line); display: flex; align-items: center; justify-content: center;
  font-size: 1.5rem; font-weight: 750; color: var(--text); font-variant-numeric: tabular-nums; }
.budget-sheet__note { margin-top: 14px; font-size: .74rem; color: var(--muted); line-height: 1.5; text-align: center; }
.budget-sheet__or { display: flex; align-items: center; gap: 10px; margin: 18px 2px 6px; color: var(--faint);
  font-size: .7rem; font-weight: 600; text-transform: uppercase; letter-spacing: .08em; }
.budget-sheet__or::before, .budget-sheet__or::after { content: ""; flex: 1 1 auto; height: 1px; background: var(--line); }
.budget-nolimit {
  display: flex; align-items: center; gap: 11px; width: 100%; text-align: left; cursor: pointer;
  margin: 6px 0 2px; padding: 12px 13px; border-radius: 12px; background: var(--accent-soft);
  border: 1px solid var(--accent-border); color: var(--text);
}
.budget-nolimit__icon { flex: none; width: 26px; height: 26px; border-radius: 8px; display: flex;
  align-items: center; justify-content: center; background: var(--accent); color: var(--on-accent);
  font-size: 1.15rem; font-weight: 700; line-height: 1; }
.budget-nolimit__what { min-width: 0; flex: 1 1 auto; }
.budget-nolimit__title { display: block; font-size: .86rem; font-weight: 700; color: var(--text); }
.budget-nolimit__sub { display: block; margin-top: 2px; font-size: .72rem; color: var(--muted); line-height: 1.4; }
.budget-nolimit__arrow { flex: none; color: var(--accent); font-size: 1.2rem; line-height: 1; }

.plan-hero__cap { margin-top: 9px; }
.plan-hero__capbar { height: 6px; border-radius: 3px; overflow: hidden;
  background: color-mix(in srgb, var(--faint) 26%, var(--card-bg)); }
.plan-hero__capfill { display: block; height: 100%; border-radius: 3px; background: var(--accent); }
.plan-hero__caplabel { margin-top: 6px; font-size: .74rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.plan-hero__caplabel b { color: var(--text-2); font-weight: 650; }
.plan-hero__bestnote { margin-top: 9px; font-size: .74rem; color: var(--good-ink); font-weight: 600;
  line-height: 1.4; display: flex; align-items: center; gap: 7px; }
.plan-hero__bestnote svg { flex: none; width: 15px; height: 15px; fill: currentColor; }

.whatif-lead { font-size: .8rem; color: var(--muted); line-height: 1.45; margin: 0 0 12px; }

/* Plan build loading state (the solver can take ~10s for several rooms). */
.plan-loading { text-align: center; padding: 48px 20px; }
.plan-loading__spin {
  width: 30px; height: 30px; margin: 0 auto 16px; border-radius: 50%;
  border: 3px solid color-mix(in srgb, var(--faint) 35%, transparent);
  border-top-color: var(--accent); animation: planspin .8s linear infinite;
}
@keyframes planspin { to { transform: rotate(360deg); } }
.plan-loading__lead { font-size: .95rem; font-weight: 650; color: var(--text); }
.plan-loading__sub { margin-top: 5px; font-size: .8rem; color: var(--muted); line-height: 1.45; }

/* Progressive solve (design §8.1/§10.7): REAL midway state while a long solve
   runs — the phase line, the room being swept with its live count, and provisional
   per-room picks that appear as pass 1 resolves them (marked "settling"). */
.plan-prog { padding: 6px 2px 2px; }
.plan-prog__phase { display: flex; align-items: center; gap: 10px; }
.plan-prog__spin {
  flex: none; width: 18px; height: 18px; border-radius: 50%;
  border: 2.5px solid color-mix(in srgb, var(--faint) 35%, transparent);
  border-top-color: var(--accent); animation: planspin .8s linear infinite;
}
.plan-prog__phasetxt { font-size: .92rem; font-weight: 650; color: var(--text); }
.plan-prog__sweep {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px;
  margin-top: 10px; padding-left: 28px;
}
.plan-prog__room { font-size: .82rem; font-weight: 650; color: var(--text-2); }
.plan-prog__sweeptxt { font-size: .78rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.plan-prog__bar {
  height: 5px; margin: 9px 0 2px 28px; border-radius: 3px; overflow: hidden;
  background: color-mix(in srgb, var(--faint) 30%, transparent);
}
.plan-prog__fill {
  display: block; height: 100%; border-radius: 3px; background: var(--accent);
  transition: width .3s ease;
}
.plan-prog__hint {
  margin: 14px 0 4px; padding-left: 28px; font-size: .78rem; color: var(--muted); line-height: 1.5;
}
.plan-block-title--prog { margin-top: 20px; }

/* Anti-freeze liveness line (item 3): a calm, non-destructive "still working" note
   the build heartbeat fills when progress snapshots stall, so the panel never reads
   as dead and the user doesn't re-tap Build. Informational (muted), never alarming.
   The growing "updated Ns ago" number is the motion-free liveness cue under reduced
   motion, where the phase spinner is stilled below. */
.plan-prog__live {
  display: flex; align-items: center; gap: 8px; margin: 14px 0 2px; padding-left: 28px;
  font-size: .78rem; color: var(--muted); line-height: 1.5;
}
.plan-prog__slowdot {
  flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--muted);
  animation: planpulse 1.2s ease-in-out infinite;
}
.plan-prog__slowtxt small { color: var(--faint); font-variant-numeric: tabular-nums; }
@keyframes planpulse { 0%, 100% { opacity: .35; } 50% { opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
  .plan-loading__spin, .plan-prog__spin { animation: none; }
  .plan-prog__slowdot { animation: none; opacity: .85; }
}

/* A provisional (settling) room pick — a real candidate the user can start reading,
   never presented as final: soft dashed frame + a "Settling" chip. */
.prov-card {
  margin-top: 8px; padding: 10px 12px; border-radius: 10px;
  background: var(--card-bg); border: 1px dashed var(--line);
  animation: provfade .28s ease both;
}
@keyframes provfade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
.prov-card__top { display: flex; align-items: center; gap: 8px; }
.prov-card__name { font-size: .86rem; font-weight: 700; color: var(--text); }
.prov-card__settling {
  margin-left: auto; flex: none; font-size: .6rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .05em; color: var(--muted); padding: 2px 8px; border-radius: 999px;
  background: var(--bg); border: 1px solid var(--line);
}
.prov-card__row {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; margin-top: 7px;
  font-size: .76rem; color: var(--muted); font-variant-numeric: tabular-nums;
}
.prov-card__lever {
  font-weight: 650; color: var(--text-2); padding: 2px 8px; border-radius: 999px;
  background: var(--accent-soft); border: 1px solid var(--accent-border);
}
.prov-card__met { color: var(--good-ink); font-weight: 650; }
.prov-card__part { color: var(--text-2); }
.prov-card__miss { color: var(--muted); }
.prov-card__when { color: var(--text-2); }
.prov-card__cost { margin-left: auto; font-weight: 650; color: var(--text-2); }
.prov-card__cost--free { color: var(--good-ink); }

/* Multi-leg night-precool sequence in the timed instruction (design §5.1, item 3):
   "Precool 00:30–02:10 (night rate, ~38p) · coast · Top-up 15:00–16:20 (~61p)".
   A flex-wrap row so each leg/coast chip wraps cleanly on a narrow phone rather
   than overflowing the card. */
.reach-legs { display: flex; flex-wrap: wrap; align-items: baseline; gap: 3px 7px; }
.reach-leg { white-space: nowrap; }
.reach-leg b { color: var(--accent); font-weight: 750; }
.reach-leg__rate { color: var(--muted); font-weight: 500; font-variant-numeric: tabular-nums; }
.reach-leg__coast {
  position: relative; color: var(--muted); font-weight: 500; font-style: italic;
  padding: 0 8px;
}
.reach-leg__coast::before, .reach-leg__coast::after {
  content: "→"; font-style: normal; color: var(--faint); margin: 0 3px;
}

/* ── Phase 2: active-plan tracking (design §11.11 / §13.8) ─────────────────────
   The Home face + the #detail screen, fed by one GET /api/plan/active. Four Home
   faces key off plan_status: calm .active-teaser (on_track), the same softly amber
   (.active-teaser--risk, at_risk — quiet, in-app only), the loud proactive
   .plan-alert (breaking_down — the first thing on Home after the email), and the
   neutral .plan-stale "can't confirm" card (data blackout). */

/* on_track / at_risk teaser + the soft completion face (--done). */
.active-teaser {
  position: relative; display: block; width: 100%; text-align: left; cursor: pointer;
  text-decoration: none; -webkit-tap-highlight-color: transparent;
  background: var(--card-bg); border: 1px solid var(--line);
  border-left: 4px solid var(--accent); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 13px 40px 13px 14px;
}
.active-teaser::after { content: "›"; position: absolute; right: 14px; top: 50%;
  transform: translateY(-50%); color: var(--faint); font-size: 1.3rem; line-height: 1; }
.active-teaser--risk { border-left-color: var(--warn-ink); }
.active-teaser--risk .active-teaser__label { color: var(--warn-ink); }
.active-teaser--done { border-left-color: var(--good-ink); }
.active-teaser--done .active-teaser__label { color: var(--good-ink); }
.active-teaser__top { display: flex; align-items: baseline; gap: 8px; }
.active-teaser__label { flex: none; font-size: .7rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .05em; color: var(--accent); }
.active-teaser__when { margin-left: auto; font-size: .72rem; color: var(--muted);
  font-variant-numeric: tabular-nums; }
.active-teaser__cost { margin-top: 6px; font-size: 1.05rem; font-weight: 700; color: var(--text);
  font-variant-numeric: tabular-nums; }
.active-teaser__cost span { font-size: .8rem; font-weight: 500; color: var(--text-2); }
.active-teaser__track { margin-top: 8px; display: flex; align-items: center; gap: 8px; }
.track-dots { display: flex; gap: 4px; flex: none; }
.track-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--line); }
.track-dot--ok { background: var(--good-ink); }
.track-dot--off { background: var(--bad-ink); }
.track-dot--risk { background: var(--warn-ink); }
.track-dot--none { background: transparent; border: 1.5px solid var(--faint); }
.active-teaser__tracklabel { min-width: 0; font-size: .74rem; color: var(--text-2); line-height: 1.35; }

/* Dismiss a completed, already-retired plan's summary from Home. */
.plan-dismiss { display: block; width: 100%; margin-top: 10px; padding: 10px;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--card-bg);
  color: var(--text-2); font-size: .82rem; font-weight: 600; cursor: pointer; }

/* Proactive breakdown alert — the loud face + the #detail banner. */
.plan-alert {
  display: flex; gap: 12px; align-items: flex-start;
  background: var(--card-bg); border: 1px solid var(--bad-border);
  border-left: 4px solid var(--bad-ink); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 13px 14px 14px;
}
.plan-alert__icon {
  flex: none; width: 26px; height: 26px; border-radius: 50%; margin-top: 1px;
  display: flex; align-items: center; justify-content: center;
  background: var(--bad-bg); color: var(--bad-ink); font-size: 1rem; font-weight: 800;
}
.plan-alert__body { min-width: 0; flex: 1 1 auto; }
.plan-alert__label { font-size: .68rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .05em; color: var(--bad-ink); }
.plan-alert__title { margin-top: 2px; font-size: 1rem; font-weight: 700; color: var(--text); }
.plan-alert__text { margin: 5px 0 0; font-size: .82rem; color: var(--text-2); line-height: 1.5; }
.plan-alert__actions { margin-top: 12px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.plan-alert__go {
  flex: none; padding: 9px 16px; border: none; border-radius: 999px; cursor: pointer;
  background: var(--bad-ink); color: var(--on-alert); font-size: .82rem; font-weight: 650;
  text-decoration: none; display: inline-flex; align-items: center; gap: 4px;
}

/* The fourth Home face — a whole-plan data blackout. Neutral: not calm, not alarming. */
.plan-stale {
  position: relative; display: flex; gap: 11px; align-items: flex-start;
  width: 100%; text-align: left; text-decoration: none; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  background: var(--card-bg); border: 1px solid var(--line);
  border-left: 4px solid var(--faint); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 13px 40px 13px 14px;
}
.plan-stale::after { content: "›"; position: absolute; right: 14px; top: 50%;
  transform: translateY(-50%); color: var(--faint); font-size: 1.3rem; line-height: 1; }
.plan-stale__icon { flex: none; width: 26px; height: 26px; border-radius: 50%; margin-top: 1px;
  display: flex; align-items: center; justify-content: center; background: var(--bg); color: var(--muted); }
.plan-stale__icon svg { width: 16px; height: 16px; fill: currentColor; }
.plan-stale__body { min-width: 0; }
.plan-stale__label { font-size: .68rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .05em; color: var(--muted); }
.plan-stale__title { margin-top: 2px; font-size: .95rem; font-weight: 700; color: var(--text); }
.plan-stale__text { margin: 4px 0 0; font-size: .8rem; color: var(--text-2); line-height: 1.45; }
.plan-stale__meta { margin-top: 7px; font-size: .72rem; color: var(--muted); font-variant-numeric: tabular-nums; }

/* Light-touch completion readout on #detail (NOT a live monitor). */
.detail-status { display: flex; align-items: flex-start; gap: 10px; padding: 12px 14px;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--card-bg); box-shadow: var(--shadow); }
.detail-status__dot { flex: none; margin-top: 4px; width: 10px; height: 10px; border-radius: 50%; background: var(--good-ink); }
.detail-status__text { min-width: 0; font-size: .82rem; color: var(--text); line-height: 1.4; }
.detail-status__text b { font-weight: 650; }
.detail-status__sub { display: block; margin-top: 3px; font-size: .72rem; color: var(--muted); line-height: 1.45; }

/* Per-room status list on #detail. */
.room-status__hint { margin: 0 0 8px; font-size: .74rem; color: var(--muted); }
.room-status { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden;
  background: var(--card-bg); box-shadow: var(--shadow); }
.room-accordion { border-bottom: 1px solid var(--line-soft); }
.room-accordion:last-child { border-bottom: none; }
/* The row is a full-width tap target (button reset) — every room is expandable. */
.room-status__row { display: flex; align-items: center; gap: 10px; width: 100%;
  min-height: 44px; padding: 10px 14px; background: none; border: none;
  text-align: left; font: inherit; color: inherit; cursor: pointer; }
.room-status__row:hover { background: var(--bg); }
.room-accordion.is-open > .room-status__row { background: var(--bg); }
.room-status__main { display: flex; flex-direction: column; gap: 2px;
  flex: 1 1 auto; min-width: 0; }
.room-status__name { font-size: .86rem; font-weight: 650; color: var(--text); }
.room-status__sub { display: block; font-size: .72rem; line-height: 1.35; }
.room-status__sub--muted { color: var(--muted); }
.room-status__sub--warn { color: var(--warn-ink); }
.room-status__temp { flex: none; font-size: .8rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.room-status__badge { flex: none; }
.room-status__chevron { flex: none; width: 18px; height: 18px; fill: none;
  stroke: var(--faint); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
  transition: transform .18s ease; }
.room-accordion.is-open .room-status__chevron { transform: rotate(180deg); }

/* Expanded plan-vs-actual panel (committed plan + calibrated band + realised). */
.room-plot { padding: 6px 12px 14px; border-top: 1px solid var(--line-soft); background: var(--bg); }
.room-plot[hidden] { display: none; }
.room-plot__canvas { position: relative; height: 240px; padding: 8px 8px 6px;
  background: var(--card-bg); border: 1px solid var(--line); border-radius: var(--radius); }
.room-plot__note { margin: 8px 2px 0; font-size: .72rem; color: var(--muted); }
.room-plot__msg, .room-plot__loading { padding: 22px 8px; text-align: center;
  font-size: .78rem; color: var(--muted); }

/* Per-leg adherence list — did each committed step actually happen? */
.adh-list { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden;
  background: var(--card-bg); box-shadow: var(--shadow); }
.adh-row { display: flex; align-items: center; gap: 11px; padding: 11px 14px; border-bottom: 1px solid var(--line-soft); }
.adh-row:last-child { border-bottom: none; }
.adh-row__dot { flex: none; width: 9px; height: 9px; border-radius: 50%; }
.adh-row--adhered .adh-row__dot { background: var(--good-ink); }
.adh-row--late    .adh-row__dot { background: var(--warn-ink); }
.adh-row--missed  .adh-row__dot { background: var(--bad-ink); }
.adh-row--pending .adh-row__dot { background: var(--card-bg); border: 1.5px solid var(--faint); }
.adh-row__what { flex: 1 1 auto; min-width: 0; }
.adh-row__lever { font-size: .84rem; font-weight: 600; color: var(--text); }
.adh-row__plan { margin-top: 1px; font-size: .72rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.adh-row__state { flex: none; font-size: .72rem; font-weight: 650; white-space: nowrap; font-variant-numeric: tabular-nums; }
.adh-row--adhered .adh-row__state { color: var(--good-ink); }
.adh-row--late    .adh-row__state { color: var(--warn-ink); }
.adh-row--missed  .adh-row__state { color: var(--bad-ink); }
.adh-row--pending .adh-row__state { color: var(--muted); }
/* r11.8: a heating plan's boiler leg — the controller's to run, never judged
   against the plan (§6.3): a filled neutral dot, a plain state. */
.adh-row--controlled .adh-row__dot { background: var(--muted); }
.adh-row--controlled .adh-row__state { color: var(--text-2); }
/* A late-but-completed leg: the action happened, the plan just slipped. Primary
   read is DONE (green dot + ink); the amber lives only on the "· N late" clause.
   Placed after the .adh-row--late rules so it overrides them at equal
   specificity (the row carries both classes). */
.adh-row--donelate .adh-row__dot { background: var(--good-ink); }
.adh-row--donelate .adh-row__state { color: var(--good-ink); }
.adh-row--donelate .adh-row__late { color: var(--warn-ink); font-weight: 600; }

/* "Record that this happened" — the trailing action on an unobserved leg. A slim
   accent-outline pill; the ::after grows the touch target to ≥44px without
   bulking the row. */
.adh-row__log {
  position: relative; flex: none; white-space: nowrap;
  padding: 7px 13px; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--accent-border); background: var(--accent-soft);
  color: var(--accent); font-size: .76rem; font-weight: 650;
}
.adh-row__log::after { content: ""; position: absolute; inset: -9px -4px; }
.adh-row__log:active { filter: brightness(.97); }

/* ── Rename-a-room sheet (roomRename.ts) ──────────────────────────────────────
   The shared .m-sheet grammar in a settings-app shape: one labelled field with a
   live character count, the rule underneath it, an inline error where the
   mistake was made (never a toast — the fix is right here), then Save/Cancel,
   and a separated "reset to default" only when there IS something to reset. */
.rename-field {
  padding: 10px 12px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg);
}
.rename-field__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 6px;
}
.rename-field__label {
  font-size: .72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--muted);
}
.rename-field__count {
  font-size: .72rem;
  font-variant-numeric: tabular-nums;
  color: var(--faint);
}
.rename-field__count.is-full { color: var(--accent); font-weight: 650; }
.rename-field__input {
  display: block;
  width: 100%;
  min-height: 44px;
  padding: 8px 10px;
  font-family: inherit;
  font-size: 1rem;
  color: var(--text);
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}
.rename-field__input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.rename-field.is-bad { border-color: var(--bad-border); }
.rename-field.is-bad .rename-field__input { border-color: var(--bad-ink); }
.rename-hint { margin: 10px 2px 0; font-size: .74rem; line-height: 1.45; color: var(--muted); }

/* The icon picker under the name field: a radiogroup of ≥44px cells, "None"
   first, the chosen one ringed in the accent (selection IS what the accent is
   for here). The chosen option's NAME is spelled out beside the eyebrow — a
   24px picture is a guess until something names it, and that line is also what
   a "which one is the pan?" moment needs. */
.rename-icons { margin-top: 16px; }
.rename-icons__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(46px, 1fr));
  gap: 8px;
}
.ri-cell {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 46px;
  padding: 0;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 12px;
  color: var(--text-2);
  cursor: pointer;
}
.ri-cell.is-on {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent); /* the ring, without moving the grid */
}
.ri-cell:active { filter: brightness(.97); }
.ri-cell:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ri--pick { width: 25px; height: 25px; }
.ri-none {
  font-size: .66rem;
  font-weight: 650;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
}
.ri-cell.is-on .ri-none { color: var(--accent); }
.rename-note {
  margin: 10px 0 0;
  padding: 9px 12px;
  border-radius: 10px;
  font-size: .76rem;
  line-height: 1.4;
  color: var(--bad-ink);
  background: var(--bad-bg);
  border: 1px solid var(--bad-border);
}
.rename-note[hidden] { display: none; }
.rename-actions { display: flex; gap: 10px; margin-top: 14px; }
.rename-actions .plan-action:disabled { opacity: .45; cursor: default; }
.rename-reset {
  display: block;
  width: 100%;
  margin-top: 14px;
  padding: 13px 16px;
  font-family: inherit;
  font-size: .84rem;
  font-weight: 600;
  color: var(--text-2);
  background: none;
  border: none;
  border-top: 1px solid var(--line);
  border-radius: 0;
  cursor: pointer;
}
.rename-reset[hidden] { display: none; }
.rename-reset:active { color: var(--accent); }

/* Mark-done sheet: a time control + confirm/cancel inside the shared .m-sheet. */
.mark-when { display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 12px 14px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg); }
.mark-when__label { font-size: .86rem; font-weight: 600; color: var(--text); }
.mark-when__input {
  font-size: 1rem; font-variant-numeric: tabular-nums; padding: 8px 10px;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--card-bg); color: var(--text); min-height: 44px;
}
.mark-note { margin: 10px 0 0; font-size: .74rem; line-height: 1.4; }
.mark-actions { display: flex; gap: 10px; margin-top: 14px; }

/* ── Living weather cards (weatherAnim.ts) ─────────────────────────────────────
   A cheap, glanceable animation layer that lives BEHIND a weather card's
   temperature/humidity shading — the sky brought to life, never covering the
   numbers. Built by wxSkyHtml() as a `.wx-sky` element per hour card and per
   banner. Every animated child's delay is anchored to wall-clock time via
   --wx-epoch, so the poll re-render resumes each loop in phase instead of
   snapping to frame 0. Hosts set position/overflow/isolation (see .m-hour /
   .outdoor-banner); the sky sits at z-index:-1 within that isolated context. */
.wx-sky {
  position: absolute;
  inset: 0;
  z-index: -1;                 /* over the card tint, under the reading */
  overflow: hidden;
  border-radius: inherit;
  pointer-events: none;
  --wx-epoch: 0s;              /* wall-clock phase anchor, set inline per render */
  --wx-slant: 0deg;
  --wx-dx: 0px;
  --wx-i: 0;
  --wx-cloud-dur: 30s;
}
.wx-sky > i { position: absolute; display: block; will-change: transform, opacity; }

/* Battery: freeze every weather animation while the tab/board is backgrounded
   (initWeatherAnim toggles .wx-hidden on <html> from the visibility API). */
.wx-hidden .wx-sky > i { animation-play-state: paused !important; }

/* Rain / drizzle — thin streaks falling at the wind's slant. dx is tan(slant)·220
   so the rotate() stays tangent to the fall. Opacity fades in/out at the edges
   so drops don't pop into existence. */
.wx-drop {
  top: 0;
  width: 1.4px;
  height: 20px;
  border-radius: 2px;
  background: linear-gradient(to bottom, transparent, var(--wx-rain));
  animation: wxFall var(--dur, .8s) linear infinite;
  animation-delay: calc(var(--wx-epoch) - var(--o, 0) * var(--dur, .8s));
  --peak: .8;
}
.wx-drop--fine { width: 1px; height: 12px; --peak: .5; }
@keyframes wxFall {
  0%   { transform: translate3d(0, -30px, 0) rotate(var(--wx-slant)); opacity: 0; }
  12%  { opacity: var(--peak, .8); }
  85%  { opacity: var(--peak, .8); }
  100% { transform: translate3d(var(--wx-dx), 190px, 0) rotate(var(--wx-slant)); opacity: 0; }
}

/* Snow — lazy tumbling dots that sway as they fall and drift with the wind. */
.wx-flake {
  top: 0;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--wx-snow);
  box-shadow: 0 0 2px var(--wx-snow);
  transform: scale(var(--s, 1));
  animation: wxTumble var(--dur, 3s) linear infinite;
  animation-delay: calc(var(--wx-epoch) - var(--o, 0) * var(--dur, 3s));
}
@keyframes wxTumble {
  0%   { transform: translate3d(0, -20px, 0) scale(var(--s, 1)); opacity: 0; }
  12%  { opacity: .9; }
  50%  { transform: translate3d(calc(var(--wx-dx) * .5 + 8px), 90px, 0) scale(var(--s, 1)); }
  88%  { opacity: .9; }
  100% { transform: translate3d(var(--wx-dx), 200px, 0) scale(var(--s, 1)); opacity: 0; }
}

/* Clouds — a periodic soft band drifting one tile per loop (seamless via
   background-position). Density tracks the actual cover (--wx-cloud) CONTINUOUSLY,
   not the WMO code, so a "mainly clear" hour with real cover still greys over and
   an overcast hour reads heavy; speed from --wx-cloud-dur. */
.wx-clouds {
  inset: -20% -10%;
  background-image:
    radial-gradient(60% 80% at 30% 50%, var(--wx-cloud-c), transparent 72%),
    radial-gradient(50% 70% at 75% 45%, var(--wx-cloud-c), transparent 74%);
  background-size: 70% 100%, 90% 100%;
  background-repeat: repeat-x;
  opacity: calc(.25 + .55 * var(--wx-cloud, .5));
  animation: wxDrift var(--wx-cloud-dur, 30s) linear infinite;
  animation-delay: var(--wx-epoch);
}
.wx-clouds--thin { opacity: calc(.22 + .3 * var(--wx-i)); }
/* Wisps over an otherwise-clear (code 0/1) sky — the same drifting band, kept
   faint and cover-scaled so light cover barely registers and heavier cover on a
   "clear" code still shows. */
.wx-clouds--wisp { opacity: calc(.1 + .5 * var(--wx-cloud, 0)); }
.wx-clouds--dark {
  background: linear-gradient(to bottom, rgba(70, 84, 110, .3), transparent 62%);
  opacity: 1;
  animation: none;
}
@keyframes wxDrift {
  from { background-position: 0 0, 0 0; }
  to   { background-position: 70% 0, 90% 0; }
}

/* Fog — a diffuse veil that softens the card, breathing gently side to side. */
.wx-fog {
  inset: -10%;
  background: radial-gradient(120% 80% at 50% 60%, var(--wx-fog), transparent 75%);
  opacity: .6;
  animation: wxHaze 12s ease-in-out infinite alternate;
  animation-delay: var(--wx-epoch);
}
.wx-fog--2 { opacity: .4; animation-duration: 18s; animation-delay: calc(var(--wx-epoch) - 4s); }
@keyframes wxHaze {
  from { transform: translateX(-6%) scale(1.05); opacity: .35; }
  to   { transform: translateX(6%) scale(1.16); opacity: .7; }
}

/* Clear — a soft corner glow that breathes; the shading shows through. */
.wx-glow {
  top: -30%;
  right: -20%;
  width: 80%;
  height: 90%;
  background: radial-gradient(circle at 70% 30%, var(--wx-glow), transparent 65%);
  opacity: .5;
  animation: wxBreathe 7s ease-in-out infinite alternate;
  animation-delay: var(--wx-epoch);
}
.wx-glow--hot { opacity: .68; }
/* A touch livelier than the original .3→.6/9s — on a small card that read as
   static; this widened swing at 7s is perceptibly alive on a direct look. */
@keyframes wxBreathe {
  from { opacity: .32; transform: scale(1); }
  to   { opacity: .66; transform: scale(1.12); }
}
/* Clear + strong wind — the glow leans and sways (a single keyframe carries the
   breathe AND the sway so the transforms don't fight). Faster than the calm
   breathe so a blustery clear card is unmistakable without anything falling. */
.wx-glow--windy {
  animation: wxGlowWindy 4.6s ease-in-out infinite alternate;
  animation-delay: var(--wx-epoch);
}
@keyframes wxGlowWindy {
  0%   { opacity: .34; transform: translateX(0) scale(1) rotate(0deg); }
  50%  { opacity: .64; transform: translateX(-4%) scale(1.09) rotate(-1.4deg); }
  100% { opacity: .4;  transform: translateX(1.5%) scale(1.03) rotate(.5deg); }
}

/* Clear + hot — a faint rising heat shimmer over the glow. */
.wx-shimmer {
  inset: 0;
  background: repeating-linear-gradient(0deg, transparent 0, rgba(255, 255, 255, .05) 2px, transparent 4px);
  mix-blend-mode: soft-light;
  opacity: .5;
  animation: wxHeat 3.2s ease-in-out infinite;
  animation-delay: var(--wx-epoch);
}
@keyframes wxHeat {
  0%, 100% { transform: translateY(0) skewX(0deg); }
  50%      { transform: translateY(-3px) skewX(1.1deg); }
}

/* ── Night skies (Met Office code space only) ─────────────────────────────────
   Nine of the thirty Met Office codes are night variants, so a 2 am card no
   longer draws the same warm corner glow as a sunny afternoon. The restraint IS
   the design: cooler, smaller, quieter — never an inverted dark tile in a row of
   light ones, and nothing new that moves. Every other kind takes night as
   PALETTE ONLY (a night rain card's drops are the same drops), which keeps this
   to one branch rather than nine. */
.wx-glow--night {
  /* Smaller and higher than the day glow — a moon, not a sun. */
  top: -22%;
  right: -10%;
  width: 58%;
  height: 68%;
  background: radial-gradient(circle at 70% 30%, var(--wx-night), transparent 62%);
  opacity: .5;
}
.wx-star {
  /* Measured on the real card: at 2px in the light theme's ink the points were
     invisible over the temperature wash while the dark theme's read fine. The
     two must be equally quiet, not one quiet and one absent. */
  width: 2.5px;
  height: 2.5px;
  border-radius: 50%;
  background: var(--wx-star);
  transform: scale(var(--s, 1));
  opacity: .85;
  /* Deliberately no animation: a twinkling card would be louder at 2 am than
     the sunny one it replaces, and reduced-motion would then need a third rule. */
}
/* Palette shift for the kinds that keep their motif: the sky under them cools
   and settles a little. One rule, no per-kind night variants. */
.wx-sky--night .wx-clouds { filter: saturate(.7) brightness(.92); }
.wx-sky--night .wx-drop, .wx-sky--night .wx-flake, .wx-sky--night .wx-hail {
  filter: brightness(.92);
}

/* Sleet — flakes and drops falling together, the flakes slower. Same particles
   as snow and rain (same tokens, both themes) at a smaller size, because the
   MIX is the message. */
.wx-flake--sleet {
  width: 4px;
  height: 4px;
  box-shadow: none;
  opacity: .9;
}

/* Hail — small, fast, sparse pellets with a short bounce at the bottom of the
   tile. Round and hard-edged where a drop is a streak: the silhouette is what
   separates them at a glance on a 58 px card. */
.wx-hail {
  top: 0;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--wx-snow);
  transform: scale(var(--s, 1));
  animation: wxHail var(--dur, .7s) linear infinite;
  animation-delay: calc(var(--wx-epoch) - var(--o, 0) * var(--dur, .7s));
}
/* The bounce: down fast, a small rebound, then gone. The vertical distances
   match wxFall's 190px travel so hail and rain share one apparent floor. */
@keyframes wxHail {
  0%   { transform: translate3d(0, -20px, 0) scale(var(--s, 1)); opacity: 0; }
  10%  { opacity: .9; }
  74%  { transform: translate3d(calc(var(--wx-dx) * .74), 186px, 0) scale(var(--s, 1)); opacity: .9; }
  84%  { transform: translate3d(var(--wx-dx), 168px, 0) scale(var(--s, 1)); opacity: .7; }
  100% { transform: translate3d(var(--wx-dx), 192px, 0) scale(var(--s, 1)); opacity: 0; }
}

/* Thunder — a rare, quick double flicker, dark for the rest of the loop. */
.wx-flash {
  inset: 0;
  background: var(--wx-flash);
  opacity: 0;
  animation: wxFlash 9s ease-out infinite;
  animation-delay: var(--wx-epoch);
}
@keyframes wxFlash {
  0%, 6%, 100% { opacity: 0; }
  2%   { opacity: .85; }
  3%   { opacity: .12; }
  4.5% { opacity: .6; }
}

/* Dry wind — thin streaks sweeping across at varied heights, low duty cycle. */
.wx-streak {
  left: 0;
  width: 46%;
  height: 1.5px;
  background: linear-gradient(90deg, transparent, var(--wx-streak), transparent);
  opacity: 0;
  animation: wxSweep var(--dur, 2.4s) ease-in infinite;
  animation-delay: calc(var(--wx-epoch) - var(--o, 0) * var(--dur, 2.4s));
}
/* The streak (width 46%, left:0) is only physically over the card while its
   translate is roughly -46%→100% — progress ~0.22..0.65 of this -120%→220%
   sweep. The opacity envelope is aligned to THAT window so the streak is bright
   as it crosses, not (as before) lit up while still off the left edge. */
@keyframes wxSweep {
  0%   { transform: translateX(-120%); opacity: 0; }
  18%  { opacity: 0; }
  28%  { opacity: .8; }
  58%  { opacity: .8; }
  72%  { opacity: 0; }
  100% { transform: translateX(220%); opacity: 0; }
}

/* Gust EVENT — a wider, faster streak that crosses ONCE per long loop, so the
   card is calm most of the time and an occasional gust blows through. Open-Meteo
   gives a single hourly peak gust with no frequency, so this stays deliberately
   sparse (one blast per ~18s) — honest, not a fabricated storm. Per-card phase
   (--o, seeded) staggers the strip so cards don't gust in unison; --go scales the
   brightness with gust magnitude. Width 60%, so it's on-screen while translateX
   is roughly -100%→167% (element-relative); the lit window sits inside that at
   EVERY surface width (card/banner/strip). */
.wx-gust-streak {
  left: 0;
  width: 60%;
  height: 1.5px;
  background: linear-gradient(90deg, transparent, var(--wx-streak), transparent);
  opacity: 0;
  animation: wxGust 18s linear infinite;
  animation-delay: calc(var(--wx-epoch) - var(--o, 0) * 18s);
}
@keyframes wxGust {
  0%, 86% { transform: translateX(-120%); opacity: 0; }
  88%     { transform: translateX(-70%);  opacity: var(--go, .8); }
  91%     { transform: translateX(60%);   opacity: var(--go, .8); }
  94%     { transform: translateX(200%);  opacity: 0; }
  100%    { transform: translateX(200%);  opacity: 0; }
}

/* Reduced motion: no movement, but keep a static condition treatment so the
   card still reads "rainy / foggy / snowy" at a glance. Particles are dropped;
   the gradient layers freeze (they're legible without motion). */
@media (prefers-reduced-motion: reduce) {
  .wx-sky > i { animation: none !important; }
  .wx-drop, .wx-flake, .wx-hail, .wx-streak, .wx-gust-streak, .wx-flash { display: none; }
  .wx-glow, .wx-fog, .wx-clouds { opacity: .5; }
  /* The stars are the exception that stays: they never moved, so there is
     nothing to reduce, and they are the only mark that says "night". */
  .wx-sky--rain::after, .wx-sky--drizzle::after, .wx-sky--sleet::after {
    content: "";
    position: absolute;
    inset: 0;
    background: repeating-linear-gradient(115deg,
      transparent 0 7px, var(--wx-rain) 7px 8px, transparent 8px 15px);
    opacity: .18;
  }
  .wx-sky--snow::after, .wx-sky--hail::after {
    content: "";
    position: absolute;
    inset: 0;
    background: radial-gradient(circle at 30% 30%, var(--wx-snow) 0 1.5px, transparent 2px),
      radial-gradient(circle at 70% 60%, var(--wx-snow) 0 1.5px, transparent 2px);
    background-size: 40% 55%;
    opacity: .4;
  }
  /* Hail's static stand-in is sparser than snow's — the same dots on a wider
     tile — so the two still read differently without motion. */
  .wx-sky--hail::after { background-size: 62% 78%; opacity: .5; }
}

/* ── Winter heating: the Home card, the role shapes, #schedule (UI §3/§5/§6.1) ──
   One vocabulary for every winter surface. Two rules run through all of it:

   1. ROLE IS A SHAPE, NEVER A COLOUR. The floor plan and the Home room cards are
      already washed by temperature, so a second colour over that encodes nothing
      a reader can separate. Filled = drives this call, hollow = rides along free,
      absent = nothing asked of it now — and the key to the shapes is printed
      permanently under the first card that uses them.
   2. MONEY WRAPS RATHER THAN SHRINKING (UI §7). "£2.59 +£1.12/−£1.11" is a long
      string at 390 px; it folds at the slash (marked with a <wbr> in the
      renderer, since a slash is not a CSS break opportunity) and keeps its type
      size. Every figure is tabular so a column of them lines up. */

.wh-card {
  position: relative;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-left: 4px solid var(--accent);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 13px 14px 12px;
}
/* The faces (UI §6.1). Calm keeps the accent rail; risk and attention re-colour
   the rail alone — the card body never becomes a wash, because a tinted card is
   read as "broken" long before its words are. */
.wh-card--risk { border-left-color: var(--warn-ink); }
.wh-card--attention { border-left-color: var(--faint); }
/* breaking_down: the loud face, but still a card that explains itself — its own
   red block does the shouting, so the shell keeps its padding and only the rail
   changes. (The one card that DOES change shape is --silent, below.) */
.wh-card--alarm { border-left-color: var(--bad-ink); }
/* The controller-silent alarm alone. No rail, no padding of its own: the bar is
   the card's top edge. */
.wh-card--silent { border: 1px solid var(--text); border-left-width: 1px; padding: 0; overflow: hidden; }

.wh-card__head { display: flex; align-items: flex-start; gap: 10px; }
/* min-width:0 — the head is a flex row and the title must be free to wrap
   (or ellipsise) rather than shouldering the profile pill off the card. */
.wh-card__headtext { min-width: 0; flex: 1 1 auto; }
.wh-card__title {
  font-size: 1.12rem; font-weight: 700; color: var(--text);
  line-height: 1.25; letter-spacing: -.01em;
}
.wh-card__sub { margin-top: 2px; font-size: .88rem; color: var(--text-2); line-height: 1.35; }
.wh-card__sub b { font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; }
/* The active profile, named on every winter surface: what you are looking at is
   always one profile's output, never "the heating" in the abstract. */
/* A long profile name used to shoulder the head's title under itself ("Heatin|g
   stopped"). It may take at most half the head and WRAPS there — never an
   ellipsis: a truncated name's only rescue is a tooltip, which a phone has no
   way to open (U2 review). The pill's radius drops so two lines still read as
   one chip. */
/* U6: it no longer SHRINKS. With `flex-shrink: 1` a long head title ("Heating
   won't start again by itself") squeezed the pill to its min-content — which,
   under `overflow-wrap: anywhere`, is one letter ("Workda|y"). Not shrinking,
   it sits at its own width up to the 48% cap and wraps only there, at words. */
.wh-profile {
  flex: 0 0 auto; min-width: 0; max-width: 48%;
  overflow-wrap: anywhere; text-align: center; line-height: 1.3;
  padding: 4px 11px; border-radius: 12px;
  border: 1px solid var(--line); background: var(--bg);
  font-size: .76rem; font-weight: 600; color: var(--text-2);
}
/* Under the profile calendar the chip opens #calendar (§6.5): the same chip,
   with a door's chevron and a 44px reach (the ::after hit-area idiom). */
.wh-profile--link { position: relative; text-decoration: none; color: var(--text); }
.wh-profile--link::after { content: ""; position: absolute; inset: -10px -4px; }
.wh-profile__chev { color: var(--faint); }

/* A kept hand-turn: renewals are suspended ON PURPOSE (spec §12.2), so this is a
   quiet statement of fact and deliberately not the alarm's vocabulary. */
.wh-held {
  margin-top: 10px; padding: 6px 10px; border-radius: var(--radius-sm);
  background: var(--bg); color: var(--text-2);
  font-size: .78rem; font-weight: 600; font-variant-numeric: tabular-nums;
}

.wh-card__why {
  margin-top: 11px; padding-top: 11px; border-top: 1px solid var(--line-soft);
}
.wh-why { font-size: .92rem; color: var(--text); line-height: 1.4; }
.wh-why b { font-weight: 700; }
.wh-ride { margin-top: 4px; font-size: .84rem; color: var(--muted); line-height: 1.4; }
/* r11.8: who can stop the running call — the asking band rooms. The ride
   line's quiet voice, with the names in weight; it qualifies, never alarms. */
.wh-canstop {
  margin-top: 4px; font-size: .84rem; color: var(--muted); line-height: 1.4;
  font-variant-numeric: tabular-nums;
}
.wh-canstop b { color: var(--text-2); font-weight: 700; }
.wh-risk {
  margin-top: 10px; font-size: .84rem; font-weight: 600;
  color: var(--warn-ink); line-height: 1.4;
}

/* breaking_down — loud, but still a card that explains itself. */
.wh-loud {
  margin-top: 11px; padding: 10px 12px; border-radius: var(--radius-sm);
  background: var(--bad-bg); border: 1px solid var(--bad-border);
}
.wh-loud__title { font-size: .92rem; font-weight: 700; color: var(--bad-ink); line-height: 1.35; }
.wh-loud__text { margin-top: 4px; font-size: .8rem; color: var(--text-2); line-height: 1.45; }

/* stale — an ATTENTION state, never the alarm: a sensor gap dressed as a dead
   controller trains the loud face away. */
/* The rule separates the attention block from the saved-at line ABOVE it inside
   the same card (the stale face). When it is the card's only child — the winter
   failure face — there is nothing to separate it from, and the hairline reads as
   an empty row above the title. */
.wh-attn { margin-top: 11px; padding-top: 11px; border-top: 1px solid var(--line-soft); }
.wh-attn:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
.wh-attn__title { font-size: .95rem; font-weight: 700; color: var(--text); line-height: 1.3; }
.wh-attn__text { margin-top: 4px; font-size: .84rem; color: var(--text-2); line-height: 1.45; }
.wh-attn__note { margin-top: 5px; font-size: .78rem; color: var(--muted); line-height: 1.45; }

/* The alarm (UI §6.1). An inverted bar rather than a red one: red is already the
   app's "a number is bad" colour, and this is the one state that says nothing is
   watching a boiler. --text on --card-bg inverts correctly in both themes. */
.wh-alarm__bar {
  padding: 11px 14px; background: var(--text); color: var(--card-bg);
  font-size: 1rem; font-weight: 700; line-height: 1.3;
}
.wh-alarm__body { padding: 12px 14px 13px; }
.wh-alarm__text { font-size: .88rem; color: var(--text); line-height: 1.5; font-variant-numeric: tabular-nums; }
.wh-alarm__note { margin-top: 6px; font-size: .8rem; color: var(--text-2); line-height: 1.45; }

/* Actions. 44px targets; a pill row that wraps rather than squeezing. */
.wh-actions { margin-top: 12px; display: flex; flex-wrap: wrap; gap: 10px; }
.wh-act {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 44px; padding: 10px 16px; border-radius: 999px;
  border: 1px solid var(--accent-border); background: var(--card-bg);
  color: var(--accent); font-size: .84rem; font-weight: 650;
  text-decoration: none; cursor: pointer;
}
.wh-act--primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.wh-act:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* In flight: the button stays in place (no spinner swap, no layout jump) and
   simply stops being tappable — a write this app makes takes one round trip. */
.wh-act[disabled] { opacity: .55; pointer-events: none; }

.wh-card__foot { margin-top: 12px; padding-top: 11px; border-top: 1px solid var(--line-soft); }
/* The money row: what the figure is ABOUT on the left, the figure on the right,
   baselines aligned at the bottom so the caption sits under the count. */
.wh-costrow { display: flex; align-items: flex-end; gap: 12px; }
/* min-width:0 on the flexible side: the caption wraps and the money column keeps
   its intrinsic width instead of overflowing the card. */
.wh-costrow__left { min-width: 0; flex: 1 1 auto; }
.wh-count { font-size: .84rem; color: var(--text-2); font-variant-numeric: tabular-nums; }

/* Money — one block form (a headline figure over its window) and one inline form
   (a single row inside a list). Both from costBand.formatCost, one rounding pass. */
.wh-cost__figure {
  flex: none; min-width: 0;
  display: flex; align-items: baseline; justify-content: flex-end;
  flex-wrap: wrap; gap: 6px;
}
.wh-cost__central { font-size: 1.3rem; font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; }
.wh-cost__band { font-size: .82rem; color: var(--text-2); font-variant-numeric: tabular-nums; }
.wh-cost__cap { margin-top: 2px; font-size: .74rem; color: var(--muted); }
.wh-learn { color: var(--muted); font-size: .78rem; }
.wh-cost-inline {
  display: inline-flex; align-items: baseline; flex-wrap: wrap; gap: 5px;
  font-variant-numeric: tabular-nums;
}
.wh-cost-inline .wh-cost__central { font-size: .95rem; }

.wh-go {
  display: inline-flex; align-items: center; min-height: 44px; margin-top: 10px;
  padding: 10px 16px; border: 1px solid var(--accent-border); border-radius: 999px;
  background: var(--card-bg); color: var(--accent); font-size: .86rem; font-weight: 650;
  text-decoration: none;
}
/* The card's foot row of doors ("Full schedule ›", and while a heating plan runs
   "Plan details ›" into #detail — the stood-down plan face's tap). A row that
   wraps, so a large text size drops the second pill under the first rather
   than pushing it off the card. */
.wh-gos { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.wh-gos > .wh-go { margin-top: 0; }
.wh-go--plan { gap: 6px; }
/* The plan has alerts: the door says so — a count, in the plan-problem tone —
   and stays a door. The badge's ink is the warn BACKGROUND, which is the
   contrasting end of the pair in both themes (pale on #b54708, dark on #f5c065). */
.wh-go--attn { border-color: color-mix(in srgb, var(--warn-ink) 55%, var(--card-bg)); }
.wh-go__badge {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 20px; height: 20px; padding: 0 6px; border-radius: 10px;
  background: var(--warn-ink); color: var(--warn-bg);
  font-size: .74rem; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1;
}

/* ── The role shapes and their key ─────────────────────────────────────────── */
.wh-dot {
  display: inline-block; flex: none; width: 9px; height: 9px; margin-right: 6px;
  border-radius: 50%; vertical-align: baseline;
}
/* currentColor, not a palette colour: the dot is the same ink as the text it
   labels, so it never reads as a second data channel. */
.wh-dot--drive { background: currentColor; }
.wh-dot--ride { background: transparent; box-shadow: inset 0 0 0 1.5px currentColor; }

/* The key, printed permanently under the first card that uses the shapes. One
   small line — at true size the dots read as decoration until someone says what
   they mean. */
.wh-key {
  margin: 8px 2px 0; font-size: .69rem; color: var(--muted); line-height: 1.5;
}
.wh-key .wh-dot { width: 7px; height: 7px; margin-right: 4px; color: var(--text-2); }
.wh-key__sep { margin: 0 6px; color: var(--muted); }

/* The role line on a Home room card. It sits under the trend and above the meta
   row, and carries the objective the card's headline temperature is being judged
   against. :empty collapses it entirely, so nothing changes out of season. */
.m-room__role {
  display: flex; flex-direction: column; gap: 1px; margin-top: 3px;
  font-size: .72rem; color: var(--text-2); line-height: 1.35;
  /* The card is ~150px of content at 390px and the ETA sentence is long. It
     WRAPS (UI §7) — it never shrinks its type and never ellipsises a number. */
  overflow-wrap: anywhere;
}
.m-room__role:empty { display: none; }
.wh-room__role { display: block; font-weight: 600; font-variant-numeric: tabular-nums; }
.wh-room__eta { display: block; color: var(--muted); font-variant-numeric: tabular-nums; }

/* ── #schedule ─────────────────────────────────────────────────────────────── */

/* The one sentence this design exists to say. Permanent and undismissible. */
.wh-creed {
  padding: 13px 14px; border-radius: var(--radius);
  background: var(--bg); border: 1px solid var(--line-soft);
}
.wh-creed__title { font-size: .98rem; font-weight: 700; color: var(--text); }
.wh-creed__text { margin-top: 3px; font-size: .86rem; color: var(--text-2); line-height: 1.45; }

.wh-iv {
  margin-top: 12px; background: var(--card-bg); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden;
}
/* The call that is running right now — the only interval the accent belongs to. */
.wh-iv--now { border-color: var(--accent-border); box-shadow: 0 0 0 1px var(--accent-border), var(--shadow); }
.wh-iv--off { background: var(--bg); box-shadow: none; }

.wh-iv__head {
  display: flex; align-items: center; gap: 10px; width: 100%;
  min-height: 52px; padding: 12px 12px 12px 14px;
  background: none; border: 0; text-align: left; cursor: pointer; color: inherit;
}
.wh-iv__headtext { min-width: 0; flex: 1 1 auto; }
.wh-iv__range {
  display: block; font-size: 1.05rem; font-weight: 700; color: var(--text);
  font-variant-numeric: tabular-nums;
}
/* An interval that crosses midnight ("18:20 -> 05:40 tomorrow"): quiet, and in
   the range's own line box, so a list read downwards never looks as if a call
   ran backwards. */
.wh-iv__day {
  margin-left: 7px; font-size: .72rem; font-weight: 500; color: var(--muted);
  letter-spacing: 0;
}

/* The collapsed form's tail ("for Dad's Office") — the fact you would want back
   once the body is folded away, and noise while it is open. */
.wh-iv__for { display: none; font-size: .78rem; color: var(--muted); }
.wh-iv__head[aria-expanded="false"] .wh-iv__for { display: block; }
.wh-iv__state {
  flex: none; padding: 5px 11px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--bg);
  font-size: .78rem; font-weight: 650; color: var(--text-2);
  font-variant-numeric: tabular-nums;
}
.wh-iv--now .wh-iv__state {
  border-color: var(--accent-border); background: var(--accent-soft); color: var(--accent);
}
.wh-iv__chev {
  flex: none; width: 20px; height: 20px; fill: none; stroke: var(--faint);
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
  transition: transform .15s ease;
}
.wh-iv__head[aria-expanded="false"] .wh-iv__chev { transform: rotate(-90deg); }
.wh-iv__body { padding: 0 14px 12px; }
/* A variant with its own display would beat [hidden] — assert the guard on the
   element that actually toggles, exactly as .m-room-screen[hidden] does. */
.wh-iv__body[hidden] { display: none; }

/* Label/value rows. A fixed label column keeps four rows' values on one left
   edge; min-width:0 on the value is what lets a long free-ride list wrap instead
   of pushing the row sideways. */
.wh-rows {
  display: flex; flex-direction: column; gap: 7px;
  padding-top: 11px; border-top: 1px solid var(--line-soft);
}
.wh-row { display: flex; align-items: baseline; gap: 12px; }
.wh-row__k { flex: 0 0 76px; font-size: .78rem; color: var(--muted); }
.wh-row__v {
  min-width: 0; flex: 1 1 auto; font-size: .86rem; color: var(--text);
  line-height: 1.45; overflow-wrap: anywhere;
}
.wh-row__v b { font-weight: 700; }

/* Infeasibility: a bordered warn row, never a red screen. Dashed, because it is
   a plan-family "this cannot happen" rather than a fault that has happened. */
.wh-infeas {
  margin-top: 12px; padding: 12px 14px; border-radius: var(--radius);
  border: 1px dashed var(--warn-ink); background: var(--warn-bg);
}
.wh-infeas__head { display: flex; align-items: baseline; gap: 10px; }
.wh-infeas__title { min-width: 0; flex: 1 1 auto; font-size: .95rem; font-weight: 700; color: var(--warn-ink); }
.wh-infeas__when { flex: none; font-size: .76rem; color: var(--warn-ink); opacity: .85; font-variant-numeric: tabular-nums; }
.wh-infeas__text { margin-top: 5px; font-size: .84rem; color: var(--text); line-height: 1.5; }
.wh-infeas .wh-act { margin-top: 11px; }
/* r11.8's too-warm row: information, nothing to do — the same shape on a solid
   hairline and the card's own ground, so it never reads as the warn row above. */
.wh-infeas--info { border: 1px solid var(--line); background: var(--card-bg); }
.wh-infeas--info .wh-infeas__title { color: var(--text); }
.wh-infeas--info .wh-infeas__when { color: var(--muted); opacity: 1; }

.wh-total { margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--line); }

.wh-empty {
  margin-top: 12px; padding: 16px 14px; border-radius: var(--radius);
  background: var(--card-bg); border: 1px solid var(--line);
}
.wh-empty__title { font-size: .98rem; font-weight: 700; color: var(--text); }
.wh-empty__text { margin-top: 5px; font-size: .85rem; color: var(--text-2); line-height: 1.5; }

/* The designed wait: three interval-shaped blocks, so the page does not jump
   when the real ones land. */
.wh-skels { margin-top: 12px; display: flex; flex-direction: column; gap: 12px; }
.wh-skel {
  height: 128px; border-radius: var(--radius);
  background: linear-gradient(100deg, var(--line-soft) 30%, var(--line) 50%, var(--line-soft) 70%);
  background-size: 220% 100%;
  animation: whSkel 1.3s ease-in-out infinite;
}
@keyframes whSkel { from { background-position: 120% 0; } to { background-position: -20% 0; } }
@media (prefers-reduced-motion: reduce) {
  .wh-skel { animation: none; background: var(--line-soft); }
  .wh-iv__chev { transition: none; }
}

/* ── #heating: the monitor (UI §6.2) ────────────────────────────────────────
   Three stacked sections in one fixed order — rooms worst-first, the diary,
   the controller — sharing the wh-* vocabulary above. Three rules specific to
   this screen:

   1. DASHED = A QUESTION, SOLID RAIL = A PROBLEM (UI §6.6's two families). A
      room with a pending ask takes a dashed accent border, the same shape the
      infeasible row uses for a plan-family "this cannot happen"; a room merely
      out of band takes a solid warn rail. Faults never get a dashed edge.
   2. COLOUR IS RESERVED. Only three `Right now` values are coloured — commissioning
      not passed and the controller's own hold (warn, U6: it is refusing the plan's
      starts), and a controller that has stopped renewing (bad). A kept hand-turn
      is deliberate and reads in words, not in amber.
   3. Every figure is tabular, because the controller's rows are a column of
      numbers a reader compares down the card, not across it. */

/* A banner card whose first child is a block that normally follows a card head
   (.wh-attn / .wh-loud both carry their own top rule). On this screen they lead,
   so the rule and the space above them would be a hairline against nothing. */
.wh-lead > :first-child { margin-top: 0; padding-top: 0; border-top: 0; }

/* The designed wait: room-row-shaped, not interval-shaped. */
.wh-skel--row { height: 84px; }

/* The lead section holds the saved-at line and (when there is one) the face
   banner. `.wh-saved`'s own top margin is for following a card; at the very top
   of the screen it is space against the header. */
#heating-body .wh-saved:first-child { margin-top: 0; }
#heating-body .wh-saved + .wh-card { margin-top: 12px; }

/* ── Section 1: rooms ─────────────────────────────────────────────────────── */

.wh-mon {
  margin-bottom: 10px; padding: 12px 14px;
  background: var(--card-bg); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
}
/* Out of band: a solid warn rail, and the card keeps its ink. A tinted card is
   read as "broken" long before its words are. */
.wh-mon--out { border-left: 3px solid var(--warn-ink); }
/* A pending ask — a question waiting on a person, in the app's action colour.
   The answer itself is item U4's sheet; until that exists the row says what
   happens if nobody answers, which is why the missing button is survivable. */
.wh-mon--ask { border: 1px dashed var(--accent-border); box-shadow: none; }

.wh-mon__head { display: flex; align-items: baseline; gap: 10px; }
/* min-width:0 — the name wraps rather than shouldering the mode word off the
   card. Room names are the household's own and can be long. */
.wh-mon__name {
  min-width: 0; flex: 1 1 auto; overflow-wrap: anywhere;
  font-size: 1rem; font-weight: 700; color: var(--text); line-height: 1.3;
}
/* §6.3's trailing words, read-only here (planHeatingLogic.nudgeModeShort).
   `auto` is the default and stays quiet; `ask` ("asks before changing plan") is
   the exception and takes the pill, so the eye finds the rooms that will wait
   for you. The words are a phrase now, not a token, so the chip may shrink and
   wrap rather than push past the card edge beside a long room name. */
.wh-mon__mode { flex: none; max-width: 60%; font-size: .76rem; color: var(--muted); text-align: right; }
.wh-mon__mode--ask {
  padding: 3px 10px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--bg);
  font-weight: 600; color: var(--text-2);
}

.wh-mon__line {
  margin-top: 4px; font-size: .92rem; color: var(--text); line-height: 1.4;
  font-variant-numeric: tabular-nums;
  /* The full line is "15.6°C → 17 ±1.5 · 0.4° below band, 35m" — at 390 px it
     wraps rather than shrinking or ellipsising a number (UI §7). */
  overflow-wrap: anywhere;
}
/* A window carried over from another date (calendar §6.5): a quiet dashed
   tag, the plan family's "this comes from elsewhere" shape. */
.wh-mon__tag {
  display: inline-block; margin-top: 6px; padding: 2px 9px; border-radius: 8px;
  border: 1px dashed var(--line); font-size: .76rem; color: var(--text-2);
}
.wh-mon__eta {
  margin-top: 3px; font-size: .82rem; color: var(--text-2); line-height: 1.45;
  font-variant-numeric: tabular-nums;
}
.wh-mon__conf { color: var(--muted); }
.wh-mon__nudge {
  margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line-soft);
  font-size: .84rem; color: var(--text-2); line-height: 1.5;
}

/* ── Section 2: what I changed ────────────────────────────────────────────── */

.wh-diary__scope {
  margin-bottom: 10px; font-size: .78rem; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.wh-diary {
  margin-bottom: 10px; padding: 12px 14px;
  background: var(--card-bg); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
}
/* A row still waiting on an answer, in the same dashed shape as the room card
   that carries it — one question, two places, one look. */
.wh-diary--pending { border: 1px dashed var(--accent-border); box-shadow: none; }

.wh-diary__head { display: flex; align-items: baseline; gap: 10px; }
.wh-diary__when {
  min-width: 0; flex: 1 1 auto;
  font-size: .92rem; color: var(--text-2); line-height: 1.35;
  font-variant-numeric: tabular-nums;
}
.wh-diary__when b { font-weight: 700; color: var(--text); }
/* One chip style for every ending: the WORD is the signal, and colouring four
   endings would say some are worse than others when the diary's whole point is
   that it records all of them the same way. */
.wh-diary__chip {
  flex: none; padding: 3px 10px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--bg);
  font-size: .72rem; font-weight: 600; color: var(--text-2);
}
.wh-diary__text {
  margin-top: 5px; font-size: .85rem; color: var(--text-2); line-height: 1.5;
  font-variant-numeric: tabular-nums;
}
/* "If nobody answers by 06:18, the plan runs unchanged." — the one sentence on a
   pending row that is not narration, so it gets its own line and its own weight
   rather than trailing five others. An unstated default is what breeds distrust
   (UI §6.5), and buried is a kind of unstated. */
.wh-diary__note {
  margin-top: 7px; padding-top: 7px; border-top: 1px solid var(--line-soft);
  font-size: .82rem; font-weight: 600; color: var(--text); line-height: 1.45;
  font-variant-numeric: tabular-nums;
}

/* A disclosure, not a destination: it expands in place, so it takes a chevron
   that turns rather than the "›" this app uses for "a new screen opens". */
.wh-more {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 44px; padding: 8px 2px;
  background: none; border: 0; cursor: pointer;
  color: var(--accent); font-size: .88rem; font-weight: 650;
}
.wh-more__chev {
  width: 18px; height: 18px; fill: none; stroke: currentColor;
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
  transition: transform .15s ease;
}
.wh-more[aria-expanded="true"] .wh-more__chev { transform: rotate(180deg); }
.wh-more:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ── Section 3: the controller ────────────────────────────────────────────── */

.wh-ctrl {
  padding: 13px 14px; background: var(--card-bg); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
}
/* The rows lead the card, so they drop the top rule .wh-rows carries for use
   inside an interval body. */
.wh-rows--lead { padding-top: 0; border-top: 0; }
/* "Last command" needs more than #schedule's 76px label column, and all six
   values must land on one left edge or the card stops reading as a table. */
.wh-ctrl .wh-row__k { flex: 0 0 104px; }
.wh-ctrl .wh-row__v { font-weight: 600; font-variant-numeric: tabular-nums; }
.wh-row--lead .wh-row__v { font-size: 1rem; font-weight: 700; }
/* Colour only where it is a claim about trouble. `following` and `held by hand`
   read in words: a hand-turn is deliberate, and amber would call it a fault.
   So is control switched off (U1b): a person's choice, said in words. */
.wh-now--following, .wh-now--held, .wh-now--control_off, .wh-now--idle { color: var(--text); }
.wh-now--not_commissioned, .wh-now--paused, .wh-now--handing_back { color: var(--warn-ink); }
.wh-now--silent, .wh-now--none { color: var(--bad-ink); }

.wh-ctrl__note {
  margin-top: 11px; padding-top: 11px; border-top: 1px solid var(--line-soft);
  font-size: .84rem; color: var(--text-2); line-height: 1.45;
}

.wh-fail {
  display: flex; flex-direction: column; gap: 8px;
  margin-top: 2px; padding: 10px 12px;
  background: var(--bg); border-radius: var(--radius-sm);
  list-style: none;
}
/* A class with its own `display` beats [hidden]'s — assert the guard on the
   element that actually toggles. */
.wh-fail[hidden] { display: none; }
.wh-fail__row {
  font-size: .8rem; color: var(--text-2); line-height: 1.5;
  font-variant-numeric: tabular-nums;
}

/* ── The #status mirror ───────────────────────────────────────────────────── */
/* One compact line and a door. The card itself stays on #heating (§6.2) — this
   is the "and by the way" placement, sized like every other row on that screen. */
/* The sensor list above it is a plain stack with no trailing margin, so the
   mirror's heading would otherwise sit 12 px under the last sensor card while
   every other section on that screen gets 28. */
#status-heating { margin-top: 24px; }
.wh-mirror {
  display: flex; align-items: center; gap: 12px;
  min-height: 56px; padding: 11px 14px;
  background: var(--card-bg); border: 1px solid var(--line);
  border-radius: var(--radius); text-decoration: none;
}
.wh-mirror__text { min-width: 0; flex: 1 1 auto; }
.wh-mirror__now {
  display: block; font-size: .95rem; font-weight: 700; color: var(--text);
  line-height: 1.3;
}
.wh-mirror__sub {
  display: block; margin-top: 2px; font-size: .78rem; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.wh-mirror__go { flex: none; color: var(--accent); font-size: 1.1rem; }
.wh-mirror:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .wh-more__chev { transition: none; }
}

/* ── The three asks (UI §6.4–§6.6, item U4) ──────────────────────────────────
   Two families, and the CSS is where they are kept apart:

   • A QUESTION is dashed and in the action colour — the pending room row and the
     pending diary row already are, and the ask sheet is their answer surface.
   • A FAULT is a solid DARK rule (--text, the ink of the words themselves, the
     same weight the controller-silent alarm card uses). Never dashed, never the
     warn amber a plan-family problem takes: "keeping them apart is what stops
     the fault style becoming wallpaper before the first real one" (§6.6).

   Both themes come free — every colour here is a token. */

/* ── The ask sheet (#nudge/<id>) ─────────────────────────────────────────── */
/* The header's text column must be free to wrap: the lead is a whole sentence
   and the ✕ is a fixed 44px beside it. */
.wh-ask__head { min-width: 0; flex: 1 1 auto; }
.wh-ask__lead {
  margin-top: 3px; font-size: .84rem; color: var(--text-2); line-height: 1.45;
  white-space: normal;
}

/* The two-outcome box. A border rather than a fill: this is the sheet's centre
   of gravity and a tinted panel would read as a warning. */
.wh-ask__box {
  margin-top: 2px; padding: 12px 14px;
  border: 1px solid var(--line); border-radius: var(--radius);
  border-top: 1px solid var(--line);  /* .wh-rows' rule is for a card body */
  gap: 0;
}
.wh-ask__box .wh-row + .wh-row {
  margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line-soft);
}
/* "If you do nothing" / "If you keep it" need more than #schedule's 76px, and
   both labels must land on one left edge or the box stops reading as a pair. */
.wh-ask__box .wh-row__k { flex: 0 0 96px; }
.wh-ask__box .wh-row__v { font-variant-numeric: tabular-nums; }

/* The printed default (§6.5). Full text weight, its own line, above the buttons:
   an unstated differing default breeds distrust, and buried is a kind of
   unstated. The two words that carry it are bold, nothing else. */
.wh-ask__default {
  margin-top: 12px; font-size: .88rem; color: var(--text); line-height: 1.5;
  font-variant-numeric: tabular-nums;
}
.wh-ask__default b { font-weight: 700; }
.wh-ask__expiry {
  margin-top: 12px; font-size: .88rem; color: var(--text); line-height: 1.5;
  font-variant-numeric: tabular-nums;
}

/* Two buttons of equal width — this is a choice, not an action with an escape
   hatch, so neither is a text link. 44px minimum, primary on the right. */
.wh-ask__actions { display: flex; gap: 10px; margin-top: 14px; }
.wh-ask__btn {
  flex: 1 1 0; min-width: 0; min-height: 46px;
  justify-content: center; text-align: center;
}
.wh-ask__err {
  margin-top: 10px; padding: 9px 12px; border-radius: var(--radius-sm);
  background: var(--bad-bg); color: var(--bad-ink);
  font-size: .8rem; line-height: 1.45;
}
.wh-ask__note {
  margin-top: 10px; font-size: .78rem; color: var(--muted); line-height: 1.5;
}
.wh-ask__gone { margin-top: 4px; font-size: .86rem; color: var(--text-2); line-height: 1.5; }
.wh-ask__gone b { color: var(--text); }

/* ── A pending diary row is answerable ──────────────────────────────────────
   The settled rows are records (a div); the pending one is a question, and a
   question you cannot answer from the log it appears in is a dead end. It keeps
   the dashed shape it already had and gains a real tap target. */
.wh-diary--tap { display: block; text-decoration: none; color: inherit; }
.wh-diary--tap:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The body's two paragraphs are <span> in BOTH variants now (a <p> may not live
   inside an <a> that also wraps a heading row), so they have to be told to be
   blocks — an inline span would ignore its own margin-top and run into the head. */
.wh-diary__text, .wh-diary__note { display: block; }
.wh-diary__answer {
  display: inline-flex; align-items: center; min-height: 34px; margin-top: 8px;
  color: var(--accent); font-size: .84rem; font-weight: 650;
}
.wh-diary__answer::after { content: " ›"; margin-left: 4px; }

/* ── The fault family (§6.6) ───────────────────────────────────────────────── */
/* The banner: the rollup, persisting while anything is unacknowledged, with the
   rule that acknowledging is not fixing directly under it. */
.wh-fault-banner {
  margin-bottom: 12px; padding: 11px 14px;
  background: var(--card-bg); border: 1px solid var(--line);
  border-left: 4px solid var(--text);
  border-radius: var(--radius); box-shadow: var(--shadow);
}
.wh-fault-banner__line {
  font-size: .88rem; font-weight: 650; color: var(--text); line-height: 1.45;
  font-variant-numeric: tabular-nums;
}
.wh-fault-banner__note {
  margin-top: 4px; font-size: .78rem; color: var(--muted); line-height: 1.45;
}

/* One fault. Solid dark rule, and the card keeps its own ink: a red wash would
   make the SECOND fault unreadable, and this family exists to be read. */
.wh-fault {
  margin-bottom: 10px; padding: 12px 14px;
  background: var(--card-bg); border: 1px solid var(--line);
  border-left: 4px solid var(--text);
  border-radius: var(--radius); box-shadow: var(--shadow);
}
/* Acknowledged: still here (the log is the point), just no longer shouting. */
.wh-fault--acked { border-left-color: var(--faint); box-shadow: none; }
.wh-fault__head { display: flex; align-items: baseline; gap: 10px; }
.wh-fault__title {
  min-width: 0; flex: 1 1 auto;
  font-size: .96rem; font-weight: 700; color: var(--text); line-height: 1.3;
}
.wh-fault__when {
  flex: none; font-size: .76rem; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.wh-fault__what { margin-top: 5px; font-size: .85rem; color: var(--text); line-height: 1.5; }
.wh-fault__mean { margin-top: 5px; font-size: .82rem; color: var(--text-2); line-height: 1.5; }
.wh-fault__acked {
  margin-top: 9px; padding-top: 9px; border-top: 1px solid var(--line-soft);
  font-size: .78rem; color: var(--muted); line-height: 1.45;
}
.wh-fault__none { margin-bottom: 10px; font-size: .82rem; color: var(--muted); line-height: 1.5; }
.wh-fault .wh-actions { margin-top: 11px; }

/* Home's winter card carries the same rollup — one line and a door, in the same
   dark rule, so the family reads identically on the two screens it appears on. */
.wh-faultline {
  margin-top: 11px; padding: 10px 12px;
  border-left: 3px solid var(--text); background: var(--bg);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.wh-faultline__text {
  font-size: .84rem; font-weight: 650; color: var(--text); line-height: 1.45;
  font-variant-numeric: tabular-nums;
}
.wh-faultline .wh-actions { margin-top: 8px; }

/* The centre's one extra line on a heating ask (UI §6.4b) — the deadline, before
   you open it. Quieter than the body it follows: it is a fact about the ask, not
   more of the message. */
.m-notif-item__meta {
  display: block; margin-top: 3px;
  /* Same ink and weight as the body above it (frame 3-1): a deadline is a fact
     ABOUT the ask, not a louder version of it. It was briefly 600/--text-2 and
     read as the most important thing in the row, which is exactly backwards —
     the message is what you scan, the expiry is what you check. */
  font-size: .74rem; color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* ── The notice block + the runbook (UI §10, §11, item U9) ───────────────── */

/* The notice block (wireframe 6-2's shape): a sentence that qualifies
   everything above it — a stale forecast, a server that did not answer — gets
   a neutral block with a dark rule down its left edge, deliberately OUTSIDE the
   fault family's vocabulary (no amber, no red, no dashed edge): it is not a
   problem with the heating. The rail is --text-2, so in dark it is a light bar
   on a dark card and reads identically. */
.wh-saved {
  margin-top: 12px; padding: 11px 13px;
  background: var(--card-bg); border: 1px solid var(--line);
  border-left: 4px solid var(--text-2);
  border-radius: var(--radius); box-shadow: var(--shadow);
}
.wh-saved__title {
  font-size: .92rem; font-weight: 700; color: var(--text); line-height: 1.3;
  font-variant-numeric: tabular-nums;
}
.wh-saved__text { margin-top: 3px; font-size: .84rem; color: var(--text-2); line-height: 1.45; }

/* The stale-forecast notice (heating repairs U1). The "block" shape IS
   `.wh-saved` — same neutral rail, same outside-the-fault-family weight,
   because while the last schedule carries on nothing is wrong at the boiler.
   The "inset" shape lives inside a card that already has a head (Home, the Plan
   tab's running card): the saved block's rail and type on a --bg wash, with no
   border or shadow of its own, so it is not a card in a card. The attention tone
   (the carried-on schedule ran out, planning has stopped) moves the rail to the
   plan-problem ink and changes nothing else. */
.wh-fc__title {
  font-size: .92rem; font-weight: 700; color: var(--text); line-height: 1.3;
}
.wh-fc__text {
  margin-top: 3px; font-size: .84rem; color: var(--text-2); line-height: 1.45;
  font-variant-numeric: tabular-nums;
}
.wh-fc__note { margin-top: 5px; font-size: .78rem; color: var(--muted); line-height: 1.45; }
.wh-fc--inset {
  margin-top: 10px; padding: 9px 11px;
  background: var(--bg); border-left: 3px solid var(--text-2);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.wh-fc--inset .wh-fc__title { font-size: .86rem; }
.wh-fc--inset .wh-fc__text { font-size: .8rem; }
.wh-fc--inset .wh-fc__note { font-size: .76rem; }
.wh-fc--attention { border-left-color: var(--warn-ink); }
/* The ran-out Home card carries the notice's sentences as its own body. */
.wh-fc__text--card { margin-top: 6px; }

/* ── The band-top stop (heating repairs U2, LAH's rule 3) ────────────────────
   A PLAN-family state — the rule working, not a fault — so it lives on the warn
   rail its card already carries (.wh-card--risk) and adds no colour of its own.
   Each sentence is its own paragraph on one rhythm: the stopping room and its
   reading, the room left cold, when it lifts. The two notes are quieter (they
   qualify, they do not add), and the door to widen is the one primary action. */
.wh-stop { margin-top: 8px; }
.wh-card__title + .wh-stop { margin-top: 6px; }
.wh-stop__line {
  margin-top: 6px; font-size: .88rem; color: var(--text); line-height: 1.45;
  font-variant-numeric: tabular-nums;
}
.wh-stop__line:first-child { margin-top: 0; }
.wh-stop__line b { font-weight: 700; }
.wh-stop__note {
  margin-top: 6px; font-size: .78rem; color: var(--muted); line-height: 1.45;
  font-variant-numeric: tabular-nums;
}
/* A long room name wraps INSIDE the pill instead of pushing it off the card:
   the pill is a flex item, and without min-width:0 its label sets its width. */
.wh-stop__act { max-width: 100%; min-width: 0; text-align: center; }

/* ── The controller's own hold (heating repairs U6, P6b) ─────────────────────
   Timed (an hour after the controller ended a call itself): the stop's rhythm on
   the warn rail — why, then the one thing to do now, quieter. `needs_person`
   (a second such stop in 24 h): the loud block, because nothing will heat the
   house until somebody acts — the way out as its title, why under it, then the
   two doors and the same quiet note. No role=alert: the card re-renders on every
   poll and a live region would re-announce it each minute. */
.wh-hold { margin-top: 8px; }
.wh-card__title + .wh-hold, .wh-card__head + .wh-hold { margin-top: 8px; }
.wh-hold__line {
  font-size: .88rem; color: var(--text); line-height: 1.45; font-variant-numeric: tabular-nums;
}
.wh-hold__note {
  margin-top: 8px; font-size: .78rem; color: var(--muted); line-height: 1.45;
}
.wh-hold > .wh-loud { margin-top: 0; }
.wh-hold .wh-loud__title { margin: 0; }
.wh-hold .wh-loud__text { margin: 4px 0 0; }
.wh-hold .wh-actions { margin-top: 12px; }
/* A long profile name wraps inside its pill rather than pushing it off the card. */
.wh-hold__act { max-width: 100%; min-width: 0; text-align: center; }
/* As a card of its own (#heating's lead, #schedule's head): the stop card's head. */
.wh-holdcard .wh-card__title + .wh-hold { margin-top: 6px; }
.section > .wh-holdcard { margin-bottom: 12px; }

/* ── The hand-back after a stopped plan (heating repairs U8, P4) ─────────────
   `/now`'s `last_release` while it is still retrying or has given up, with no
   plan running: the stop card's rhythm on the same warn rail — the title, then
   the one sentence (what went out, until when, or "set it by hand"). */
.wh-hb__text {
  margin-top: 6px; font-size: .88rem; color: var(--text); line-height: 1.45;
  font-variant-numeric: tabular-nums;
}
/* #schedule's head: the list's own rhythm (every follower carries margin-top 12px). */
.section > .wh-hb.wh-lead { margin-top: 12px; }
/* The Plan tab's running card: under the when-line, a card inside a card, so the
   block keeps its words and drops nothing but the outer shell. */
.ph-card .wh-hold { margin-top: 6px; }

/* Disarmed: the plan is shown as a plan. The held-by-hand chip's quiet shape —
   a statement of fact, not an alarm. On #schedule it stands alone in the list,
   so it takes the list's own spacing. */
.wh-disarmed { line-height: 1.4; }
.section > .wh-disarmed { margin: 0 0 12px; }

/* The monitor's room rows: which room stopped the heating (or was left cold by
   it), and a room whose sensor is out of the rules (§6.6, default 8). The
   nudge row's hairline, so a row carrying several reads as one list. */
.wh-mon__stop, .wh-mon__gap {
  margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line-soft);
  font-size: .84rem; color: var(--text-2); line-height: 1.5;
}
.wh-mon__stop { color: var(--warn-ink); font-weight: 600; }
.wh-mon__stop + .wh-mon__gap { margin-top: 4px; padding-top: 0; border-top: 0; }

/* ── The widen sheet (#widen/<room>) ─────────────────────────────────────────
   The ask sheet's box, with one stepper in it. Edges are the point (default
   3), so the top edge is in weight and its reading sits beside it. */
.wh-widen__box .wh-row { align-items: center; }
.wh-widen__box .wh-row__k { flex-basis: 64px; }
.wh-widen__reads { color: var(--muted); font-weight: 400; }
/* Frozen while the save is with the server, and "−" at the saved band: a
   stepper that looks live and does nothing is a dead control. */
.wh-widen .obj-stepper__btn:disabled { opacity: .4; cursor: default; }
.wh-widen__verdict {
  margin-top: 12px; font-size: .88rem; color: var(--text); line-height: 1.5;
  font-variant-numeric: tabular-nums;
}
.wh-widen__verdict--lifts { font-weight: 650; }
.wh-widen__outcome {
  margin-top: 2px; font-size: .95rem; color: var(--text); line-height: 1.5;
  font-variant-numeric: tabular-nums;
}
.wh-widen__more { display: flex; margin-top: 10px; }
.wh-widen__more .wh-act { flex: 1 1 0; }
.wh-widen .wh-actions .wh-act { flex: 1 1 0; }

/* The two limits the alarm must carry ON the card (§6.1): quieter than the note
   above it, because it qualifies the claim rather than adding to it. */
.wh-alarm__limits {
  margin-top: 11px; padding-top: 10px; border-top: 1px solid var(--line-soft);
  font-size: .76rem; color: var(--muted); line-height: 1.45;
}

/* The empty runbook screen's third line: what would have prevented it. */
.wh-empty__hint { margin-top: 8px; font-size: .8rem; color: var(--muted); line-height: 1.5; }

/* The runbook card (§11). The accent rail is deliberate and it is the only card
   in the winter set that gets one at full weight: this is not a warning, it is
   the app's single most useful instruction, and on the day it is read it has to
   be the thing the eye lands on. */
.wh-run {
  margin-top: 12px; padding: 13px 14px;
  background: var(--card-bg); border: 1px solid var(--accent-border);
  border-left: 4px solid var(--accent);
  border-radius: var(--radius); box-shadow: var(--shadow);
}
.wh-run__title {
  font-size: 1rem; font-weight: 700; color: var(--text); line-height: 1.3;
  letter-spacing: -.01em;
}
.wh-run__derived {
  margin-top: 3px; font-size: .8rem; color: var(--text-2); line-height: 1.45;
  font-variant-numeric: tabular-nums;
}
/* A real table, because it IS one — and because somebody is reading it a row at
   a time while typing into another device, which is exactly what row headers and
   a fixed layout are for. `table-layout: fixed` keeps the time column from
   collapsing when a long "why" arrives. */
.wh-run__table {
  margin-top: 11px; width: 100%; table-layout: fixed;
  border-collapse: collapse; font-variant-numeric: tabular-nums;
}
.wh-run__table th, .wh-run__table td {
  padding: 9px 8px; text-align: left; vertical-align: baseline;
  border-bottom: 1px solid var(--line-soft);
}
.wh-run__table thead th {
  padding-top: 0; font-size: .72rem; font-weight: 650; color: var(--muted);
  text-transform: uppercase; letter-spacing: .04em;
  border-bottom: 1px solid var(--line);
}
.wh-run__table tbody tr:last-child th,
.wh-run__table tbody tr:last-child td { border-bottom: 0; }
.wh-run__table th:first-child, .wh-run__table td:first-child { padding-left: 0; }
.wh-run__table th:last-child, .wh-run__table td:last-child { padding-right: 0; }
/* The two typed values carry the weight; the reason is context. */
.wh-run__time { font-size: .92rem; font-weight: 650; color: var(--text); }
.wh-run__set { font-size: .92rem; font-weight: 700; color: var(--text); }
.wh-run__why { font-size: .8rem; color: var(--text-2); line-height: 1.35; }
/* The column widths belong on the HEAD row: with `table-layout: fixed` the first
   row decides, so a width set only on the body's cells is silently ignored and
   the unspecified columns split the rest equally (the time column was 112px wide
   for "05:40"). Two columns: the dial value takes the room the missing third
   would have had. Three: the reason gets what is left, and wraps. */
.wh-run__table th:first-child { width: 4.4rem; }
.wh-run__table th:nth-child(2), .wh-run__table td:nth-child(2) { width: 6.4rem; }
.wh-run__note { margin-top: 11px; font-size: .8rem; color: var(--text-2); line-height: 1.5; }
.wh-run .wh-actions { margin-top: 12px; }
.wh-act.is-done { background: var(--card-bg); border-color: var(--line); color: var(--muted); }
.wh-run__status { margin-top: 8px; font-size: .78rem; color: var(--warn-ink); line-height: 1.45; }
.wh-run__status[hidden] { display: none; }

/* ── Tuning: the two write paths (UI §9.2/§9.3, item U7) ─────────────────────
   Every colour is a token, so both themes come free.

   One deliberate weight decision, from the frames: the lockshield cost box
   carries the FAULT weight (a solid --text rule, .wh-fault's family) and nothing
   else on these screens does. It is the one action here that invalidates other
   rooms' measurements, and a box that looked like the rest would be a warning
   nobody reads. The dial sheet has no such box — a dial move is cheap and
   reversible, and dressing it up would train the eye to ignore the real one. */

/* The room screen's dial row is `hidden` when there is nothing to record against
   — and `.m-setup-link` sets `display:flex`, which OUTRANKS the UA's
   `[hidden]{display:none}`. Without this line the row paints on every room
   screen all summer, which is this codebase's most repeated CSS bug (see the
   .m-sheet[hidden] note above). Written on the shared class, so the next hidden
   setup-link inherits the fix. */
.m-setup-link[hidden] { display: none; }

.wt-lead {
  margin: 2px 0 14px;
  font-size: .88rem; color: var(--text); line-height: 1.55;
}
.wt-sec {
  margin: 18px 0 10px;
  font-size: .74rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--muted);
}
.wt-hint { margin: 8px 2px 0; font-size: .78rem; color: var(--muted); line-height: 1.5; }
.wt-hint[hidden] { display: none; }
.wt-note { margin: 10px 2px 0; font-size: .8rem; color: var(--text-2); line-height: 1.5; }
.wt-note[hidden] { display: none; }
.wt-foot {
  margin: 18px 2px 4px; padding-top: 13px; border-top: 1px solid var(--line-soft);
  font-size: .78rem; color: var(--muted); line-height: 1.5;
}
.wt-sum {
  margin: 14px 2px 0; font-size: .82rem; color: var(--text-2);
  font-variant-numeric: tabular-nums;
}

/* The two doors. A row, not a button: each leads to a screen, and the chevron
   says so. --off is a door that exists and cannot be walked through yet — kept
   VISIBLE with its reason, because a screen's own affordance disappearing is
   the least explainable state there is. */
.wt-doors { display: flex; flex-direction: column; gap: 10px; }
.wt-door {
  display: flex; align-items: center; gap: 12px;
  min-height: 60px; padding: 13px 14px;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--card-bg); color: inherit; text-decoration: none;
}
.wt-door__text { min-width: 0; flex: 1 1 auto; display: block; }
.wt-door__title { display: block; font-size: .95rem; font-weight: 650; color: var(--text); }
.wt-door__sub {
  display: block; margin-top: 3px; font-size: .78rem; color: var(--text-2); line-height: 1.45;
}
.wt-door__chev { flex: none; color: var(--accent); font-size: 1.15rem; }
.wt-door:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.wt-door--off { background: var(--bg); border-style: dashed; }
.wt-door--off .wt-door__title { color: var(--text-2); }
/* No chevron on a door that leads nowhere yet — a "›" is a promise of a screen. */
.wt-door--off .wt-door__chev { display: none; }

/* The map's own missing state. Not an error box: an inset panel in the accent
   family, the same weight the app uses for "this hasn't happened yet". */
.wt-nomap {
  margin-top: 14px; padding: 12px 14px;
  border: 1px solid var(--accent-border); border-radius: var(--radius);
  background: var(--accent-soft);
}
.wt-nomap__line { font-size: .86rem; font-weight: 650; color: var(--text); line-height: 1.45; }
.wt-nomap__note { margin-top: 6px; font-size: .8rem; color: var(--text-2); line-height: 1.5; }

/* Pick a radiator (both flows start here). 58px rows, chevron right. */
.wt-pick { display: flex; flex-direction: column; }
.wt-pick__row {
  display: flex; align-items: center; gap: 12px;
  min-height: 58px; padding: 11px 2px;
  border-bottom: 1px solid var(--line-soft);
  color: inherit; text-decoration: none;
}
.wt-pick__row:last-child { border-bottom: 0; }
.wt-pick__text { min-width: 0; flex: 1 1 auto; display: block; }
.wt-pick__title { display: block; font-size: .9rem; font-weight: 600; color: var(--text); }
.wt-pick__meta {
  display: block; margin-top: 2px; font-size: .78rem; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.wt-pick__chev { flex: none; color: var(--accent); font-size: 1.1rem; }
.wt-pick__row:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* A labelled value. The label column is fixed so Was/Now/Direction line up on
   one left edge; the control side is free to be a stepper or a plain field. */
.wt-field {
  display: flex; align-items: center; gap: 12px;
  margin-top: 12px; padding: 10px 0 0; border-top: 1px solid var(--line-soft);
}
.wt-field:first-child { border-top: 0; padding-top: 0; }
.wt-field--flat { margin-top: 0; }
.wt-field__k { flex: 0 0 108px; font-size: .84rem; color: var(--text-2); }
.wt-field__v {
  min-width: 0; flex: 1 1 auto;
  display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: 4px 10px;
}
.wt-field__strong { font-weight: 650; color: var(--text); }
/* Its own line, right-aligned: inline it stole ~59px from the value column, so
   the un-noted field below (a stepper) put its input 7px to the right of this
   one — two stacked boxes that did not line up. */
.wt-field__note { flex: 1 0 100%; text-align: right; font-size: .76rem; color: var(--muted); }

/* The number itself. Tabular, centred, big enough to read at arm's length while
   kneeling at a radiator. */
.wt-input {
  width: 4.6rem; min-width: 0; min-height: 44px; padding: 9px 10px;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--card-bg); color: var(--text);
  font: inherit; font-size: 1rem; font-weight: 650; text-align: center;
  font-variant-numeric: tabular-nums;
}
.wt-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* A plain value box sits exactly where a stepped one's input sits: 44px button
   + 8px gap of reserved room on the right, so Was and Now share a centre line. */
.wt-plain { display: flex; align-items: center; margin-right: 52px; }
.wt-stepper { display: flex; align-items: center; gap: 8px; }
.wt-stepper__btn {
  flex: none; width: 44px; height: 44px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--accent-border); border-radius: 999px;
  background: var(--card-bg); color: var(--accent);
  font-size: 1.15rem; font-weight: 700; line-height: 1; cursor: pointer;
}
.wt-stepper__btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Direction. Two stacked options rather than a two-up segment: "turned up from
   below" does not fit half of 390px without wrapping to two lines, and a
   direction that has to be read twice is a direction that gets guessed. */
.wt-opts {
  margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--line-soft);
  display: flex; flex-direction: column; gap: 8px;
}
.wt-opts__k { font-size: .84rem; color: var(--text-2); }
.wt-opt {
  display: flex; align-items: center; min-height: 46px; padding: 10px 14px;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--card-bg); color: var(--text);
  font: inherit; font-size: .88rem; text-align: left; cursor: pointer;
}
.wt-opt.is-on {
  border-color: var(--accent); background: var(--accent-soft);
  color: var(--accent); font-weight: 650;
}
.wt-opt:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Too small to tell / nothing changed. Warn ink: it does not stop the record,
   it says what the record can and cannot answer. */
.wt-warn {
  margin: 12px 0 0; padding: 9px 12px;
  border-left: 3px solid var(--warn-ink); border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  background: var(--warn-bg); color: var(--warn-ink);
  font-size: .8rem; line-height: 1.5;
}
.wt-warn[hidden] { display: none; }
.wt-err {
  margin: 10px 0 0; padding: 9px 12px; border-radius: var(--radius-sm);
  background: var(--bad-bg); color: var(--bad-ink); font-size: .8rem; line-height: 1.45;
}

/* The cost of touching a lockshield — the fault weight, deliberately (§9.2). */
.wt-cost {
  margin-top: 14px; padding: 12px 14px;
  border: 1px solid var(--line); border-left: 3px solid var(--text);
  border-radius: 0 var(--radius) var(--radius) 0; background: var(--card-bg);
}
.wt-cost__title { margin: 0; font-size: .9rem; font-weight: 700; color: var(--text); }
.wt-cost__text { margin-top: 6px; font-size: .85rem; color: var(--text); line-height: 1.55; }

/* The baseline procedure. Numbered because it is done in order, at the valve. */
.wt-steps { margin: 0; padding: 0; list-style: none; counter-reset: wtstep; }
.wt-steps__item {
  position: relative; counter-increment: wtstep;
  padding: 0 0 12px 34px; font-size: .86rem; color: var(--text); line-height: 1.5;
}
.wt-steps__item::before {
  content: counter(wtstep);
  position: absolute; left: 0; top: -1px;
  width: 24px; height: 24px; border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  background: var(--accent-soft); color: var(--accent);
  font-size: .76rem; font-weight: 700;
}

/* Recorded, then the honest wait. */
.wt-done {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 13px 14px; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--card-bg);
}
.wt-done__tick {
  flex: none; width: 26px; height: 26px; border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  background: var(--good-bg); color: var(--good-ink); font-size: .85rem; font-weight: 700;
}
.wt-done__text {
  min-width: 0; flex: 1 1 auto; margin: 3px 0 0;
  font-size: .86rem; color: var(--text); line-height: 1.5; font-variant-numeric: tabular-nums;
}
.wt-remeasure {
  padding: 13px 14px; border: 1px dashed var(--accent-border); border-radius: var(--radius);
  background: var(--card-bg);
}
.wt-remeasure__title { margin: 0; font-size: .9rem; font-weight: 700; color: var(--text); }
.wt-remeasure__text { margin-top: 6px; font-size: .84rem; color: var(--text-2); line-height: 1.55; }
.wt-remeasure__note { margin-top: 8px; font-size: .78rem; color: var(--muted); line-height: 1.5; }

/* Two equal actions, 46px, primary right — the ask sheet's own rule. */
.wt-actions { display: flex; gap: 10px; margin-top: 16px; }
.wt-btn { flex: 1 1 0; min-width: 0; min-height: 46px; justify-content: center; text-align: center; }
.wt-sheet { padding-bottom: 4px; }
.wt-q { margin: 4px 0 2px; font-size: .95rem; font-weight: 700; color: var(--text); }
.wt-head { min-width: 0; flex: 1 1 auto; }
.wt-head__lead { margin-top: 3px; white-space: normal; line-height: 1.45; }

/* The dial ask's link to the record sheet. Outside #nudge-actions on purpose:
   it is a navigation, not an answer. */
.wh-ask__record { display: flex; margin-top: 10px; }
.wh-ask__record .wh-act { flex: 1 1 0; }

/* ═══ The Plan tab's Heating half (UI §1, §4, §5) — item U1 ═══════════════════
   `ph-*` is the vocabulary of the profile library, the switch confirm sheet and
   the day editor. It borrows rather than re-invents wherever an idiom exists:
   `.m-sheet` for the sheets, `.obj-stepper` / `.obj-seg` for the edit sheet's
   controls (§4.2 asks for "the steppers the objective sheet already uses"),
   and `.wh-iv` / `.wh-costrow` / `.wh-infeas` for the inline compiled intervals. */

/* The product switch. It is the first thing on the tab and the most consequential
   control on it, so it takes a real 44px row rather than the 30px chip the
   inline mode-toggle is elsewhere. */
/* No horizontal padding of its own: the view already insets its content by
   16px, and a second 16 here left the switcher 32px narrower than every card
   under it — the one element on the screen that shared edges with nothing. */
.ph-segbar { padding: 14px 0 6px; max-width: 720px; margin: 0 auto; }
.ph-segbar .mode-toggle__btn {
  min-height: 44px; font-size: .88rem; font-weight: 650; color: var(--text-2);
}
/* The one segment in the app that FILLS when active. Everywhere else a segment
   picks a view of one thing (a range, an overlay) and the tinted state is right;
   this one picks between two PRODUCTS, and "which half of the Plan tab am I on"
   has to be unmistakable from across the room.
   The selector carries `.mode-toggle.m-seg--inline` deliberately: the shared
   tinted rule is (0,4,0) and a plain `.ph-segbar .mode-toggle__btn.is-active`
   is (0,3,0), so it lost and the segment shipped tinted — caught in a shot. */
.ph-segbar .mode-toggle.m-seg--inline .mode-toggle__btn.is-active {
  background: var(--accent); color: var(--on-accent); font-weight: 700;
}

.ph-lead { margin: 2px 0 14px; font-size: .82rem; color: var(--text-2); line-height: 1.5; }
.ph-sub {
  margin: 18px 0 8px; font-size: .72rem; font-weight: 650; letter-spacing: .07em;
  text-transform: uppercase; color: var(--faint);
}
.ph-sub:first-of-type { margin-top: 4px; }

/* One profile. The active card carries an accent rail; the others are plain
   cards with the switch action on the right. */
.ph-card {
  display: flex; align-items: center; gap: 12px;
  margin-bottom: 10px; padding: 13px 14px;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--card-bg);
}
.ph-card--active {
  display: block; border-color: var(--accent-border);
  /* 3px bar + 12px padding = the 1px border + 14px padding of every other card,
     so the names down the list share one left edge instead of stepping 1px. */
  border-left: 3px solid var(--accent); padding-left: 12px;
}
.ph-card__what {
  min-width: 0; flex: 1 1 auto; display: flex; flex-direction: column; gap: 3px;
  color: inherit; text-decoration: none;
}
.ph-card__head { display: flex; align-items: center; gap: 10px; }
.ph-card__name { min-width: 0; font-size: 1.02rem; font-weight: 700; color: var(--text); }
/* The whole card body opens the editor, so it says so. The active card carries
   an explicit "Edit ›" button instead — every profile is editable, and a card
   with no way in would be the only screen in the app that hides its own door. */
.ph-card__what .ph-card__name::after { content: " ›"; color: var(--faint); font-weight: 600; }
.ph-card--active .ph-card__name { flex: 1 1 auto; }
.ph-card__line { margin: 0; font-size: .8rem; color: var(--text-2); line-height: 1.45; }
.ph-card--active .ph-card__line { margin-top: 4px; }
.ph-card__when {
  margin: 6px 0 0; font-size: .88rem; color: var(--text); font-variant-numeric: tabular-nums;
}
.ph-card__when b { font-weight: 700; }
.ph-card__when--idle { color: var(--text-2); font-size: .82rem; }
.ph-pill {
  flex: none; padding: 3px 11px; border-radius: 999px; font-size: .7rem; font-weight: 650;
  color: var(--accent); background: var(--accent-soft); border: 1px solid var(--accent-border);
}
/* A half-made edit, marked on the card whose summary no longer describes it
   (item U2). Warn colours rather than accent: this is not a state to admire, it
   is a discrepancy between what the card says and what the reader last typed —
   and the card's own `›` is already the way in to fix it. `align-self: start`
   so the chip is its own width inside the card's column, not a full-width bar. */
.ph-unsaved {
  align-self: flex-start; margin-top: 2px; padding: 2px 9px; border-radius: 999px;
  font-size: .68rem; font-weight: 650; letter-spacing: .01em;
  color: var(--warn-ink); background: var(--warn-bg);
  border: 1px solid color-mix(in srgb, var(--warn-ink) 28%, transparent);
}
/* The active card is a column of its own, so the chip needs the same treatment
   one level up — and it sits under the summary line it contradicts. */
.ph-card--active .ph-unsaved { display: inline-block; margin-top: 8px; }

.ph-use {
  flex: none; min-height: 44px; padding: 0 18px; border: none; border-radius: 12px;
  background: var(--accent); color: var(--on-accent); font-size: .88rem; font-weight: 650; cursor: pointer;
}
.ph-edit {
  display: inline-flex; align-items: center; min-height: 44px; margin-top: 10px;
  padding: 0 15px; border: 1px solid var(--accent-border); border-radius: 999px;
  background: var(--card-bg); color: var(--accent);
  font-size: .84rem; font-weight: 650; text-decoration: none;
}
/* The running card's two doors (heating repairs U4): Edit and Stop heating,
   one row, same pill. Stop is the quieter of the two — it opens a confirm sheet
   that says what will happen before anything is sent, so it needs no alarm
   colour; the sheet carries the weight. Wraps rather than squeezes at 320px. */
.ph-card__acts { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; }
.ph-card__acts .ph-edit { margin-top: 0; }
.ph-stop {
  display: inline-flex; align-items: center; min-height: 44px; padding: 0 15px;
  border: 1px solid var(--line); border-radius: 999px; background: var(--card-bg);
  color: var(--text-2); font: inherit; font-size: .84rem; font-weight: 650; cursor: pointer;
}
.ph-new {
  width: 100%; min-height: 46px; margin-top: 6px; padding: 0 16px;
  border: 1px dashed var(--accent-border); border-radius: 12px; background: none;
  color: var(--accent); font-size: .88rem; font-weight: 650; cursor: pointer;
}
.ph-next { margin-top: 6px; }
.ph-none { margin: 4px 0 12px; font-size: .85rem; color: var(--text-2); line-height: 1.5; }

/* ── The house block: two floor cards + the replay door (floor-plan design §3) ─
   The plan is a NOW surface on a PLAN tab, so it lands last, under `Next calls`
   — and the role key printed up there under the first `on` interval serves the
   dots down here. There is deliberately no second key.

   Each floor is ONE card: a tinted band stating the house-level call, then the
   plan itself. The band is edge-to-edge (the card clips it) so it reads as a
   property of the whole floor rather than as a first row inside it. */
.ph-house { margin-top: 4px; }
.ph-floor {
  margin-bottom: 12px;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--card-bg); box-shadow: var(--shadow);
  overflow: hidden;                 /* the band's corners follow the card's */
}
.ph-floor__band {
  display: flex; align-items: baseline; gap: 8px;
  margin: 0; padding: 10px 14px;
  border-bottom: 1px solid var(--line);
  background: var(--line-soft);
  font-size: .78rem; line-height: 1.4; color: var(--text-2);
}
/* One text child, so the row's gap sits between the dot and the sentence and
   never inside it. `min-width: 0` because the sentence is the flex child that
   must be allowed to wrap at a narrow width rather than push the dot out. */
.ph-floor__says { min-width: 0; }
.ph-floor__band b { color: var(--text); font-weight: 650; }
.ph-floor__band--on { background: var(--accent-soft); }
/* A state dot, not a role shape: it sits in the band's own ink beside the words
   it belongs to, and never on a room. Hollow while nothing is running, so "off"
   is a shape as well as a sentence. */
.ph-floor__state {
  flex: none; align-self: center; width: 8px; height: 8px; border-radius: 50%;
  box-shadow: inset 0 0 0 1.5px currentColor;
}
.ph-floor__band--on .ph-floor__state { background: currentColor; }
.ph-floor__body { padding: 12px 14px 14px; }
.ph-floor__name { margin: 0 0 10px; font-size: .95rem; font-weight: 700; color: var(--text); }
.ph-floor__plan { position: relative; }

/* ── The two sheets ─────────────────────────────────────────────────────────── */
.ph-sheet__head { display: flex; align-items: baseline; gap: 12px; }
.ph-sheet__title {
  min-width: 0; flex: 1 1 auto; font-size: 1.08rem; font-weight: 700; color: var(--text);
}
.ph-sheet__lead { margin: 6px 0 0; font-size: .82rem; color: var(--text-2); line-height: 1.5; }
.ph-del {
  flex: none; min-height: 40px; padding: 0 10px; border: none; background: none;
  color: var(--bad-ink); font-size: .84rem; font-weight: 600; cursor: pointer;
}

.ph-rows {
  margin-top: 14px; border: 1px solid var(--line); border-radius: 12px; overflow: hidden;
}
.ph-row {
  display: flex; align-items: baseline; gap: 12px; padding: 11px 13px;
  border-top: 1px solid var(--line-soft);
}
.ph-row:first-child { border-top: none; }
.ph-row__k { flex: 0 0 88px; font-size: .8rem; color: var(--text-2); }
.ph-row__v {
  min-width: 0; flex: 1 1 auto; text-align: right; font-size: .86rem; color: var(--text);
  line-height: 1.45; font-variant-numeric: tabular-nums;
}
/* The two cost figures sit in the same table as the consequences, each with its
   own band and NEVER a difference between them (spec §12.1). */
.ph-row--cost { display: block; }
.ph-row--was { opacity: .68; }
.ph-note { margin: 10px 0 0; font-size: .8rem; color: var(--text-2); line-height: 1.5; }
.ph-skels { margin-top: 14px; display: flex; flex-direction: column; gap: 10px; }
.ph-skels .wh-skel { height: 46px; }

/* The designed wait (item U1a). Both of this sheet's requests solve the whole
   house, so the honest answer to "is it stuck?" is a number that moves: what is
   usual on the left, how long THIS one has taken on the right. The counter is
   anchored to the right edge so it grows leftwards — a left-aligned one would
   shove the expectation sideways every time it gained a digit — and tabular, so
   the digits themselves do not shuffle. Nothing here animates: the seconds are
   the liveness signal, and the skeleton below already carries the shimmer (and
   drops it under prefers-reduced-motion with everything else). */
.ph-wait {
  margin-top: 14px; padding: 11px 13px;
  border: 1px solid var(--line); border-radius: 12px;
}
.ph-wait__meta {
  margin: 0; display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; font-size: .78rem; color: var(--muted);
}
.ph-wait__expect { min-width: 0; }
.ph-wait__el {
  flex: none; font-size: .82rem; font-weight: 650; color: var(--text-2);
  font-variant-numeric: tabular-nums;
}
.ph-wait__note { margin: 8px 0 0; font-size: .78rem; color: var(--text-2); line-height: 1.5; }
/* Why `Not now` is dead, immediately above `Not now` — the sheet is tall enough
   while a switch is going through that a reason left up in the wait strip would
   be off screen beside the button it explains. */
.ph-wait__hold { margin-top: 14px; }
/* The redirected tap's explanation leads the body, so it is the first thing
   read after the title that names the profile actually switching. */
.ph-wait__rejoin { margin-top: 12px; }
/* Same slot, same reason: what the consequences below are being computed FROM
   has to be read before them, not after. Tinted rather than left as a third
   grey paragraph — the reader has just come from the editor believing the table
   below includes their change, and a sentence that reads like the lead above it
   is a sentence they will skim. Same warn family as the card's chip: one colour
   for one fact, said twice on the way here. */
.ph-wait__unsaved {
  margin-top: 12px; padding: 10px 12px; border-radius: 10px;
  color: var(--warn-ink); background: var(--warn-bg);
  border: 1px solid color-mix(in srgb, var(--warn-ink) 22%, transparent);
}

.ph-actions { display: flex; gap: 10px; margin-top: 20px; }
.ph-btn {
  flex: 1 1 0; min-height: 48px; padding: 0 14px; border: 1px solid var(--line);
  border-radius: 12px; background: var(--card-bg); color: var(--accent);
  font-size: .92rem; font-weight: 650; cursor: pointer;
}
.ph-btn--go { border: none; background: var(--accent); color: var(--on-accent); }
.ph-btn:disabled { opacity: .45; cursor: default; }
.ph-err { margin-top: 12px; }

/* The stop sheet's one sentence (U4). It IS the sheet — what the thermostat
   goes back to — so it reads at body size in full ink, not as a grey lead. */
.hb-lead { margin: 10px 0 0; font-size: .95rem; color: var(--text); line-height: 1.5; }
#pstop-sheet .ph-note { margin-top: 8px; }

/* ── The day editor ─────────────────────────────────────────────────────────── */
/* Days are chosen ONCE for the profile (UI §4.2). "some" is a real third state:
   B7's own seed set mixes (the office is Mon-Fri inside a seven-day profile), so
   the chip is dashed — this app's mark for a question — rather than lying either
   way. */
.ph-days { display: flex; justify-content: space-between; gap: 6px; }
.ph-days--sheet { margin-top: 10px; }
.ph-day {
  flex: 0 1 44px; width: 44px; height: 44px; min-width: 0; border-radius: 999px;
  cursor: pointer; font-size: .88rem; font-weight: 650; font-variant-numeric: tabular-nums;
  border: 1px solid var(--line); background: var(--card-bg); color: var(--faint);
}
/* A day the WHOLE profile runs on is filled; a day only some of its windows run
   on is dashed (this app's mark for a question) and a day none runs on is a
   plain outline. Three states that must be told apart at a glance — the tinted
   "all" this started as was indistinguishable from the dashed "some" against the
   page background. */
.ph-day--all { border-color: var(--accent); background: var(--accent); color: var(--on-accent); }
.ph-day--some { border: 1px dashed var(--accent-border); color: var(--accent); }
.ph-daysnote { margin: 9px 0 0; font-size: .74rem; color: var(--muted); line-height: 1.45; }

.ph-ed { margin-top: 20px; }
/* The tick labels ride the same box the tracks do, so they line up with the hour
   they name. The two end labels are pushed inward instead of centred, or "00"
   and "24" would hang off the edges of the screen. */
.ph-ticks { position: relative; height: 15px; }
.ph-tick {
  position: absolute; top: 0; font-size: .69rem; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.ph-row-room { margin-top: 6px; }
.ph-rowhead { display: flex; align-items: center; gap: 8px; min-height: 40px; }
.ph-rowhead__name {
  min-width: 0; flex: 1 1 auto; font-size: .88rem; font-weight: 650; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* "18 ±1.5 · asks before changing plan" is too long to sit beside a room name
   on one line at 360 px. `flex: none` + max-width = its natural width, capped
   at 62% and wrapping inside the cap — so a short meta ("no windows — add one")
   never shares the squeeze with the name (a shrinkable meta did, and wrapped),
   and a long one never ellipsises the name to a stub. */
.ph-rowhead__meta {
  flex: none; max-width: 62%; text-align: right; line-height: 1.3;
  font-size: .8rem; color: var(--text-2); font-variant-numeric: tabular-nums;
}
.ph-rowhead__meta--none { color: var(--muted); font-style: italic; }
/* Quiet on purpose: the empty track beside it is the primary way to add a
   window, and eight bordered boxes marching down the right-hand edge competed
   with the room names. This is the keyboard/AT route to the same action. */
.ph-add {
  flex: none; width: 44px; height: 44px; border: none; border-radius: 10px;
  background: none; color: var(--accent); opacity: .75; font-size: 1.15rem;
  cursor: pointer; display: flex; align-items: center; justify-content: center;
}
.ph-add:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; opacity: 1; }
/* The track READS the day; it is never dragged (a 15-minute band is 4px at this
   width). Tapping empty track adds a window at that hour; tapping a band opens
   it. */
/* 44px tall so the row is a real tap column: the band is the primary control
   on this screen and was 22px, and a tap 2px off it used to ADD a window on top
   of the one you meant to edit. The handler resolves a tap by x against the
   bands first (profileEditor.ts), so the whole 44px opens the band under it. */
.ph-track {
  position: relative; height: 44px; border-radius: 9px; background: var(--track-bg);
  overflow: hidden; cursor: pointer;
}
/* Its own token, not `--line`: in dark the track IS `--line`, so a `--line`
   gridline was invisible the moment the track got a readable fill. */
.ph-grid { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--track-grid); }
/* The element is the full 44px of the track; the PAINT is the inset bar. The
   transparent borders + background-clip do that in one rule, so the band is a
   real 44px target and looks exactly as it did when it was a 22px one. */
.ph-band {
  position: absolute; top: 0; bottom: 0; padding: 0; border-radius: 5px;
  /* TOP AND BOTTOM ONLY. A left/right border would eat 6px of the band under
     border-box sizing and shift every window 3px off the hour it names — the
     one thing this track exists to get right. */
  border: 0 solid transparent; border-top-width: 3px; border-bottom-width: 3px;
  background: var(--accent); background-clip: content-box; cursor: pointer;
}
.ph-band:focus-visible { outline: 2px solid var(--text); outline-offset: 1px; }
/* A window that crosses midnight is two pieces at the two ends of one day. The
   edges that meet midnight are square, so the pair reads as one window cut by
   the end of the track rather than as two separate windows. */
.ph-band--tail { border-radius: 5px 0 0 5px; }
.ph-band--wrap { border-radius: 0 5px 5px 0; }
/* r11.8: a FLOOR has no top, so its bar fades out upward rather than ending in
   a hard edge — the band reads "at least", the floor's whole meaning. The
   bottom stays solid; content-box clipping keeps the 44px target. */
.ph-band--floor {
  background: linear-gradient(to top, var(--accent) 45%, color-mix(in srgb, var(--accent) 25%, transparent));
  background-clip: content-box;
}
.ph-hint { margin: 16px 0 0; font-size: .76rem; color: var(--muted); line-height: 1.5; font-style: italic; }
.ph-missing { margin: 10px 0 0; font-size: .74rem; color: var(--muted); line-height: 1.5; }
/* A widen from the heating stop landed under an unsaved draft that had changed
   that band itself (heating repairs U2): the draft is kept as typed, and says so
   above the days row — the first thing read on entering the editor. */
.ph-under {
  margin: 0 0 12px; padding: 9px 12px; border-radius: var(--radius-sm);
  background: var(--warn-bg); color: var(--warn-ink);
  font-size: .8rem; line-height: 1.45;
}

/* The editor's save bar. It cannot reuse `.build-cta`'s show rule (that one is
   scoped to `#view-plan.is-building`) and it sits inside a full-screen sub-view,
   which covers the tab bar — so it anchors to the bottom of the screen, not
   above a tab bar that is not there. */
.ph-cta {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 61;
  display: flex; align-items: center; gap: 12px;
  padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));
  background: var(--glass); backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px); border-top: 1px solid var(--line);
}
.ph-cta .build-cta__go { flex: 1 1 auto; }
/* The two shapes that carry words above the buttons (the save's wait and its
   refusal) stack instead: the bar is the only thing on screen at that moment
   that is saying anything, and a sentence squeezed beside two controls on a
   390px phone is a sentence nobody reads. */
.ph-cta--tall { flex-direction: column; align-items: stretch; gap: 10px; }
/* The buttons are one row inside a bar that is sometimes a column, so the row
   takes the bar's whole width in both arrangements rather than shrinking to its
   two buttons in the flat one. */
.ph-cta__row { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 12px; }
/* Neither of these buttons had a disabled face, which mattered little when the
   state lasted 20 ms and matters a lot now that a save can hold it for half a
   minute: a full-contrast control that ignores the finger is the complaint this
   plan item started from. */
.ph-cta .build-cta__go:disabled,
.ph-cta .ph-discard:disabled { opacity: .5; cursor: default; }
.ph-cta__row .build-cta__go { flex: 1 1 auto; }
.ph-cta__err { margin: 2px 0 0; font-size: .84rem; color: var(--bad-ink); line-height: 1.45; }
.ph-cta__errnote { margin: 0; font-size: .78rem; color: var(--text-2); line-height: 1.45; }
/* U1a's wait strip, inside the bar. It drops the border — the bar is already a
   box with a hairline of its own, and a second outline 10px inside the first
   reads as a card that slid down the screen — and keeps everything else: the
   expectation left, the counter right-anchored and tabular, the ladder note
   under both. */
.ph-wait--bar { margin-top: 0; padding: 0; border: none; border-radius: 0; }
.ph-wait__lead {
  margin: 0 0 6px; font-size: .82rem; color: var(--text); line-height: 1.45;
}
/* A refused save (U5): two long labels, neither squeezable at 390px, so the
   row stacks — the primary first, both the bar's full width and ≥44px tall. */
.ph-cta__row--stack { flex-direction: column; align-items: stretch; gap: 8px; }
.ph-cta__row--stack .build-cta__go,
.ph-cta__row--stack .ph-discard { flex: none; width: 100%; min-height: 44px; }
/* What the other phone saved, above the day — the same warn family as
   `.ph-under`, with a title and one line per window. */
.ph-conflict {
  margin: 0 0 12px; padding: 10px 12px; border-radius: var(--radius-sm);
  background: var(--warn-bg); color: var(--warn-ink); font-size: .8rem; line-height: 1.45;
}
.ph-conflict__title { margin: 0 0 4px; font-weight: 650; }
.ph-conflict__list { margin: 0; padding-left: 18px; }
.ph-conflict__list li { overflow-wrap: anywhere; }
.ph-conflict__list li + li { margin-top: 2px; }
.ph-conflict__same { margin: 0; }
/* U6 (A7): on the conflict face the reader's own windows carry a ring in the
   card's own warn ink, so the card's key and the marks read as one thing. The
   ring is an INSET shadow: it paints over the content-box bar and takes no
   width, so no band moves off the hour it names. */
.ph-band--mine { box-shadow: inset 0 0 0 2px var(--warn-ink); }
.ph-conflict__mine { margin: 8px 0 0; display: flex; align-items: center; gap: 8px; }
.ph-mine-key {
  flex: none; width: 22px; height: 12px; border-radius: 4px;
  background: var(--accent); box-shadow: inset 0 0 0 2px var(--warn-ink);
}
/* "Put my changes on top" refused (A4): the reason, under the list it is about. */
.ph-conflict__refused { margin: 8px 0 0; font-weight: 650; overflow-wrap: anywhere; }
/* After "Put my changes on top": the lead, then the named changes not applied. */
.ph-under__lead { margin: 0; }
.ph-under__sub { margin-top: 8px; }
.ph-discard {
  flex: none; min-height: 44px; padding: 0 16px; border: 1px solid var(--line);
  border-radius: 999px; background: var(--card-bg); color: var(--text-2);
  font-size: .84rem; font-weight: 600; cursor: pointer;
}

/* ── The edit sheet's fields ────────────────────────────────────────────────── */
.ph-field {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  width: 100%; margin-top: 10px; padding: 7px 8px 7px 14px; min-height: 58px;
  border: 1px solid var(--line); border-radius: 12px; background: var(--card-bg);
  color: var(--text); text-align: left;
}
.ph-field--time, .ph-field--btn { position: relative; cursor: pointer; }
.ph-field__k { flex: none; font-size: .9rem; color: var(--text-2); }
.ph-field__v {
  min-width: 0; flex: 1 1 auto; text-align: right; padding-right: 6px;
  font-size: 1rem; font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums;
}
.ph-field--btn .ph-field__v { padding-right: 0; }
/* The field already IS the box, so the shared stepper drops its own borders
   here — nested outlines read as two controls where there is one. The 40x44
   targets are unchanged. */
.ph-field .obj-stepper__btn,
.ph-field .obj-stepper__val { border: none; background: none; }
.ph-field .obj-stepper__val { min-width: 74px; }
.ph-field__chev { flex: none; width: 18px; text-align: center; color: var(--faint); font-size: .8rem; }
.ph-field--time input { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
.ph-seg { flex: 1 1 auto; display: grid; grid-template-columns: repeat(2, 1fr); gap: 6px; }
.ph-seg .obj-seg__btn { min-height: 42px; }
/* The nudge mode (2026-10-04): its choices are sentences ("Ask before changing
   the plan") because the old one-word labels were misread as gating the
   heating itself. Two sentences cannot share a row with the key at 360 px, so
   the key sits above and the two choices take the full width, each wrapping to
   two balanced lines rather than ellipsising the half that carries the
   meaning. */
.ph-field--choice {
  flex-direction: column; align-items: stretch; gap: 8px;
  padding: 10px 10px 10px 14px;
}
.ph-field--choice .ph-seg { width: 100%; }
.ph-field--choice .obj-seg__btn {
  min-width: 0; min-height: 52px; padding: 6px 10px;
  line-height: 1.3; text-wrap: balance;
}
.ph-fieldnote { margin: 8px 2px 0; font-size: .74rem; color: var(--muted); line-height: 1.45; }
.ph-input {
  min-width: 0; flex: 1 1 auto; border: none; background: none; color: var(--text);
  font-size: 1rem; font-weight: 650; text-align: right; padding: 0 6px 0 0;
}
.ph-input:focus { outline: none; }
/* The field rows all print their value right-aligned and bold, so an unstyled
   placeholder in the same slot reads as a name that is already set — and Create
   would then refuse a name the reader believed they had given. Italic, light and
   faint: unmistakably a hint. */
.ph-input::placeholder { color: var(--faint); font-weight: 400; font-style: italic; }

/* ── The gas fire: the evening lane + the priced suggestion (UI §8, item U6) ──
   Two pieces, both of which must survive being ABSENT: `fire_suggestion` is null
   until backend item B11 exists, and an evening with no call and no session
   renders nothing at all rather than an empty track. Nothing here is ever an
   action — there is no actuator (spec §1.4.3). */

.fl { margin-top: 18px; }
.fl__title { margin-top: 0; }
/* Inside a compiled interval there is no second card: the lane is a block of the
   card it is already in, separated by the same hairline the rows use. */
.fl--flat { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line-soft); }
.fl--flat .fl__card {
  padding: 0; background: none; border: 0; border-radius: 0; box-shadow: none;
}
.fl__card {
  padding: 12px 13px 13px;
  background: var(--card-bg); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
}

/* One lane, both heat sources. The track is the day editor's (`.ph-track`)
   geometry at a seven-hour span: a read-only rail, never dragged. */
.fl-track {
  position: relative; height: 24px; border-radius: 7px;
  background: var(--track-bg); overflow: hidden;
}
/* ONE rail, two heights. A fire lit DURING a call is the normal winter evening
   (spec §1.4.1(b) exists because of it), so the two tracks overlap constantly —
   at equal height the later one would simply hide the earlier, and the lane
   would lie about the very case it was built for. The boiler is the house, so
   it takes the full rail; the room's own appliance is inset and drawn over it,
   which reads as "inside that call" rather than "instead of it". */
.fl-bar {
  position: absolute; top: 3px; bottom: 3px; border-radius: 4px;
  background: var(--text-2); z-index: 1;
}
/* The fire is warm ink, the boiler is the page's own slate: two heat sources
   that must be told apart at a glance without a second data colour. */
.fl-bar--fire { top: 7px; bottom: 7px; background: var(--warn-ink); z-index: 2; }
/* A GUESS is hatched and dashed — the app's existing inferred-state style
   (`.device-glyph--guessed`), and what qualifies the price beside it. */
.fl-bar--guessed {
  background: var(--warn-bg);
  border: 1px dashed var(--warn-ink);
  background-image: repeating-linear-gradient(115deg,
    color-mix(in srgb, var(--warn-ink) 34%, transparent) 0 3px,
    transparent 3px 7px);
}
.fl-bar--call.fl-bar--guessed {
  background: var(--line-soft);
  border-color: var(--text-2);
  background-image: repeating-linear-gradient(115deg,
    color-mix(in srgb, var(--text-2) 34%, transparent) 0 3px,
    transparent 3px 7px);
}
/* Still running: the right edge is square, so the block reads as cut by the
   present moment rather than as a session that has ended. */
.fl-bar--open { border-top-right-radius: 0; border-bottom-right-radius: 0; }

/* Three labels on the track's own box, the ends pushed inward so neither hangs
   off a 390 px screen (the editor settled this argument first). */
.fl-ticks { position: relative; height: 15px; margin-top: 4px; }
.fl-tick {
  position: absolute; top: 0; font-size: .69rem; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.fl-tick--0 { left: 0; }
.fl-tick--1 { left: 50%; transform: translateX(-50%); }
.fl-tick--2 { right: 0; }

/* The legend IS the reading: one row per block, name left, times + price right.
   `min-width: 0` on the name so a long room word ellipses instead of shoving
   the figure off the card. */
.fl-rows { margin-top: 6px; border-top: 1px solid var(--line-soft); }
.fl-row {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 0 0;
}
.fl-row__name {
  display: flex; align-items: center; gap: 7px; min-width: 0; flex: 1 1 auto;
  font-size: .86rem; color: var(--text);
}
.fl-row__label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fl-row__meta {
  flex: none; font-size: .78rem; color: var(--text-2);
  font-variant-numeric: tabular-nums;
}
/* The swatches echo the rail's two heights, so the legend is readable as the
   same picture rather than as a separate colour key. */
.fl-swatch {
  flex: none; width: 20px; height: 12px; border-radius: 3px;
  background: var(--text-2);
}
.fl-swatch--fire { height: 8px; background: var(--warn-ink); }
.fl-swatch--guessed {
  background: var(--warn-bg); border: 1px dashed var(--warn-ink);
}
.fl-swatch--call.fl-swatch--guessed { background: var(--line-soft); border-color: var(--text-2); }
.fl-chip {
  flex: none; padding: 1px 8px; border-radius: 999px;
  font-size: .69rem; font-weight: 600; color: var(--muted);
  border: 1px dashed color-mix(in srgb, var(--muted) 45%, transparent);
}

/* Reach to the EXISTING correction sheet (UI §8.2) — the same two-tap sheet the
   Home pill opens, so a wrong guess is fixed where it was spotted. Only ever
   shown for the live guess: a finished session has none to retract. It IS the
   winter action button (`.wh-act`), not a lookalike — one action shape across
   the whole feature — so all this adds is where it sits. */
.fl-correct { margin-top: 12px; gap: 6px; }
.fl-foot { margin: 9px 0 0; font-size: .72rem; color: var(--muted); line-height: 1.45; }

/* The priced suggestion (UI §8.3). Dashed, like everything in this app that is
   a suggestion rather than a fact, and never a button: the closing sentence is
   load-bearing, not decoration. */
.fl-sugg {
  margin-top: 14px; padding: 12px 13px; border: 1px dashed var(--accent-border);
  border-radius: 12px; background: var(--card-bg);
}
.fl-sugg__title { font-size: .9rem; font-weight: 700; color: var(--text); line-height: 1.4; }
.fl-sugg__meta {
  margin-top: 2px; font-size: .76rem; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
/* The two figures stack and align — the comparison is the point, and two
   `+X/−Y` bands inline at 390 px wrap into porridge. */
.fl-sugg__figs { margin-top: 8px; }
.fl-sugg__figs .wh-costrow + .wh-costrow { margin-top: 5px; }
/* The advisory's two figures are the same CLASS of figure as the interval's own
   Cost row above them (one call, one evening), so they are the same size — the
   1.3rem block figure is the day-total headline and would outshout the plan. */
.fl-sugg__figs .wh-cost__central { font-size: .95rem; }
.fl-sugg__figs .wh-cost__cap { margin-top: 0; font-size: .78rem; }
.fl-sugg__advice { margin: 9px 0 0; font-size: .76rem; color: var(--muted); line-height: 1.45; }

/* ── Suggestions, r11 (heating repairs U3) ───────────────────────────────────
   The fire row generalised to fire / AC on heat / open windows, and made
   QUIETER than an ask on purpose: an ask (`.wh-mon--ask`, `.wh-diary--pending`)
   is dashed because it wants an answer; a suggestion wants nothing — a solid
   hairline, no shadow, an "Optional" eyebrow, no button. Several options share
   one box and one closing sentence. */
.sg {
  margin-top: 14px; padding: 11px 13px 12px;
  border: 1px solid var(--line); border-radius: 12px; background: var(--card-bg);
}
.sg__head {
  font-size: .7rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--muted);
}
.sg__item { margin-top: 6px; }
.sg__item + .sg__item { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line); }
/* Room names are the household's own and can be long: the title wraps. */
.sg__title {
  font-size: .9rem; font-weight: 700; color: var(--text); line-height: 1.4;
  overflow-wrap: anywhere;
}
.sg__meta { margin-top: 2px; font-size: .76rem; color: var(--muted); font-variant-numeric: tabular-nums; }
/* Two figures, stacked and aligned — the same class of figure as the call's
   own Cost row, so the same size (the fire row's reasoning, .fl-sugg__figs). */
.sg__figs { margin-top: 8px; }
.sg__figs .wh-costrow + .wh-costrow { margin-top: 5px; }
.sg__figs .wh-cost__central { font-size: .95rem; }
.sg__figs .wh-cost__cap { margin-top: 0; font-size: .78rem; }
/* The boiler's caption says it "runs either way" (U3 review, G1) and wraps at
   390px: the figure belongs beside the caption's FIRST line, where the row
   starts reading, not under its tail (the block form's flex-end). */
.sg__figs .wh-costrow { align-items: baseline; }
/* The two centrals are COMPARED, so they line up down the column whatever
   their bands: the band sits in its own column after the figure (empty for an
   exact £0.00), instead of pushing each central left by its own width. */
.sg__figs .wh-cost__figure {
  display: grid; grid-template-columns: auto minmax(5.2em, max-content);
  column-gap: 6px; align-items: baseline; justify-items: end;
}
.sg__figs .wh-cost__band { justify-self: start; }
.sg__meets { margin: 8px 0 0; font-size: .8rem; color: var(--text-2); line-height: 1.45; overflow-wrap: anywhere; }
.sg__close { margin: 10px 0 0; font-size: .76rem; color: var(--muted); line-height: 1.45; }

/* ══ The heating replay (#replay, floor-plan design §4–§6) ══════════════════
   Two floor plans washed by the replay's own banded scale, over a rail that IS
   the boiler. The layout rule is the whole design constraint: the plans AND the
   track fit one 390×844 screen with no scrolling, so everything but the plan
   block is its own fixed height and the plan block takes what is left. Nothing
   may be added to this screen without taking something out. */
.m-replay-screen { display: flex; flex-direction: column; overflow: hidden; }
/* An author `display:flex` outranks the UA [hidden] rule, so restore it —
   otherwise this fixed, inset:0 view covers the whole app forever. Same guard
   .m-chart-screen needs, and for the same reason. */
.m-replay-screen[hidden] { display: none; }
.m-replay-screen .m-room-screen__header { flex: 0 0 auto; }
/* Every block inside is display:flex/grid at some point, and each would beat the
   UA's [hidden] rule on its own. One guard, declared after the vocabularies it
   has to outrank (the segmented bar's is 0,2,0, same as this) rather than eleven
   copies of `.x[hidden]`. */
.m-replay-screen [hidden] { display: none; }
.m-replay-screen__body {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-height: 0;
  /* Fits at 390×844 by construction; a genuinely short phone scrolls rather
     than clipping the sentence off the bottom, which is the worse failure. */
  overflow-y: auto;
  padding: 12px;
  padding-left: max(12px, env(safe-area-inset-left));
  padding-right: max(12px, env(safe-area-inset-right));
  padding-bottom: max(12px, env(safe-area-inset-bottom));
}
/* U14's slot: the date chip lives at the bar's right edge, beside the title. */
.rp-dateslot { margin-left: auto; flex: 0 0 auto; }

/* The three views (this call / 12 h / 24 h) reuse the app's own segmented bar
   rather than a second one that only looks like it. "This call" is DISABLED
   when there is no call in the read, never removed — the quiet line under the
   legend is its explanation, and an explanation with nothing to explain is
   worse than a dimmed control. */
.rp-views { flex: 0 0 auto; }
.rp-views .mode-toggle__btn:disabled { opacity: .42; cursor: default; }

/* The legend row carries the wash switch, because §4's height budget is spent:
   a second segment would cost the 40 px that keeps both floors and the track on
   one screen. A labelled chip, never a bare tap on the gradient. */
.rp-legend { flex: 0 0 auto; display: flex; align-items: center; gap: 10px; }
.rp-wash {
  position: relative; flex: 0 0 auto;
  padding: 7px 12px; font-size: .78rem; font-weight: 650;
  color: var(--accent); background: var(--card-bg);
  border: 1px solid var(--line); border-radius: 999px; cursor: pointer;
}
/* 32px tall reads better in this row than 44 — so the TOUCH target is grown by
   an invisible ::after, the trick the sub-view back button already uses. */
.rp-wash::after { content: ""; position: absolute; inset: -6px; border-radius: inherit; }
.rp-wash:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The scale itself, drawn from the same banded ladder the rooms are painted
   from (washGradientCSS samples washToColour) — so the reference is honest, and
   visibly STEPPED, which is the wash's whole claim. */
.rp-bar {
  flex: 1 1 auto; min-width: 0; height: 10px;
  border: 1px solid var(--line); border-radius: 5px;
}
.rp-set {
  flex: 0 0 auto; font-size: .72rem; color: var(--muted);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* Something is off the end of the scale: its colour stops at the edge, and the
   legend says so rather than letting the flat block pass for a reading. */
.rp-clamp { color: var(--warn-ink); margin-right: 4px; }

.rp-key {
  flex: 0 0 auto; display: flex; flex-direction: column; gap: 2px;
  margin: 0; font-size: .72rem; line-height: 1.4; color: var(--faint);
  font-variant-numeric: tabular-nums;
}
/* Why this view, when it is not the one asked for. Darker than the key above
   it: it is news, not a legend. */
.rp-key__why { color: var(--text-2); }
/* What the dashed room outline means (design §4.3). A legend clause like the
   gain key above it, not news — so it stays at the key's own quiet colour and
   is stated explicitly rather than inherited by accident. */
.rp-key__ac { color: var(--faint); }

.rp-notes { flex: 0 0 auto; display: flex; flex-direction: column; gap: 8px; }
/* The state cards borrow `.wh-saved` whole: a neutral block with a dark rule
   down its left edge, deliberately outside the fault family's vocabulary. None
   of these is a problem with the heating — they are things I cannot claim. */
/* A shade tighter than the card on a scrolling screen: here every line it takes
   comes straight off the floor plans, which are the screen. */
.rp-note.wh-saved { margin-top: 0; padding: 9px 12px; }
.rp-note .wh-saved__title { font-size: .88rem; }
.rp-note .wh-saved__text { font-size: .8rem; }

/* The plan block is the ONLY thing that grows. The two floors take height in
   proportion to their own aspect ratios (ground is 102×114 units, first is
   100×100), which is what makes them come out the SAME WIDTH — equal flex would
   draw one house 12% wider than the other on the floor above it. */
.rp-plans {
  /* basis ZERO, not auto: on `auto` the block takes the SVGs' intrinsic height
     (409 px + 366 px at full width) and the screen scrolls — which is the one
     thing §4's layout rule forbids. */
  flex: 1 1 0; min-height: 150px;
  display: flex; flex-direction: column; align-items: stretch; gap: 6px;
  /* The field the plans sit in IS the outside (item U17): its background is the
     outdoor temperature, written per frame by replay.ts — on its own banded
     −5…37° set since U20, which its caption names because the legend bar above
     belongs to the rooms. The padding is what turns two side gutters into a field the
     house sits INSIDE, and the radius makes it one object rather than a colour
     that ran to the edges of the screen. */
  padding: 4px 8px 7px; border-radius: 12px;
  transition: background-color .18s linear; /* one bucket of playback is 100ms */
}
/* The outdoor figure, printed once where the colour is — the wash is a claim and
   this is what makes it checkable. Fixed dark ink ON the wash (the ramp is light
   in both themes by construction, exactly like the room numbers it sits beside),
   and the page's own muted ink when there is nothing painted under it. */
.rp-outside {
  /* One INLINE line rather than a flex row: the spacing is then real spaces, so
     the caption reads as a sentence to a screen reader as well as to an eye.
     `align-self` keeps it hugging its own text, so the scrim below is a label
     and not a bar across the field. */
  flex: 0 0 auto; align-self: flex-start; max-width: 100%;
  margin: 0; min-width: 0;
  font-size: .72rem; line-height: 1.3; color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* ON the wash, the caption needs a surface. Fixed dark ink alone measured 4.08:1
   against the cool set's clamp (#4a7cb0) and 5.31:1 against the warm one
   (#c47c20) — under AA for 12 px text, and the cool clamp is the ORDINARY winter
   frame, not an edge case. A 58% white scrim lifts the same ink to 10.4 / 11.7 /
   16.5:1 (cool clamp / warm clamp / neutral band) and has the property that it
   disappears where it is not needed: over the light-neutral middle of the ramp
   the surface is invisible, and it only shows itself as the wash deepens.
   Deliberately a scrim rather than the SVG text halo the room numbers use —
   `paint-order: stroke` is an SVG property and does nothing to HTML text. */
.rp-plans--outside .rp-outside {
  color: #0f172a;
  background: rgba(255, 255, 255, .58);
  padding: 1px 7px; border-radius: 7px;
}
.rp-outside__val { font-weight: 700; }
/* The change since the window started, in the change wash — the room's own
   value/sub-number relationship, at caption size: the absolute stays the bold
   figure and the signed one sits under it in weight, so two numbers side by side
   cannot be read as two temperatures. */
.rp-outside__change { font-size: .68rem; font-weight: 600; }
/* The two quiet clauses: when the painted reading is older than its own hour,
   and why there is no colour at all. Quiet by INK, never by opacity — the same
   italic at .85 opacity measured 3.5:1 on the light page background, i.e. the
   caveat was the least readable thing in the caption. */
.rp-outside__note { font-style: italic; }
.rp-plans--outside .rp-outside__note,
.rp-plans--outside .rp-outside__age { color: #3f4b5b; }
.rp-floor { min-height: 0; display: flex; align-items: center; justify-content: center; }
#replay-ground { flex: 1.118 1 0; }
#replay-first { flex: 1 1 0; }
.rp-svg {
  display: block; height: 100%; width: auto; max-width: 100%;
  overflow: visible; /* the outer wall stroke is drawn ON the polygon edge */
  filter: drop-shadow(0 1px 2px rgba(16, 24, 40, .14));
  /* Every mark on THIS plan is fixed in both themes — the fills are (a wash is a
     wash), so the ink, the hatch and the AC outline are too. The cloned wall and
     door/window detail were the exception, and after U17 that shows: in dark
     theme --plan-wall is a pale #cbd5e1, which against the warm set's sand field
     is about 1.1:1 — the house dissolves into the outside it is being compared
     with. The light theme's values, pinned, in both. */
  --plan-wall: #1e293b;
  --plan-detail: #64748b;
  --plan-detail-soft: #94a3b8;
}
/* The window openings are a gap punched through the wall, filled with the card
   colour — which in dark theme is nearly black, i.e. a dark tick on a light
   wall rather than an opening in it. Pinned with the rest of the chrome. */
.rp-svg .fp-window-gap { fill: #ffffff; }
/* The asset never arrived (a failed shell read): the readout still plays, so
   this collapses rather than leaving two empty boxes above it. */
.rp-plans--empty { display: none; }

/* The room cell: a wash, the absolute temperature large, the gain underneath.
   Fixed dark ink on a white halo in BOTH themes — the rooms are colour-washed,
   so this plan's text has never been theme-dependent, exactly as the live one's
   is not. The type is a size up from the live plan's because this SVG renders
   ~220 px wide rather than ~330. */
.rp-room__name, .rp-room__val, .rp-room__sub {
  text-anchor: middle;
  pointer-events: none;
}
.rp-room__name {
  font-size: 4px; font-weight: 600; fill: #1e293b;
  text-transform: uppercase; letter-spacing: .02em;
}
.rp-room--compact .rp-room__name { font-size: 3px; }
/* Second rung of the name ladder, declared after the base rule so it wins at
   equal specificity and BELOW the compact rule so a small room keeps its size. */
.rp-room__name--tight { font-size: 3px; }
.rp-room__val {
  font-size: 7px; font-weight: 700; fill: #0f172a;
  font-variant-numeric: tabular-nums;
  paint-order: stroke; stroke: #ffffff; stroke-width: .45; stroke-linejoin: round;
}
.rp-room--compact .rp-room__val { font-size: 4.4px; }
/* A room too narrow for the value at any anchor (replayPlan.anchorFor) — one rung
   down rather than a number through a wall. Declared after the compact rule so a
   small room that needs both ends up here. */
.rp-room--tightval .rp-room__val { font-size: 5.4px; }
.rp-room--tightval .rp-room__sub { font-size: 3.6px; }
/* "no sensor" is a label, not a reading, and must not be mistaken for one. */
.rp-room__val--absent {
  font-size: 3.2px; font-weight: 600; fill: #475569; stroke: none;
  text-transform: none;
}
.rp-room__sub {
  font-size: 4.4px; font-weight: 600; fill: #334155;
  font-variant-numeric: tabular-nums;
  paint-order: stroke; stroke: #ffffff; stroke-width: .3; stroke-linejoin: round;
}
/* Went quiet: the value is HELD, so it is greyed and its gain figure is replaced
   by the word. A room that dropped to hatch mid-playback would read as a bug. */
.rp-room--held .rp-room__val { fill: #64748b; }
.rp-room--held .rp-room__sub { fill: #64748b; font-weight: 500; }
.rp-room__fill {
  fill: #cbd5e1;
  stroke: #94a3b8; stroke-width: .4; stroke-linejoin: round;
  transition: fill .18s linear; /* one bucket of playback is 100ms — never a fade */
}
/* No instrument: HATCHED, never washed. On every anchor set the light-neutral
   band means "the middle of the range", so a grey room would read as a reading.
   Fixed light ink in both themes, like every other mark on this plan. */
.rp-hatch__bg { fill: #f1f5f9; }
.rp-hatch__line { stroke: #a2b0c2; stroke-width: 1.2; }
/* No band to paint (a held room on the GAIN wash): a slate that is visibly not
   the scale's warm light-neutral, so it cannot be read as "no change". */
.rp-room--nowash .rp-room__fill { fill: #c4cbd4; }
.rp-room--hatched .rp-room__fill { transition: none; }
/* That room's AC was running at this instant (design §4.3): the live plan's own
   dashed edge (.fp-room--ac), at this plan's scale. An EDGE and never a fill —
   every fill here belongs to the wash, so a device that borrowed one would be
   read as a temperature. The colour is FIXED in both themes, like every other
   mark on this plan: --accent inverts to a pale blue in dark mode, which over
   the warm set's ochre is the one place it would disappear. Lighter than the
   live plan's 2.5 because this SVG renders ~220px wide, not ~330. */
.rp-room--ac .rp-room__fill {
  stroke: #1d4ed8;
  stroke-width: 1.3;
  stroke-dasharray: 3.2 2;
}

/* ── The readout and the track (design §5, figure 6) ────────────────────────── */
.rp-readout { flex: 0 0 auto; display: flex; flex-direction: column; gap: 8px; }
.rp-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
/* Tabular and a fixed minimum width: the frame clock must not jitter as the
   digits change ten times a second. */
.rp-time {
  flex: 0 0 auto; min-width: 3.6em;
  font-size: 1.55rem; font-weight: 700; letter-spacing: -.01em; color: var(--text);
  font-variant-numeric: tabular-nums;
}
.rp-boiler {
  min-width: 0; text-align: right;
  font-size: .8rem; color: var(--muted); font-variant-numeric: tabular-nums;
}
.rp-track { display: flex; align-items: center; gap: 11px; }
.rp-play {
  width: 44px; height: 44px; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  color: var(--accent); background: var(--card-bg);
  border: 1px solid var(--accent-border); border-radius: 999px; cursor: pointer;
}
.rp-play svg { width: 22px; height: 22px; fill: currentColor; stroke: none; }
.rp-play:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The playback speed (item U19), at the transport row's right end: a speed
   control is a control, so it sits with the play button and the rail rather
   than in the readout above, which is information. It is the app's own chip —
   the same pill as the wash and date chips, not a second 44px disc competing
   with play for the eye.
   FIXED WIDTH, and every label is two glyphs (½× 1× 2× 4×): the rail beside it
   is `flex: 1`, so a chip that grew with its label would change the scrub's
   length — and its scale — every time the speed changed. */
.rp-speed {
  position: relative; flex: 0 0 auto;
  box-sizing: border-box; width: 42px; padding: 8px 0; text-align: center;
  font-size: .78rem; font-weight: 650; font-variant-numeric: tabular-nums;
  color: var(--accent); background: var(--card-bg);
  border: 1px solid var(--line); border-radius: 999px; cursor: pointer;
}
/* 32px tall reads better beside a 12px rail than a full-height control, so the
   TOUCH target is grown to exactly 44 (32 + 6 + 6, measured) by the invisible
   ::after the wash chip uses — the height of the track row it sits in. It
   grows into the track's own 11px gap on one side and the body's 12px padding
   on the other, so it never covers the rail's hit area (5px clear) and never
   leaves the screen. */
.rp-speed::after { content: ""; position: absolute; inset: -6px; border-radius: inherit; }
.rp-speed:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* 44px of hit height around a 12px rail (the ::after idiom, inverted: here the
   HOST is the target and the rail is the ink inside it). `touch-action: pan-y`
   keeps a vertical page scroll working through the track. */
.rp-rail {
  position: relative; flex: 1 1 auto; min-width: 0; height: 44px;
  touch-action: pan-y; cursor: pointer;
}
.rp-rail:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 8px; }
.rp-rail__bar {
  position: absolute; left: 0; right: 0; top: 50%; transform: translateY(-50%);
  display: block; height: 12px; border-radius: 6px;
  background: var(--track-bg); overflow: hidden;
}
/* The band = the coalesced CALL, the blocks = the BURNS inside it, and the
   RELATIONSHIP has to survive both themes: the burns are the strongest mark on
   the rail, the band a tint of it. A literal "pale band, dark blocks" inverts in
   dark mode — nothing can be darker than the background there — so the band is
   the accent at low opacity (which lands light-on-light and dark-on-dark by
   construction) and the burns take the page's own strongest ink. `--accent-soft`
   was the first try and is a CARD tint: two points from --track-bg, i.e. a call
   window nobody can see, which is the one thing this rail exists to show. */
.rp-rail__band {
  position: absolute; top: 0; bottom: 0;
  background: var(--accent); opacity: .30;
}
.rp-rail__burn { position: absolute; top: 0; bottom: 0; background: var(--text-2); border-radius: 2px; }
/* Nothing ever reported the thermostat over this stretch — neither a call nor a
   quiet boiler, so it is drawn as neither. */
.rp-rail__unknown {
  position: absolute; top: 0; bottom: 0;
  background: repeating-linear-gradient(45deg,
    var(--line) 0 3px, transparent 3px 6px);
}
/* The head is a pin: a disc above the rail on a stem through it, so it reads at
   the end of the rail as clearly as in the middle. */
.rp-rail__head {
  position: absolute; top: 50%; width: 2px; height: 26px;
  margin: -13px 0 0 -1px; background: var(--accent); pointer-events: none;
}
.rp-rail__head::after {
  content: ""; position: absolute; left: 50%; top: -7px;
  width: 14px; height: 14px; margin-left: -7px;
  border-radius: 999px; background: var(--accent);
  box-shadow: 0 0 0 2px var(--bg);
}
/* The ticks label the RAIL, not the row, so both margins are the track's own
   geometry: play (44) + gap (11) on the left, the speed chip (42) + gap (11) on
   the right. They move together with .rp-track's gap and the two controls. */
.rp-ticks {
  position: relative; height: 15px; margin-left: 55px; margin-right: 53px;
  font-size: .7rem; color: var(--faint); font-variant-numeric: tabular-nums;
}
.rp-tick { position: absolute; top: 0; white-space: nowrap; }
.rp-tick--start { left: 0; }
.rp-tick--mid { left: 50%; transform: translateX(-50%); }
.rp-tick--end { right: 0; }
.rp-sentence {
  margin: 0; font-size: .78rem; line-height: 1.45; color: var(--text-2);
  min-height: 2.3em; /* two lines: the sentence must not shove the plans about */
}

/* Loading and the read that never came back. Not one of §6's designed states,
   but a screen with no face for it shows an empty box. */
.rp-face { flex: 1 1 auto; display: flex; flex-direction: column; justify-content: center; }
.rp-face__loading { margin: 0; text-align: center; color: var(--muted); font-size: .84rem; }
.rp-face__fail { margin-top: 0; }

/* The date chip (design §4.2, figure 5): the bar's one control, and the name of
   the view once a day is chosen. Deliberately the same object as the wash chip
   in the legend — same height, same radius, same 44px touch target grown by an
   invisible ::after — so the screen has one kind of chip, not two. */
.rp-datechip {
  position: relative; display: inline-flex; align-items: center; gap: 5px;
  max-width: 40vw;
  padding: 7px 11px; font-size: .78rem; font-weight: 650;
  color: var(--accent); background: var(--card-bg);
  border: 1px solid var(--line); border-radius: 999px; cursor: pointer;
  font-variant-numeric: tabular-nums;
}
/* The label ellipsises rather than pushing the caret out of the pill. */
.rp-datechip > span:first-child {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rp-datechip::after { content: ""; position: absolute; inset: -6px; border-radius: inherit; }
.rp-datechip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.rp-datechip__caret { flex: 0 0 auto; font-size: .66rem; opacity: .75; }

/* Where the outdoor number came from, on a chosen day: a forecast archive and a
   measurement are not the same claim (§7). A quiet tag ON the sentence rather
   than a line under it — this screen's height budget has no line to spare. */
.rp-src {
  display: inline-block; padding: 0 5px; margin-left: 3px;
  font-size: .68rem; color: var(--muted);
  border: 1px solid var(--line); border-radius: 5px;
  white-space: nowrap;
}

/* ── The date sheet (§4.2, figure 5) ────────────────────────────────────────
   Four ways to name a day, in the app's own list vocabulary: 44px rows in a
   card, the shared section titles between them, and the payload's own coverage
   dates as the footnote (never a constant — the retention window moves). */
.rpd-group { display: flex; flex-direction: column; gap: 8px; }
.rpd-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  width: 100%; min-height: 44px; box-sizing: border-box;
  padding: 10px 14px; text-align: left;
  color: var(--text); font: inherit;
  background: var(--card-bg); border: 1px solid var(--line);
  border-radius: var(--radius); cursor: pointer;
}
.rpd-row.is-selected { border-color: var(--accent-border); background: var(--accent-soft); }
.rpd-row:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.rpd-row__label {
  font-size: .95rem; font-weight: 600;
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The right-hand side is the quieter fact: the date behind a named day, or the
   note the day was starred with. It gives way first when the row is tight. */
.rpd-row__meta {
  flex: 0 1 auto; min-width: 0; text-align: right;
  font-size: .8rem; color: var(--muted); font-variant-numeric: tabular-nums;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The launch state, and an invitation rather than an empty box: dashed, so it
   reads as a place something will go and not as a card that failed to load. */
.rpd-empty {
  margin: 0; padding: 12px 14px;
  font-size: .82rem; line-height: 1.45; color: var(--text-2);
  border: 1px dashed var(--line); border-radius: var(--radius);
}
/* The picker row is a label wrapping a native date input, so the whole 44px row
   opens the OS picker — the input alone is a 20px tap target. */
.rpd-pick { cursor: default; }
.rpd-input {
  flex: 0 0 auto; font: inherit; font-size: .88rem; font-weight: 650;
  color: var(--accent); background: transparent; border: 0; padding: 0;
  font-variant-numeric: tabular-nums;
}
.rpd-foot {
  margin: 12px 0 0; font-size: .74rem; line-height: 1.5; color: var(--faint);
}
/* Today sits OUTSIDE this block, so the block owes the gap the group inside it
   would have given: at 0 the two named days meet border-to-border and read as
   one doubled 2px seam. */
.rpd-past { margin-top: 8px; }

/* ── The profile calendar (Reference/profile-calendar-design.md §6) ───────────
   pc-* is the calendar's family, built ON the Plan tab's ph-* (cards, pills,
   sheets) rather than beside it: the Today card IS the ph-card--active, the
   "Use today" cards ARE ph-cards. What is new is the grouped LIST (Coming up,
   the overrides, the weekday rows) — one card, hairline rows, 48px each, a
   chevron where a row opens something — and the month grid. */

/* A section title that follows another section keeps the app's rhythm; the
   Plan tab's lead sentence sits under its title like every other lead. */
.pc-lead { margin-top: -2px; }

/* Today, nothing running (figure 1.2): the pill goes quiet — a grey outline,
   because "not heating" is a fact about the house, not a selection. */
.ph-pill--quiet {
  color: var(--text-2); background: var(--card-bg); border-color: var(--line);
}
.pc-card--idle { border-left-color: var(--line); border-color: var(--line); }
.pc-body { margin: 10px 0 0; font-size: .86rem; color: var(--text); line-height: 1.5; }
.pc-next { margin: 8px 0 0; font-size: .82rem; color: var(--text-2); line-height: 1.45; }
.pc-next b { color: var(--text); font-weight: 650; }
.pc-start {
  width: 100%; min-height: 48px; margin-top: 14px; padding: 0 16px;
  border: none; border-radius: 12px; background: var(--accent); color: var(--on-accent);
  font: inherit; font-size: .92rem; font-weight: 700; cursor: pointer;
}

/* The grouped list. Rows are flex with the key free to wrap and the value held
   whole: a profile name never ellipsises, a long date range wraps instead. */
.pc-list {
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--card-bg);
  overflow: hidden;
}
.pc-row {
  display: flex; align-items: center; gap: 12px;
  min-height: 48px; padding: 10px 14px;
  border-top: 1px solid var(--line-soft);
  color: var(--text); text-decoration: none; font-size: .9rem;
}
.pc-list > .pc-row:first-child { border-top: none; }
.pc-row__k { min-width: 0; flex: 1 1 auto; color: var(--text-2); font-variant-numeric: tabular-nums; }
.pc-row__v {
  flex: 0 0 auto; display: inline-flex; align-items: baseline; gap: 8px;
  text-align: right; color: var(--text);
}
.pc-row__v b { font-weight: 700; }
.pc-tag { font-size: .74rem; color: var(--muted); font-weight: 500; }
.pc-row__meta { font-size: .8rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.pc-chev { flex: none; color: var(--faint); font-size: 1.05rem; line-height: 1; }
.pc-row--link .pc-link { color: var(--accent); font-weight: 650; }
.pc-row--note { margin: 0; color: var(--text-2); font-size: .84rem; line-height: 1.45; }
button.pc-row {
  width: 100%; border-left: none; border-right: none; border-bottom: none;
  background: none; font: inherit; font-size: .9rem; text-align: left; cursor: pointer;
}
.pc-list > button.pc-row:first-child { border-top: none; }

/* A "Use today" card's second line: when the calendar gives it this profile. */
.pc-when { color: var(--muted); font-variant-numeric: tabular-nums; }

/* The 24 h preview (figures 3.2, 4.1, 4.2): one hairline-ruled list, the sign
   in its own fixed column so the room names start on one edge, and a window
   that will not happen any more greyed — present, but not part of the plan. */
.pc-changes { list-style: none; margin: 14px 0 0; padding: 0; }
.pc-change {
  display: flex; align-items: baseline; gap: 10px; padding: 9px 0;
  border-top: 1px solid var(--line-soft); font-size: .88rem; line-height: 1.45; color: var(--text);
}
.pc-change:first-child { border-top: none; }
.pc-change__sign {
  flex: 0 0 14px; text-align: center; color: var(--muted); font-variant-numeric: tabular-nums;
}
.pc-change__text { min-width: 0; flex: 1 1 auto; overflow-wrap: anywhere; }
.pc-change--gone { color: var(--muted); }
.pc-costs { margin-top: 12px; display: flex; flex-direction: column; gap: 6px; }

/* ── #calendar (figure 2.1) ──
   The weekly rule, once, in a card with its one door ("Change"). */
.pc-rule {
  display: flex; align-items: center; gap: 12px; margin-bottom: 14px; padding: 12px 12px 12px 14px;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--card-bg);
}
.pc-rule__text { min-width: 0; flex: 1 1 auto; }
.pc-rule__k {
  font-size: .7rem; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--faint);
}
.pc-rule__v { margin-top: 3px; font-size: .9rem; line-height: 1.45; color: var(--text); }
.pc-rule__v b { font-weight: 700; }
/* One part never breaks inside itself ("Mon–Fri Workday"); the list wraps between parts. */
.pc-rule__part { white-space: nowrap; }
.pc-rule__go {
  flex: none; display: inline-flex; align-items: center; min-height: 44px; padding: 0 16px;
  border: 1px solid var(--accent-border); border-radius: 12px; color: var(--accent);
  font-size: .88rem; font-weight: 650; text-decoration: none; background: var(--card-bg);
}

/* The month grid: seven equal columns, each week one row of tap targets. A day
   is a button the whole height of its week (number on top, its bar painted
   under the number by the bar layer), so a tap anywhere in a day's box opens
   that day — the bars are paint only (`pointer-events: none`). */
.pc-cal {
  padding: 4px 10px 8px; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--card-bg);
}
.pc-cal__head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.pc-cal__title { min-width: 0; font-size: 1rem; font-weight: 700; color: var(--text); text-align: center; }
.pc-cal__nav {
  flex: none; width: 44px; height: 44px; border: none; background: none; border-radius: 10px;
  color: var(--accent); font-size: 1.25rem; line-height: 1; cursor: pointer;
}
.pc-cal__nav:disabled { color: var(--faint); opacity: .45; cursor: default; }
.pc-cal__dn, .pc-wk__days, .pc-wk__bars { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }
.pc-cal__dn span {
  padding-bottom: 4px; text-align: center; font-size: .68rem; color: var(--faint); font-weight: 600;
}
.pc-wk { position: relative; border-top: 1px solid var(--line-soft); }
.pc-wk__days { height: 52px; }
.pc-wk__bars { position: absolute; left: 0; right: 0; top: 31px; pointer-events: none; }
.pc-d {
  height: 52px; min-width: 0; padding: 4px 0 0; margin: 0;
  display: flex; align-items: flex-start; justify-content: center; gap: 2px;
  border: none; background: none; cursor: pointer;
  font: inherit; font-size: .88rem; color: var(--text); font-variant-numeric: tabular-nums;
}
.pc-d span {
  display: inline-grid; place-items: center; min-width: 24px; height: 24px; border-radius: 12px;
}
/* "Dec 1": the month rides beside the number on the same 24px line. */
.pc-d i { font-style: normal; font-size: .62rem; line-height: 24px; color: var(--muted); }
.pc-d.is-past { color: var(--faint); cursor: default; }
.pc-d.is-today span { background: var(--accent); color: var(--on-accent); font-weight: 700; }
.pc-d:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 8px; }
/* An override, painted under its days. Square at an end that carries on into
   the next/previous week, so a three-week block reads as one thing. */
.pc-bar {
  grid-row: 1; height: 18px; margin: 0 2px; padding: 0 4px; min-width: 0;
  border-radius: 6px; border: 1px solid var(--accent-border);
  background: var(--accent-soft); color: var(--accent);
  font-size: .68rem; font-weight: 650; line-height: 16px; text-align: center;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis; pointer-events: none;
}
.pc-bar--quiet {
  border-color: var(--line); color: var(--text-2); background-color: var(--track-bg);
  background-image: repeating-linear-gradient(135deg, transparent 0 5px, var(--line) 5px 7px);
}
.pc-bar--off { border: 1px dashed var(--text-2); background: var(--card-bg); color: var(--text-2); }
/* A one-day bar has one column (~48px at 390): it gives up its padding and a
   point of size so "Workday" fits whole; a longer label still ellipsises. */
.pc-bar--one { margin: 0 1px; padding: 0 1px; font-size: .62rem; letter-spacing: -.01em; }
.pc-bar--l { margin-left: 0; border-left: none; border-top-left-radius: 0; border-bottom-left-radius: 0; }
.pc-bar--r { margin-right: 0; border-right: none; border-top-right-radius: 0; border-bottom-right-radius: 0; }
/* A run of DEFAULT days (LAH 2026-10-05): the weekday's profile named once,
   muted and light, with hairlines out to the run's ends — present, but never
   competing with an override bar. Same row and height as a bar, no fill. */
.pc-def {
  grid-row: 1; height: 18px; margin: 0 6px; min-width: 0;
  display: flex; align-items: center; gap: 5px; pointer-events: none;
  color: var(--muted); font-size: .62rem; font-weight: 450; line-height: 16px;
}
.pc-def::before, .pc-def::after {
  content: ""; flex: 1 1 0; min-width: 0; height: 0; border-top: 1px solid var(--line);
}
.pc-def > span { flex: 0 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.pc-def--one { margin: 0 1px; gap: 0; justify-content: center; }
.pc-def--one::before, .pc-def--one::after { display: none; }
.pc-cal__miss { margin: 8px 4px; }
.pc-legend { margin: 8px 4px 0; font-size: .78rem; line-height: 1.45; color: var(--muted); }
.pc-overrides { margin-top: 18px; margin-bottom: 16px; }
.pc-cta {
  width: 100%; min-height: 48px; padding: 0 16px; border: none; border-radius: 12px;
  background: var(--accent); color: var(--on-accent); font: inherit; font-size: .92rem;
  font-weight: 700; cursor: pointer;
}
.pc-weeklist .pc-row__k { color: var(--text); }

/* ── The calendar's sheet: radios, chips, two dates, the summary ── */
.pc-opts {
  margin-top: 12px; border: 1px solid var(--line); border-radius: 12px; overflow: hidden;
}
.pc-opt {
  width: 100%; display: flex; align-items: center; gap: 12px; min-height: 48px; padding: 10px 14px;
  border: none; border-top: 1px solid var(--line-soft); background: var(--card-bg);
  font: inherit; text-align: left; color: var(--text); cursor: pointer;
}
.pc-opt:first-child { border-top: none; }
.pc-opt__dot {
  flex: none; width: 20px; height: 20px; border-radius: 50%; border: 2px solid var(--faint);
  box-sizing: border-box;
}
.pc-opt[aria-checked="true"] .pc-opt__dot {
  border-color: var(--accent); background: radial-gradient(circle, var(--accent) 0 4px, transparent 4.5px);
}
.pc-opt__text { min-width: 0; flex: 1 1 auto; display: flex; flex-direction: column; gap: 2px; }
.pc-opt__label { font-size: .92rem; }
.pc-opt__sub { font-size: .8rem; color: var(--text-2); line-height: 1.45; }
.pc-opt__meta { flex: none; font-size: .78rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.pc-dates { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 10px; margin-top: 12px; }
/* A whole box opens the phone's date picker: the native input lies over it,
   transparent, so the tap target is the 54px box and not a 20px glyph. */
.pc-date {
  position: relative; display: flex; flex-direction: column; gap: 2px; min-width: 0;
  padding: 9px 12px; border: 1px solid var(--line); border-radius: 12px; background: var(--card-bg);
}
.pc-date__k { font-size: .74rem; color: var(--muted); }
.pc-date__v { font-size: .95rem; font-weight: 700; color: var(--text); white-space: nowrap; }
.pc-date input {
  position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer;
  border: 0; margin: 0; padding: 0;
}
.pc-date:focus-within { border-color: var(--accent); }
.pc-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
/* Chips keep their whole name and fill their row: five short names share one
   line at 390, a sixth (a household's own profile) wraps to a second rather
   than every chip being cut to "Work…". */
.pc-chip {
  flex: 1 0 auto; min-height: 44px; padding: 0 12px;
  border: 1px solid var(--line); border-radius: 12px; background: var(--card-bg);
  color: var(--text-2); font: inherit; font-size: .84rem; font-weight: 650; cursor: pointer;
  white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
}
.pc-chip[aria-pressed="true"] {
  border-color: var(--accent); background: var(--accent-soft); color: var(--accent);
}
.pc-sum {
  margin-top: 12px; padding: 11px 13px; border-radius: 10px; background: var(--line-soft);
  font-size: .9rem; font-weight: 700; color: var(--text); line-height: 1.4;
}
.pc-note { margin: 10px 0 0; font-size: .84rem; color: var(--text-2); line-height: 1.5; }
.pc-err { color: var(--bad-ink); }
.pc-rule-sep { margin: 14px 0 0; border: none; border-top: 1px dashed var(--line); }
.pc-24 { margin-top: 14px; }
.pc-24__head {
  font-size: .7rem; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--faint);
}
.pc-24 .pc-changes { margin-top: 6px; }
.pc-conflict { margin-top: 12px; }
.pc-row--pair .pc-row__v { flex: 0 1 auto; min-width: 0; text-align: right; }
#pcal-sheet .ph-wait { margin-top: 14px; }

/* ── Room snooze (U2): the sheet, the room row, the stop sheet's link ─────────
   Reference/room-snooze-design.md §4.2/§4.3/§4.6. The mark is one glyph (Zz,
   snoozeMark.ts) in currentColor, so it takes the ink of the line it leads. */
.sz-zz { flex: none; width: 1.25em; height: 1.25em; vertical-align: -.25em; }

/* The sheet. Equal cells — the count rides --sz-n, since "To 17:30" is offered
   only while the room has a window today. A cell wraps rather than clips at a
   narrow width (min-width:0, no nowrap). */
.sz-choices {
  display: grid; grid-template-columns: repeat(var(--sz-n, 4), minmax(0, 1fr));
  gap: 8px; margin-top: 16px;
}
.sz-choice {
  min-width: 0; min-height: 48px; padding: 4px 6px; border: 1px solid var(--line);
  border-radius: 12px; background: var(--card-bg); color: var(--text);
  font-size: .92rem; font-weight: 650; line-height: 1.2; font-variant-numeric: tabular-nums;
  cursor: pointer;
}
.sz-choice.is-on {
  border-color: var(--accent); background: var(--accent-soft); color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}
.sz-choice:disabled { cursor: default; }
.sz-choice:disabled:not(.is-on) { opacity: .55; }
/* The answer to "until when?" — the one figure the sheet is about. */
.sz-sum {
  margin-top: 12px; padding: 12px 14px; border-radius: 12px; background: var(--line-soft);
  font-size: 1rem; font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums;
}
.sz-lines { margin-top: 14px; }
.sz-line {
  margin: 0; padding: 10px 0; font-size: .9rem; color: var(--text); line-height: 1.45;
  font-variant-numeric: tabular-nums;
}
.sz-line:first-child { padding-top: 0; }
.sz-line:last-child { padding-bottom: 0; }
.sz-line + .sz-line { border-top: 1px solid var(--line); }
.sz-err {
  margin: 14px 0 0; padding: 10px 12px; border-radius: 10px;
  border: 1px solid var(--bad-border); background: var(--bad-bg); color: var(--bad-ink);
  font-size: .86rem; line-height: 1.45;
}
.sz-msg { margin-top: 12px; }

/* The room row, first in the room screen's body (figures 3.1, 3.2). A card in
   the room screen's own card vocabulary (.m-setup-link's), so it reads as the
   room's first fact rather than a banner. */
.sz-row {
  margin: 0 0 14px; padding: 14px; background: var(--card-bg);
  border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow);
}
.sz-row__main { display: flex; align-items: center; gap: 12px; }
.sz-row__text { min-width: 0; flex: 1 1 auto; }
.sz-row__eyebrow {
  font-size: .7rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  color: var(--muted); overflow-wrap: anywhere;
}
.sz-row__head {
  display: flex; align-items: center; gap: 8px; margin-top: 4px;
  font-size: 1rem; font-weight: 700; color: var(--text); line-height: 1.3;
  font-variant-numeric: tabular-nums;
}
.sz-row__head > span { min-width: 0; }
.sz-row__head .wh-dot { margin-right: 0; }
.sz-row__head .sz-row__mark { width: 22px; height: 22px; }
.sz-row__sub {
  margin-top: 4px; font-size: .84rem; color: var(--text-2); line-height: 1.45;
  font-variant-numeric: tabular-nums;
}
.sz-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  min-height: 44px; padding: 0 16px; border: 1px solid var(--accent); border-radius: 12px;
  background: var(--card-bg); color: var(--accent); font: inherit; font-size: .92rem;
  font-weight: 650; text-decoration: none; cursor: pointer;
}
.sz-btn--snooze { flex: none; }
.sz-btn--quiet { border-color: var(--line); color: var(--text); }
.sz-btn .sz-zz { width: 20px; height: 20px; }
.sz-btn:disabled, .sz-btn.is-busy { opacity: .5; cursor: default; }
.sz-row__acts { display: flex; gap: 10px; margin-top: 12px; }
.sz-row__acts .sz-btn { flex: 1 1 0; min-width: 0; }

/* The stop sheet's door (figure 4.1): a box, so it reads as an alternative to
   the sheet's own write rather than a second sentence about it. */
.sz-stoplink {
  margin-top: 14px; padding: 12px 14px 2px; border: 1px solid var(--line); border-radius: 12px;
}
.sz-stoplink__line { margin: 0; font-size: .9rem; color: var(--text); line-height: 1.45; }
.sz-stoplink__go {
  display: flex; align-items: center; gap: 8px; min-height: 44px;
  color: var(--accent); font-size: .92rem; font-weight: 650; text-decoration: none;
}
.sz-stoplink__go .sz-zz { width: 20px; height: 20px; }
.sz-stoplink__label { min-width: 0; flex: 1 1 auto; }
.sz-stoplink__chev { flex: none; color: var(--faint); font-size: 1.1rem; }

/* ── Room boost (U2): the mark, the room row, the sheet ──────────────────────
   Reference/room-boost-design.md §4.1–§4.3, §4.7. The flame lives in the Zz's
   own box (boostMark.ts), so it takes the same sizes wherever the Zz does, and
   the row reuses the snooze row's card, buttons and acts — a boost is the same
   kind of fact about a room, and the two swap in place. */
.bt-flame { flex: none; width: 1.25em; height: 1.25em; vertical-align: -.25em; }
.sz-btn .bt-flame { width: 20px; height: 20px; }
/* .sz-row__head .sz-row__mark already sizes the row's lead mark (22px). */

/* In Boost's place, where the room cannot be boosted (§4.1): one muted line. */
.bt-why { margin: 10px 0 0; font-size: .84rem; color: var(--muted); line-height: 1.45; }

/* The boosted row's part in the call: the dot hangs, the words wrap beside it. */
.bt-role { display: flex; align-items: baseline; }
.bt-role > span { min-width: 0; }
.bt-role b { font-weight: 650; color: var(--text); }

/* "Boost instead ›" / "Snooze instead ›" (figures 3.1, 5.1): a full-width link
   row under a divider — an alternative to the row's writes, not a third
   button. The 44px target sits below the 6px of air under the divider. */
.sz-instead {
  display: flex; align-items: center; gap: 8px; min-height: 50px; margin-top: 12px;
  padding-top: 6px; border-top: 1px solid var(--line);
  color: var(--accent); font-size: .92rem; font-weight: 650; text-decoration: none;
}
.sz-instead .sz-zz, .sz-instead .bt-flame { width: 20px; height: 20px; }
.sz-instead__label { min-width: 0; flex: 1 1 auto; }
.sz-instead__chev { flex: none; color: var(--faint); font-size: 1.1rem; }
.sz-instead.is-busy { opacity: .5; cursor: default; }

/* The sheet (figure 1.2). The stepper is the sheet's one quantity: a large
   tabular figure between two 52px round buttons; the figure has a fixed width
   so 20° ↔ 20.5° never moves the buttons under a thumb mid-run. */
.bt-stepper {
  display: flex; align-items: center; justify-content: center; gap: 18px; margin-top: 18px;
}
.bt-step {
  flex: none; width: 52px; height: 52px; display: inline-flex; align-items: center;
  justify-content: center; border: 1px solid var(--line); border-radius: 999px;
  background: var(--card-bg); color: var(--accent); font: inherit; font-size: 1.6rem;
  font-weight: 600; line-height: 1; cursor: pointer;
}
.bt-step:disabled { opacity: .35; cursor: default; }
.bt-step:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.bt-value {
  min-width: 2.7em; text-align: center; font-size: 2.6rem; font-weight: 750; line-height: 1.1;
  color: var(--text); font-variant-numeric: tabular-nums; letter-spacing: -.01em;
}
.bt-note {
  margin: 6px 0 0; text-align: center; font-size: .84rem; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
/* The summary: the phrase, and the estimate in grey under it — absent when the
   server has none (the box is one line shorter, figure 5.2), dimmed while a new
   one is asked. */
.bt-sum {
  display: flex; align-items: center; gap: 10px; margin-top: 12px; padding: 12px 14px;
  border-radius: 12px; background: var(--line-soft);
}
.bt-sum .bt-sum__mark { width: 22px; height: 22px; color: var(--text); }
.bt-sum__text { min-width: 0; flex: 1 1 auto; }
.bt-sum__phrase {
  font-size: 1rem; font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums;
}
/* --text-2, not --muted: on the summary box's tint --muted measured 4.42:1 in
   light (U4 contrast probe) — and this is the line the boost is decided on. */
.bt-est {
  margin-top: 2px; font-size: .84rem; color: var(--text-2); line-height: 1.4;
  font-variant-numeric: tabular-nums; transition: opacity .15s;
}
.bt-est.is-stale { opacity: .45; }
.sz-line strong { font-weight: 700; }
.bt-warm { color: var(--text-2); }
/* The valve note: the TRV honesty (§4.2) — boxed, so it reads as advice about
   the radiator rather than one more line about the boiler. */
.bt-valve {
  display: flex; align-items: flex-start; gap: 10px; margin-top: 14px; padding: 10px 12px;
  border: 1px solid var(--line); border-radius: 12px;
  font-size: .86rem; color: var(--text); line-height: 1.45;
}
.bt-valve__i { flex: none; width: 20px; height: 20px; margin-top: 0; color: var(--muted); }
.bt-valve > span { min-width: 0; }

/* ── Room snooze (U3): the glances ────────────────────────────────────────────
   §4.1 Home, §4.4 the floor plan, §4.5 #schedule / #detail / #heating. One
   mark (Zz, in the ink of its line) and one phrase. */

/* The snooze strip on Home's card — after the why / stop block, before the
   doors; its own hairline so it reads as a section, never as part of the why.
   The text wraps (it never ellipsises the time) beside a fixed 44px Remove. */
.sz-glance { margin-top: 11px; border-top: 1px solid var(--line-soft); }
.sz-glance__row { display: flex; align-items: center; gap: 10px; min-height: 52px; padding: 4px 0; }
.sz-glance__row + .sz-glance__row { border-top: 1px solid var(--line-soft); }
.sz-glance__mark { width: 20px; height: 20px; color: var(--text); }
/* A list row's two lines — the room, then "snoozed until 10:06" — rather than
   one sentence: beside a 44px Remove at 390px the sentence broke between
   "until" and its time, which is the one part a glance is reading for. */
.sz-glance__text {
  min-width: 0; flex: 1 1 auto; line-height: 1.35; font-variant-numeric: tabular-nums;
}
.sz-glance__name { display: block; font-size: .92rem; font-weight: 600; color: var(--text); }
.sz-glance__tail { display: block; font-size: .82rem; color: var(--muted); }
.sz-rm { flex: none; }
/* #schedule: the same rows as a card of their own above the calls (fig 4.2). */
.sz-glance--block {
  margin-top: 12px; padding: 0 14px; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--card-bg); box-shadow: var(--shadow);
}
/* The snooze-alone card's one room: its head already says whose and until when. */
.sz-glance--alone { padding-top: 12px; }
/* Home's quiet doors into the sheet — one per asking room, at most two. A long
   room name wraps inside the button instead of pushing it off the card. */
.sz-acts { margin-top: 12px; display: flex; flex-wrap: wrap; gap: 10px; }
.sz-act { max-width: 100%; min-width: 0; }
.sz-act__label { min-width: 0; overflow-wrap: anywhere; text-align: left; }
/* The muted note under the planned figure while the plan is being re-made. */
.sz-replan { margin-top: 8px; font-size: .78rem; color: var(--muted); line-height: 1.4; }
/* The calm `snoozed` stop: the why block's hairline and rhythm, no warn rail. */
.sz-stop { margin-top: 11px; padding-top: 11px; border-top: 1px solid var(--line-soft); }

/* A Home room card: the mark in the role shape's place. Words break at spaces
   only here, so "10:06" can never be split across lines. */
.m-room__role .sz-room { overflow-wrap: normal; }
.sz-room__mark { width: 14px; height: 14px; margin-right: 5px; vertical-align: -3px; }
.wh-room__eta .wh-dot { width: 7px; height: 7px; margin-right: 4px; }

/* The floor plan: the Zz box at the role dot's corner — dark ink on the dot's
   white halo, so it reads on any wash in either theme. */
.fp-room__role--snooze { color: #1e293b; }
/* One line per snoozed room under the floor's call band, each the room's door. */
.ph-floor__snooze {
  display: flex; align-items: center; gap: 8px; min-height: 44px; padding: 0 14px;
  border-bottom: 1px solid var(--line); color: var(--text); font-size: .84rem;
  text-decoration: none;
}
.ph-floor__snooze-mark { width: 18px; height: 18px; }
.ph-floor__snooze-text { min-width: 0; flex: 1 1 auto; line-height: 1.35; font-variant-numeric: tabular-nums; }
.ph-floor__snooze-chev { flex: none; color: var(--faint); font-size: 1.1rem; }

/* #detail: a neutral badge (no good/warn colour — a snooze is a choice). */
.met-badge--snoozed { background: var(--bg); color: var(--text-2); border: 1px solid var(--line); }
/* The re-plan note sits under an OPEN row, above its chart. */
.sz-plotnote { display: none; margin: 0; padding: 0 14px 8px; background: var(--bg);
  font-size: .74rem; color: var(--muted); line-height: 1.4; }
.room-accordion.is-open .sz-plotnote { display: block; }

/* #heating: the snoozed room's line, the mark leading it. */
.sz-monline { display: flex; align-items: center; gap: 6px; }
.sz-monline > span { min-width: 0; }
.sz-monline__mark { width: 18px; height: 18px; }

/* ── Room boost (U3): the glances ─────────────────────────────────────────────
   Reference/room-boost-design.md §4.4–§4.6. The flame sits wherever the Zz
   does, in the same box and sizes (the glance row, the room card, the floor
   plan's corner and lines, #heating), so the two overrides swap in place;
   these rules are only what a boost has and a snooze has not. */

/* The boost row (figures 2.1, 3.2): "Dad's Office 20.5° until 09:55" as ONE
   line — the name in ink, the phrase quiet and never split from its time (it
   wraps whole onto the next line before it breaks) — over what it adds. */
.bt-glance__line { display: block; font-size: .92rem; color: var(--text); }
/* At 390px beside a 44px End early the phrase wraps under the name, so the row
   can run to three lines: the mark and the FIRST line share the button's centre
   line (12px = (44 − 20) / 2), and the rest hangs below — centring the mark on
   the whole block left it floating beside nothing. */
.bt-glance__row { align-items: flex-start; }
.bt-glance__row .sz-glance__mark { margin-top: 12px; }
.bt-glance__row .sz-glance__text { padding-top: 12px; }
.bt-glance__name { font-weight: 600; }
.bt-glance__phrase { color: var(--muted); white-space: nowrap; }
/* While the boost carries `stat_room_warm`: the house fact, calm (§4.4). */
.bt-glance__warm {
  margin: 0 0 8px; font-size: .8rem; color: var(--text-2); line-height: 1.45;
  font-variant-numeric: tabular-nums;
}
/* A boost's stop and how one ended (figures 4.1, 4.2): the stop's rhythm. The
   stall's cause is the one sentence in warn ink — the server's judgement that
   the wish was not met; every other boost sentence is calm. */
.bt-cause--warn { color: var(--warn-ink); }

/* The floor plan: the flame at the role dot's corner, the Zz's dark ink. */
.fp-room__role--boost { color: #1e293b; }

/* #detail (§4.6): the neutral badge carries the mark — alone on the boost's own
   row, with "until 09:55" on each plan row it covers. */
.bt-badge { gap: 4px; }
.bt-badge__mark { width: 14px; height: 14px; }
.bt-detail__mark { width: 14px; height: 14px; margin-right: 4px; vertical-align: -3px; }
/* The boost's own row is not a door (no chart, no chevron): the accordion's
   rhythm without its tap affordance. */
.bt-detail { border-bottom: 1px solid var(--line-soft); }
.bt-detail:last-child { border-bottom: none; }
.bt-detail__row { cursor: default; }
/* The chevron's slot, empty: the badge column lines up with the doors' rows. */
.bt-detail__nochev { flex: none; width: 18px; }
.bt-detail__row:hover { background: none; }

/* ── The away app (away-app UI design §2.3–§2.5) ──────────────────────────────
   One build, two deployments: everything below only ever applies on the away
   origin, because only awayShell.ts (behind ON_AWAY) adds these classes.

   `.aw-off` takes an original surface out WITHOUT touching its `hidden` or its
   class display — controllers keep writing into it and nothing shows. It must
   beat every class `display:` (the [hidden] trap), hence !important. */
.aw-off { display: none !important; }

/* The grey card: the surface's real heading (a plain .section__title above),
   then a still silhouette of its shape and ONE tag. Tokens only, one step off
   the card in both themes; nothing moves — the app has no skeleton loaders, so
   a still silhouette cannot read as "loading". */
.aw__card {
  position: relative;
  padding: 14px 16px;
  background: var(--card-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
}
.aw__card + .aw__card { margin-top: 10px; }
.aw__sil { display: block; }
.aw__bar {
  display: block; height: 12px; margin: 0 0 9px;
  background: var(--track-bg); border-radius: 6px;
}
.aw__bar:last-child { margin-bottom: 0; }
.aw__bar--80 { width: 80%; }
.aw__bar--60 { width: 60%; }
.aw__bar--40 { width: 40%; }
.aw__row { display: flex; gap: 8px; margin-bottom: 9px; }
.aw__row i { flex: 1 1 0; min-width: 0; height: 30px; background: var(--track-bg); border-radius: var(--radius-sm); }
.aw__row--tall { margin: 12px 0 0; }
.aw__row--tall i { height: 46px; }
.aw__block { display: block; height: 104px; background: var(--track-bg); border-radius: var(--radius-sm); }
/* The real floorplan.svg walls, flat and unnamed: room polygons in the track
   colour, cut apart by the card colour. The slot holds its height before the
   shell asset lands, so nothing shifts; if it never lands it stays a plain block. */
.aw__floors { display: flex; justify-content: center; gap: 18px; }
.aw__floor {
  display: block; flex: 0 1 150px; min-width: 0; height: 168px;
  background: var(--track-bg); border-radius: var(--radius-sm);
}
.aw__floor--one { flex-basis: 190px; height: 212px; }
.aw__floor.is-drawn { background: none; }
.aw__walls { display: block; width: 100%; height: 100%; }
.aw__walls polygon { fill: var(--track-bg); stroke: var(--card-bg); stroke-width: 1.2; stroke-linejoin: round; }
.aw__tag {
  /* Centred by auto margins between both insets, NOT left:50% + translate: an
     absolute box with only `left` shrinks to the half-width left of it and the
     tag wrapped onto two lines. `width: max-content` keeps it one line wherever
     it fits; the max-width lets it wrap only on a phone too narrow for it. */
  position: absolute; left: 0; right: 0; top: 50%; transform: translateY(-50%);
  margin: 0 auto; width: max-content;
  display: flex; align-items: center; gap: 7px;
  max-width: calc(100% - 24px); padding: 6px 13px;
  background: var(--card-bg); border: 1px solid var(--line); border-radius: 999px;
  box-shadow: var(--shadow);
  font-size: .8rem; font-weight: 600; line-height: 1.3; color: var(--muted);
  text-align: center;
}
.aw__glyph {
  width: 14px; height: 14px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linejoin: round; stroke-linecap: round;
}
/* A short surface is one row: "Energy · not available away from home yet". */
.aw__card--line {
  display: flex; align-items: center; gap: 8px; padding: 12px 16px;
  font-size: .84rem; color: var(--muted);
}
.aw__card--line b { font-weight: 650; color: var(--text-2); }
.aw__what { min-width: 0; }
/* The Energy line stands where the cost strip stands: the strip's own margins
   (#home-outlook carries no bottom gap, the strip spaces itself), and the
   Outlook slot's state section follows the outlook's rule. */
.aw[data-away-grey="home-energy"] { margin: 14px 0 18px; }
#away-state { margin-bottom: 0; }
/* The live House away from home (cut 2, frame 5.2): one note between the House
   heading and the first floor — the offline rail when the laptop has gone quiet,
   then the muted sentence — and each floor's window advice as a grey line in the
   window hint's own place (its 10px gap above the cards). */
.aw-house { margin: 0 0 14px; }
.aw-house .aw-state.wh-saved { margin-bottom: 10px; }
.aw-house .aw-state.wh-saved:last-child { margin-bottom: 0; }
.aw-house__note { margin: 0; font-size: .84rem; line-height: 1.45; color: var(--muted); }
#floors .aw__card--line { margin: 0 0 10px; }

/* A grey sub-view keeps its real header and back; every other child goes. */
.aw-screen > :not(.m-room-screen__header):not(.aw-screen__body) { display: none !important; }
.aw-screen .m-rename-btn:disabled { cursor: default; opacity: 1; }
.aw-screen .m-rename-btn__pencil { display: none; }
/* The header's second line is that screen's live status ("Loading…", a count,
   a reading) — none of which is true of a screen that asks nothing. */
.aw-screen .m-room-screen__header .m-sheet__reading { display: none; }

/* The state lines in the Outlook slot (§2.4): unreachable and sign-in failed
   are the home app's own calm rail (.wh-saved.wh-fc); refused is the ONE amber
   on the away app — it needs an action from you. */
#away-state-body:empty { display: none; }
.aw-state.wh-saved { margin-top: 0; }
#home-outlook .aw-state.wh-saved { margin-bottom: 12px; }
.aw-state--warn.wh-saved {
  background: var(--warn-bg); border-color: transparent; border-left-color: var(--warn-ink);
}
.aw-state--warn .wh-fc__title { color: var(--warn-ink); }
.aw-state__link {
  display: inline-flex; align-items: center; min-height: 44px; margin: 0 0 -10px;
  font-size: .86rem; font-weight: 600; color: var(--accent); text-decoration: none;
}
/* The Enrol panel (frame 1.1): the one accent-ringed card — it is the next step. */
.aw-enrol {
  padding: 16px; background: var(--card-bg);
  border: 1px solid var(--accent); border-radius: var(--radius); box-shadow: var(--shadow);
}
.aw-enrol__title { margin: 0 0 6px; font-size: 1.05rem; font-weight: 700; color: var(--text); }
.aw-enrol__lead, .aw-enrol__steps { font-size: .86rem; line-height: 1.5; color: var(--text-2); }
.aw-enrol__lead { margin: 0 0 8px; }
.aw-enrol__steps { margin: 0 0 10px; padding-left: 20px; display: flex; flex-direction: column; gap: 4px; }
/* The command on its own line: inline, it broke inside "--label". */
.aw-enrol__cmd {
  display: block; width: fit-content; max-width: 100%; margin: 4px 0;
  font-family: ui-monospace, Consolas, monospace; font-size: .78rem; line-height: 1.5;
  padding: 3px 7px; border-radius: 5px; background: var(--track-bg); color: var(--text);
  overflow-wrap: anywhere;
}
.aw-enrol__foot { margin: 0; font-size: .78rem; color: var(--muted); }

/* More → "This phone" (frames 3.1, 3.2). */
.aw-phone {
  padding: 14px 16px; background: var(--card-bg);
  border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow);
}
.aw-phone > .aw-state { margin-bottom: 12px; }
.aw-kv {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 7px 18px;
  margin: 0; font-size: .88rem; line-height: 1.35;
}
.aw-kv dt { color: var(--muted); }
.aw-kv dd { margin: 0; min-width: 0; color: var(--text); overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
.aw-kv__v.is-muted { color: var(--muted); }
.aw-phone__note { margin: 12px 0 8px; font-size: .84rem; line-height: 1.45; color: var(--text-2); }
.aw-paste { display: flex; gap: 8px; }
.aw-paste__input {
  flex: 1 1 auto; min-width: 0; min-height: 44px; padding: 0 12px;
  font: inherit; font-size: .86rem; color: var(--text);
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-sm);
}
.aw-paste__input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.aw-btn {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  min-height: 44px; padding: 0 16px;
  font: inherit; font-size: .88rem; font-weight: 650; white-space: nowrap;
  color: var(--accent); background: var(--card-bg);
  border: 1px solid var(--accent); border-radius: var(--radius-sm); cursor: pointer;
}
.aw-btn--quiet { color: var(--text-2); border-color: var(--line); }
.aw-btn--warn { color: var(--warn-ink); border-color: var(--warn-ink); }
.aw-btn--wide { width: 100%; margin-top: 14px; }
.aw-confirm { display: flex; gap: 8px; }
.aw-confirm .aw-btn { flex: 1 1 0; }
.aw-install { font-size: .86rem; line-height: 1.5; color: var(--text-2); }

/* Tools rows on the away app: "Not yet" keeps its chevron; "Home only" has none
   and is not a link. Muted text, the pill says why. */
.m-links__item--off { color: var(--muted); }
.m-links__item[aria-disabled="true"] { cursor: default; }
.m-links__item .aw-pill {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2px 9px; border: 1px solid var(--line); border-radius: 999px;
  font-size: .74rem; font-weight: 600; line-height: 1.4; color: var(--muted);
  white-space: nowrap;
}
.m-links__item .aw-pill .aw__glyph { width: 12px; height: 12px; }
.m-links__item .m-links__chev { color: var(--faint); font-size: 1.1rem; }

/* The shell line, last on More (§2.7). */
.aw-shell {
  margin: 4px 0 8px; text-align: center;
  font-size: .76rem; color: var(--muted); font-variant-numeric: tabular-nums;
}
