@charset "UTF-8";
/* Solar Meter — operations dashboard.

   Two palettes live here and they must not blur into each other:

     * the UI accent (flame orange in light, gold in dark) dresses chrome only
       — active nav, buttons, focus, the live pulse. It never encodes data.
     * --s-* are the validated categorical series colours (see SERIES in
       core.js). They encode entities and nothing else.

   Mixing the two is how a dashboard starts lying: an orange number next to an
   orange chart line reads as the same thing.

   Both grounds are deliberately neutral — white / soft white / smoke in light,
   black / near-black / heavy grey in dark. A tinted surface would put a third
   colour in a page that already carries an accent and four series. */
:root {
  color-scheme: light;
  /* Light: a smoke ground with white cards on it, so a card reads as a sheet
     lifted off the page rather than another shade of the page. */
  --bg: #f4f5f7;
  --surface: #ffffff;
  --surface-2: #f5f6f8;
  --surface-3: #e8eaee;
  --border: #e2e4e9;
  --border-strong: #c6cad3;
  --text: #14161a;
  --text-2: #3d434d;
  --muted: #6b7280;
  --accent: #eb5e28;
  --accent-2: #c8471a;
  --accent-ink: #ffffff;
  --accent-soft: rgba(235, 94, 40, .10);
  --good: #147a49;
  --warning: #8f5c00;
  --serious: #b8481a;
  --critical: #b3241c;
  --on-status: #ffffff;
  /* Categorical slots. Declared in the order the charts draw them - solar,
     house, grid, battery - because the validator scores adjacent pairs, and
     this order is the one that clears every gate in BOTH themes:
     worst adjacent CVD dE 13.0 light / 15.9 dark, normal-vision 27.5 / 19.7.
     Reordering them re-opens a check that currently passes. Do not.

     They are also chosen to stay clear of the UI accent, which is warm in
     both themes: an orange or yellow series would read as chrome. */
  --s-solar: #2a78d6; /* blue    */
  --s-load: #bf4276; /* magenta */
  --s-grid: #4a3aa7; /* violet  */
  --s-battery: #1baf7a; /* aqua    */
  --s-soc: #008300; /* green - the battery's own level, its own chart */
  --grid-line: rgba(61, 67, 77, .13);
  --tip-bg: #14161a;
  --tip-border: #3d434d;
  --tip-title: #ffffff;
  --tip-body: #c6cad3;
  /* Neutral, and barely there: the ground is smoke, not a wash of colour. */
  --glow-1: rgba(20, 22, 26, .035);
  --glow-2: rgba(20, 22, 26, .022);
  --shadow: 0 1px 2px rgba(20, 22, 26, .05), 0 10px 26px -20px rgba(20, 22, 26, .32);
  --shadow-pop: 0 2px 6px rgba(20, 22, 26, .07), 0 18px 44px -18px rgba(20, 22, 26, .28);
  /* Glass: the three surfaces that float over the page - the bar, the drawer
     and every popover - are the same translucent sheet, not three different
     ones. The blur is what makes them read as glass; --glass on its own is
     just a pale panel, which is what a browser without backdrop-filter gets
     (see the @supports fallback further down). */
  --glass: rgba(255, 255, 255, .62);
  --glass-blur: saturate(180%) blur(18px);
  --glass-edge: rgba(255, 255, 255, .70);
  --glass-solid: #ffffff;
  /* density 9/10 — dashboard spacing scale */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-7: 44px;
  /* Type scale. Everything moved up a step from the first cut - the old
     11px labels were unreadable on a wall display and hostile on a laptop.
     Each step is multiplied by --fs-scale, the Text size setting on App
     config (see the data-font rules just above body). */
  --fs-scale: 1;
  --fs-xs: calc(12.5px * var(--fs-scale));
  --fs-sm: calc(14px * var(--fs-scale));
  --fs-md: calc(16px * var(--fs-scale));
  --fs-lg: calc(18px * var(--fs-scale));
  --fs-xl: calc(21px * var(--fs-scale));
  --fs-2xl: calc(27px * var(--fs-scale));
  --fs-3xl: calc(34px * var(--fs-scale));
  --r-sm: 10px;
  --r-md: 14px;
  --r-lg: 20px;
  --r-xl: 26px;
  --nav-h: 64px;
  /* One stacking scale for the whole page, so a new overlay is placed rather
     than guessed at. Anything that has to sit above a card belongs here. */
  --z-sticky: 1; /* a table's own sticky header, inside its scroller */
  --z-pop: 40; /* KPI popovers and anything else that hangs off a tile */
  --z-drawer: 50; /* the slide-over panel and its scrim - above cards, below the nav */
  --z-nav: 60; /* the top bar, which outranks everything below it */
  --z-menu: 70; /* the bar's own menus, which sit outside it to keep their glass */
  /* The "Last 24 hours" chart box height, quoted by its table so the two
     cannot drift apart when one of them changes. */
  --chart-tall: 330px;
  /* Latin comes from Fira, Thai from Noto Sans Thai, and the fallback chain
     ends at the faces Windows and macOS ship - per-glyph fallback means the
     plant name renders in Thai even with no network at all. */
  --mono: "Fira Code", "Noto Sans Thai Looped", "Noto Sans Thai",
          ui-monospace, SFMono-Regular, Consolas, monospace;
  --sans: "Fira Sans", "Noto Sans Thai", "Leelawadee UI", "Thonburi",
          system-ui, -apple-system, "Segoe UI", sans-serif;
  --ease: cubic-bezier(.22,.61,.36,1);
}

/* The dark half, declared twice on purpose: the media query answers the OS
   setting, the attribute answers the toggle, and the :not() guard lets an
   explicit "light" beat an OS that says dark. */
@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme=light])) {
    color-scheme: dark;
    /* Dark: black ground, near-black cards, heavy grey lines, white text.
       No hue in the surfaces at all — the only colour on a dark screen should
       be the accent and the four series. */
    --bg: #08090a;
    --surface: #121316;
    --surface-2: #1a1c20;
    --surface-3: #24272c;
    --border: #272a30;
    --border-strong: #3c4048;
    --text: #ffffff;
    --text-2: #c6cad2;
    --muted: #8c919b;
    --accent: #ffc300;
    --accent-2: #ffd60a;
    --accent-ink: #08090a;
    --accent-soft: rgba(255, 195, 0, .13);
    --good: #4ac97e;
    --warning: #f0b429;
    --serious: #f08a4b;
    --critical: #ff6b6b;
    --on-status: #08090a;
    /* Dark steps of the same five hues - selected for this surface, not flipped */
    --s-solar: #3987e5;
    --s-load: #d55181;
    --s-grid: #9085e9;
    --s-battery: #199e70;
    --s-soc: #3fa93f;
    --grid-line: rgba(198, 202, 210, .15);
    --tip-bg: #1a1c20;
    --tip-border: #3c4048;
    --tip-title: #ffffff;
    --tip-body: #c6cad2;
    --glow-1: rgba(255, 255, 255, .035);
    --glow-2: rgba(255, 195, 0, .045);
    --shadow: 0 1px 2px rgba(0, 0, 0, .6), 0 14px 34px -24px rgba(0, 0, 0, 1);
    --shadow-pop: 0 2px 6px rgba(0, 0, 0, .5), 0 18px 44px -18px rgba(0, 0, 0, .9);
    --glass: rgba(18, 19, 22, .60);
    --glass-blur: saturate(150%) blur(20px);
    --glass-edge: rgba(255, 255, 255, .07);
    --glass-solid: #121316;
  }
}
:root[data-theme=dark] {
  color-scheme: dark;
  /* Dark: black ground, near-black cards, heavy grey lines, white text.
     No hue in the surfaces at all — the only colour on a dark screen should
     be the accent and the four series. */
  --bg: #08090a;
  --surface: #121316;
  --surface-2: #1a1c20;
  --surface-3: #24272c;
  --border: #272a30;
  --border-strong: #3c4048;
  --text: #ffffff;
  --text-2: #c6cad2;
  --muted: #8c919b;
  --accent: #ffc300;
  --accent-2: #ffd60a;
  --accent-ink: #08090a;
  --accent-soft: rgba(255, 195, 0, .13);
  --good: #4ac97e;
  --warning: #f0b429;
  --serious: #f08a4b;
  --critical: #ff6b6b;
  --on-status: #08090a;
  /* Dark steps of the same five hues - selected for this surface, not flipped */
  --s-solar: #3987e5;
  --s-load: #d55181;
  --s-grid: #9085e9;
  --s-battery: #199e70;
  --s-soc: #3fa93f;
  --grid-line: rgba(198, 202, 210, .15);
  --tip-bg: #1a1c20;
  --tip-border: #3c4048;
  --tip-title: #ffffff;
  --tip-body: #c6cad2;
  --glow-1: rgba(255, 255, 255, .035);
  --glow-2: rgba(255, 195, 0, .045);
  --shadow: 0 1px 2px rgba(0, 0, 0, .6), 0 14px 34px -24px rgba(0, 0, 0, 1);
  --shadow-pop: 0 2px 6px rgba(0, 0, 0, .5), 0 18px 44px -18px rgba(0, 0, 0, .9);
  --glass: rgba(18, 19, 22, .60);
  --glass-blur: saturate(150%) blur(20px);
  --glass-edge: rgba(255, 255, 255, .07);
  --glass-solid: #121316;
}

* {
  box-sizing: border-box;
}

/* Panels toggle between chart and table via [hidden]; flex/grid would win. */
[hidden] {
  display: none !important;
}

/* A flex column down the whole viewport, so the footer can be pushed to the
   bottom of a short page rather than floating up under it: App config's Local
   data pane is three cards tall, and a sign-off halfway up the screen with a
   field of empty ground below it reads as content that failed to load. */
/* Text size, App config > Appearance. Default is the size the page was
   designed at and is stored as nothing, so it needs no rule. */
:root[data-font=compact] {
  --fs-scale: .875;
}

:root[data-font=medium] {
  --fs-scale: .9375;
}

:root[data-font=large] {
  --fs-scale: 1.125;
}

:root[data-font=xlarge] {
  --fs-scale: 1.25;
}

body {
  margin: 0;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;
  background: radial-gradient(1100px 520px at 6% -12%, var(--glow-1) 0%, transparent 62%), radial-gradient(900px 480px at 97% -8%, var(--glow-2) 0%, transparent 60%), var(--bg);
  background-attachment: fixed;
  color: var(--text);
  font: 400 var(--fs-md)/1.55 var(--sans);
  -webkit-font-smoothing: antialiased;
}

.shell {
  min-width: 0;
  flex: 1 0 auto;
  display: flex;
  flex-direction: column;
}

h1, h2, h3, h4 {
  margin: 0;
  font-weight: 600;
  letter-spacing: -0.015em;
}

p {
  margin: 0;
}

code {
  font-family: var(--mono);
  font-size: 0.92em;
  color: var(--text-2);
}

/* Thai has taller ascenders and deeper descenders than Latin; without this the
   plant name clips against the line above it. */
:lang(th), .th {
  line-height: 1.75;
}

/* Numbers are the content here — keep them monospaced and non-jittery. */
.num, .display, .chip-value, .chip-meta, .tile-value, .ring-value,
.readouts dd, .mini-stats dd, td.n, .ev-time, .stat-v, .logview,
.split-v, .sum-v {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

.label {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: 0.085em;
  text-transform: uppercase;
  color: var(--muted);
}

/* Bootstrap Icons are a font: sized by font-size, coloured by color. Each
   container below sets its own size beside the old svg rule it replaced. */
.bi {
  display: inline-block;
  line-height: 1;
  flex: none;
}
.bi::before {
  vertical-align: 0;
}

.muted {
  color: var(--muted);
}

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

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

/* A browser with no backdrop-filter (older Firefox with the pref off) would
   otherwise get a bar, a drawer and a menu you can read the page through, so
   the translucency is withdrawn rather than the blur simply being missing. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .topbar, .drawer, .popover, .notif-menu {
    background: var(--glass-solid);
  }
  #drawer-scrim {
    background: rgba(20, 22, 26, 0.38);
  }
  #dayrec-scrim {
    background: color-mix(in srgb, #000 52%, transparent);
  }
}
/* -------------------------------------------------------------- navbar
   One sticky bar across the top: brand left, destinations centre, connection
   state right. It replaced a fixed 88px icon rail — five destinations fit
   across a bar without crowding, and the charts get the width back. */
.topbar {
  position: sticky;
  top: 0;
  z-index: var(--z-nav);
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: var(--sp-4);
  min-height: var(--nav-h);
  padding: var(--sp-2) var(--sp-5);
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  /*border-bottom: 1px solid var(--border);*/
  /* The hairline that sells it: a sheet of glass catches the light on its top
     edge, and without it the bar reads as a flat panel that happens to be
     see-through. */
  box-shadow: inset 0 1px 0 var(--glass-edge);
}

/* Two links, not one: the mark goes home, the wordmark goes to the hardware
   vendor whose portal this reads. A single link doing both would have to pick
   one, and whichever it picked would surprise half the people who clicked. */
.brand {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-width: 0;
  color: inherit;
}

.brand-home {
  display: inline-flex;
  flex: none;
  border-radius: 11px;
  text-decoration: none;
}

.brand-word {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  max-width: 100%;
  color: var(--text);
  text-decoration: none;
  font: 700 var(--fs-md)/1.2 var(--sans);
  letter-spacing: -0.005em;
  /* The name is the one thing in the bar that may be cut: everything to its
     right is a control, and a control that has been trimmed to fit is a control
     you cannot press. */
}
.brand-word > span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.brand-word:hover {
  color: var(--accent);
}

.brand-out {
  width: 11px;
  height: 11px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
  color: var(--muted);
}

.brand-word:hover .brand-out {
  color: var(--accent);
}

.brand-mark {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  flex: none;
  border-radius: 11px;
  background: linear-gradient(150deg, var(--accent), var(--accent-2));
  color: var(--accent-ink);
}
.brand-mark svg {
  width: 18px;
  height: 18px;
  fill: currentColor;
  stroke: none;
}
.brand-mark .bi {
  font-size: 18px;
  line-height: 1;
}

.brand-text {
  min-width: 0;
}

.brand-sub {
  font-size: var(--fs-xs);
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The five destinations. A pill track rather than five loose links: it reads
   as one control, and the active item is unmistakable inside it. The wrapper
   exists for the narrow layout, where the track scrolls sideways and needs a
   fade on its right edge to say so. */
.nav-scroll {
  display: flex;
  justify-content: center;
  min-width: 0;
}

.nav {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 4px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
}

.nav-item {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 8px 15px;
  border-radius: 999px;
  font-size: var(--fs-sm);
  font-weight: 500;
  letter-spacing: 0.005em;
  color: var(--text-2);
  text-decoration: none;
  white-space: nowrap;
  transition: background 0.18s var(--ease), color 0.18s var(--ease);
}
.nav-item svg {
  width: 17px;
  height: 17px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.nav-item .bi {
  font-size: 17px;
  line-height: 1;
}
.nav-item:hover {
  color: var(--text);
  background: var(--surface-2);
}
.nav-item.is-on {
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 600;
}

.nav-badge {
  display: inline-grid;
  place-items: center;
  min-width: 17px;
  height: 17px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--critical);
  color: var(--on-status);
  font-size: calc(10.5px * var(--fs-scale));
  font-weight: 700;
  font-family: var(--mono);
}

.nav-item .nav-badge {
  margin-left: 1px;
}

.nav-item.is-on .nav-badge {
  background: var(--accent-ink);
  color: var(--accent);
}

/* The connection state, on the destination it is about. It replaced a pill in
   the bar that said the same thing three controls away from the page it
   described.

   It pulses only at `ok`. A pulse is the claim "a reading is arriving right
   now"; stale, offline and faulted are all still dots, so the animation is
   never decoration - its presence IS one of the four states. `idle` is the
   moment before the first answer and is deliberately the muted colour, not
   the good one: nothing has been confirmed yet. */
.nav-live {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex: none;
  margin-left: 1px;
  background: var(--muted);
  transition: background 0.2s var(--ease), box-shadow 0.2s var(--ease);
}
.nav-live[data-state=ok] {
  background: var(--good);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--good) 22%, transparent);
  animation: nav-live-beat 2.2s var(--ease) infinite;
}
.nav-live[data-state=warn] {
  background: var(--warning);
}
.nav-live[data-state=error] {
  background: var(--critical);
}

/* On the selected pill the dot sits on the accent, where --good has nothing
   like enough contrast to read as a dot at 7px. The ink the pill's own text
   uses is the one colour guaranteed to carry there. */
.nav-item.is-on .nav-live {
  background: var(--accent-ink);
  box-shadow: none;
}

@keyframes nav-live-beat {
  0%, 100% {
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--good) 22%, transparent);
  }
  50% {
    box-shadow: 0 0 0 6px color-mix(in srgb, var(--good) 0%, transparent);
  }
}
@media (prefers-reduced-motion: reduce) {
  .nav-live[data-state=ok] {
    animation: none;
  }
}
.topbar-right {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--sp-2);
  min-width: 0;
}

/* The hamburger exists only where the destination rail does not; above
   1366px the six pills are on screen and a menu to reach them as well would
   be two ways to do one thing. */
.navmenu {
  display: none;
  position: relative;
}

#nav-btn {
  position: relative;
}

.pill {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 6px 13px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  font-size: var(--fs-sm);
  font-weight: 500;
}
.pill .dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--muted);
}
.pill[data-state=ok] {
  color: var(--good);
  border-color: color-mix(in srgb, var(--good) 34%, transparent);
}
.pill[data-state=ok] .dot {
  background: var(--good);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--good) 18%, transparent);
}
.pill[data-state=warn] {
  color: var(--warning);
  border-color: color-mix(in srgb, var(--warning) 34%, transparent);
}
.pill[data-state=warn] .dot {
  background: var(--warning);
}
.pill[data-state=error] {
  color: var(--critical);
  border-color: color-mix(in srgb, var(--critical) 38%, transparent);
}
.pill[data-state=error] .dot {
  background: var(--critical);
}

.topbar-right .icon-btn {
  width: 34px;
  height: 34px;
  border-radius: 11px;
}

/* The status pill's own tooltip. `title` alone is mouse-only and slow; this
   shows on hover *and* on keyboard focus, which is why the pill is focusable.
   The sentence is also in #status-detail for screen readers, so this is the
   sighted half of the same information and never the only copy of it. */
.pill[data-tip] {
  position: relative;
  cursor: default;
}
.pill[data-tip]::after {
  content: attr(data-tip);
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: var(--z-pop);
  max-width: min(300px, 70vw);
  width: max-content;
  padding: 8px 11px;
  border-radius: var(--r-sm);
  background: var(--tip-bg);
  color: var(--tip-body);
  border: 1px solid var(--tip-border);
  font-size: var(--fs-xs);
  font-weight: 400;
  line-height: 1.45;
  text-align: left;
  white-space: normal;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.14s var(--ease);
}
.pill[data-tip]:hover::after, .pill[data-tip]:focus-visible::after {
  opacity: 1;
  visibility: visible;
}

/* -------------------------------------------------------- account menu
   One button for four destinations that are not readings: the profile, the
   settings, this browser's own preferences, and the hardware vendor's site.
   Three buttons on the right instead of six - and the theme switch gained a
   third state by moving onto a page that has room to label it. */
.usermenu {
  position: relative;
}

.userbtn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 4px 10px 4px 4px;
  min-height: 34px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text-2);
  font: 500 var(--fs-sm)/1 var(--sans);
  cursor: pointer;
  transition: background 0.18s var(--ease), color 0.18s var(--ease);
}
.userbtn:hover {
  background: var(--surface-2);
  color: var(--text);
}
.userbtn.is-on {
  border-color: var(--accent);
  color: var(--text);
}
.userbtn[aria-expanded=true] {
  background: var(--surface-2);
  color: var(--text);
}

.avatar {
  display: grid;
  place-items: center;
  flex: none;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: linear-gradient(150deg, var(--accent), var(--accent-2));
  color: var(--accent-ink);
  font: 700 calc(11px * var(--fs-scale))/1 var(--mono);
  letter-spacing: 0.02em;
}
.avatar svg {
  width: 14px;
  height: 14px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.avatar .bi {
  font-size: 14px;
  line-height: 1;
}

.userbtn-name {
  max-width: 11ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.userbtn-caret {
  width: 13px;
  height: 13px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
  color: var(--muted);
  font-size: 13px;
  line-height: 1;
}

/* Every menu that hangs off a control in the bar. The account menu had no
   base rule of its own and so laid out in flow, which widened the header
   instead of floating over the page. .notif-menu restates these because it
   sizes itself differently; the shared half lives here. */
.popover {
  /* Fixed, not absolute: these are no longer inside the bar (see the comment
     on the menu layer in index.html), so there is no anchor to hang off - the
     bar's own height and gutter place them instead. */
  position: fixed;
  z-index: var(--z-menu);
  top: calc(var(--nav-h) + 6px);
  right: var(--sp-5);
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-pop), inset 0 1px 0 var(--glass-edge);
}

/* The wrapper the outside-click handler closes over, carrying no box of its
   own so the menu it holds is placed by the viewport alone. */
.menu-layer {
  display: contents;
}

.user-menu {
  width: min(270px, 100vw - 2 * var(--sp-4));
  padding: 6px;
}

.user-menu-head {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--sp-3) var(--sp-3) var(--sp-2);
  font-size: var(--fs-sm);
  font-weight: 600;
  margin: 0;
}

.user-menu-sub {
  font-size: calc(11.5px * var(--fs-scale));
  font-weight: 400;
  color: var(--muted);
  font-family: var(--mono);
  overflow-wrap: anywhere;
}

.user-menu-rule {
  display: block;
  height: 1px;
  margin: 5px 0;
  background: var(--border);
}

.user-item {
  display: grid;
  grid-template-columns: 18px 1fr;
  gap: 2px 10px;
  padding: 9px var(--sp-3);
  border-radius: var(--r-sm);
  color: var(--text);
  text-decoration: none;
  font-size: var(--fs-sm);
  font-weight: 500;
}
.user-item svg {
  grid-row: span 2;
  align-self: center;
  width: 17px;
  height: 17px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
  color: var(--muted);
}
.user-item .bi {
  font-size: 17px;
  line-height: 1;
  color: var(--muted);
}

.user-item-sub {
  grid-column: 2;
  font-size: calc(11.5px * var(--fs-scale));
  font-weight: 400;
  color: var(--muted);
}

.user-item:hover, .user-item:focus-visible {
  background: var(--surface-2);
}

.user-item.is-on {
  background: var(--accent-soft);
  color: var(--text);
}
.user-item.is-on svg {
  color: var(--accent);
}
.user-item.is-on .bi {
  color: var(--accent);
}

/* Sign out is the one row in this menu that is a <button>, because it POSTs
   rather than navigating. These are the properties a button brings that an
   anchor does not, and that the grid above assumes are absent. */
button.user-item {
  width: 100%;
  background: none;
  border: 0;
  cursor: pointer;
  font-family: inherit;
  text-align: left;
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 14px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text);
  font: 500 var(--fs-sm) var(--sans);
  cursor: pointer;
  transition: background 0.18s var(--ease), border-color 0.18s var(--ease);
}
.btn:hover:not(:disabled) {
  background: var(--surface-3);
  border-color: var(--accent-2);
}
.btn:disabled {
  opacity: 0.5;
  cursor: progress;
}
.btn svg {
  width: 14px;
  height: 14px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
}
.btn .bi {
  font-size: 14px;
  line-height: 1;
}

/* -------------------------------------------------------------- banner */
.banner {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin: var(--sp-4) var(--sp-5) 0;
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--warning);
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--warning) 12%, var(--surface));
  color: var(--text);
  font-size: var(--fs-sm);
}
.banner[data-kind=error] {
  border-color: var(--critical);
  background: color-mix(in srgb, var(--critical) 12%, var(--surface));
  color: var(--text);
}
.banner svg {
  width: 17px;
  height: 17px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
}
.banner .bi {
  font-size: 17px;
  line-height: 1;
}

/* The catch-up notice. Sits where the banner sits and wears the accent, not a
   series colour: it is chrome reporting on the app's own work, not data. */
.preload {
  margin: var(--sp-4) var(--sp-5) 0;
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
}

.preload {
  border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
}
.preload[data-state=running] .preload-bar {
  animation: preload-pulse 1.6s ease-in-out infinite;
}
.preload[data-state=done] {
  border-color: var(--border);
}

.preload-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-3);
}
.preload-head h2 {
  margin: 0;
  font-size: var(--fs-sm);
  font-weight: 600;
}
.preload-head .btn {
  margin-left: auto;
}

.preload-eta {
  font-size: var(--fs-xs);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

@keyframes preload-pulse {
  50% {
    opacity: 0.55;
  }
}
@media (prefers-reduced-motion: reduce) {
  .preload[data-state=running] .preload-bar {
    animation: none;
  }
}
.preload-track {
  margin-top: var(--sp-3);
  height: 6px;
  border-radius: 999px;
  background: var(--surface-3);
  overflow: hidden;
}

.preload-bar {
  height: 100%;
  width: 0;
  background: var(--accent);
  transition: width 0.4s ease;
}

.preload-note {
  margin: var(--sp-2) 0 0;
  font-size: var(--fs-xs);
  color: var(--muted);
}

/* --------------------------------------------------------------- motion

   Three moments get movement, and nothing else does: the page arriving, a
   view being switched to, and a card crossing into the viewport. Each one is
   a cause the reader can name, which is the whole test - a card that fades
   because it was scrolled to is reporting an arrival; a card that fades on a
   timer is decoration.

   Everything here animates opacity and transform only, and the fill is
   `backwards`, never `forwards`: a retained transform would leave every
   animated element a stacking context for good, and the KPI popovers hang off
   exactly such elements. Reduced motion switches the lot off at the bottom of
   this block. */
@keyframes rise-in {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
@keyframes fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
@keyframes slide-down {
  from {
    opacity: 0;
    transform: translateY(-100%);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
/* The order everything arrives in, and the one place it is written down.
   Read top to bottom, it is the order the page is read: the bar, then the
   greeting's three lines, then the cards under them. The cards' lead is not
   here because it is per-sweep - revealVisible(lead) in app.js passes it, and
   the three numbers it passes are keyed to the last delay in this list.

   The bar is the only chrome that slides its whole height: it is a sheet of
   glass across the top, and a sheet of glass arrives from off-screen or not
   at all. The view itself only fades - a transform on the view would make a
   stacking context out of the container every popover is measured against. */
.topbar {
  animation: slide-down 0.46s var(--ease) backwards;
}

.view:not([hidden]) {
  animation: fade-in 0.28s var(--ease) 60ms backwards;
}

.hello h2 {
  animation: rise-in 0.5s var(--ease) 160ms backwards;
}

.hello-sub {
  animation: rise-in 0.5s var(--ease) 220ms backwards;
}

.hello .hello-cta {
  animation: rise-in 0.5s var(--ease) 280ms backwards;
}

/* Scroll reveal. The class is added by JS (wireReveal() in app.js) and taken
   off again the moment the animation ends, so a card is a plain card for the
   rest of its life - no lingering transform, no lingering will-change. With
   JS off, or no IntersectionObserver, the class is never added and every card
   is simply visible, which is the right failure. */
.reveal {
  opacity: 0;
}
.reveal.is-in {
  animation: rise-in 0.46s var(--ease) backwards;
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .topbar, .view:not([hidden]), .hello h2, .hello-sub, .hello .hello-cta,
  .reveal.is-in {
    animation: none !important;
  }
  .reveal {
    opacity: 1 !important;
  }
}
/* --------------------------------------------------------------- layout */
/* Full width by design: this is a wall/desk dashboard, and a centred column
   on a 2560px monitor wastes the half of the screen the charts want. */
main {
  width: 100%;
  flex: 1 0 auto;
  /* The gap is the floor the footer's auto margin sits on: with no slack left
     on a long page the margin collapses to nothing, and without this the
     sign-off would touch the last card. */
  display: flex;
  flex-direction: column;
  gap: var(--sp-6);
  padding: var(--sp-5) var(--sp-6) var(--sp-6);
}

.view {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}

.hello {
  padding: var(--sp-1) 0 var(--sp-2);
}
.hello h2 {
  font-size: var(--fs-3xl);
  letter-spacing: -0.03em;
}

.hello-sub {
  font-size: var(--fs-md);
  color: var(--muted);
  margin-top: 5px;
}

/* The dashboard's greeting is the one that shares its line. While the sun is
   still up the day's record has nothing final to say, so it rides here as a
   pill beside the greeting instead of a band across the page - a state, not a
   headline. When production wraps it becomes news, takes the full width on its
   own row, and brings the figures with it. */
#view-dashboard .hello {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3) var(--sp-4);
}

#view-dashboard .hello-text {
  flex: 1 1 320px;
  min-width: 0;
}

/* The one thing on the dashboard that is allowed to ask to be read. It wears
   the accent because it is chrome - a control, not a reading - and the dot
   pulses only while the record is finished and unread, so a card that is
   still filling in does not nag.

   It was a pill with the whole sentence and both figures inside it, which made
   the two numbers the line exists for the least legible part of it. Now it is a
   band the width of the page, in four zones read left to right: a marker, the
   sentence, the figures as labelled readings, and the action. Same content,
   given a hierarchy. */
.hello-cta {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  flex: 1 1 100%;
  width: 100%;
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-lg);
  cursor: pointer;
  text-align: left;
  color: var(--text);
  background: linear-gradient(90deg, color-mix(in srgb, var(--accent) 13%, var(--surface)) 0%, var(--surface) 62%);
  border: 1px solid color-mix(in srgb, var(--accent) 32%, transparent);
  box-shadow: var(--shadow);
  transition: border-color 0.18s var(--ease), box-shadow 0.18s var(--ease);
}
.hello-cta:hover {
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  box-shadow: var(--shadow-pop);
}

/* The marker: a disc in the accent, with the pulse riding its corner rather
   than standing in the sentence's way. */
.hello-cta-ic {
  position: relative;
  flex: none;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
}
.hello-cta-ic svg {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.hello-cta-ic .bi {
  font-size: 18px;
  line-height: 1;
}

.hello-cta-dot {
  position: absolute;
  top: -1px;
  right: -1px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 2px var(--surface);
}

.hello-cta-body {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.hello-cta-eyebrow {
  font-size: calc(10px * var(--fs-scale));
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent);
}

.hello-cta-text {
  font-size: var(--fs-md);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text);
}

/* The figures. Pushed to the far end of the band so the sentence and the
   readings never compete for the same starting edge, and separated by a rule
   rather than a gap - two numbers side by side with no divider read as one
   number twice as long. */
.hello-cta-figs {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  margin-left: auto;
  flex: none;
}

.hello-cta-fig {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding-left: var(--sp-4);
  border-left: 1px solid var(--border);
}
.hello-cta-fig:first-child {
  padding-left: 0;
  border-left: none;
}

.hello-cta-fig-v {
  font-family: var(--mono);
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.hello-cta-fig-k {
  font-size: calc(10px * var(--fs-scale));
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}

.hello-cta-act {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex: none;
  padding: 7px 10px 7px 13px;
  border-radius: 999px;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 34%, transparent);
  transition: background 0.18s var(--ease);
}

.hello-cta:hover .hello-cta-act {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
}

.hello-cta .hello-cta-go {
  width: 15px;
  height: 15px;
  flex: none;
  font-size: 15px;
  line-height: 1;
}

.hello-cta[data-state=ready] .hello-cta-dot {
  animation: cta-pulse 2.2s ease-in-out infinite;
}

/* Still producing: the band is worth opening, it is just not news yet, so it
   drops the accent entirely rather than shouting a quieter version of it -
   and shrinks to a pill on the greeting's own line, where it states the one
   thing it knows and stays out of the eye's way until dusk. */
.hello-cta[data-state=running] {
  flex: 0 0 auto;
  width: auto;
  gap: var(--sp-2);
  padding: 6px 7px 6px 9px;
  border-radius: 999px;
  box-shadow: none;
  background: var(--surface);
  border-color: var(--border);
}
.hello-cta[data-state=running] .hello-cta-ic {
  width: 26px;
  height: 26px;
}
.hello-cta[data-state=running] .hello-cta-ic svg {
  width: 14px;
  height: 14px;
}
.hello-cta[data-state=running] .hello-cta-ic .bi {
  font-size: 14px;
  line-height: 1;
}
.hello-cta[data-state=running] .hello-cta-dot {
  width: 7px;
  height: 7px;
}
.hello-cta[data-state=running] {
  /* One label, not two: the eyebrow and the sentence said the same thing at two
     sizes, which at pill width is the same thing twice. The figures are the
     day's headline and the day has not got one yet, so they wait for the band. */
}
.hello-cta[data-state=running] .hello-cta-eyebrow, .hello-cta[data-state=running] .hello-cta-figs {
  display: none;
}
.hello-cta[data-state=running] .hello-cta-text {
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--text-2);
  white-space: nowrap;
}
.hello-cta[data-state=running]:hover .hello-cta-text {
  color: var(--text);
}
.hello-cta[data-state=running] .hello-cta-act {
  padding: 4px 8px 4px 10px;
  font-size: var(--fs-xs);
}
.hello-cta[data-state=running] .hello-cta-eyebrow, .hello-cta[data-state=running] .hello-cta-ic, .hello-cta[data-state=running] .hello-cta-act {
  color: var(--text-2);
}
.hello-cta[data-state=running] .hello-cta-ic, .hello-cta[data-state=running] .hello-cta-act {
  background: var(--surface-2);
  border-color: var(--border);
}
.hello-cta[data-state=running] .hello-cta-dot {
  background: var(--muted);
}

/* Narrow: the band becomes two rows - the sentence, then the figures and the
   action on one line. Nothing is dropped; the order is what changes. */
@media (max-width: 900px) {
  .hello-cta {
    flex-wrap: wrap;
    gap: var(--sp-3);
  }
  .hello-cta-body {
    flex: 1 1 60%;
  }
  .hello-cta-figs {
    margin-left: 0;
    order: 3;
    flex: 1 1 auto;
  }
  .hello-cta-act {
    order: 4;
    margin-left: auto;
  }
  /* The pill is four small things on one line; wrapping it would give each
     of them a row of its own for no gain. */
  .hello-cta[data-state=running] {
    flex-wrap: nowrap;
    gap: var(--sp-2);
  }
  .hello-cta[data-state=running] .hello-cta-body {
    flex: 0 1 auto;
  }
  .hello-cta[data-state=running] .hello-cta-act {
    order: 0;
    margin-left: 0;
  }
}
@media (max-width: 520px) {
  .hello-cta-act-t {
    display: none;
  }
  .hello-cta-act {
    padding: 7px;
  }
  /* The pill's action is a word, not an icon: "Review" is the only thing on
     it that says what pressing it does. */
  .hello-cta[data-state=running] .hello-cta-act-t {
    display: inline;
  }
  .hello-cta[data-state=running] .hello-cta-act {
    padding: 4px 8px 4px 10px;
  }
}
@keyframes cta-pulse {
  0%, 100% {
    opacity: 1;
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent);
  }
  60% {
    opacity: 0.7;
    box-shadow: 0 0 0 6px transparent;
  }
}
@media (prefers-reduced-motion: reduce) {
  .hello-cta-dot {
    animation: none !important;
  }
}
/* --------------------------------------------- the day's record, as a modal

   A centred sheet rather than a slide-over: the board is a grid of tiles that
   wants width, and the drawer's 380px would stack all eleven into a column.
   It reuses the drawer's scrim rules and z-tier so there is one overlay
   layer, not two arguing. */
#dayrec-scrim {
  position: fixed;
  inset: 0;
  z-index: var(--z-drawer);
  background: color-mix(in srgb, #000 34%, transparent);
  /* The page behind a sheet of glass is out of focus, not merely darkened -
     the same blur the panel over it wears, so the two read as one layer. */
  backdrop-filter: blur(7px) saturate(120%);
  -webkit-backdrop-filter: blur(7px) saturate(120%);
  opacity: 0;
  transition: opacity 0.22s var(--ease);
}
#dayrec-scrim.is-on {
  opacity: 1;
}

.modal-wrap {
  position: fixed;
  inset: 0;
  z-index: var(--z-drawer);
  display: grid;
  place-items: center;
  padding: var(--sp-4);
  overflow-y: auto;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 0.22s var(--ease), transform 0.22s var(--ease);
}
.modal-wrap.is-open {
  opacity: 1;
  transform: none;
}
.modal-wrap {
  /* 90% of the viewport, not a 920px column: the board is eleven tiles of
     `auto-fit, minmax(190px, 1fr)`, so every extra 190px of width is one more
     tile on the row and one less row to scroll. */
}
.modal-wrap > .card {
  width: 90%;
  max-height: 90vh;
  overflow-y: auto;
  box-shadow: var(--shadow-pop);
}

@media (max-width: 700px) {
  .modal-wrap > .card {
    width: 100%;
    max-height: calc(100vh - 2 * var(--sp-4));
  }
}
.daycard-head-end {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex: none;
}

.grid-hero {
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
  gap: var(--sp-4);
}

.grid-2 {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-4);
}

.grid-3 {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr) minmax(0, 1fr);
  gap: var(--sp-4);
}

.grid-4 {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--sp-4);
}
.grid-4 > * {
  min-width: 0;
}

@media (min-width: 1700px) {
  .grid-hero {
    grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
  }
}
/* Grid items default to min-width:auto — a wide canvas would blow the page
   sideways without this. */
.grid-hero > *, .grid-2 > *, .grid-3 > * {
  min-width: 0;
}

/* ---------------------------------------------------------------- cards */
.card {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  min-width: 0;
  padding: var(--sp-4) var(--sp-5) var(--sp-5);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow);
}

/* One head for every card on every view. Left: an icon, the title, and the
   sentence of context if the card needs one. Right: the card's own note, or
   its controls. Nineteen cards used to carry three different heads between
   them - a muted micro-label here, a 21px heading there, an icon on four of
   them - which is why a card read as belonging to its page rather than to
   the dashboard. Adding a card means copying this, icon included. */
.card-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-4);
  flex-wrap: wrap;
}

.card-titles {
  min-width: 0;
}

/* 16px, not the 21px it was: a card title should outrank its own description
   and nothing else on the card. The numbers stay the loudest thing. */
.card-head h3 {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-md);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text);
}

/* The head icon names the card's subject, so it follows the text rather than
   the accent - the accent is chrome, and a card head is not chrome. */
.card-sub {
  font-size: var(--fs-sm);
  color: var(--muted);
  margin-top: 4px;
  max-width: 95ch;
}

.card-note {
  font-size: var(--fs-xs);
  color: var(--muted);
  font-family: var(--mono);
}

.note-block {
  font-family: var(--sans);
  line-height: 1.5;
  max-width: 95ch;
}

.tag {
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--surface-3);
  color: var(--text-2);
  font-size: calc(11px * var(--fs-scale));
  font-weight: 600;
}
.tag[data-state=charging] {
  background: color-mix(in srgb, var(--s-battery) 18%, transparent);
  color: var(--s-battery);
}
.tag[data-state=discharging] {
  background: color-mix(in srgb, var(--s-soc) 18%, transparent);
  color: var(--s-soc);
}
.tag[data-state=idle] {
  background: var(--surface-3);
  color: var(--muted);
}
.tag {
  /* Held at the backup floor: the same limit colour the ring's reserve arc
     wears, so one glance ties the tag to the red slice beside it. */
}
.tag[data-state=reserve] {
  background: color-mix(in srgb, var(--critical) 16%, transparent);
  color: var(--critical);
}

/* ----------------------------------------------------------- hero card */
.card-hero {
  position: relative;
  overflow: hidden;
  background: radial-gradient(560px 260px at 0% 0%, var(--accent-soft), transparent 62%), var(--surface);
}

/* The live card's refresh: turns while its one forced read is in flight. */
#live-refresh.is-busy .bi {
  animation: spin 0.9s linear infinite;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}
.display {
  font-size: clamp(44px, 6vw, 66px);
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.045em;
  margin-top: var(--sp-1);
}
.display .unit {
  font-size: 0.3em;
  font-weight: 500;
  letter-spacing: 0;
  color: var(--muted);
  margin-left: 0.35em;
}

.hero-cap {
  font-size: calc(13px * var(--fs-scale));
  color: var(--text-2);
}

.hero-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
}

.hero-chip {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px 14px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
}
.hero-chip .k {
  font-size: calc(10px * var(--fs-scale));
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.hero-chip .v {
  font-family: var(--mono);
  font-size: calc(14px * var(--fs-scale));
  font-weight: 600;
}

.hero-spark {
  height: 116px;
  margin-top: auto;
}
.hero-spark canvas {
  width: 100% !important;
  height: 100% !important;
}

/* ---------------------------------------------------------- flow diagram */
/* The diagram is a render of the house with the readings laid over it.
   One coordinate space does the whole job: `.flow-stage` carries the
   render's own 1200:896, the SVG's viewBox *is* that render in its own
   pixels, and the chips are positioned in percentages of the same box - so a
   pin drawn at (620,195) and a chip anchored at 3.3%/22.9% agree at every
   width, with no JS measuring anything. `background-size: cover` on a stage
   of the render's ratio scales it and crops nothing: change one ratio and the
   other has to move with it, or every pin slides off the house. */
.flow-scene {
  /* The dark-theme photo treatment, kept as tokens because it is declared
     twice below - once for the system default, once for the toggle. The
     night render is *already* dark, so unlike the old studio shot it is not
     dimmed, only settled a shade behind the glass. */
  --flow-photo-dark: brightness(.92) contrast(1.02) saturate(.95);
  --flow-scrim-dark:
    radial-gradient(120% 90% at 50% 45%, transparent 46%, rgba(0, 0, 0, .42) 100%),
    linear-gradient(180deg, rgba(0, 0, 0, .22), transparent 34%);
  position: relative;
  border-radius: var(--r-lg);
  overflow: hidden;
  border: 1px solid var(--border);
  background: var(--surface-2);
  container-type: inline-size;
}

/* 75/56 is 1200x896 - the render's own ratio, so nothing is cropped and
   the viewBox is the whole image. */
.flow-stage {
  position: relative;
  aspect-ratio: 75/56;
}

/* Renders of one scene, one pair per theme, and the theme picks which is
   fetched at all - a set of <img> tags in the page would download every one. */
.flow-photo {
  position: absolute;
  inset: 0;
  background: url("../yo-house-light-cover.jpeg") center/cover no-repeat;
}

/* The detail render, stacked exactly over the cover one and revealed by a
   cross-fade rather than a swap: same camera, same 1200x896, so nothing in
   the frame moves and only the picture under the plates changes. Opacity is
   the only thing that animates - a transition on `background-image` does not
   interpolate, it cuts. */
.flow-photo-detail {
  background-image: url("../yo-house-light.jpeg");
  /* The daylight detail render is a near-white studio shot. Left alone it
     out-glares the glass sitting on it, so it is pushed back a step - the
     opposite correction to the cover layer above, which is why the two do not
     share a filter. */
  filter: brightness(0.97) contrast(0.96) saturate(0.92);
  opacity: 0;
  transition: opacity 0.45s ease;
}

.flow-scene:hover .flow-photo-detail,
.flow-scene:focus-within .flow-photo-detail {
  opacity: 1;
}

/* The scrim is what keeps the plates readable: it darkens the corners the
   chips live in without flattening the middle, where the house is. */
.flow-scrim {
  position: absolute;
  inset: 0;
  background: radial-gradient(120% 90% at 50% 45%, transparent 38%, rgba(0, 0, 0, 0.1) 100%), linear-gradient(180deg, rgba(255, 255, 255, 0.24), transparent 30%);
}

.flow-links {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* ---- the hairlines from a plate to the point on the house it reads

   Dots rather than dashes: a hairline drawn beside a power line that is
   itself dashed reads as a second, quieter flow. A zero-length dash under a
   round cap is a round dot of exactly the stroke width, which is the one way
   to draw a dotted line that stays dotted at any scale. Each path stops short
   of its pin in the markup - a lead that runs under the marker and out the
   far side makes the pin look like a bead on the string. */
.lead {
  stroke: var(--text-2);
  stroke-width: 3;
  stroke-dasharray: 0 8;
  stroke-linecap: round;
  opacity: 0.45;
}

/* ---- the power lines

   Three layers, thin ones. The base is the route, drawn whether anything is
   moving or not. Over it, only while energy moves: `.link-glow`, a steady
   line in the entity's colour saying this route is live, and `.link-pulse`,
   a train of short bright packets running along it. A single fat dashed
   stroke - what this was - had to be heavy enough to be seen travelling, and
   at that weight it covered the house it was drawn on. Splitting the steady
   part from the moving part lets both be hairlines: the colour carries the
   route, the packets carry the direction, and the glow under them is what
   makes it read as current rather than as a marching ant. */
.link-base {
  stroke: var(--border-strong);
  stroke-width: 3;
  opacity: 0.35;
}

/* The group is what renderFlow() toggles, so both layers switch together and
   `--dash-dur` set on it inherits down to the packets. `color` is set per
   link and both layers stroke with it - one place per entity, not four. */
.link {
  opacity: 0;
  transition: opacity 0.35s var(--ease);
  /* Set per link by renderFlow(): the packets travel faster when more power
     is moving, which is the one thing a still picture cannot say. */
  --dash-dur: 1.4s;
}
.link.active {
  opacity: 1;
}

.link-glow {
  stroke: currentColor;
  stroke-width: 3.5;
  opacity: 0.5;
}

.link-pulse {
  stroke: currentColor;
  stroke-width: 5;
  /* 22 lit, 54 dark: a period of 76 puts five or six packets on a line of
     this length - a stream, not a dotted line and not a single comet. The
     keyframe must travel exactly one period or the loop visibly jumps. */
  stroke-dasharray: 22 54;
  filter: drop-shadow(0 0 4px currentColor);
}

.link.active .link-pulse {
  animation: dash var(--dash-dur) linear infinite;
}

.link.rev .link-pulse {
  animation-direction: reverse;
}

@keyframes dash {
  to {
    stroke-dashoffset: -76;
  }
}
#flow-solar {
  color: var(--s-solar);
}

#flow-battery {
  color: var(--s-battery);
}

#flow-grid {
  color: var(--s-grid);
}

#flow-load {
  color: var(--s-load);
}

/* ---- the pin on the building each plate points at

   All four are one radius and stay it. They used to swell to r13 while their
   line was busy, which meant the four markers were never the same size and a
   bigger dot read as a bigger reading - a claim the pin does not make. Live
   is a colour and a breathing halo instead, neither of which changes where
   the marker's edge is. */
.pin {
  r: 9;
  fill: var(--surface);
  stroke: var(--border-strong);
  stroke-width: 3;
}
.pin[data-pin=solar].live {
  color: var(--s-solar);
}
.pin[data-pin=battery].live {
  color: var(--s-battery);
}
.pin[data-pin=grid].live {
  color: var(--s-grid);
}
.pin[data-pin=load].live {
  color: var(--s-load);
}
.pin.live {
  fill: currentColor;
  stroke: currentColor;
  animation: pin-pulse 2.4s var(--ease) infinite;
}

@keyframes pin-pulse {
  0%, 100% {
    filter: drop-shadow(0 0 0 currentColor);
  }
  50% {
    filter: drop-shadow(0 0 7px currentColor);
  }
}
/* ---- the inverter, where every line meets, sitting on the house */
.flow-hub {
  position: absolute;
  /* (626,566) in the viewBox: the wall-mounted inverter in the render itself,
     which is where the four lines meet on the real house too. */
  left: 52.2%;
  top: 63.2%;
  transform: translate(-50%, -50%);
  width: clamp(32px, 7.4cqw, 60px);
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  border-radius: 999px;
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--border);
  box-shadow: inset 0 1px 0 var(--glass-edge), var(--shadow);
}
.flow-hub svg {
  width: 52%;
  height: 52%;
  fill: none;
  stroke: var(--text-2);
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.flow-hub .bi {
  font-size: clamp(17px, 3.9cqw, 31px);
  color: var(--text-2);
}

/* ---- the glass plates */
/* Each plate is anchored by one *corner*, because the hairline that leads to
   it is drawn from that exact point in the SVG. `data-corner` says which
   corner, and the translate puts it there; change one and you must change the
   matching `.lead` path. */
.flow-chip {
  position: absolute;
  display: grid;
  gap: 1px;
  padding: clamp(6px, 1.5cqw, 13px) clamp(9px, 2.1cqw, 17px);
  border-radius: clamp(10px, 1.9cqw, 17px);
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--border);
  box-shadow: inset 0 1px 0 var(--glass-edge), var(--shadow-pop);
  transition: box-shadow 0.3s var(--ease);
  min-width: 150px;
  min-height: 90px;
}
.flow-chip[data-corner=bl] {
  transform: translateY(-100%);
}
.flow-chip[data-corner=br] {
  transform: translate(-100%, -100%);
}
.flow-chip {
  /* Each pair is the `.lead` path's start point, expressed as a percentage of
     the same box - x/1200 and y/896, the viewBox being the whole render. The four
     take a corner each and leave the middle of the frame, where the house and
     its hardware are, uncovered: solar in the sky left of the roof, grid in the
     sky beside the service pole, house on the paving under its own front door,
     battery on the ground out past the wing. */
}
.flow-chip[data-node=solar] {
  left: 3.3%;
  top: 22.9%;
}
.flow-chip {
  /* (40,205)   */
}
.flow-chip[data-node=grid] {
  left: 97%;
  top: 22.9%;
}
.flow-chip {
  /* (1164,205) */
}
.flow-chip[data-node=load] {
  left: 3.3%;
  top: 94%;
}
.flow-chip {
  /* (40,842)   */
}
.flow-chip[data-node=battery] {
  left: 97%;
  top: 94%;
}

/* (1164,842) */
.chip-label {
  display: flex;
  align-items: center;
  gap: 0.45em;
  font: 600 clamp(8.5px, 1.65cqw, 11.5px) var(--sans);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}
.chip-label svg {
  width: 1.25em;
  height: 1.25em;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.chip-label .bi {
  font-size: 1.25em;
  line-height: 1;
}

.chip-value {
  font: 600 clamp(14px, 3.1cqw, 23px) var(--mono);
  color: var(--text);
  white-space: nowrap;
  line-height: 1.15;
}

.chip-meta {
  font: 400 clamp(8.5px, 1.7cqw, 12px) var(--mono);
  color: var(--muted);
  white-space: nowrap;
}

/* On a phone the card is ~360px wide, every clamp above is pinned at its
   minimum, and four plates at their minimum size cover the house they are
   pointing at. Below that width they step down past the clamp floor: still
   readable, and the building stays visible under them. */
@container (max-width: 480px) {
  .flow-chip {
    padding: 5px 8px;
    border-radius: 10px;
  }
  .chip-label {
    font-size: calc(8px * var(--fs-scale));
    letter-spacing: 0.07em;
  }
  .chip-label svg {
    width: 1.15em;
    height: 1.15em;
  }
  .chip-label .bi {
    font-size: 1.15em;
    line-height: 1;
  }
  .chip-value {
    font-size: calc(13px * var(--fs-scale));
  }
  .chip-meta {
    font-size: calc(8.5px * var(--fs-scale));
  }
}
/* Live is a tint of the entity's own colour on the *number* - never the
   accent, which is chrome on this page and would make orange mean two things,
   and no longer the plate's border either: four plates each outlined in their
   own hue turned the card into a ring of colour competing with the lines that
   actually carry the reading. The border stays the same glass edge the bar and
   drawer wear, live or not; the figure is where the colour belongs. */
.flow-chip[data-node=solar].live .chip-value {
  color: var(--s-solar);
}

.flow-chip[data-node=battery].live .chip-value {
  color: var(--s-battery);
}

.flow-chip[data-node=grid].live .chip-value {
  color: var(--s-grid);
}

.flow-chip[data-node=load].live .chip-value {
  color: var(--s-load);
}

/* Dark theme: a second render of the same house at night, framed identically -
   same 1200x896, same camera - so every pin, lead and link above lands on the
   same brick in both. It is dark already, so the scrim only has to settle the
   corners rather than dim the whole picture the way the daylight one needed.
   Declared twice, as every themed rule here is: the media query is the system
   default, the attribute is the toggle, and a rule that has only one of them
   follows the theme in one direction only. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme=light]) .flow-photo {
    background-image: url("../yo-house-night-cover.jpeg");
    filter: var(--flow-photo-dark);
  }
  :root:not([data-theme=light]) .flow-photo-detail {
    background-image: url("../yo-house-night.jpeg");
  }
  :root:not([data-theme=light]) .flow-scrim {
    background: var(--flow-scrim-dark);
  }
  :root:not([data-theme=light]) .lead {
    opacity: 0.38;
  }
}
:root[data-theme=dark] .flow-photo {
  background-image: url("../yo-house-night-cover.jpeg");
  filter: var(--flow-photo-dark);
}

:root[data-theme=dark] .flow-photo-detail {
  background-image: url("../yo-house-night.jpeg");
}

:root[data-theme=dark] .flow-scrim {
  background: var(--flow-scrim-dark);
}

:root[data-theme=dark] .lead {
  opacity: 0.38;
}

/* Without backdrop-filter the plates would be a window onto the photograph
   with type over it. Withdraw the translucency entirely rather than ship
   that: the same trade the bar and drawer make. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .flow-chip, .flow-hub {
    background: var(--glass-solid);
  }
}
/* Motion is the signal for "energy is moving". Without it, brightness is. */
@media (prefers-reduced-motion: reduce) {
  .link.active .link-pulse {
    animation: none;
    stroke-dasharray: none;
    opacity: 0.75;
  }
  .pin.live {
    animation: none;
  }
  #live-refresh.is-busy .bi {
    animation: none;
  }
  /* The picture still changes - it is the point of the hover - it just stops
     being an animation about changing. */
  .flow-photo-detail {
    transition: none;
  }
}
/* ------------------------------------------------------- split-share bar */
.split {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.split-bar {
  display: flex;
  height: 9px;
  gap: 2px; /* 2px surface gap between segments */
  border-radius: 999px;
  overflow: hidden;
  background: var(--surface-3);
}
.split-bar i {
  display: block;
  transition: flex-basis 0.5s var(--ease);
}

.split-keys {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-4);
}

.split-key {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: calc(12px * var(--fs-scale));
  color: var(--text-2);
}
.split-key .swatch {
  width: 9px;
  height: 9px;
  border-radius: 3px;
  flex: none;
}

.split-v {
  color: var(--text);
  font-weight: 600;
}

.split-pct {
  color: var(--muted);
}

/* ---------------------------------------------------------- battery rings

   Two gauges side by side. Level and runtime are different questions and one
   ring can only answer one of them - the second used to live as a caption
   under the first, which is where a number goes to be ignored. */
.ring-pair {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-3);
}

.ring-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
  padding: var(--sp-3) var(--sp-2);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}

.ring-wrap {
  position: relative;
  display: grid;
  place-items: center;
  width: 100%;
}

.ring {
  width: min(148px, 100%);
  height: auto;
  aspect-ratio: 1;
  transform: rotate(-90deg);
}

.ring-track {
  fill: none;
  stroke: var(--surface-3);
  stroke-width: 10;
}

.ring-fill {
  fill: none;
  stroke: var(--s-soc);
  stroke-width: 10;
  stroke-linecap: round;
  stroke-dasharray: 326.7;
  stroke-dashoffset: 326.7;
  transition: stroke-dashoffset 0.7s var(--ease), stroke 0.3s var(--ease);
}

/* The backup reserve the installer set. It is a limit, not a series, so it
   wears --critical like the low state does - and it sits over the fill rather
   than beside it, because the reserved kWh are part of what is stored, just
   not part of what the house may spend. */
.ring-reserve {
  fill: none;
  stroke: var(--critical);
  stroke-width: 10;
  stroke-dasharray: 326.7;
  stroke-dashoffset: 326.7;
  opacity: 0.85;
  transition: stroke-dashoffset 0.7s var(--ease);
}

.ring-mark {
  stroke: var(--critical);
  stroke-width: 2;
  stroke-linecap: round;
}

/* At or below the floor the whole arc is the warning - there is nothing
   spendable left to draw in the battery's own colour. */
.ring-fill[data-level=reserve] {
  stroke: var(--critical);
}

/* Low is a state, and states wear the reserved status colours. "Mid" used to
   wear --warning, which is warm in both themes and so read as the UI accent
   on a neutral ground - two meanings for one colour, and a half-full battery
   is not a warning anyway. Only genuinely low breaks the ring's own colour. */
.ring-fill[data-level=low] {
  stroke: var(--critical);
}

/* The runtime arc is the battery's own colour when it is filling and the
   level colour when it is emptying: same entity, opposite directions. */
#eta-fill {
  stroke: var(--s-battery);
}
#eta-fill[data-mode=discharging] {
  stroke: var(--s-soc);
}
#eta-fill[data-mode=idle] {
  stroke: var(--border-strong);
}

.ring-centre {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 1px;
  text-align: center;
}

.ring-value {
  font-size: calc(31px * var(--fs-scale));
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1.05;
}
.ring-value .unit {
  font-size: 0.46em;
  font-weight: 500;
  color: var(--muted);
  margin-left: 0.12em;
  font-family: var(--sans);
  letter-spacing: 0;
}

.ring-unit {
  font-size: calc(10px * var(--fs-scale));
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  max-width: 12ch;
  line-height: 1.3;
}

.ring-cap {
  font-size: var(--fs-xs);
  color: var(--text-2);
  text-align: center;
  line-height: 1.45;
  max-width: 22ch;
}

.mini-stats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-3);
  margin: 0;
}
.mini-stats > div {
  padding: var(--sp-3);
  background: var(--surface-2);
  border-radius: var(--r-md);
  min-width: 0;
}
.mini-stats dt {
  font-size: calc(10px * var(--fs-scale));
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
}
.mini-stats dd {
  margin: 3px 0 0;
  font-size: calc(14px * var(--fs-scale));
  font-weight: 600;
}

/* Battery card: three sub-cards under the rings, then one sentence reading
   the day. The battery's own hue for both bars - charged and discharged are
   the same entity in two directions, not two entities, so discharge takes a
   lighter step of the same colour rather than the accent or a second hue. */
.subcards {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-3);
}

.subcard {
  padding: var(--sp-3);
  min-width: 0;
  background: var(--surface-2);
  border-radius: var(--r-md);
}

.subcard-k {
  display: block;
  font-size: calc(10px * var(--fs-scale));
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
}

.subcard-v {
  display: block;
  margin-top: 3px;
  font-size: calc(19px * var(--fs-scale));
  font-weight: 600;
  letter-spacing: -0.02em;
}
.subcard-v .unit {
  font-size: 0.55em;
  color: var(--muted);
  margin-left: 0.25em;
}

.subcard-f {
  display: block;
  margin-top: 1px;
  font-size: calc(11px * var(--fs-scale));
  color: var(--muted);
}

.bat-analysis {
  font-size: var(--fs-sm);
  color: var(--text-2);
}

/* ------------------------------------------------------------- insights

   A block of grey paragraphs is a block nobody reads. Each reading is its own
   card now, led by a badge in its level's colour and closed by the level in a
   word — the word is there because the colour cannot be the only carrier. */
.insights {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--sp-3);
}
.insights li {
  --ins: var(--s-solar);
  display: grid;
  grid-template-columns: 34px minmax(0, 1fr) auto;
  align-items: start;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: linear-gradient(180deg, color-mix(in srgb, var(--ins) 7%, transparent), transparent 70%), var(--surface-2);
  border: 1px solid var(--border);
  border-left: 3px solid var(--ins);
  border-radius: var(--r-md);
}
.insights li[data-level=good] {
  --ins: var(--good);
}
.insights li[data-level=warn] {
  --ins: var(--warning);
}
.insights li[data-level=critical] {
  --ins: var(--critical);
}
.insights li[data-level=info] {
  --ins: var(--s-solar);
}

.ins-badge {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  flex: none;
  border-radius: 11px;
  background: color-mix(in srgb, var(--ins) 15%, transparent);
  color: var(--ins);
}

.ins-ic {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  font-size: 18px;
  line-height: 1;
}

.ins-body {
  min-width: 0;
}

.ins-title {
  display: block;
  font-size: var(--fs-sm);
  font-weight: 600;
  line-height: 1.35;
}

.ins-text {
  display: block;
  font-size: var(--fs-xs);
  color: var(--text-2);
  margin-top: 3px;
  line-height: 1.5;
}

.ins-level {
  align-self: center;
  padding: 3px 9px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--ins) 14%, transparent);
  color: var(--ins);
  font-size: calc(10px * var(--fs-scale));
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  white-space: nowrap;
}

/* --------------------------------------------------------- status list */
.status-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}
.status-list li {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) 0;
  border-bottom: 1px solid var(--border);
  font-size: calc(13px * var(--fs-scale));
}
.status-list li:last-child {
  border-bottom: 0;
}
.status-list .st-ic {
  width: 30px;
  height: 30px;
  flex: none;
  padding: 7px;
  border-radius: 10px;
  background: var(--surface-2);
  fill: none;
  stroke: var(--text-2);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  font-size: 30px;
  line-height: 1;
  color: var(--text-2);
}
.status-list .st-name {
  flex: 1;
  min-width: 0;
}
.status-list .st-sub {
  display: block;
  font-size: calc(11px * var(--fs-scale));
  color: var(--muted);
}

.st-state {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: calc(12px * var(--fs-scale));
  font-weight: 600;
  font-family: var(--mono);
}
.st-state::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentColor;
}
.st-state[data-state=ok] {
  color: var(--good);
}
.st-state[data-state=warn] {
  color: var(--warning);
}
.st-state[data-state=bad] {
  color: var(--critical);
}
.st-state[data-state=idle] {
  color: var(--muted);
}

/* -------------------------------------------------------------- summary

   Three periods, each a card rather than a row of a table: one headline (what
   the roof made), the period's five energy figures drawn as bars against the
   largest of them, and the money underneath. The bars are scaled inside the
   card and never across cards, so a lifetime in MWh and a day in kWh can sit
   side by side without either pretending to be the other. */
.summary {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--sp-4);
}

.sum-card {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  min-width: 0;
  padding: var(--sp-4);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}

.sum-head .label svg {
  width: 15px;
  height: 15px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
}

.sum-head .label .bi {
  font-size: 15px;
  line-height: 1;
}

.sum-hero {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  flex-wrap: wrap;
}

.sum-hero-v {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-2xl);
  font-weight: 600;
  letter-spacing: -0.025em;
  line-height: 1.05;
}
.sum-hero-v .unit {
  font-size: 0.5em;
  font-weight: 500;
  color: var(--muted);
  margin-left: 0.25em;
  font-family: var(--sans);
  letter-spacing: 0;
}

.sum-hero-k {
  font-size: var(--fs-xs);
  color: var(--muted);
}

.sum-bars {
  display: flex;
  flex-direction: column;
  gap: 7px;
}

.sum-bar {
  display: grid;
  grid-template-columns: 5.6rem minmax(0, 1fr) 5.2rem;
  align-items: center;
  gap: var(--sp-2);
}

.sum-k {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: var(--fs-xs);
  color: var(--text-2);
  min-width: 0;
}
.sum-k .swatch {
  width: 8px;
  height: 8px;
  border-radius: 3px;
  flex: none;
}

.sum-track {
  height: 7px;
  border-radius: 999px;
  background: var(--surface-3);
  overflow: hidden;
}
.sum-track i {
  display: block;
  height: 100%;
  border-radius: 999px;
  transition: width 0.5s var(--ease);
}

.sum-v {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-xs);
  font-weight: 600;
  text-align: right;
  white-space: nowrap;
}
.sum-v .u {
  color: var(--muted);
  font-weight: 500;
  margin-left: 0.25em;
  font-family: var(--sans);
}

.sum-foot {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-4);
  margin-top: auto;
  padding-top: var(--sp-3);
  border-top: 1px solid var(--border);
}

.sum-fig {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.sum-fig-k {
  font-size: calc(10px * var(--fs-scale));
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
}

.sum-fig-v {
  font-family: var(--mono);
  font-size: var(--fs-sm);
  font-weight: 600;
}

/* Money is the page's headline answer, so it wears the accent as emphasis —
   on a figure, never on a mark that encodes a quantity. */
.sum-fig.is-accent .sum-fig-v {
  color: var(--accent);
  font-size: var(--fs-md);
}

.sum-card.is-loading .sum-hero-v {
  color: var(--muted);
}

/* --------------------------------------------------------------- subnav */
.subnav {
  display: flex;
  gap: var(--sp-1);
  padding: 4px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  align-self: flex-start;
}

.subnav-item {
  padding: 7px 20px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--text-2);
  font: 500 calc(13px * var(--fs-scale)) var(--sans);
  cursor: pointer;
  transition: background 0.18s var(--ease), color 0.18s var(--ease);
}
.subnav-item:hover {
  color: var(--text);
  background: var(--surface-2);
}
.subnav-item.is-on {
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 600;
}

.tabpane {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}

/* ---------------------------------------------- the same tabs, on a rail

   A pill track across the top works while the panes are readings you flick
   between. App config's panes are settings, and settings are a list you scan
   before you pick one: the rail shows all four with their icons and their
   names at once, and stays on screen while the pane beside it scrolls, so
   "where else can I go" never costs a scroll back to the top.

   Same contract as .subnav - .subnav-item[data-tab] plus one pane each - so
   showTab()/wireTabs() drive it unchanged. */
.sidelayout {
  display: grid;
  grid-template-columns: 232px minmax(0, 1fr);
  gap: var(--sp-5);
  align-items: start;
}

.sidenav {
  position: sticky;
  top: calc(var(--nav-h) + var(--sp-4));
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px;
  border-radius: var(--r-lg);
  background: var(--surface);
  border: 1px solid var(--border);
  /* Words only. Six icons down one edge read as a toolbar, and a rail wide
     enough for the names does not need them. */
}
.sidenav .subnav-item {
  display: block;
  width: 100%;
  padding: 10px var(--sp-3);
  border-radius: var(--r-md);
  text-align: left;
  font-size: var(--fs-sm);
}

.sidelayout-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  min-width: 0;
}

/* Appearance holds the theme cards and the text-size picker. Stretched across
   a 1440px pane they are small controls adrift in a wide empty box, so each
   card stops at the width its own contents need. */
#acpane-appearance > .card {
  max-width: 640px;
}

/* Narrow: the rail becomes the pill track it was built on. */
@media (max-width: 900px) {
  .sidelayout {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sp-4);
  }
  .sidenav {
    position: static;
    flex-direction: row;
    flex-wrap: wrap;
    padding: 4px;
    align-self: flex-start;
    /* Not a 999px pill: on a phone the six wrap onto two rows, and a
       capsule around two rows of pills reads as a mistake. */
    border-radius: var(--r-lg);
  }
  .sidenav .subnav-item {
    width: auto;
    border-radius: 999px;
    padding: 7px 14px;
  }
}
/* ------------------------------------------------------------- controls */
.controls {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
}

.icon-btn {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--text-2);
  cursor: pointer;
  text-decoration: none; /* #config-btn is an <a>, not a <button> */
  transition: background 0.18s var(--ease), color 0.18s var(--ease);
}
.icon-btn:hover:not(:disabled) {
  background: var(--surface-3);
  color: var(--text);
}
.icon-btn:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}
.icon-btn svg {
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.icon-btn .bi {
  font-size: 15px;
  line-height: 1;
}
.icon-btn.toggle[aria-pressed=true] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}

#config-btn.is-on {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}

.date-input {
  padding: 6px 10px;
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--text);
  font: 500 calc(12.5px * var(--fs-scale)) var(--mono);
  color-scheme: dark;
}

.year-label {
  font: 600 calc(14px * var(--fs-scale)) var(--mono);
  min-width: 4ch;
  text-align: center;
}

.chip {
  padding: 5px 13px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text-2);
  font: 500 calc(12px * var(--fs-scale)) var(--sans);
  cursor: pointer;
  transition: background 0.18s var(--ease), color 0.18s var(--ease);
}
.chip:hover {
  background: var(--surface-3);
  color: var(--text);
}
.chip.is-on {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
  font-weight: 600;
}

/* ---------------------------------------------------------- stats strip */
.daystat {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: var(--sp-3);
}

.stat {
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface-2);
  border-radius: var(--r-md);
  min-width: 0;
}

.stat-k {
  display: block;
  font-size: calc(10px * var(--fs-scale));
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

.stat-v {
  display: block;
  margin-top: 3px;
  font-size: calc(19px * var(--fs-scale));
  font-weight: 600;
  letter-spacing: -0.02em;
}
.stat-v .unit {
  font-size: 0.55em;
  color: var(--muted);
  margin-left: 0.25em;
}

.stat-f {
  display: block;
  font-size: calc(11px * var(--fs-scale));
  color: var(--muted);
  margin-top: 1px;
}

/* --------------------------------------------------------------- charts */
.chart-stack {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  min-width: 0;
}

.chart-box {
  position: relative;
  min-width: 0;
}
.chart-box.tall {
  height: var(--chart-tall);
}
.chart-box.short {
  height: 190px;
}
.chart-box canvas {
  width: 100% !important;
  height: 100% !important;
}

.legend {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-5);
  margin: 0;
  padding: 0;
}
.legend li {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--fs-sm);
  color: var(--text-2);
}
.legend .swatch {
  width: 10px;
  height: 10px;
  border-radius: 3px;
  flex: none;
}
.legend .lv {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--muted);
}

/* ---------------------------------------------------------------- table */
.table-scroll {
  overflow-x: auto;
  max-height: 460px;
  overflow-y: auto;
  border-radius: var(--r-md);
}
.table-scroll.compact {
  max-height: 210px;
}

/* The "Today, 24 Hrs" table replaces its chart, but it does not have to be as
   tall as one: it grows to its rows and stops well short of the chart box.
   "State of grid" holds the same shape of table - a row per sample - so it
   scrolls at the same height rather than pushing the card twice as long. */
#d24-table-wrap.table-scroll,
#gridstate-table-wrap.table-scroll,
#an-cal-table-wrap.table-scroll {
  max-height: 230px;
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: calc(12.5px * var(--fs-scale));
}

thead th {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
  padding: var(--sp-2) var(--sp-3);
  background: var(--surface-2);
  color: var(--muted);
  font-size: calc(10.5px * var(--fs-scale));
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  text-align: left;
  white-space: nowrap;
  /* A header sits over its column, not beside it: .n is the numeric column in
     both rows of the table, so the two can never drift apart. */
}
thead th.n {
  text-align: right;
}

tbody td {
  padding: 7px var(--sp-3);
  border-top: 1px solid var(--border);
  white-space: nowrap;
}
tbody td.n {
  text-align: right;
}

tbody tr:hover td {
  background: var(--surface-2);
}

.empty {
  padding: var(--sp-5);
  text-align: center;
  color: var(--muted);
  font-size: calc(13px * var(--fs-scale));
  grid-template-columns: auto !important;
}

/* An empty state that replaced a chart should sit where the chart sat, not
   pinned to the top of the box it inherited. */
.chart-box .empty {
  display: grid;
  place-content: center;
  height: 100%;
  margin: 0;
}

/* --------------------------------------------------------------- events */
.events {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  max-height: 520px;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.events li {
  display: grid;
  grid-template-columns: 20px minmax(0, 1fr) auto;
  gap: var(--sp-3);
  align-items: center;
  padding: var(--sp-3) 0;
  border-bottom: 1px solid var(--border);
}
.events li:last-child {
  border-bottom: 0;
}

.ev-icon {
  width: 17px;
  height: 17px;
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  font-size: 17px;
  line-height: 1;
}

.ev-fault {
  stroke: var(--critical);
}

.ev-warning {
  stroke: var(--warning);
}

.ev-ok {
  stroke: var(--good);
}

.ev-body {
  min-width: 0;
}

.ev-text {
  display: block;
  font-size: calc(13px * var(--fs-scale));
}

.ev-time {
  display: block;
  font-size: calc(11px * var(--fs-scale));
  color: var(--muted);
}

.ev-tag {
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--surface-3);
  color: var(--text-2);
  font-size: calc(10.5px * var(--fs-scale));
  font-weight: 600;
  font-family: var(--mono);
}

/* -------------------------------------------------------------- weather */
.weather {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  flex-wrap: wrap;
}

.wx-icon {
  width: 38px;
  height: 38px;
  flex: none;
  fill: none;
  stroke: var(--text-2);
  stroke-width: 1.6;
  font-size: 38px;
  line-height: 1;
  color: var(--text-2);
}

.wx-main {
  flex: 1;
  min-width: 160px;
}

.wx-cond {
  font-size: calc(14px * var(--fs-scale));
  font-weight: 600;
}

.wx-temp {
  font-size: calc(12px * var(--fs-scale));
  color: var(--muted);
  font-family: var(--mono);
}

.wx-predict {
  display: flex;
  gap: var(--sp-4);
}
.wx-predict > div {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--sp-2) var(--sp-4);
  background: var(--surface-2);
  border-radius: var(--r-md);
}
.wx-predict .v {
  font-family: var(--mono);
  font-size: calc(17px * var(--fs-scale));
  font-weight: 600;
}
.wx-predict .unit {
  font-size: 0.6em;
  color: var(--muted);
}

/* ---------------------------------------------------------- maintenance */
.readouts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: var(--sp-3);
  margin: 0;
}
.readouts > div {
  padding: var(--sp-3);
  background: var(--surface-2);
  border-radius: var(--r-md);
  min-width: 0;
}
.readouts dt {
  font-size: calc(10px * var(--fs-scale));
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
}
.readouts dd {
  margin: 3px 0 0;
  font-size: calc(14px * var(--fs-scale));
  font-weight: 600;
}
.readouts dd small {
  font-family: var(--sans);
  font-size: calc(11px * var(--fs-scale));
  font-weight: 400;
  color: var(--muted);
  margin-left: 5px;
}

.filelist, .linklist {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
}

.filelist button {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: var(--text);
  font: 500 var(--fs-sm) var(--sans);
  text-align: left;
  cursor: pointer;
}
.filelist button:hover {
  background: var(--surface-3);
}
.filelist button.is-on {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.filelist .fl-meta {
  font-family: var(--mono);
  font-size: calc(11px * var(--fs-scale));
  color: var(--muted);
  white-space: nowrap;
}

.logview {
  margin: 0;
  padding: var(--sp-3);
  max-height: 300px;
  overflow: auto;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  font-size: calc(11.5px * var(--fs-scale));
  line-height: 1.6;
  color: var(--text-2);
  white-space: pre-wrap;
  word-break: break-word;
}

.linklist a {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-3);
  border-radius: var(--r-md);
  background: var(--surface-2);
  color: var(--text);
  text-decoration: none;
  font-size: calc(13px * var(--fs-scale));
}
.linklist a:hover {
  background: var(--surface-3);
}

.linklist .ll-note {
  display: block;
  font-size: calc(11px * var(--fs-scale));
  color: var(--muted);
  font-weight: 400;
}

.linklist svg {
  width: 15px;
  height: 15px;
  flex: none;
  fill: none;
  stroke: var(--muted);
  stroke-width: 2;
}

.linklist .bi {
  font-size: 15px;
  line-height: 1;
  color: var(--muted);
}

/* ---------------------------------------------------------------- footer

   A sign-off, not a second navigation, and not a card: the surface, border and
   radius it used to wear are what the thirty-five cards above it wear, and a
   sign-off in that costume reads as one more reading to take. Plain text is
   the whole of it - not even the rule over the top any more, which drew a
   line under a page whose last card already ends in one. Three parts still:
   what this is, what it is reading, and the one promise worth repeating on
   every page. */
.foot {
  /* flex-end, not flex-start: the two halves are three lines each and the
     right one is set a shade smaller, so hanging them from the top left the
     last line of one floating above the last line of the other. Sat on a
     common bottom edge they read as one band. */
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--sp-3) var(--sp-5);
  /* auto, not a fixed gap: on a long page it collapses to the padding below,
     and on a short one it takes up whatever slack is left under the last
     card. */
  margin-top: auto;
  padding: var(--sp-5) 0 var(--sp-2);
}

.foot-lede {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
  flex: 1 1 280px;
}

.foot-title {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: var(--fs-sm);
  font-weight: 600;
}

/* The bolt is the brand mark, not a chip: the tile it used to sit in was the
   last card-shaped thing down here. */
.foot-mark {
  width: 15px;
  height: 15px;
  flex: none;
  fill: currentColor;
  stroke: none;
  color: var(--accent);
}

.foot-sub {
  font-size: var(--fs-xs);
  color: var(--muted);
  line-height: 1.5;
}
.foot-sub a {
  color: var(--text-2);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
}
.foot-sub a:hover {
  color: var(--accent);
}

/* The right-hand column, read top to bottom: the promise, what keeps it, and
   who made it. The nameplate chips that used to sit here said the same three
   figures the KPI strip says, one screen above. */
/* No gap: three lines of running text, so the line-height is the spacing.
   The 5px it used to carry made them three separate readings. */
.foot-end {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0;
  text-align: right;
}

.foot-note {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: var(--fs-xs);
  color: var(--muted);
}
.foot-note svg {
  width: 14px;
  height: 14px;
  flex: none;
  fill: none;
  stroke: var(--good);
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.foot-note .bi {
  font-size: 14px;
  line-height: 1;
  color: var(--good);
}

.foot-tech {
  font-size: calc(11.5px * var(--fs-scale));
  color: var(--muted);
  font-family: var(--mono);
}
.foot-tech a {
  color: var(--muted);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-color: var(--border-strong);
}
.foot-tech a:hover {
  color: var(--accent);
}

.foot-by {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: calc(11.5px * var(--fs-scale));
  color: var(--muted);
}

/* Pink, and its own colour on purpose: it is the one mark on the page that is
   neither chrome nor a series, so it borrows from neither. */
.foot-heart {
  width: 12px;
  height: 12px;
  flex: none;
  color: #e0457b;
}

.foot-by a {
  color: var(--text-2);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
}
.foot-by a:hover {
  color: var(--accent);
}

/* Below 992px the two columns stack, and a right-aligned block sitting under a
   left-aligned one reads as a mistake rather than as a column - there is no
   longer an edge for it to be flush with. Both halves centre together, which
   is the only alignment that stays true all the way down to 320px. The old
   rule flipped the right column to the left at 720px and left the tablet
   width, where the stack actually happens, wearing the desktop alignment. */
@media (max-width: 992px) {
  .foot {
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: var(--sp-3);
    padding-top: var(--sp-4);
  }
  .foot-lede {
    align-items: center;
    flex: none;
  }
  .foot-end {
    align-items: center;
    text-align: center;
  }
  /* The promise is the longest line down here and wraps to two or three at
     phone width. Centred, the tick has to flow with the first line: left as a
     flex child it hangs beside a ragged block, centred on a height it has
     nothing to do with. */
  .foot-note {
    display: block;
  }
  .foot-note svg {
    display: inline-block;
    vertical-align: -2px;
    margin-right: 6px;
  }
}
/* ==================================================================== money
   The cost view. These are the numbers the whole dashboard exists to produce,
   so they get the largest type on the page and the plainest language. */
.bill-hero {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: var(--sp-4);
  align-items: stretch;
}

.bill-card {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  padding: var(--sp-4) var(--sp-5);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
}
.bill-card.is-saved {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.bill-k {
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: 0.085em;
  text-transform: uppercase;
  color: var(--muted);
}

.bill-v {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-3xl);
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.02em;
}

.bill-card.is-saved .bill-v {
  color: var(--accent);
}

.bill-v .cur {
  font-size: 0.5em;
  font-weight: 500;
  color: var(--muted);
  margin-right: 0.3em;
}

.bill-f {
  font-size: var(--fs-sm);
  color: var(--muted);
  line-height: 1.45;
}

.bill-strike {
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}

/* The tariff ladder. A stepped tariff is invisible in a number and obvious in
   a picture, so the steps are drawn to scale with the month's position marked. */
.ladder {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.ladder-row {
  display: grid;
  grid-template-columns: 8.5rem minmax(0, 1fr) 7rem;
  align-items: center;
  gap: var(--sp-3);
  font-size: var(--fs-sm);
}

.ladder-band {
  font-family: var(--mono);
  color: var(--text-2);
}

.ladder-track {
  position: relative;
  height: 22px;
  border-radius: 6px;
  background: var(--surface-3);
  overflow: hidden;
}

.ladder-fill {
  position: absolute;
  inset: 0 auto 0 0;
  background: var(--s-grid);
  opacity: 0.85;
}

.ladder-row.is-now {
  position: relative;
  padding-left: 9px;
  margin-left: -9px;
  border-left: 3px solid var(--accent);
}

.ladder-rate {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  text-align: right;
  color: var(--text-2);
}

.ladder-row.is-now .ladder-band,
.ladder-row.is-now .ladder-rate {
  color: var(--text);
  font-weight: 600;
}

.ladder-legend {
  font-size: var(--fs-sm);
  color: var(--muted);
  line-height: 1.5;
}

.ladder-head {
  color: var(--muted);
}
.ladder-head .ladder-band, .ladder-head .ladder-rate {
  font-family: var(--sans);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: 0.085em;
  text-transform: uppercase;
}

/* Itemised bill - a real receipt, so the total can be checked rather than
   trusted. */
.receipt {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-sm);
}
.receipt td {
  padding: 7px 0;
  border-bottom: 1px solid var(--border);
}
.receipt td.n {
  text-align: right;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}
.receipt tr:last-child td {
  border-bottom: 0;
  font-weight: 600;
  font-size: var(--fs-md);
}
.receipt tr.rule td {
  border-bottom-color: var(--border-strong);
}

/* ================================================================= signals
   The attribute explorer built on the dayLine endpoint. Four dropdowns across
   the top, one per side of the inverter, above the stats and the chart. It was
   a scrolling column of ninety chips, which put the register you wanted below
   the fold almost every time; a <select> holds the same list in one line and
   brings the platform's keyboard handling with it. */
.sig-picker {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: var(--sp-3);
  margin-bottom: var(--sp-4);
}

.sig-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
  padding: var(--sp-3);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
  /* The group holding the signal on screen is the one wearing the accent border -
     the other three are waiting, not competing. */
}
.sig-group[data-on="1"] {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.sig-group-label {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: calc(10.5px * var(--fs-scale));
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 600;
}
.sig-group-label .swatch {
  width: 9px;
  height: 9px;
  border-radius: 3px;
  flex: none;
}

.sig-select {
  width: 100%;
  min-width: 0;
  min-height: 38px;
  padding: 8px 10px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--text);
  font: 500 var(--fs-sm)/1.3 var(--sans);
}
.sig-select:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.sig-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: var(--sp-3);
}

/* The raw register table is wide by nature - it scrolls inside its own box
   rather than pushing the page sideways. */
.rawtable {
  max-height: 60vh;
  overflow: auto;
}
.rawtable table {
  font-size: var(--fs-xs);
}
.rawtable th {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
  background: var(--surface-2);
}

/* ============================================================== responsive */
@media (max-width: 1180px) {
  .grid-hero, .grid-3, .grid-2, .grid-4 {
    grid-template-columns: minmax(0, 1fr);
  }
  .ladder-row {
    grid-template-columns: 7rem minmax(0, 1fr) 5.5rem;
    font-size: var(--fs-xs);
  }
}
/* From 1366px down the six destinations no longer fit beside the brand and
   the four controls without the track scrolling, and a rail you have to drag
   sideways hides half the site. The same element becomes the panel the
   hamburger opens instead - the same element, not a copy of the six links,
   because a second list is a second place to forget a new view. */
@media (max-width: 1366px) {
  .topbar {
    grid-template-columns: minmax(0, 1fr) auto;
  }
  .navmenu {
    display: inline-flex;
  }
  .nav-scroll {
    /* Hung under the bar by placeBarMenu(), like the bell's menu and the
       account menu. No backdrop-filter on this one: it is still a child of
       the bar, which is itself a backdrop root, so a filter here would sample
       the bar's output rather than the page. The solid glass is what the
       unsupported-browser case already ships. */
    position: fixed;
    z-index: var(--z-menu);
    top: calc(var(--nav-h) + 6px);
    right: var(--sp-5);
    display: none;
    width: min(248px, 100vw - 2 * var(--sp-4));
    max-height: calc(100vh - var(--nav-h) - var(--sp-5));
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 6px;
    background: var(--glass-solid);
    border: 1px solid var(--border-strong);
    border-radius: var(--r-md);
    box-shadow: var(--shadow-pop), inset 0 1px 0 var(--glass-edge);
  }
  .nav-scroll.is-open {
    display: block;
  }
  /* A column of rows, not a pill rail: the track's own border and background
     would draw a second panel inside the one it is now sitting in. */
  .nav {
    flex-direction: column;
    align-items: stretch;
    gap: 1px;
    padding: 0;
    border: 0;
    border-radius: 0;
    background: none;
  }
  .nav-item {
    border-radius: var(--r-sm);
    padding: 10px 12px;
  }
  .nav-item .nav-badge {
    margin-left: auto;
  }
  /* Everything on the right of the bar is the same 34px square from here down.
     Wide, the status pill and the account button are labelled chips and earn
     their own shapes; once the labels no longer fit, a 999px pill holding one
     dot and a round avatar beside three rounded squares reads as three
     unrelated kinds of control rather than one row of them. The word and the
     name are the only things dropped, and neither was the information: the
     status sentence is in the tooltip and in #status-detail, and the account
     menu names every destination inside it. */
  .topbar-right .pill,
  .topbar-right .userbtn {
    width: 34px;
    height: 34px;
    min-height: 0;
    padding: 0;
    gap: 0;
    justify-content: center;
    border-radius: 11px;
    background: var(--surface-2);
  }
  .topbar-right .pill {
    border-color: var(--border-strong);
  }
  .topbar-right .pill #status-text,
  .userbtn-name, .userbtn-caret {
    display: none;
  }
  /* The gradient disc was the account button's whole shape; inside a square it
     is a circle in a box. The icon it holds is what the other three wear, so
     it wears it the same way and the accent goes back to being chrome. */
  .topbar-right .avatar {
    width: auto;
    height: auto;
    border-radius: 0;
    background: none;
    color: inherit;
  }
  .topbar-right .avatar svg {
    width: 15px;
    height: 15px;
  }
  .topbar-right .avatar .bi {
    font-size: 15px;
    line-height: 1;
  }
}
@media (max-width: 720px) {
  main {
    padding: var(--sp-4) var(--sp-3) var(--sp-6);
  }
  .banner {
    margin: var(--sp-3) var(--sp-3) 0;
  }
  .card {
    padding: var(--sp-4);
    border-radius: var(--r-md);
  }
  .display {
    font-size: calc(40px * var(--fs-scale));
  }
  .hello h2 {
    font-size: var(--fs-2xl);
  }
  :root {
    --chart-tall: 260px;
  }
  .topbar {
    padding: var(--sp-2) var(--sp-3);
    gap: var(--sp-2);
  }
  .brand-sub {
    display: none;
  }
  .freshness {
    display: none;
  }
  .btn span {
    display: none;
  }
  .subnav {
    width: 100%;
    overflow-x: auto;
  }
  .subnav-item {
    flex: 1 0 auto;
    padding: 9px 12px;
  }
  .bill-v {
    font-size: var(--fs-2xl);
  }
}
/* Under ~560px the five things on the right of the bar - the status pill, the
   three controls and the hamburger - no longer leave the brand a readable
   width, so the same five squares step down to 32px together. They are one
   row of one control, and a row that shrinks unevenly stops looking like one. */
@media (max-width: 560px) {
  .topbar {
    gap: var(--sp-2) var(--sp-3);
  }
  .brand {
    gap: var(--sp-2);
  }
  .brand-mark {
    width: 30px;
    height: 30px;
    border-radius: 9px;
  }
  .brand-mark svg {
    width: 16px;
    height: 16px;
  }
  .brand-mark .bi {
    font-size: 16px;
    line-height: 1;
  }
  .brand-word {
    font-size: var(--fs-sm);
  }
  .topbar-right {
    gap: 5px;
  }
  .topbar-right .icon-btn, .topbar-right .pill, .topbar-right .userbtn {
    width: 32px;
    height: 32px;
    border-radius: 10px;
  }
  /* The tooltip hangs off a pill that is now a few pixels wide at the right
     edge of the screen; without this it is pinned to that edge and wraps into
     a column. */
  .pill[data-tip]::after {
    max-width: min(280px, 100vw - var(--sp-5));
  }
}
/* The battery card carries two gauges now, so its six readings run three
   across rather than two - the card is no taller than the diagram beside it. */
.ring-pair + .mini-stats {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

@media (max-width: 1180px) {
  .ring-pair + .mini-stats {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}
@media (max-width: 520px) {
  .ring-pair {
    grid-template-columns: minmax(0, 1fr);
  }
  .ring-cap {
    max-width: none;
  }
  .ring-pair + .mini-stats {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .insights li {
    grid-template-columns: 30px minmax(0, 1fr);
  }
  .ins-level {
    grid-column: 2;
    justify-self: start;
  }
  .sum-bar {
    grid-template-columns: 5rem minmax(0, 1fr) 4.6rem;
  }
}
/* Every interactive target clears 44px on touch, per the checklist. */
@media (pointer: coarse) {
  .icon-btn, .chip, .btn, .subnav-item {
    min-height: 44px;
    min-width: 44px;
  }
  /* The pill and the account button are squares in the bar beside those, and a
     34px target next to a 44px one is both hard to hit and visibly the odd one
     out. */
  .topbar-right .pill, .topbar-right .userbtn {
    min-height: 44px;
    min-width: 44px;
  }
}
/* ======================================================= dashboard (2026) */
/* The KPI row. Six tiles, each one question: a primary number large enough to
   read across the room, the figure most often wanted next beneath it, and the
   rest in a drawer the tile opens. */
.kpis {
  display: grid;
  /* Six explicit columns rather than auto-fit: with six cards, auto-fit lands
     on five and leaves the sixth stranded on a row of its own. */
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: var(--sp-4);
}

/* A button, so the drawer is reachable by keyboard and announced as expandable
   — hence the resets: a button inherits none of the page's type or alignment. */
.kpi {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow);
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color 0.18s var(--ease), box-shadow 0.18s var(--ease), transform 0.18s var(--ease);
}

/* Positioned so the popover can hang off it, and lifted a hair on hover so
   the tile that is about to speak is obvious before the panel appears.

   The lift is also why the tile itself has to be raised: `transform` makes
   the tile a stacking context, which traps the popover's z-index inside it,
   and the tile then paints in DOM order — underneath the flow and battery
   cards that follow it. Raising the tile raises the whole context with it. */
.kpi {
  position: relative;
}
.kpi:hover, .kpi:focus-visible {
  border-color: var(--border-strong);
  box-shadow: var(--shadow-pop);
  transform: translateY(-1px);
  z-index: var(--z-pop);
}
.kpi.is-open {
  border-color: var(--accent);
  z-index: var(--z-pop);
}

.kpi-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
}

/* The state chip rides the baseline of the number rather than sitting beside
   the label: six tiles across leaves no room for both on one line, and a
   wrapped chip made one tile taller than its neighbours. */
.kpi-mid {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-2);
  min-width: 0;
}

.kpi-label {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-width: 0;
}
.kpi-label svg {
  width: 14px;
  height: 14px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
}
.kpi-label .bi {
  font-size: 14px;
  line-height: 1;
}

.kpi-state {
  font-size: calc(10px * var(--fs-scale));
  letter-spacing: 0.07em;
  text-transform: uppercase;
  font-weight: 600;
  padding: 3px 8px;
  border-radius: 999px;
  background: var(--surface-3);
  color: var(--muted);
  white-space: nowrap;
}
.kpi-state[data-state=charging], .kpi-state[data-state=importing] {
  color: var(--good);
}
.kpi-state[data-state=discharging], .kpi-state[data-state=exporting] {
  color: var(--warning);
}

.kpi-value {
  font-size: calc(34px * var(--fs-scale));
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1.05;
  font-family: var(--mono);
}
.kpi-value .unit {
  font-size: 0.42em;
  font-weight: 500;
  color: var(--muted);
  margin-left: 0.3em;
  letter-spacing: 0;
  font-family: var(--sans);
}

/* The one supporting figure that stays on the tile. Its own rule, so the tile
   still reads as a number with a footnote rather than a two-row table. */
.kpi-sub {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-2);
  border-top: 1px solid var(--border);
  padding-top: var(--sp-2);
  font-size: var(--fs-xs);
  color: var(--muted);
  min-width: 0;
  /* When a two-column strip runs out of width it is the caption that gives, not
     the reading: "Imported to... 2.4 kWh" beats "Imported today 2.4 k...". */
}
.kpi-sub > span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.kpi-sub b {
  flex: none;
  font-family: var(--mono);
  font-weight: 600;
  color: var(--text);
  text-align: right;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 60%;
}

.kpi.is-spec .kpi-sub b {
  font-family: var(--sans);
  font-weight: 500;
}

/* The popover. The supporting figures used to open in a panel below the whole
   strip, which pushed the flow diagram down the page every time you asked a
   tile a question. They hang off the tile itself now: hover reveals it,
   click pins it (so it survives on a touch screen, where there is no hover,
   and while you read it with the mouse elsewhere), Escape or a second click
   puts it away. `pointer-events: none` keeps the panel from swallowing the
   hover that is holding it open. */
.kpi-pop {
  position: absolute;
  z-index: var(--z-pop);
  top: calc(100% + 9px);
  left: 0;
  right: auto;
  min-width: min(272px, 100vw - 2 * var(--sp-4));
  padding: var(--sp-3) var(--sp-4) var(--sp-4);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-pop);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-5px);
  transition: opacity 0.16s var(--ease), transform 0.16s var(--ease), visibility 0.16s var(--ease);
  pointer-events: none;
}

.kpi:hover .kpi-pop,
.kpi:focus-visible .kpi-pop,
.kpi.is-open .kpi-pop {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

.kpi.is-open .kpi-pop {
  border-color: var(--accent);
}

/* The notch. Two triangles - the border colour behind the surface colour -
   because a single rotated square would cut the panel's own shadow. */
.kpi-pop::before, .kpi-pop::after {
  content: "";
  position: absolute;
  left: var(--sp-5);
  border: 7px solid transparent;
}

.kpi-pop::before {
  bottom: 100%;
  border-bottom-color: var(--border-strong);
}

.kpi-pop::after {
  bottom: calc(100% - 1px);
  border-bottom-color: var(--surface);
}

.kpi.is-open .kpi-pop::before {
  border-bottom-color: var(--accent);
}

.kpi:nth-child(n+5) .kpi-pop::before,
.kpi:nth-child(n+5) .kpi-pop::after {
  left: auto;
  right: var(--sp-5);
}

.kpi-pop-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding-bottom: var(--sp-2);
  border-bottom: 1px solid var(--border);
}
.kpi-pop-head .label {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}
.kpi-pop-head .label svg {
  width: 14px;
  height: 14px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
}
.kpi-pop-head .label .bi {
  font-size: 14px;
  line-height: 1;
}

.kpi-pop-hint {
  font-size: calc(10.5px * var(--fs-scale));
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}

.kpi.is-open .kpi-pop-hint {
  color: var(--accent);
}

/* Inside the panel the rows stack: a label and its reading, one per line, so
   long values ("Deye SUN-8K · 8 kW") wrap instead of colliding with the key.
   Spans rather than a <dl>: the tile is a <button>, whose content model is
   phrasing only, and a definition list inside one is invalid markup that
   browsers reparse out from under you. */
.kpi-pop .kpi-rows {
  display: flex;
  flex-direction: column;
}
.kpi-pop .kpi-rows > span {
  display: block;
  padding-top: var(--sp-2);
}
.kpi-pop .kpi-rows > span + span {
  border-top: 1px solid var(--border);
  margin-top: var(--sp-2);
}

.kpi-pop .kpi-k {
  display: block;
  font-size: var(--fs-xs);
  color: var(--muted);
}

.kpi-pop .kpi-v {
  display: block;
  margin-top: 2px;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-sm);
  font-weight: 600;
  line-height: 1.4;
}

.kpi.is-spec .kpi-pop .kpi-v {
  font-family: var(--sans);
  font-weight: 500;
}

/* Which side the panel opens on. The last two tiles of a six-across strip
   would run off the right edge, so they hang from their right corner - and
   each narrower grid re-states the rule for its own column count. */
.kpi:nth-child(n+5) .kpi-pop {
  left: auto;
  right: 0;
}

/* Money is the page's headline answer, so it wears the accent as emphasis -
   on the surround and the value, never on a mark that encodes a quantity. */
.kpi.is-accent {
  border-color: var(--accent);
  background: linear-gradient(180deg, var(--accent-soft), transparent 70%), var(--surface);
}
.kpi.is-accent .kpi-value {
  color: var(--accent);
}

/* The nameplate is context, not a reading: it recedes. */
.kpi.is-spec {
  background: var(--surface-2);
}
.kpi.is-spec .kpi-value {
  font-size: calc(27px * var(--fs-scale));
  color: var(--text-2);
}

/* Flow row: the diagram is the centrepiece, the battery reads beside it.
   The two cards stretch to a common height - the row reads as one band, and
   two cards that stop at different lines look like one of them failed to
   load. The flow card cannot absorb that height in its picture: the render
   is 1200x896, the stage already shows all of it, and anything taller would
   have to crop the sides - which is where the service pole and three of the
   four plates are. So the slack goes into the card's own rhythm instead,
   `space-between` opening the gaps around the scene evenly. */
.grid-flow {
  display: grid;
  grid-template-columns: minmax(0, 1.45fr) minmax(0, 1fr);
  gap: var(--sp-4);
}
.grid-flow > * {
  min-width: 0;
}

.card-flow {
  justify-content: space-between;
}

.head-right {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
}

/* A legend whose entries switch their series off. It stays a legend first -
   the value is still readable when the series is hidden, because that number
   is the secondary encoding the palette depends on. */
.legend-toggle li {
  display: block;
}

.legend-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 5px 10px;
  border-radius: 999px;
  border: 1px solid transparent;
  background: none;
  cursor: pointer;
  font: inherit;
  font-size: var(--fs-sm);
  color: var(--text-2);
}
.legend-btn:hover {
  background: var(--surface-2);
  border-color: var(--border);
}
.legend-btn .swatch {
  width: 10px;
  height: 10px;
  border-radius: 3px;
  flex: none;
}
.legend-btn .lv {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--muted);
}
.legend-btn[aria-pressed=false] {
  color: var(--muted);
}
.legend-btn[aria-pressed=false] .swatch {
  background: var(--border-strong) !important;
}
.legend-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* String performance. Two strings on one roof should track each other, so the
   bars share a scale and the gap between them is the whole point. */
.strings {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.strings-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
}

.string-row {
  display: grid;
  grid-template-columns: 3.2rem minmax(0, 1fr) 5rem;
  align-items: center;
  gap: var(--sp-2) var(--sp-3);
}

.string-name {
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--text-2);
}

.string-bar {
  height: 8px;
  border-radius: 999px;
  background: var(--surface-3);
  overflow: hidden;
}
.string-bar i {
  display: block;
  height: 100%;
  border-radius: 999px;
  transition: width 0.5s var(--ease);
}

.string-v {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  font-weight: 600;
  text-align: right;
}

.string-meta {
  grid-column: 2/-1;
  font-size: calc(11px * var(--fs-scale));
  color: var(--muted);
}

.warn-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: calc(11px * var(--fs-scale));
  font-weight: 600;
  color: var(--warning);
}
.warn-chip svg {
  width: 13px;
  height: 13px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
}
.warn-chip .bi {
  font-size: 13px;
  line-height: 1;
}

/* Efficiency meters: each ratio against its own 100%, read as a sentence. */
.meters {
  list-style: none;
  margin: 0 0 var(--sp-4);
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

.meter-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-2);
}

.meter-k {
  font-size: var(--fs-sm);
  color: var(--text-2);
}

.meter-v {
  font-family: var(--mono);
  font-size: var(--fs-sm);
  font-weight: 600;
}

.meter-bar {
  display: block;
  height: 8px;
  margin: 6px 0 3px;
  border-radius: 999px;
  background: var(--surface-3);
  overflow: hidden;
}
.meter-bar i {
  display: block;
  height: 100%;
  border-radius: 999px;
  transition: width 0.5s var(--ease);
}

.meter-note {
  display: block;
  font-size: calc(11px * var(--fs-scale));
  color: var(--muted);
}

/* Lanes: the Sankey this page does not need a second library for. */
.lanes + .lanes {
  margin-top: var(--sp-4);
}

.lane-title {
  font-size: calc(11px * var(--fs-scale));
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 600;
  margin: 0 0 var(--sp-2);
}

.lane {
  display: grid;
  grid-template-columns: 8.5rem minmax(0, 1fr) 5.6rem 2.6rem;
  align-items: center;
  gap: var(--sp-2);
  padding: 4px 0;
}

.lane-k {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: var(--fs-xs);
  color: var(--text-2);
}
.lane-k .swatch {
  width: 9px;
  height: 9px;
  border-radius: 3px;
  flex: none;
}

.lane-bar {
  height: 10px;
  border-radius: 999px;
  background: var(--surface-3);
  overflow: hidden;
}
.lane-bar i {
  display: block;
  height: 100%;
  border-radius: 999px;
  transition: width 0.5s var(--ease);
}

.lane-v {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  font-weight: 600;
  text-align: right;
}

.lane-pct {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--muted);
  text-align: right;
}

/* Today's events, newest first — drawn as a timeline rather than a table.
   The rail is a segment per entry: continuous however many there are, and it
   stops at the first and last beads instead of running off both ends. */
.timeline {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* The day's events are unbounded - a busy day runs to dozens - while the card
   beside it ("where today's energy went") is a fixed handful of lanes. Left
   alone the list sets the row height and the pair stops being a pair. So the
   list flexes to whatever height the grid row already has and scrolls past
   that; the cap is what stops it on the narrow layout, where there is no
   neighbour to match. */
.timeline-scroll {
  flex: 1 1 auto;
  min-height: 0;
  max-height: 360px;
  overflow-y: auto;
  overscroll-behavior: contain;
  /* Room for the rail and the focus ring the scroller would otherwise clip. */
  padding-right: var(--sp-2);
  margin-right: calc(-1 * var(--sp-2));
}

.timeline li {
  position: relative;
  display: grid;
  grid-template-columns: 30px minmax(0, 1fr);
  align-items: start;
  gap: var(--sp-3);
  padding: var(--sp-2) 0;
}
.timeline li::before {
  content: "";
  position: absolute;
  left: 14px;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--border);
}
.timeline li:first-child::before {
  top: 23px;
}
.timeline li:last-child::before {
  bottom: auto;
  height: 23px;
}
.timeline li:first-child:last-child::before {
  display: none;
}

.tl-rail {
  position: relative;
  z-index: 1;
  display: grid;
  place-items: center;
  padding-top: 8px;
}

.tl-dot {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--surface);
  border: 2px solid var(--border-strong);
  color: var(--muted);
}

.tl-ic {
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  font-size: 15px;
  line-height: 1;
}

.timeline li[data-kind=solar] .tl-dot {
  border-color: var(--s-solar);
  color: var(--s-solar);
}

.timeline li[data-kind=battery] .tl-dot {
  border-color: var(--s-battery);
  color: var(--s-battery);
}

.timeline li[data-kind=grid] .tl-dot {
  border-color: var(--s-grid);
  color: var(--s-grid);
}

.tl-body {
  min-width: 0;
  padding: var(--sp-2) var(--sp-3);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}

.tl-time {
  display: block;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: calc(11px * var(--fs-scale));
  letter-spacing: 0.04em;
  color: var(--muted);
}

.tl-text {
  display: block;
  font-size: var(--fs-sm);
  color: var(--text);
  margin-top: 1px;
  line-height: 1.45;
}

.timeline li.empty {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-sm);
  color: var(--muted);
}
.timeline li.empty::before {
  display: none;
}

.tl-empty-ic {
  width: 16px;
  height: 16px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  font-size: 16px;
  line-height: 1;
}

/* The technical readouts sit under system health, deliberately the smallest
   type on the page: they are for a power user, not for the glance. */
.mini-stats.tech {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  margin-top: var(--sp-4);
  gap: var(--sp-2);
}
.mini-stats.tech > div {
  padding: var(--sp-2);
}
.mini-stats.tech dd {
  font-size: calc(12.5px * var(--fs-scale));
}

/* System health, full width: the status list and the technical readouts side
   by side rather than fighting for a half-width card each. */
.health-split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
  gap: var(--sp-5);
  align-items: start;
}

@media (max-width: 900px) {
  .health-split {
    grid-template-columns: minmax(0, 1fr);
  }
}
/* Six across holds much further down than it did when each tile carried four
   figures: all a tile has to fit now is its label, its number and one row. */
@media (max-width: 1180px) {
  .kpis {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
  .kpi:nth-child(n+1) .kpi-pop {
    left: 0;
    right: auto;
  }
  .kpi:nth-child(n+1) .kpi-pop::before, .kpi:nth-child(n+1) .kpi-pop::after {
    left: var(--sp-5);
    right: auto;
  }
  .kpi:nth-child(3n) .kpi-pop {
    left: auto;
    right: 0;
  }
  .kpi:nth-child(3n) .kpi-pop::before, .kpi:nth-child(3n) .kpi-pop::after {
    left: auto;
    right: var(--sp-5);
  }
}
@media (max-width: 1180px) {
  .grid-flow {
    grid-template-columns: minmax(0, 1fr);
  }
}
@media (max-width: 860px) {
  .kpis {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .kpi:nth-child(n+1) .kpi-pop {
    left: 0;
    right: auto;
  }
  .kpi:nth-child(n+1) .kpi-pop::before, .kpi:nth-child(n+1) .kpi-pop::after {
    left: var(--sp-5);
    right: auto;
  }
  .kpi:nth-child(2n) .kpi-pop {
    left: auto;
    right: 0;
  }
  .kpi:nth-child(2n) .kpi-pop::before, .kpi:nth-child(2n) .kpi-pop::after {
    left: auto;
    right: var(--sp-5);
  }
}
@media (max-width: 720px) {
  .kpi-value {
    font-size: calc(30px * var(--fs-scale));
  }
  .lane {
    grid-template-columns: 5.4rem minmax(0, 1fr) 4.2rem;
  }
  .lane-pct {
    display: none;
  }
  .mini-stats.tech {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
/* ------------------------------------------------------------------ drawer

   A right-hand slide-over rather than a modal: today's weather and the two
   things that used to live inside the dashboard's own tabs (insights,
   events) belong beside the page, not blocking it - on desktop the page
   stays fully operable while it is open. Below 900px there is no "beside" to
   have, so it goes full width and behaves like the modal it has become:
   the scrim appears and the main content is inert while it is open. */
#drawer-scrim {
  position: fixed;
  inset: 0;
  z-index: var(--z-drawer);
  /*background: rgba(20, 22, 26, .26);*/
  /* Same glass as the panel it sits under - see #dayrec-scrim. */
  backdrop-filter: blur(7px) saturate(120%);
  -webkit-backdrop-filter: blur(7px) saturate(120%);
  opacity: 0;
  transition: opacity 0.2s var(--ease);
  pointer-events: none;
}
#drawer-scrim.is-on {
  opacity: 1;
  pointer-events: auto;
}

.drawer {
  position: fixed;
  top: var(--nav-h);
  right: 0;
  bottom: 0;
  width: min(400px, 100vw);
  z-index: var(--z-drawer);
  display: flex;
  flex-direction: column;
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-left: 1px solid var(--border);
  box-shadow: var(--shadow-pop), inset 1px 0 0 var(--glass-edge);
  transform: translateX(100%);
  transition: transform 0.22s var(--ease);
  overflow-y: auto;
}
.drawer.is-open {
  transform: translateX(0);
}

/* A user who asked the OS for less motion gets the panel appearing, not
   sliding - hidden already removes it from layout, so there is nothing left
   to animate away accidentally. */
@media (prefers-reduced-motion: reduce) {
  .drawer {
    transition: none;
  }
}
.drawer-head {
  /* The heading is for screen readers only, so the close button is the one
     visible thing here and sits at the far edge on its own. */
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--border);
}

.drawer-body {
  padding: var(--sp-4);
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
}

/* Three blocks in one scroll, in reading order, separated by a rule rather
   than by three card boxes - a 360px column nested inside a panel that is
   already a box does not need three more borders to say "these are three
   things". */
.drawer-sec + .drawer-sec {
  border-top: 1px solid var(--border);
  padding-top: var(--sp-5);
}

.drawer-sec .card-head {
  margin-bottom: var(--sp-3);
}

.drawer-sec h3 {
  font-size: var(--fs-sm);
}

/* The sentence the bar's tooltip used to hide. */
.apist-why {
  font-size: var(--fs-sm);
  color: var(--text-2);
  line-height: 1.5;
  margin-bottom: var(--sp-3);
}

.apist {
  display: flex;
  flex-direction: column;
  gap: 1px;
  margin: 0;
}

.apist-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: 5px 0;
}
.apist-row + .apist-row {
  border-top: 1px dashed var(--border);
}

.apist dt {
  font-size: calc(11.5px * var(--fs-scale));
  color: var(--muted);
}

.apist dd {
  margin: 0;
  font-family: var(--mono);
  font-size: calc(11.5px * var(--fs-scale));
  color: var(--text);
  text-align: right;
  min-width: 0;
}

/* Plain text like its neighbours; only the dot carries the colour. */
.apist-state {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-weight: 600;
}
.apist-state .dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex: none;
  background: var(--muted);
}
.apist-state[data-state=ok] .dot {
  background: var(--good);
}
.apist-state[data-state=warn] .dot {
  background: var(--warning);
}
.apist-state[data-state=error] .dot {
  background: var(--critical);
}

@media (max-width: 900px) {
  .drawer {
    width: 100vw;
  }
}
/* --------------------------------------------------------- weather block
   (drawer, and Analysis > Forecast's outlook card - reused by both so the
   two never drift into two different-looking weather widgets. Prefixed
   "wxd" deliberately: the Analysis view's older "Forecast against reality"
   card already owns plain .wx-* class names, and this must never collide
   with it. */
.wxd-place {
  font-size: var(--fs-sm);
  color: var(--muted);
  margin-bottom: var(--sp-2);
}

.wxd-now {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  flex-wrap: wrap;
}

.wxd-now-ic {
  width: 46px;
  height: 46px;
  flex: none;
  fill: none;
  stroke: var(--text-2);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  font-size: 46px;
  line-height: 1;
  color: var(--text-2);
}

.wxd-now-temp {
  font-size: calc(30px * var(--fs-scale));
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1;
}

.wxd-now-cond {
  font-size: var(--fs-sm);
  color: var(--text-2);
  margin-top: 2px;
}

.wxd-now-figs {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-2) var(--sp-4);
  margin-top: var(--sp-3);
  font-size: var(--fs-xs);
}
.wxd-now-figs dt {
  color: var(--muted);
  display: inline;
}
.wxd-now-figs dd {
  display: inline;
  margin: 0 0 0 4px;
  font-weight: 600;
  color: var(--text);
}
.wxd-now-figs > div {
  display: flex;
  justify-content: space-between;
  gap: var(--sp-2);
}

.wxd-predict {
  display: flex;
  justify-content: space-between;
  gap: var(--sp-3);
  margin-top: var(--sp-3);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--border);
  font-size: var(--fs-xs);
}
.wxd-predict span {
  display: block;
}
.wxd-predict b {
  display: block;
  font-size: calc(15px * var(--fs-scale));
  color: var(--text);
  margin-top: 2px;
}

.wxd-strip {
  display: flex;
  justify-content: space-between;
  gap: var(--sp-1);
  margin-top: var(--sp-4);
}

.wxd-day {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  font-size: calc(10.5px * var(--fs-scale));
  color: var(--muted);
  flex: 1;
  min-width: 0;
}

.wxd-day-ic {
  width: 20px;
  height: 20px;
  fill: none;
  stroke: var(--text-2);
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
  font-size: 20px;
  line-height: 1;
  color: var(--text-2);
}

.wxd-day b {
  color: var(--text);
  font-weight: 600;
  font-size: calc(11px * var(--fs-scale));
}

.wxd-day .lo {
  color: var(--muted);
}

.wxd-day .rain {
  color: var(--s-grid);
}

.wxd-day .uv {
  font-family: var(--mono);
}

/* The band word and the day's peak are context for the number, not figures. */
.wxd-now-figs .wxd-uv, .wxd-now-figs .lo {
  font-weight: 400;
  color: var(--muted);
}

.wxd-unavailable {
  font-size: var(--fs-sm);
  color: var(--muted);
}

/* ----------------------------------------------------------- grid-split
   Cost > Every month: a chart card and a narrower reading beside it. Grid
   items stretch to the row's height by default, so the insight column
   already matches the chart card without extra rules - it only needs
   permission to scroll internally rather than grow the row further. */
.grid-split {
  display: grid;
  grid-template-columns: 7fr 3fr;
  gap: var(--sp-4);
  align-items: stretch;
}

.grid-split-side {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.grid-split-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

@media (max-width: 900px) {
  .grid-split {
    grid-template-columns: minmax(0, 1fr);
  }
  .grid-split-scroll {
    max-height: 420px;
  }
}
/* ------------------------------------------- method note, and the coverage
   table on Performance > Forecast.

   Every figure in that tab is either measured or predicted, and the two are
   never drawn in the same weight. `method` is where a predicted card writes
   out what went into it - a projection with no stated method is a number a
   reader has no way to disbelieve. */
.method {
  margin-top: var(--sp-3);
  padding-top: var(--sp-3);
  border-top: 1px dashed var(--border);
}

.method-h {
  font-size: calc(10px * var(--fs-scale));
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 600;
  margin-bottom: var(--sp-2);
}

.method ul {
  margin: 0;
  padding-left: 1.1em;
}

.method li {
  font-size: var(--fs-sm);
  color: var(--text-2);
  line-height: 1.55;
}
.method li + li {
  margin-top: 3px;
}

.cover-lead {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
}

.cover-lead-v {
  font-size: var(--fs-xl, 26px);
  font-weight: 600;
  letter-spacing: -0.01em;
}

.cover-lead-k {
  font-size: var(--fs-sm);
  color: var(--muted);
}

.cover-note {
  font-size: calc(11.5px * var(--fs-scale));
  color: var(--muted);
  line-height: 1.5;
  margin-top: var(--sp-2);
}

.cover-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: var(--sp-3) 0;
}

.cover-row {
  display: grid;
  grid-template-columns: 3.1rem minmax(0, 1fr) auto;
  grid-template-areas: "year bar figs" ". src src";
  align-items: center;
  gap: 3px var(--sp-2);
  padding: 5px 0;
}
.cover-row + .cover-row {
  border-top: 1px dashed var(--border);
}

.cover-year {
  grid-area: year;
  font-family: var(--mono);
  font-size: calc(11.5px * var(--fs-scale));
  color: var(--text-2);
}

.cover-bar {
  grid-area: bar;
  position: relative;
  display: block;
  height: 7px;
  border-radius: 999px;
  background: var(--surface-3);
  overflow: hidden;
}

.cover-fill {
  position: absolute;
  inset: 0 auto 0 0;
  background: var(--s-solar);
  border-radius: 999px;
}

/* A projected row is the same bar in a lighter weight - the reading is the
   same quantity, and only its standing differs. Never a second hue: a second
   hue would say "a different thing", and this is the same thing guessed at. */
.cover-row[data-source=projected] .cover-fill {
  background: color-mix(in srgb, var(--s-solar) 38%, transparent);
}

.cover-figs {
  grid-area: figs;
  display: flex;
  align-items: baseline;
  gap: 6px;
}

.cover-pct {
  font-family: var(--mono);
  font-size: calc(11.5px * var(--fs-scale));
  color: var(--text);
}

.cover-saved {
  font-size: calc(10.5px * var(--fs-scale));
  color: var(--muted);
}

.cover-src {
  grid-area: src;
  font-size: calc(10px * var(--fs-scale));
  color: var(--muted);
  letter-spacing: 0.04em;
}

.cover-row[data-source=measured] .cover-src {
  color: var(--good);
}

/* ------------------------------------------------------- signals layout */
.sig-main {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  min-width: 0;
}

/* Two columns of one reading - the partition and the numbers - the same
   height, because a chart taller than the table beside it reads as the more
   important of the two when they are halves of the same thing. The height is
   set here rather than on the chart, so the table sits inside the row instead
   of setting it. */
.slot-split {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-4);
  align-items: stretch;
  --slot-h: 232px;
}

.slot-col {
  min-width: 0;
  height: var(--slot-h);
  display: flex;
  flex-direction: column;
}
.slot-col > * {
  min-height: 0;
}
.slot-col .table-scroll {
  flex: 1;
  max-height: none;
  overflow-y: auto;
}

@media (max-width: 760px) {
  .slot-split {
    grid-template-columns: minmax(0, 1fr);
    --slot-h: 148px;
  }
  .slot-split .slot-col:last-child {
    height: auto;
  }
}
/* ------------------------------------------------------- partition (C6)

   One block divided by share: each band's width IS its percentage, so the four
   add up to the whole block and the eye compares them along a straight edge
   rather than against arc length. Pure flex, no plugin - the vendored Chart.js
   has no partition type.

   flex-grow carries the share; the 3px gaps come off every band equally, so the
   proportions stay honest at any width. */
.partition {
  display: flex;
  gap: 3px;
  width: 100%;
  height: 100%;
  min-height: 0;
}

/* flex-basis 0, not auto: on auto a band is at least as wide as its content,
   which is a floor under the smallest share - and a floor is the one thing
   this chart must not have. Nothing but the number decides a band's width. */
.part-seg {
  flex: 0 1 0;
  min-width: 0;
  overflow: hidden;
  border-radius: 3px;
  background: var(--p-fill);
}

/* One hue at four strengths, because the four slots are one entity (the
   battery) in four states - not four different things, which is what four
   series colours would claim. Which way the ramp fades is the part JS must not
   decide: fading towards the surface is invisible on a dark card, so the steps
   are a percentage here and the theme picks what they are mixed into. That also
   means a theme flip needs no redraw - these are boxes, not a canvas. */
.partition, .slot-table {
  --p-into: var(--surface);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme=light]) .partition,
  :root:not([data-theme=light]) .slot-table {
    --p-into: #fff;
  }
}
:root[data-theme=dark] .partition,
:root[data-theme=dark] .slot-table {
  --p-into: #fff;
}

.part-seg, .slot-table .swatch {
  --p-fill: color-mix(in srgb, var(--s-battery) calc(var(--p) * 1%), var(--p-into));
}

.part-seg:first-child {
  border-radius: var(--r-md) 3px 3px var(--r-md);
}

.part-seg:last-child {
  border-radius: 3px var(--r-md) var(--r-md) 3px;
}

.part-seg:only-child {
  border-radius: var(--r-md);
}

/* The table is the legend too, so the swatch sits in the name cell and the rows
   keep the bands' order. Rows share the column's height between them, which is
   what lines the block up with the numbers that describe it. */
.slot-table {
  height: 100%;
}
.slot-table td {
  vertical-align: middle;
}
.slot-table {
  /* The name takes every spare pixel so the figures cluster at the right edge
     where they can be compared, instead of each column claiming a fifth of a
     wide card and leaving the numbers stranded apart. */
}
.slot-table td:first-child {
  width: 100%;
  font-weight: 600;
  white-space: nowrap;
}
.slot-table .swatch {
  display: inline-block;
  width: 11px;
  height: 11px;
  background: var(--p-fill);
  border-radius: 3px;
  margin-right: 8px;
  vertical-align: -1px;
}
.slot-table {
  /* No header row above them, so the first row has nothing to be divided from. */
}
.slot-table tr:first-child td {
  border-top: none;
}
.slot-table {
  /* Five short columns in half a card: the row is what has to fit, so the padding
     gives way before any figure is allowed to wrap or scroll out of sight. */
}
.slot-table td {
  padding-inline: var(--sp-2);
}
.slot-table td:first-child {
  padding-inline-start: 0;
}
.slot-table td:last-child {
  padding-inline-end: 0;
}
.slot-table {
  /* The share is the figure the block is drawn from, so it is the one the row
     ends on and the only one that is not muted. */
}
.slot-table td.s {
  font-weight: 700;
  color: var(--text);
}

/* ---------------------------------------------------- production calendar */
.cal-wrap {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding-bottom: 2px;
}

/* Every row is the same 31-column grid, label included, so day 14 sits under
   day 14 in every month and a 28-day February simply runs out of cells rather
   than shifting. Fixed 13px squares left a third of a wide card empty; letting
   the columns share the width is what fills it, and squares up to the cap. */
.cal-row {
  display: grid;
  grid-template-columns: 60px minmax(0, 1fr);
  align-items: center;
  gap: 8px;
}

.cal-row-label {
  font-size: calc(10.5px * var(--fs-scale));
  color: var(--muted);
  font-family: var(--mono);
}

/* Capped at the width 31 squares and their gaps actually need. Letting the
   columns take a share of a 2000px card made every cell 40px wide and 26px
   tall - a heatmap of rectangles, with more gutter between the rows than
   inside them. The cells now sit at one pitch in both directions. */
.cal-cells {
  display: grid;
  grid-template-columns: repeat(31, minmax(0, 1fr));
  gap: 3px;
  max-width: 710px;
}

.cal-head .cal-cells span {
  font-size: calc(9.5px * var(--fs-scale));
  color: var(--muted);
  font-family: var(--mono);
  text-align: center;
}

.cal-cell {
  width: auto;
  height: auto;
  aspect-ratio: 1;
  border-radius: 3px;
  background: var(--surface-3);
  border: none;
  padding: 0;
  cursor: default;
}
.cal-cell[data-has="1"] {
  cursor: pointer;
}

/* Narrow enough and a shared 31st of the width is four pixels, which is not a
   cell any more. Below this the calendar goes back to fixed squares and the
   wrapper scrolls, which is the only honest option at phone width. */
@media (max-width: 700px) {
  .cal-wrap {
    overflow-x: auto;
  }
  .cal-row {
    grid-template-columns: 52px max-content;
    gap: 6px;
  }
  .cal-cells {
    grid-template-columns: repeat(31, 13px);
    gap: 3px;
  }
  .cal-cell {
    max-height: none;
  }
}
.cal-scale {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: var(--sp-3);
  font-size: calc(11px * var(--fs-scale));
  color: var(--muted);
}

.cal-scale-ramp {
  display: flex;
  gap: 2px;
}
.cal-scale-ramp i {
  width: 13px;
  height: 10px;
  border-radius: 2px;
  display: block;
}

/* ------------------------------------------------------- the day's record

   The one card on the dashboard that is not live. It wears a state chip
   rather than a colour change, because the accent is chrome and a card that
   turns green at dusk would be reading as data. */
.daycard-state {
  flex: none;
  align-self: start;
  padding: 3px 10px;
  border-radius: 999px;
  font-size: calc(11px * var(--fs-scale));
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--text-2);
  background: var(--surface-3);
  border: 1px solid var(--border);
}

.daycard[data-state=done] .daycard-state {
  color: var(--good);
  border-color: color-mix(in srgb, var(--good) 40%, transparent);
  background: color-mix(in srgb, var(--good) 12%, transparent);
}

/* ---------------------------------------------------- battery summary board */
/* The board's footnote: what window the figures cover, under the tiles that
   carry them rather than in a tooltip nobody opens. */
.daycard-foot {
  margin: var(--sp-4) 0 0;
  padding-top: var(--sp-3);
  border-top: 1px solid var(--border);
  font-size: calc(11.5px * var(--fs-scale));
  line-height: 1.5;
  color: var(--muted);
}
.daycard-foot:empty {
  display: none;
}

/* The handover to Lifetime > Day, closing the caption's own sentence. */
.daycard-link {
  color: var(--accent);
  font-weight: 600;
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-underline-offset: 2px;
  white-space: nowrap;
}
.daycard-link:hover {
  color: var(--text);
}

.summary-board {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: var(--sp-3);
}

.summary-item {
  position: relative;
  padding: var(--sp-3);
  min-width: 0;
  background: var(--surface-2);
  border-radius: var(--r-md);
  /* A tile whose reading is about a limit. The rule down its edge and its
     footnote carry the colour; the number stays in the text colour, because a
     measurement is not a verdict. --critical is already the limit colour on the
     battery ring, and reusing it is what ties the two cards together. */
}
.summary-item[data-tone] {
  padding-left: calc(var(--sp-3) - 3px);
  border-left: 3px solid var(--critical);
}
.summary-item[data-tone=reserve] {
  border-left-color: color-mix(in srgb, var(--critical) 55%, transparent);
}
.summary-item[data-tone] .stat-f {
  color: var(--critical);
}

/* Still used by Cost's insight board, which is prose by design - the tiles
   there are a paragraph each, not a reading with an explanation attached. */
.summary-read {
  display: block;
  margin-top: 5px;
  font-size: calc(11.5px * var(--fs-scale));
  color: var(--text-2);
  line-height: 1.4;
}

/* The explanation, on demand. It rides the label rather than sitting under
   the number, so the tile is still a label, a number and a comparison - three
   lines, whatever the sentence behind it is worth. */
.tip {
  display: inline-flex;
  place-items: center;
  vertical-align: -2px;
  margin-left: 5px;
  padding: 0;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: none;
  border: 0;
  cursor: help;
  color: var(--muted);
}
.tip:hover, .tip:focus-visible {
  color: var(--accent);
}
.tip svg {
  width: 13px;
  height: 13px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.tip .bi {
  font-size: 13px;
  line-height: 1;
}
.tip {
  /* The panel is positioned against the tile, not against the 14px icon: a
     max-content tooltip hanging off an icon in the right-hand column runs off
     the board, and the board is a grid whose column count changes with the
     width, so no nth-child could tell which tile that is. Spanning the tile
     cannot overflow at any width. */
}
.tip[data-tip]::after {
  content: attr(data-tip);
  position: absolute;
  top: 24px;
  left: var(--sp-2);
  right: var(--sp-2);
  z-index: var(--z-pop);
  padding: 8px 11px;
  border-radius: var(--r-sm);
  background: var(--tip-bg);
  color: var(--tip-body);
  border: 1px solid var(--tip-border);
  font: 400 var(--fs-xs)/1.45 var(--sans);
  letter-spacing: 0;
  text-transform: none;
  text-align: left;
  white-space: normal;
  box-shadow: var(--shadow-pop);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.14s var(--ease);
}
.tip[data-tip]:hover::after, .tip[data-tip]:focus-visible::after {
  opacity: 1;
  visibility: visible;
}

/* Today against yesterday. The arrow and the colour say the same thing twice
   on purpose - colour alone is not a reading - and a direction is only
   coloured where one of the two directions is actually better news. A
   reading that is neither (the house used more; the battery took more in)
   keeps the arrow and drops the colour. */
.summary-delta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 5px;
  margin-top: 6px;
  font-size: calc(11.5px * var(--fs-scale));
  line-height: 1.4;
}
.summary-delta .dsign {
  font-weight: 700;
}
.summary-delta .dv {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}
.summary-delta .dk {
  color: var(--muted);
}
.summary-delta[data-sense=good] {
  color: var(--good);
}
.summary-delta[data-sense=bad] {
  color: var(--serious);
}
.summary-delta[data-sense=flat] {
  color: var(--text-2);
}

/* --------------------------------------------------- hardware sub-cards (D1)
   One column of cards beside a rail of their titles. The rail is the
   App config .sidelayout grid, but its items are jump links rather than tabs,
   so it has its own look: a tab rail says "one of these at a time", this one
   says "all of these, further down". */
.hw-toc-wrap {
  position: sticky;
  top: calc(var(--nav-h) + var(--sp-4));
  min-width: 0;
}

.hw-toc {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px;
  border-radius: var(--r-lg);
  background: var(--surface);
  border: 1px solid var(--border);
  max-height: calc(100vh - var(--nav-h) - var(--sp-6));
  overflow-y: auto;
}

.hw-toc-item {
  display: block;
  width: 100%;
  padding: 9px var(--sp-3);
  border: 0;
  border-radius: var(--r-md);
  background: none;
  cursor: pointer;
  text-align: left;
  font: inherit;
  font-size: var(--fs-sm);
  color: var(--text-2);
}
.hw-toc-item:hover {
  background: var(--surface-2);
  color: var(--text);
}
.hw-toc-item.is-on {
  background: var(--accent-soft);
  color: var(--text);
  font-weight: 600;
}
.hw-toc-item:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.hw-toc-select {
  display: none;
}

.hw-cards > .card {
  scroll-margin-top: calc(var(--nav-h) + var(--sp-4));
}

/* One card per row is wide enough for readouts to flow as many across as fit. */
.hw-cards .readouts {
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
}

.hw-cards .status-list {
  max-height: 420px;
  overflow-y: auto;
}

#mt-cache.table-scroll {
  max-height: 340px;
}

/* Tablet and phone: the rail becomes one full-width dropdown that stays under
   the bar, so the jump is still one tap away from anywhere in the column. */
@media (max-width: 1024px) {
  .hw-layout {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sp-3);
  }
  .hw-toc-wrap {
    z-index: var(--z-sticky);
    padding: var(--sp-2) 0;
    background: var(--bg);
  }
  .hw-toc {
    display: none;
  }
  .hw-toc-select {
    display: block;
    width: 100%;
    min-height: 44px;
    padding: 0 var(--sp-3);
    border-radius: var(--r-md);
    border: 1px solid var(--border);
    background: var(--surface);
    color: var(--text);
    font: inherit;
    font-size: var(--fs-sm);
  }
  .hw-toc-select:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
  }
}
/* Remote Set reference. A reading list, not a form: the same .readouts tiles
   the hardware cards use, grouped under the portal's own headings. The one
   tile that carries --critical is the discharge cut-off, because that is the
   battery floor the rest of the dashboard measures down to - the same limit
   colour the SoC ring wears, never a SERIES hue. */
.rs-groups {
  display: grid;
  gap: var(--sp-5);
}

.rs-group h4 {
  margin: 0 0 var(--sp-1);
  font-size: calc(12px * var(--fs-scale));
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}

.rs-group .rs-note {
  margin: 0 0 var(--sp-3);
  font-size: calc(12px * var(--fs-scale));
  color: var(--muted);
  max-width: 62ch;
}

.rs-group .readouts dd {
  font-weight: 600;
  font-family: var(--mono);
  font-size: calc(13px * var(--fs-scale));
}

.rs-hint {
  display: block;
  margin-top: 4px;
  font-family: var(--sans);
  font-size: calc(11px * var(--fs-scale));
  font-weight: 400;
  line-height: 1.45;
  color: var(--muted);
}

.readouts > div[data-tone] {
  border-left: 3px solid var(--critical);
  padding-left: calc(var(--sp-3) - 3px);
}

/* The way out to the portal, sitting where the Reset group used to be listed
   - a rule, not a card, because it is the tail of this reference and not a
   second subject. */
.rs-out {
  margin-top: var(--sp-5);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--border);
}
.rs-out .linklist {
  margin: var(--sp-3) 0 var(--sp-2);
}

/* --------------------------------------------------- notifications (D3)
   .notif-btn carries no transform, so its popover's --z-pop is not trapped
   the way the KPI tile's was (see the note on .kpi) - nothing here needs the
   same "raise the parent" fix. */
.notif-wrap {
  position: relative;
}

#notif-btn {
  position: relative;
}
#notif-btn .nav-badge {
  position: absolute;
  top: -3px;
  right: -3px;
  min-width: 15px;
  height: 15px;
  font-size: 9.5px;
  padding: 0 4px;
}
#notif-btn {
  /* Red only when something is actually wrong; a warm inverter or an open
     warning is amber. */
}
#notif-btn .nav-badge[data-level=warn] {
  background: var(--warning);
}

.notif-menu {
  position: fixed;
  z-index: var(--z-menu);
  top: calc(var(--nav-h) + 6px);
  right: var(--sp-5);
  /* Wider than it was: it now holds the day's timeline as well as the event
     list, and a timeline entry is a sentence, not a code. */
  width: min(420px, 100vw - 2 * var(--sp-4));
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-3);
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-pop), inset 0 1px 0 var(--glass-edge);
}

.notif-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  font-size: calc(10px * var(--fs-scale));
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 600;
  padding: 0 4px;
}
.notif-head > span:first-child {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.notif-head .card-note {
  text-transform: none;
  letter-spacing: 0;
}

/* The panel is a flex column; each pane has to be one too, or the list inside
   it loses the max-height that keeps the panel on screen. */
.notif-pane {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  min-height: 0;
}
.notif-pane[hidden] {
  display: none;
}

.notif-menu .subnav {
  margin-bottom: 2px;
}

/* Three tabs now: one line each, and the track scrolls sideways on a phone
   rather than breaking a label over two lines. */
.notif-menu .subnav-item {
  white-space: nowrap;
  padding-left: 12px;
  padding-right: 12px;
}

/* The timeline is the taller of the two panes, so it gets the same cap the
   event list has rather than running the popover off the bottom. */
.notif-pane .timeline-scroll {
  max-height: min(400px, 100vh - 210px);
}

/* The cap is on the list, not the panel, so the head and the "see all" button
   stay put while the events scroll - and it gives way on a short screen (a
   phone held sideways) rather than running the panel off the bottom. */
.notif-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
  max-height: min(360px, 100vh - 180px);
  overflow-y: auto;
}
.notif-list li {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 7px 6px;
  border-radius: var(--r-sm);
  cursor: pointer;
}
.notif-list li:hover, .notif-list li:focus-visible {
  background: var(--surface-2);
}
.notif-list li.empty {
  cursor: default;
  color: var(--muted);
  font-size: var(--fs-sm);
}
.notif-list li.empty:hover {
  background: none;
}

.notif-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex: none;
  margin-top: 6px;
  background: var(--muted);
}
.notif-dot[data-level=bad] {
  background: var(--critical);
}
.notif-dot[data-level=warn] {
  background: var(--warning);
}

.notif-body {
  min-width: 0;
}

.notif-code {
  display: block;
  font-family: var(--mono);
  font-size: calc(10.5px * var(--fs-scale));
  color: var(--muted);
}

.notif-text {
  display: block;
  font-size: var(--fs-sm);
  color: var(--text);
}

.notif-time {
  display: block;
  font-size: calc(11px * var(--fs-scale));
  color: var(--muted);
  margin-top: 1px;
}

.notif-all {
  align-self: stretch;
  text-align: center;
  padding: 8px;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: var(--text-2);
  font: 500 calc(12.5px * var(--fs-scale)) var(--sans);
  cursor: pointer;
}
.notif-all:hover {
  background: var(--surface-3);
  color: var(--text);
}

/* ============================================================= config (D4) */
.cfg-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  flex-wrap: wrap;
  margin-bottom: var(--sp-4);
}

.btn {
  padding: 8px 16px;
  border-radius: var(--r-sm);
  border: 1px solid var(--border-strong);
  background: var(--surface-2);
  color: var(--text-2);
  font: 600 calc(13px * var(--fs-scale)) var(--sans);
  cursor: pointer;
  transition: background 0.18s var(--ease), color 0.18s var(--ease);
}
.btn:hover:not(:disabled) {
  background: var(--surface-3);
  color: var(--text);
}
.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}
.btn-primary:hover:not(:disabled) {
  background: var(--accent-2);
  color: var(--accent-ink);
}

/* .btn's padding would leave a 2px-wide content box inside .icon-btn's fixed
   32px, and the icon would hang off the right edge rather than sit in the
   middle. This has to come after the second .btn block to win. */
.btn.icon-btn {
  padding: 0;
}

.icon-btn-text {
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  background: var(--surface-2);
  color: var(--text-2);
  font: 500 calc(12.5px * var(--fs-scale)) var(--sans);
  cursor: pointer;
}
.icon-btn-text:hover {
  background: var(--surface-3);
  color: var(--text);
}

/* One error/success banner at the top of the form - focusable so a failed
   save moves keyboard focus somewhere that actually says what happened. */
.cfg-summary {
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--critical) 10%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--critical) 34%, transparent);
  color: var(--text);
}
.cfg-summary.is-good {
  background: color-mix(in srgb, var(--good) 10%, var(--surface));
  border-color: color-mix(in srgb, var(--good) 34%, transparent);
}

.cfg-summary-title {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-weight: 600;
  margin: 0 0 var(--sp-2);
}

.cfg-summary ul {
  margin: 0;
  padding-left: 1.4em;
}

.cfg-summary a {
  color: inherit;
  text-decoration: underline;
}

.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: var(--sp-3) var(--sp-4);
}

.field {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
}
.field label {
  font-size: calc(11px * var(--fs-scale));
  letter-spacing: 0.05em;
  color: var(--text-2);
  font-weight: 600;
}
.field .req {
  color: var(--critical);
}
.field input:not([type=checkbox]), .field select {
  padding: 8px 10px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: var(--text);
  font: 500 calc(13.5px * var(--fs-scale)) var(--sans);
}
.field input:focus-visible, .field select:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.field input.is-invalid {
  border-color: var(--critical);
}

.field-err {
  font-size: 11.5px;
  color: var(--critical);
  margin: 0;
  min-height: 1px;
}

.field-hint {
  font-size: calc(11.5px * var(--fs-scale));
  color: var(--muted);
  margin: 0;
}

.field-check label {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 500;
  font-size: calc(13px * var(--fs-scale));
  color: var(--text);
}

.pw-wrap {
  display: flex;
  gap: 6px;
}
.pw-wrap input {
  flex: 1;
  min-width: 0;
}

.pw-toggle {
  width: 36px;
  height: 36px;
  flex: none;
}

.cfg-targets {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: var(--sp-3);
  display: flex;
  gap: var(--sp-4);
  flex-wrap: wrap;
}
.cfg-targets legend {
  font-size: calc(11px * var(--fs-scale));
  letter-spacing: 0.05em;
  color: var(--muted);
  padding: 0 4px;
}
.cfg-targets label {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: calc(13px * var(--fs-scale));
}

/* -------------------------------------------------------------- tier ladder */
.tier-schedule {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: var(--sp-3);
  margin-bottom: var(--sp-3);
}
.tier-schedule:last-child {
  margin-bottom: 0;
}

.tier-schedule-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--sp-2) var(--sp-3);
  margin-bottom: var(--sp-2);
}

/* Same tokens as .srow-input's inputs (border, radius, background, focus ring)
 * so the ladder does not read as a different, unfinished form from Hardware -
 * these were bare <input>s with no rule of their own before this block. */
.tier-schedule-head input,
.tier-sched-meta input {
  min-height: 36px;
  padding: 7px 10px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: var(--text);
  font: 500 var(--fs-sm) var(--sans);
}

.tier-schedule-head input:focus-visible,
.tier-sched-meta input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}

.tier-schedule-head .sched-name {
  font-weight: 600;
  flex: 1 1 12em;
  min-width: 0;
}

.tier-sched-meta {
  display: inline;
  color: var(--muted);
  font-size: var(--fs-xs);
}
.tier-sched-meta input {
  width: 6em;
  font-family: var(--mono);
  vertical-align: middle;
  margin: 2px 0;
}

.tier-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
  gap: var(--sp-2);
  align-items: center;
  margin-bottom: 6px;
  /* An <input> has an intrinsic width of about twenty characters; without this
     the two columns refuse to shrink below it and push past a phone's edge. */
}
.tier-row input {
  min-width: 0;
  width: 100%;
}
.tier-row input {
  min-height: 38px;
  padding: 8px 11px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: var(--text);
  font: 500 var(--fs-sm) var(--mono);
}
.tier-row input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}
.tier-row .tier-open {
  color: var(--muted);
  font-size: calc(12.5px * var(--fs-scale));
  padding: 6px 8px;
}
.tier-row button {
  background: none;
  border: none;
  color: var(--muted);
  cursor: pointer;
  padding: 4px;
}
.tier-row button:hover {
  color: var(--critical);
}

.tier-add {
  margin-top: 4px;
}

/* ------------------------------------------------------------- coverage */
.cov-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

.cov-month {
  width: 34px;
  height: 34px;
  border-radius: 6px;
  display: grid;
  place-items: center;
  font-size: 9px;
  font-family: var(--mono);
  background: var(--surface-3);
  color: var(--muted);
}
.cov-month[data-full="1"] {
  background: color-mix(in srgb, var(--s-solar) 55%, var(--surface-2));
  color: #fff;
}
.cov-month[data-partial="1"] {
  background: color-mix(in srgb, var(--s-solar) 25%, var(--surface-2));
}

/* --------------------------------------------------------------- progress */
.progress-track {
  height: 8px;
  border-radius: 999px;
  background: var(--surface-3);
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  background: var(--accent);
  border-radius: 999px;
  transition: width 0.3s var(--ease);
}

.crawl-errors {
  list-style: none;
  margin: var(--sp-2) 0 0;
  padding: 0;
  font-size: 11.5px;
  color: var(--critical);
  max-height: 120px;
  overflow-y: auto;
}

@media (max-width: 720px) {
  .form-grid {
    grid-template-columns: minmax(0, 1fr);
  }
  .tier-row {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
  }
}
/* --------------------------------------------------------- "state of grid" */
.gridstate-foot {
  font-size: var(--fs-xs);
  color: var(--muted);
}

/* ================================================================= layout
   Two cards abreast where they are each a short list - a definition list or a
   single control does not need the full width, and reading two of them side by
   side is how you notice that one is empty. */
.cols-2 {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 460px), 1fr));
  gap: var(--sp-4);
  align-items: start;
}

/* A short Edit next to a long description would wrap under it on the shared
   .card-head, which reads as a third paragraph rather than as a control. The
   titles keep the room they need and the button keeps the corner. */
/* One grid cell holding two cards top to bottom, for a pair that is shorter
   than whatever it sits beside. */
.col-stack {
  display: grid;
  gap: var(--sp-4);
  align-content: start;
  min-width: 0;
}

.cols-2 .card-titles {
  flex: 1 1 200px;
}

.cols-2 .card-head > .btn {
  flex: none;
  align-self: flex-start;
}

/* Account's three cards are read side by side, so they share a height rather
   than each stopping where its own list happens to end. */
#view-account .cols-2 {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  align-items: stretch;
}

@media (max-width: 1100px) {
  #view-account .cols-2 {
    grid-template-columns: minmax(0, 1fr);
  }
}
/* A label/value list. Values are the figure face when they are identifiers or
   numbers: a serial number set in the text face reads as a sentence. */
.deflist {
  display: flex;
  flex-direction: column;
  gap: 0;
  margin: 0;
}

.def {
  display: grid;
  grid-template-columns: minmax(9rem, 38%) minmax(0, 1fr);
  gap: var(--sp-3);
  align-items: baseline;
  padding: 9px 0;
  border-bottom: 1px solid var(--border);
}
.def:last-child {
  border-bottom: none;
}
.def dt {
  font-size: var(--fs-xs);
  color: var(--muted);
}
.def dd {
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--text);
  overflow-wrap: anywhere;
}
.def {
  /* Same size as a plain value - `mono` only asks for the figure face (a serial,
   * a clock reading), not a smaller one. A mono row reading noticeably tinier
   * than its neighbours in the same list is what made the Server tab's type
   * look inconsistent. */
}
.def dd.mono {
  font-family: var(--mono);
}

/* ================================================================ summary
   The trophy cabinet. Three figures at the top and the qualifiers below them -
   the one place on this dashboard where a number is allowed to be large for
   its own sake, because the question it answers is "was this worth it". */
/* Four tiles: one row when there is room, two by two when there is not - an
   auto-fit grid left the fourth tile alone on a row of its own. */
.trophy-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--sp-3);
  margin-bottom: var(--sp-4);
}

@media (max-width: 1100px) {
  .trophy-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
@media (max-width: 480px) {
  .trophy-grid {
    grid-template-columns: minmax(0, 1fr);
  }
}
.trophy-tile {
  display: grid;
  grid-template-columns: 38px 1fr;
  grid-template-rows: auto auto auto;
  gap: 2px var(--sp-3);
  align-items: center;
  padding: var(--sp-4);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: linear-gradient(155deg, var(--surface-2), var(--surface));
}

.trophy-ic {
  grid-row: span 3;
  align-self: start;
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border-radius: 12px;
  background: var(--accent-soft);
  color: var(--accent);
}
.trophy-ic svg {
  width: 20px;
  height: 20px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.trophy-ic .bi {
  font-size: 20px;
  line-height: 1;
}

.trophy-k {
  font-size: var(--fs-xs);
  color: var(--muted);
  letter-spacing: 0.02em;
}

.trophy-v {
  font: 600 var(--fs-2xl)/1.05 var(--mono);
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.trophy-v .unit {
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--muted);
  margin-left: 5px;
}

.trophy-f {
  font-size: var(--fs-xs);
  color: var(--text-2);
  line-height: 1.45;
}

/* Milestones. Earned is a tick and full contrast; unearned is a progress bar,
   because "80% of the way to a megawatt-hour" is the part worth coming back
   for. Locked state is never colour alone - the tick, the percentage and the
   opacity all say it. */
.badges {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
  gap: var(--sp-3);
}

.badge {
  display: grid;
  grid-template-columns: 34px 1fr auto;
  gap: var(--sp-3);
  align-items: center;
  padding: var(--sp-3);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
}
.badge[data-earned="1"] {
  border-color: color-mix(in srgb, var(--good) 40%, var(--border));
}
.badge[data-earned="0"] .badge-ic {
  color: var(--muted);
  background: var(--surface-3);
}

.badge-ic {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: 11px;
  background: color-mix(in srgb, var(--good) 14%, transparent);
  color: var(--good);
}
.badge-ic svg {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.badge-ic .bi {
  font-size: 18px;
  line-height: 1;
}

.badge-body {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}

.badge-k {
  font-size: var(--fs-sm);
  font-weight: 600;
}

.badge-f {
  font-size: calc(11.5px * var(--fs-scale));
  color: var(--muted);
}

.badge-track {
  height: 5px;
  border-radius: 999px;
  background: var(--surface-3);
  overflow: hidden;
}
.badge-track i {
  display: block;
  height: 100%;
  background: var(--accent);
  border-radius: 999px;
}

.badge-state {
  font: 600 var(--fs-xs)/1 var(--mono);
  color: var(--muted);
}

.badge-tick {
  width: 17px;
  height: 17px;
  fill: none;
  stroke: var(--good);
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-linejoin: round;
  font-size: 17px;
  line-height: 1;
  color: var(--good);
}

/* Payback. One bar, one marker, and the numbers under it - a ring would have
   implied a cycle, and this only ever goes one way. */
.payback {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}

.payback-track {
  position: relative;
  height: 34px;
  border-radius: 999px;
  background: var(--surface-3);
  overflow: hidden;
}
.payback-track i {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--s-solar), var(--s-battery));
  transition: width 0.6s var(--ease);
}

.payback-mark {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font: 600 var(--fs-sm)/1 var(--mono);
  color: var(--text);
}

/* The grove. Decoration that carries a number: one tree per ten tree-years,
   capped and captioned, and hidden from the accessibility tree because the
   figure itself is in the stat block above it. */
.card-green {
  background: linear-gradient(170deg, color-mix(in srgb, var(--s-battery) 7%, var(--surface)), var(--surface));
}

.grove {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: var(--sp-4) 0 var(--sp-2);
}

.grove-tree {
  width: 22px;
  height: 22px;
  fill: none;
  stroke: var(--s-battery);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  font-size: 22px;
  line-height: 1;
  color: var(--s-battery);
}

/* ================================================================ account
   A profile, not a form: read-only until Edit is pressed. Eight fields typed
   once and then read for years do not want eight live inputs sitting open. */
.profile-id {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  flex-wrap: wrap;
}

.profile-avatar {
  display: grid;
  place-items: center;
  flex: none;
  width: 64px;
  height: 64px;
  border-radius: 50%;
  background: linear-gradient(150deg, var(--accent), var(--accent-2));
  color: var(--accent-ink);
  font: 700 var(--fs-xl)/1 var(--mono);
  letter-spacing: 0.03em;
}
.profile-avatar svg {
  width: 28px;
  height: 28px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.profile-avatar .bi {
  font-size: 28px;
  line-height: 1;
}

.profile-name {
  flex: 1 1 240px;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 5px;
}
.profile-name h3 {
  font-size: var(--fs-xl);
}

.profile-meta {
  font-size: var(--fs-sm);
  color: var(--muted);
  margin: 0;
  overflow-wrap: anywhere;
}

.profile-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.chip-static {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-2);
  font-size: calc(11.5px * var(--fs-scale));
  color: var(--text-2);
}
.chip-static svg {
  width: 13px;
  height: 13px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
  color: var(--muted);
}
.chip-static .bi {
  font-size: 13px;
  line-height: 1;
  color: var(--muted);
}

/* ------------------------------------------------------------- the dialog
   <dialog> rather than a hand-rolled overlay: the focus trap, Escape, the
   scrim and the inert page behind it are the platform's job, and every
   hand-rolled version gets one of the four wrong. */
.modal {
  width: min(760px, 100vw - 2 * var(--sp-4));
  max-height: min(88vh, 900px);
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-pop);
}
.modal::backdrop {
  background: rgba(8, 9, 10, 0.55);
  backdrop-filter: blur(3px);
}
.modal form, .modal .modal-shell {
  display: flex;
  flex-direction: column;
  max-height: inherit;
}

.modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5);
  border-bottom: 1px solid var(--border);
}
.modal-head h2 {
  font-size: var(--fs-lg);
}

.modal-titles {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.modal-sub {
  font-size: var(--fs-xs);
  color: var(--muted);
  font-family: var(--mono);
}

.modal-body {
  padding: var(--sp-4) var(--sp-5);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
}

.modal-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-5) var(--sp-4);
  border-top: 1px solid var(--border);
  background: var(--surface-2);
}

.mgroup {
  border: none;
  padding: 0;
  margin: 0;
  min-width: 0;
}
.mgroup > legend {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 0 var(--sp-3);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--text);
}
.mgroup > legend svg {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
  color: var(--muted);
}
.mgroup > legend .bi {
  font-size: 16px;
  line-height: 1;
  color: var(--muted);
}

.card-head-bare {
  border: none;
  padding: 0 0 var(--sp-3);
  margin: 0;
}

/* ================================================================ setting
   Rows, not a grid of bare boxes: label and hint on the left, control and its
   unit on the right. The old form made the reader guess whether a box wanted
   watts or kilowatts, and a number without its unit is not an answer. */
.srows {
  display: flex;
  flex-direction: column;
}

.srow {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(200px, 300px);
  gap: var(--sp-3) var(--sp-5);
  align-items: start;
  padding: var(--sp-3) 0;
  border-bottom: 1px solid var(--border);
}
.srow:last-child {
  border-bottom: none;
}

.srow-label {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}
.srow-label label {
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--text);
}

.srow-hint {
  font-size: var(--fs-xs);
  color: var(--muted);
  margin: 0;
  line-height: 1.45;
  max-width: 54ch;
}

.srow-input {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.srow-input input, .srow-input select {
  flex: 1 1 120px;
  min-width: 0;
  min-height: 38px;
  padding: 8px 11px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: var(--text);
  font: 500 var(--fs-sm) var(--sans);
}
.srow-input input[type=number], .srow-input input[type=date] {
  font-family: var(--mono);
}
.srow-input input:focus-visible, .srow-input select:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}
.srow-input input.is-invalid {
  border-color: var(--critical);
}

.srow-unit {
  flex: none;
  font: 500 var(--fs-xs) var(--mono);
  color: var(--muted);
  min-width: 3.4em;
}

.srow-input .field-err {
  flex: 1 0 100%;
}

/* The row the current billing mode ignores. Dimmed rather than hidden, so
   nobody wonders where the flat rate they typed last month went. */
.srow.is-muted {
  opacity: 0.5;
}
.srow.is-muted:focus-within {
  opacity: 1;
}

/* The nameplate, read back from the form. A kWp with a digit missing is
   invisible across eight inputs and obvious in one tile. */
.specstrip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--sp-3);
}

.spec-tile {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--sp-3);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
}

.spec-k {
  font-size: calc(10.5px * var(--fs-scale));
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
}

.spec-v {
  font: 600 var(--fs-xl)/1.1 var(--mono);
  font-variant-numeric: tabular-nums;
}
.spec-v .unit {
  font-size: var(--fs-xs);
  font-weight: 500;
  color: var(--muted);
  margin-left: 4px;
}

.spec-f {
  font-size: calc(11.5px * var(--fs-scale));
  color: var(--text-2);
  overflow-wrap: anywhere;
}

/* Make and model behind a disclosure: needed once, at install, and never
   again - but hiding it outright would mean retyping it from the roof. */
.more {
  margin-top: var(--sp-3);
  border-top: 1px solid var(--border);
}
.more > summary {
  padding: var(--sp-3) 0 0;
  cursor: pointer;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--muted);
}
.more > summary:hover {
  color: var(--text);
}
.more > summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--r-sm);
}

.cfg-actions {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}

.dirty-note {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fs-xs);
  color: var(--warning);
  font-weight: 600;
}
.dirty-note::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentColor;
}

.example-out {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: var(--sp-3) var(--sp-4);
  margin: var(--sp-3) 0 0;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
  font-family: var(--mono);
  font-size: var(--fs-sm);
}

.example-parts {
  font-size: var(--fs-xs);
  color: var(--muted);
}

.estimate {
  margin: var(--sp-3) 0;
  padding: var(--sp-3) var(--sp-4);
  border-left: 3px solid var(--accent);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  background: var(--accent-soft);
  font-size: var(--fs-xs);
  color: var(--text-2);
  line-height: 1.5;
}

/* The crawl targets carry their own request cost, because the difference
   between one request a day and seven is the whole decision. */
.cfg-targets {
  flex-direction: column;
  gap: var(--sp-3);
}
.cfg-targets label {
  align-items: flex-start;
  gap: 10px;
}
.cfg-targets label span {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.cfg-targets label em {
  font-style: normal;
  font-size: calc(11.5px * var(--fs-scale));
  color: var(--muted);
}

.tier-sched-meta {
  font-size: var(--fs-xs);
  color: var(--muted);
}
.tier-sched-meta input {
  width: 74px;
}

.tier-row.tier-head span {
  font-size: calc(10.5px * var(--fs-scale));
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}

/* ============================================================= app config
   Appearance: a picture of each theme, then five text sizes.

   The previews are drawn in their own fixed colours, set here on the picker
   rather than per theme: they show what a theme looks like, so the theme that
   is showing them must not repaint them. They are the only colours in this
   file that do not change with [data-theme], on purpose. */
.theme-picker {
  --tp-light-bg: #eef0f3;
  --tp-light-panel: #ffffff;
  --tp-light-line: #d9dde3;
  --tp-light-ink: #16181c;
  --tp-dark-bg: #2b2e34;
  --tp-dark-panel: #111316;
  --tp-dark-line: #3a3e46;
  --tp-dark-ink: #f2f3f5;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-3);
}

.theme-opt {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: 0;
  border: none;
  background: none;
  color: var(--text-2);
  text-align: left;
  cursor: pointer;
  font: 500 var(--fs-sm) var(--sans);
  touch-action: manipulation;
}
.theme-opt:hover .theme-prev {
  border-color: var(--border-strong);
  transform: translateY(-1px);
}
.theme-opt:focus-visible {
  outline: none;
}
.theme-opt:focus-visible .theme-prev {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}
.theme-opt.is-on {
  color: var(--text);
  font-weight: 600;
}
.theme-opt.is-on .theme-prev {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent);
}
.theme-opt.is-on .tp-check {
  opacity: 1;
  transform: scale(1);
}

.theme-opt-label {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}
.theme-opt-label .bi {
  font-size: 15px;
  line-height: 1;
  color: var(--muted);
}

.theme-opt.is-on .theme-opt-label .bi {
  color: var(--accent);
}

.theme-prev {
  position: relative;
  display: flex;
  overflow: hidden;
  aspect-ratio: 4/3;
  border: 2px solid var(--border);
  border-radius: var(--r-md);
  transition: border-color 0.18s var(--ease), box-shadow 0.18s var(--ease), transform 0.18s var(--ease);
}

.tp-half {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  background: var(--tp-bg);
}

.tp-panel {
  position: absolute;
  left: 22%;
  top: 28%;
  right: -2px;
  bottom: -2px;
  display: block;
  padding: 10% 0 0 12%;
  border: 1px solid var(--tp-line);
  border-radius: 10px 0 0 0;
  background: var(--tp-panel);
  color: var(--tp-ink);
  font: 700 clamp(15px, 2.4vw, 22px)/1 var(--sans);
  letter-spacing: -0.01em;
}

.theme-prev-light .tp-half, .tp-light {
  --tp-bg: var(--tp-light-bg);
  --tp-panel: var(--tp-light-panel);
  --tp-line: var(--tp-light-line);
  --tp-ink: var(--tp-light-ink);
}

.theme-prev-dark .tp-half, .tp-dark {
  --tp-bg: var(--tp-dark-bg);
  --tp-panel: var(--tp-dark-panel);
  --tp-line: var(--tp-dark-line);
  --tp-ink: var(--tp-dark-ink);
}

.theme-prev-auto .tp-dark .tp-panel {
  right: 0;
  border-radius: 10px 0 0 0;
}

.theme-prev-auto .tp-panel {
  left: 30%;
  padding-left: 14%;
}

.tp-check {
  position: absolute;
  right: 8px;
  bottom: 8px;
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--accent-ink);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
  opacity: 0;
  transform: scale(0.6);
  transition: opacity 0.16s var(--ease), transform 0.2s var(--ease);
}
.tp-check .bi {
  font-size: 14px;
  line-height: 1;
}

.font-picker {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 4px;
  padding: 4px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
}

.font-opt {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  gap: 4px;
  min-height: 64px;
  padding: 8px 4px;
  border: none;
  border-radius: 10px;
  background: none;
  color: var(--text-2);
  cursor: pointer;
  font-family: var(--sans);
  touch-action: manipulation;
  transition: background 0.18s var(--ease), color 0.18s var(--ease);
}
.font-opt:hover {
  background: var(--surface-3);
  color: var(--text);
}
.font-opt:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.font-opt.is-on {
  background: var(--accent);
  color: var(--accent-ink);
}
.font-opt[data-font=compact] .font-opt-aa {
  font-size: 14px;
}
.font-opt[data-font=medium] .font-opt-aa {
  font-size: 16px;
}
.font-opt[data-font=default] .font-opt-aa {
  font-size: 18px;
}
.font-opt[data-font=large] .font-opt-aa {
  font-size: 21px;
}
.font-opt[data-font=xlarge] .font-opt-aa {
  font-size: 24px;
}

.font-opt-aa {
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.01em;
}

.font-opt-label {
  font-size: 11.5px;
  font-weight: 500;
  line-height: 1.2;
  text-align: center;
}
.font-opt.is-on .font-opt-label {
  font-weight: 600;
}

.font-sample {
  margin: var(--sp-3) 0 0;
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-md);
  background: var(--surface-2);
  font-size: var(--fs-md);
  color: var(--text);
}

@media (max-width: 560px) {
  .theme-picker {
    gap: var(--sp-2);
  }
  .tp-check {
    width: 20px;
    height: 20px;
    right: 6px;
    bottom: 6px;
  }
  .font-opt {
    min-height: 58px;
  }
  .font-opt-label {
    font-size: 10.5px;
  }
}
@media (prefers-reduced-motion: reduce) {
  .theme-prev, .tp-check {
    transition: none;
  }
  .theme-opt:hover .theme-prev {
    transform: none;
  }
}
.card-danger {
  border-color: color-mix(in srgb, var(--critical) 34%, var(--border));
}

.btn-danger {
  background: var(--critical);
  border-color: var(--critical);
  color: var(--on-status);
}
.btn-danger:hover:not(:disabled) {
  background: color-mix(in srgb, var(--critical) 84%, #000);
  color: var(--on-status);
}

.btn-danger-outline {
  background: transparent;
  border-color: color-mix(in srgb, var(--critical) 55%, var(--border));
  color: var(--critical);
}
.btn-danger-outline:hover:not(:disabled) {
  background: color-mix(in srgb, var(--critical) 10%, transparent);
  border-color: var(--critical);
  color: var(--critical);
}

/* ----------------------------------------------------------- cache files

   A row per file in cache/, on App config's Local data pane. The row is the
   control: the whole left of it opens the file, and the download sits apart
   from it on the right, because "read it" and "keep a copy of it" are two
   different intents and one of them leaves the page. */
/* Three hundred keys is a scroll wall, and the one anyone came here for has a
   name they can half-remember: the filter is what makes the table usable at
   that length. Newest-first ordering is the server's half of the same job. */
.cf-bar {
  position: relative;
  display: flex;
  align-items: center;
}

.cf-bar-ic {
  position: absolute;
  left: 11px;
  width: 14px;
  height: 14px;
  pointer-events: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
  color: var(--muted);
}

.cf-filter {
  width: 100%;
  min-height: 36px;
  padding: 7px 12px 7px 32px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  background: var(--surface-2);
  color: var(--text);
  font: 500 var(--fs-sm) var(--sans);
}
.cf-filter::placeholder {
  color: var(--muted);
}
.cf-filter:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}

.cf-row-open {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  color: var(--text);
  font: 500 calc(12.5px * var(--fs-scale)) var(--mono);
  text-align: left;
  cursor: pointer;
}
.cf-row-open:hover {
  color: var(--accent);
}
.cf-row-open svg {
  width: 13px;
  height: 13px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  color: var(--muted);
}
.cf-row-open .bi {
  font-size: 13px;
  line-height: 1;
  color: var(--muted);
}
.cf-row-open:hover svg {
  color: inherit;
}
.cf-row-open:hover .bi {
  color: inherit;
}

/* The tier the key belongs to, said once. Neutral: a cache tier is not a
   reading, and a colour here would be a fifth meaning for the series hues. */
.cf-kind {
  display: inline-block;
  padding: 2px 8px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-2);
  font: 500 calc(10.5px * var(--fs-scale)) var(--sans);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}

/* ------------------------------------------------------ the API reference

   App config > API: every call this project can make or answer, from the
   server's own table. It is a reference, so it is typeset as one - the path
   in mono at the size of the cache-file rows, the sentence beside it in the
   card's own voice, and one chip per row saying what the call costs.

   The chip is the column worth reading, and it is never colour alone: "on
   this machine" and "leaves this machine" are the words, and the tint only
   repeats them. */
.api-lede {
  margin-bottom: calc(var(--sp-4) * -1 + var(--sp-2));
  padding: 0 2px;
}
.api-lede .card-sub {
  max-width: 78ch;
}

/* The group cards are rendered into a plain <div>, not into the .tabpane, so
   they inherited none of its gap and stacked edge to edge - two bordered
   tables touching read as one table with a rule through it. */
#ac-api-groups > .card + .card {
  margin-top: var(--sp-4);
}

.ep-host {
  font: 500 calc(11.5px * var(--fs-scale)) var(--mono);
  color: var(--muted);
  overflow-wrap: anywhere;
}

.ep-path {
  font: 500 calc(12px * var(--fs-scale)) var(--mono);
  color: var(--text);
  overflow-wrap: anywhere;
}

.ep-params {
  display: block;
  margin-top: 2px;
  font-size: calc(11px * var(--fs-scale));
  color: var(--muted);
}

.ep-method {
  font: 600 calc(10.5px * var(--fs-scale)) var(--mono);
  letter-spacing: 0.04em;
  color: var(--muted);
  white-space: nowrap;
}

.ep-note {
  font-size: var(--fs-xs);
  color: var(--text-2);
  line-height: 1.5;
}

/* Not a .table-scroll: thirty-seven reference rows in a 460px pane is a scroll
   region inside a scrolling page, and the thing being scrolled is the answer
   to "what does this app call" - it should be readable in one pass. The page
   carries it, so the header does not stick either. Below 760px the table is
   wider than the phone and scrolls sideways on its own, which is the one place
   a table may. */
.ep-table {
  overflow-x: auto;
  max-height: none;
  overflow-y: visible;
  border-radius: var(--r-md);
}
.ep-table table {
  table-layout: fixed;
  min-width: 640px;
}
.ep-table thead th {
  position: static;
}
.ep-table th:nth-child(1), .ep-table td:nth-child(1) {
  width: 32%;
}
.ep-table th:nth-child(2), .ep-table td:nth-child(2) {
  width: 74px;
}
.ep-table th:nth-child(3), .ep-table td:nth-child(3) {
  width: 158px;
}
.ep-table {
  /* Every other table in the app is figures, so tbody td is nowrap by default.
     This one is sentences. */
}
.ep-table td {
  white-space: normal;
  vertical-align: top;
}

/* Activity log: the Detail column is a sentence, not a figure, so it wraps
 * like the API reference's does; the timestamp is the one column that reads
 * better in the figure face. Kept inside .table-scroll (not .ep-table) so a
 * long history still caps its height rather than growing the page. */
#ac-logs-wrap td {
  white-space: normal;
  vertical-align: top;
}
#ac-logs-wrap td.log-when {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--muted);
  white-space: nowrap;
}

.ep-src {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
  padding: 2px 9px 2px 7px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-2);
  font: 500 calc(10.5px * var(--fs-scale)) var(--sans);
  letter-spacing: 0.03em;
  color: var(--muted);
}
.ep-src::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}
.ep-src {
  /* Green is "nothing left this machine", which is the promise the whole page
     is built on - so it is the one thing down here that gets a colour. */
}
.ep-src[data-out=no] {
  color: var(--good);
  border-color: color-mix(in srgb, var(--good) 35%, var(--border));
}
.ep-src[data-out=yes] {
  color: var(--text-2);
}

.cf-dl {
  display: inline-grid;
  place-items: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface-2);
  color: var(--text-2);
  cursor: pointer;
}
.cf-dl:hover {
  border-color: var(--accent-2);
  color: var(--accent);
}
.cf-dl svg {
  width: 13px;
  height: 13px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.cf-dl .bi {
  font-size: 13px;
  line-height: 1;
}

.cf-actions {
  text-align: right;
  white-space: nowrap;
}

/* The viewer is the page's longest single block of text, so it gets the room
   the log tail does not need. */
.logview-tall {
  max-height: min(56vh, 520px);
}

/* ------------------------------------------------------------ danger zone

   Three rows in one fenced list, ordered by how much each takes. A row is
   icon, what it does, a scan line of what goes and what stays, and its one
   button. Severity climbs down the list: the icon tile and the button both
   get heavier on the last row, which is the one that takes everything. */
.danger-list {
  margin: 0;
  padding: 0;
  list-style: none;
  border: 1px solid color-mix(in srgb, var(--critical) 30%, var(--border));
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--critical) 3%, var(--surface));
  overflow: hidden;
}

.danger-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  gap: var(--sp-3) var(--sp-4);
  padding: var(--sp-4);
}
.danger-row + .danger-row {
  border-top: 1px solid color-mix(in srgb, var(--critical) 18%, var(--border));
}
.danger-row.is-severe {
  background: color-mix(in srgb, var(--critical) 7%, var(--surface));
}

.danger-body {
  min-width: 0;
}

.danger-title {
  margin: 0;
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--text);
  line-height: 1.3;
}

.danger-desc {
  margin: 4px 0 0;
  max-width: 68ch;
  font-size: var(--fs-xs);
  line-height: 1.55;
  color: var(--muted);
}

.danger-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: var(--sp-3) 0 0;
  padding: 0;
  list-style: none;
}
.danger-facts li {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 10px 3px 8px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  font-size: var(--fs-xs);
  font-weight: 500;
  line-height: 1.4;
  color: var(--text-2);
}
.danger-facts .bi {
  font-size: 13px;
  line-height: 1;
}
.danger-facts .is-lost {
  border-color: color-mix(in srgb, var(--critical) 35%, var(--border));
  background: color-mix(in srgb, var(--critical) 8%, var(--surface-2));
}
.danger-facts .is-lost .bi {
  color: var(--critical);
}
.danger-facts .is-kept .bi {
  color: var(--good);
}
.danger-facts .is-warn .bi {
  color: var(--warning);
}

.danger-note {
  margin: 0;
}

.danger-note:not(:empty) {
  margin-top: var(--sp-2);
}

.danger-act {
  align-self: center;
}

.danger-act .btn {
  white-space: nowrap;
}

@media (max-width: 640px) {
  .danger-row {
    grid-template-columns: minmax(0, 1fr);
    padding: var(--sp-3);
  }
  .danger-act {
    grid-column: 1/-1;
  }
  .danger-act .btn {
    width: 100%;
    justify-content: center;
  }
}
/* --------------------------------------------------------- changelog
   One card per release, stacked newest first. The version is the card title
   and the date is the card note, so a release wears the same head as the
   thirty-five reading cards - the only thing that changes is that the body
   is prose rather than a chart. The group headings are the small caps the
   rest of the page uses for a label, not headings competing with the title. */
#changelog {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}

.release .card-note {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  white-space: nowrap;
}

.release-group + .release-group {
  margin-top: var(--sp-4);
}

.release-kind {
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

.release-list {
  margin: 0;
  padding-left: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 7px;
  /* The dash is the bullet, drawn in the accent's place by the muted rule
     colour: a changelog is chrome about the software, not a reading, so
     nothing in it wears --accent. */
}
.release-list li {
  position: relative;
  padding-left: var(--sp-4);
  font-size: var(--fs-sm);
  line-height: 1.6;
  color: var(--text);
  max-width: 78ch;
}
.release-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.7em;
  width: 8px;
  height: 2px;
  border-radius: 1px;
  background: var(--muted);
}

/* --------------------------------------------------------------- narrow
   The bar loses the wordmark's subline and the account name before it loses a
   destination; the two-column lists and the setting rows stack. */
@media (max-width: 1100px) {
  .brand-sub {
    display: none;
  }
}
@media (max-width: 760px) {
  .srow {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sp-2);
  }
  .srow-input {
    max-width: 100%;
  }
  .def {
    grid-template-columns: minmax(0, 1fr);
    gap: 2px;
  }
  .trophy-v {
    font-size: var(--fs-xl);
  }
  .modal {
    width: 100vw;
    max-width: 100vw;
    max-height: 100vh;
    border-radius: 0;
    border: none;
  }
  .modal-head, .modal-body, .modal-foot {
    padding-left: var(--sp-4);
    padding-right: var(--sp-4);
  }
  .modal-foot {
    flex-direction: column;
    align-items: stretch;
  }
}
/* ============================================================ sign-in page
   login.html only. It is served before the app exists - no bar, no drawer, no
   charts - so it borrows the card, field and button components and adds only
   the centring it needs. Everything here is a token: the page has to follow
   both themes without core.js, which is not loaded there. */
.login-body {
  /* body is already `display: flex; flex-direction: column; min-height: 100dvh`
     with the two corner glows painted on it - inherited on purpose, so the
     sign-in page is visibly the same product. Only the centring is new, and
     the direction has to be restated: with the column direction left as it is,
     align-items centres horizontally and justify-content vertically. */
  flex-direction: row;
  align-items: center;
  justify-content: center;
  padding: var(--sp-5) var(--sp-4);
}

.login-card {
  /* main { flex: 1 0 auto } is what lets a dashboard page push the footer
     down. Here it would stretch a 380px card over the whole viewport, so this
     is the one page that opts out of it. */
  flex: none;
  width: 100%;
  max-width: 380px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow);
  padding: var(--sp-5);
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}

.login-brand {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}
.login-brand .brand-mark {
  width: 34px;
  height: 34px;
}
.login-brand .brand-mark svg {
  width: 19px;
  height: 19px;
}
.login-brand .brand-mark .bi {
  font-size: 19px;
  line-height: 1;
}

.login-word {
  display: block;
  font-weight: 650;
  font-size: calc(15px * var(--fs-scale));
  letter-spacing: -0.01em;
}

#login-form {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}

/* The bar drops .brand-sub below 1100px because there a destination matters
   more than a subtitle. Here it is the masthead of a 380px card that is the
   whole page, so it stays at every width. */
.login-brand .brand-sub {
  display: block;
}

/* Full width and taller than a toolbar button: it is the only action on the
   page, and .btn is sized for a row of them. */
.login-submit {
  width: 100%;
  justify-content: center;
  padding: 10px;
  font-size: calc(14px * var(--fs-scale));
}

.login-note {
  margin: 0;
  font-size: calc(11.5px * var(--fs-scale));
  line-height: 1.5;
  color: var(--muted);
  border-left: 2px solid var(--muted);
  padding-left: var(--sp-3);
}
.login-note code {
  font: 500 calc(11px * var(--fs-scale))/1 var(--mono);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 2px 5px;
  color: var(--text-2);
}
.login-note a {
  color: var(--accent);
}

/* One sentence above the form saying what account to use. Sized between the
   brand line and the form so it reads as part of the heading block rather
   than as the first field's label. */
.login-lede {
  margin: 0;
  font-size: var(--fs-xs);
  line-height: 1.55;
  color: var(--text-2);
}

/* The consent row. .field-check already lays a checkbox beside its label; what
   is added here is room for the sentence to wrap to two lines without the box
   drifting to the middle of it, which is what `align-items: center` on a
   wrapped label does. */
.login-consent label {
  align-items: flex-start;
  gap: 9px;
  line-height: 1.5;
  font-size: calc(12.5px * var(--fs-scale));
}

.login-consent input {
  margin-top: 2px;
  flex: none;
}

.login-consent a {
  color: var(--accent);
}

/* The last line of the card: links, not a note, so it loses the rule above it
   that .login-note carries - two rules stacked read as an empty row. */
.login-legal {
  border-left: 0;
  padding-left: 0;
  text-align: center;
  border-top: 1px solid var(--border);
  padding-top: var(--sp-3);
}

/* =============================================================== legal pages
   privacy.html is standalone - no core.js, no charts - so like login.html it
   has to follow both themes on tokens alone. It is the one page in the project
   that is a document rather than a set of readings, so it gets a measure and a
   type scale of its own instead of being poured into a card. */
.legal-body {
  /* body is `display: flex; flex-direction: column; min-height: 100dvh` with
     the corner glows on it - inherited on purpose, so this is visibly the same
     product as the dashboard it explains. */
  padding: var(--sp-6) var(--sp-4) var(--sp-7);
  align-items: center;
}

.legal {
  flex: none;
  width: 100%;
  /* ~70 characters at this size. Long enough not to fragment a sentence,
     short enough that the eye finds the next line without hunting. */
  max-width: 44rem;
  display: flex;
  flex-direction: column;
  gap: var(--sp-6);
}

.legal-head {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

.legal-brand {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  text-decoration: none;
  color: inherit;
  align-self: flex-start;
  border-radius: 11px;
}
.legal-brand:hover .login-word {
  color: var(--accent);
}
.legal-brand:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}
.legal-brand .brand-mark {
  width: 34px;
  height: 34px;
}
.legal-brand .brand-mark svg {
  width: 19px;
  height: 19px;
}
.legal-brand .brand-mark .bi {
  font-size: 19px;
  line-height: 1;
}
.legal-brand {
  /* .brand-sub is display:none under the narrow breakpoints, because in the top
     bar it is the first thing worth dropping. Here it is part of the masthead. */
}
.legal-brand .brand-sub {
  display: block;
}

.legal h1 {
  margin: var(--sp-3) 0 0;
  font-size: var(--fs-3xl);
  font-weight: 680;
  letter-spacing: -0.02em;
  line-height: 1.15;
}

.legal-meta {
  margin: 0;
  font-size: calc(11.5px * var(--fs-scale));
  color: var(--muted);
}

.legal-lede {
  margin: var(--sp-2) 0 0;
  font-size: var(--fs-md);
  line-height: 1.6;
  color: var(--text-2);
}

.legal-sec {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}
.legal-sec h2 {
  margin: 0;
  font-size: var(--fs-lg);
  font-weight: 650;
  letter-spacing: -0.01em;
  /* A rule rather than a card border: the sections are one document, and
     thirteen bordered boxes would read as thirteen unrelated notices. */
  border-top: 1px solid var(--border);
  padding-top: var(--sp-4);
}
.legal-sec p {
  margin: 0;
  font-size: var(--fs-sm);
  line-height: 1.65;
  color: var(--text-2);
}
.legal-sec strong {
  color: var(--text);
  font-weight: 620;
}
.legal-sec a {
  color: var(--accent);
}
.legal-sec code {
  font: 500 calc(12px * var(--fs-scale))/1.4 var(--mono);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 2px 5px;
  color: var(--text-2);
  /* A hostname in running text must be allowed to break, or it widens the
     measure on a phone and the whole page scrolls sideways. */
  word-break: break-word;
}

.legal-list {
  margin: 0;
  padding-left: 1.35em;
  display: flex;
  flex-direction: column;
  gap: 9px;
  font-size: var(--fs-sm);
  line-height: 1.6;
  color: var(--text-2);
}
.legal-list ::marker {
  color: var(--muted);
}

.legal-note {
  font-size: var(--fs-xs) !important;
  color: var(--muted) !important;
  border-left: 2px solid var(--border-strong);
  padding-left: var(--sp-3);
}

/* The table scrolls inside itself. Two prose columns at a 44rem measure are
   narrow on a phone, and the page body must never scroll sideways. */
.legal-table {
  overflow-x: auto;
}
.legal-table table {
  /* table-layout: fixed with an explicit split, or the browser sizes both
     columns off the longest unbreakable run in them and the second one gets
     squeezed off the end of the scroller. */
  width: 100%;
  min-width: 34rem;
  border-collapse: collapse;
  table-layout: fixed;
  font-size: var(--fs-xs);
  line-height: 1.55;
}
.legal-table col.what {
  width: 46%;
}
.legal-table col.why {
  width: 54%;
}
.legal-table th, .legal-table td {
  text-align: left;
  vertical-align: top;
  padding: var(--sp-3) var(--sp-3) var(--sp-3) 0;
  border-bottom: 1px solid var(--border);
  white-space: normal;
}
.legal-table th {
  font-size: calc(11px * var(--fs-scale));
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 600;
}
.legal-table td {
  color: var(--text-2);
}
.legal-table td:first-child {
  color: var(--text);
  padding-right: var(--sp-4);
}

.legal-foot {
  border-top: 1px solid var(--border);
  padding-top: var(--sp-4);
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.legal-foot p {
  margin: 0;
  font-size: var(--fs-xs);
  color: var(--muted);
}
.legal-foot a {
  color: var(--accent);
}

@media (max-width: 560px) {
  .legal h1 {
    font-size: var(--fs-2xl);
  }
  .legal-lede {
    font-size: var(--fs-sm);
  }
}
/* ---------------------------------------------------------- install card
   pwa.js builds it; index.html and login.html both carry it. Floats above
   the page, bottom-centred, clear of a phone's home indicator. It wears the
   accent and the brand gradient because it is chrome about the app itself,
   never data. */
.pwa-card {
  position: fixed;
  z-index: var(--z-drawer);
  left: 50%;
  bottom: calc(var(--sp-4) + env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%);
  width: min(440px, 100vw - 2 * var(--sp-4));
  box-sizing: border-box;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--sp-3);
  padding: var(--sp-4);
  border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--border));
  border-radius: var(--r-lg);
  background: var(--glass);
  -webkit-backdrop-filter: var(--glass-blur);
  backdrop-filter: var(--glass-blur);
  box-shadow: var(--shadow-pop), inset 0 1px 0 var(--glass-edge);
  color: var(--text);
  animation: pwa-in 0.35s var(--ease) both;
}

@keyframes pwa-in {
  from {
    opacity: 0;
    transform: translate(-50%, 16px);
  }
}
@media (prefers-reduced-motion: reduce) {
  .pwa-card {
    animation: none;
  }
}
.pwa-icon {
  width: 48px;
  height: 48px;
  border-radius: 12px;
}

.pwa-title {
  margin: 0;
  font-weight: 600;
  font-size: var(--fs-sm);
}

.pwa-sub {
  margin: 2px 0 0;
  font-size: var(--fs-xs);
  color: var(--muted);
  line-height: 1.45;
}

.pwa-share {
  width: 15px;
  height: 15px;
  vertical-align: -2px;
  fill: none;
  stroke: var(--accent);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.pwa-btns {
  grid-column: 1/-1;
  display: flex;
  justify-content: flex-end;
  gap: var(--sp-2);
}

.pwa-go {
  border-color: transparent;
  background: linear-gradient(135deg, #ffd60a, #ff9f1c 50%, #eb5e28);
  color: #08090a;
  font-weight: 600;
}
.pwa-go:hover:not(:disabled) {
  border-color: transparent;
  filter: brightness(1.05);
}

/*# sourceMappingURL=style.css.map */
