/* The viewport's own box, and the one that actually has to be clipped.
   `overflow` on `body` alone does not stop the page scrolling: while `html`
   is `visible` — its default — the browser takes `body`'s overflow and
   applies it to the *viewport* instead, so the scrolling simply moves up to
   `html` and `body`'s own rule clips nothing. Stated here, the propagation
   has nothing left to take.

   This was missed the first time: `body` was given the height and the clip
   while `html` was left at its default, which reads as watertight and is
   not. A document with no content at all still scrolled by a few pixels. */
html {
  height: 100%;
  overflow: hidden;
}

body {
  font-family: system-ui, sans-serif;
  background: var(--surface-page);
  color: var(--text-primary);
  /* Nothing here is a document (CF-PWR-118). Every surface is a control or
     a reading, and a long press on a phone was raising the text selection
     — handles and all — over whatever was under the finger. That press is
     also how a subject panel is opened, so the two collided on the gesture
     the app is most used through.

     On `body` rather than on each control, and with no exception carved
     out, because at present there is nothing here to take: every figure is
     drawn from the payload and is on screen again a few seconds later, and
     the one place a reader wants text out of the app — the debug panel's
     layouts — has a button that copies them. Stated the other way round it
     would be a list of selectors every new element is silently left out
     of, which is the trap the controls-row rules record twice.

     Form fields are unaffected: `user-select` does not apply to the text
     inside an editable control, so the debug panel's inputs still take a
     caret and a selection. A future surface that genuinely holds text to
     be copied wants `user-select: text` on itself.

     `-webkit-touch-callout` is the other half on iOS and is not implied by
     `user-select`: without it a long press still raises the copy/share
     callout, which is the same interruption wearing a different hat. */
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  /* No double-tap zoom, but pinch still zooms. Asked for while placing
     lights: a second tap on a lamp is an ordinary thing to do on this page
     — pressing the same device twice, or correcting a press — and the
     browser was taking it as "magnify here".

     `manipulation` is exactly this pair: it drops double-tap-to-zoom and
     the 300ms click delay that goes with it, and leaves pinch, scrolling
     and panning alone. The alternative, `user-scalable=no` on the viewport
     meta, would have taken pinch with it, which the reader explicitly
     wanted kept — and takes the browser's own accessibility zoom too. */
  touch-action: manipulation;
  /* Puts the platform's own widgets and the scrollbars in the theme's
     light or dark, not the OS's — an explicit theme has to reach the
     parts these rules cannot paint. */
  color-scheme: var(--control-scheme);
  margin: 0;
  /* The window itself never scrolls, and neither does anything above the
     page stack. Scrolling belongs to one box — `.page-stack` — and the bar
     is deliberately not inside it.

     This is not a preference. `position: fixed` is resolved against the
     viewport, but an engine may still carry a fixed element along when an
     *ancestor* of it is the box being scrolled, and engines disagree about
     when that happens: the same bar stayed put in Chrome and scrolled away
     in Firefox. Keeping every scroll container below the bar removes the
     disagreement rather than betting on one reading of it.

     Note that `overflow-x: hidden` here would not have been the harmless
     half-measure it looks like: a box with one axis hidden computes the
     other to `auto`, so it makes `body` a scroll container on *both* axes.
     That is exactly what put the bar inside a scrolling ancestor. One axis
     cannot be clipped here without the other becoming scrollable, so the
     whole job moves down to the stack. */
  overflow: hidden;
  /* The window's height, so the stack inside it has a definite box to
     scroll within. `100%` of `html`, which is itself the window's height —
     not `100vh`, which on a phone is measured against the *larger* of the
     two heights the browser's own chrome moves between, and would leave the
     page taller than the window whenever that chrome was showing. The same
     argument `--flow-view-height` makes for preferring `dvh` to `vh`.

     `border-box` so any padding on this element is *inside* that height
     rather than added to it. Without it the page came out 8px taller than
     the window and the bar hung over the bottom edge by half of that. */
  box-sizing: border-box;
  height: 100%;
  /* Stated rather than inherited from the browser's own default, so the
     sticky axis can read the same figure — see `--page-edge-gap`. */
  padding: var(--page-edge-gap);
}

/* Every button is drawn by these rules and by nothing else (CF-GEN-018,
   which generalises CF-LIT-019).

   Safari on iOS draws its own chrome on a button — a light pill with its own
   rounding and shadow — whatever `background` and `border` say, unless the
   platform appearance is suppressed. Chromium and Firefox drop their native
   look by themselves once a button is given its own background or border,
   which every button here is, so this changes nothing in either: it was
   checked at 375px over every page, panel and menu, computed styles and
   screenshots alike. Firefox's device mode does not reproduce the fault
   either — it changes the viewport, not the engine — so the only place this
   rule can be seen working is a real iPhone.

   Once, here, rather than on each control's own rule. It used to be written
   per control, which is how some forty buttons came to be without it: a
   list of selectors is one the next control is silently left out of.

   Never a bare `input`: checkboxes, radios, sliders, selects and the date
   and time fields keep the platform's own drawing, which *is* the control
   for them — `appearance: none` would erase a checkbox outright. */
button,
input[type='button'],
input[type='submit'],
input[type='reset'] {
  -webkit-appearance: none;
  appearance: none;
}

/* The boot splash: what fills the window between the document arriving and
   the first module running.

   It is in this stylesheet rather than inline in the document because the
   two stylesheets are render-blocking and already load before any script —
   so it is on screen just as early either way, and it can use the palette
   like everything else. The theme is right from the first frame for the
   same reason the rest of the interface is (CF-THEME-003).

   `#app:not(:empty)` is what removes it, and the choice is deliberate: the
   splash disappears the moment the shell puts anything into `#app`, with no
   script needing to run and nothing to keep in step. A script that hid it
   would itself have to load and run — and "the modules did not run" is one
   of the states this splash exists to sit through. Being CSS, it also
   cannot leave the splash stranded over a working app. */
/* The mark shown while waiting, in the one size and treatment that serve every
   wait there is: the splash below, and the disconnect cover and waiting page
   in §Connection overlay (CF-OFF-019). Held here rather than repeated in each,
   because the requirement is that the three agree — two places holding one
   figure disagree the first time either is touched.

   Declared on `:root` so the splash's rules, which must be usable before any
   script runs, still resolve them: custom properties inherit regardless of
   where in the sheet they are written. */
:root {
  --wait-icon-size: 96px;
  /* Close to the corner radius a home screen applies to the same file, so the
     mark is the shape the reader knows it by. */
  --wait-icon-radius: 22%;
  /* Present rather than asserted: this is a wait, not an announcement. */
  --wait-icon-opacity: 0.9;
}

#boot {
  position: fixed;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  background: var(--surface-page);
  color: var(--text-muted);
  /* Above nothing in particular — the app is empty while this shows — but
     stated so a slow module that paints early cannot land on top of it. */
  z-index: 1;
}

#boot img {
  /* The attributes on the element carry the same figures, so the box is
     reserved before the stylesheet is consulted and the text below does not
     jump when the image lands. These win where the two could differ. */
  width: var(--wait-icon-size);
  height: var(--wait-icon-size);
  border-radius: var(--wait-icon-radius);
  /* The icon is the one already in the cache for the home screen, so it
     costs no extra request on the launch this is trying to explain. */
  opacity: var(--wait-icon-opacity);
}

#boot p {
  margin: 0;
  font-size: 0.95rem;
  /* Breathing rather than spinning: a spinner claims progress it cannot
     measure, and this is a wait whose length the client does not know. */
  animation: boot-pulse 1.6s ease-in-out infinite;
}

@keyframes boot-pulse {
  0%, 100% { opacity: 0.55; }
  50% { opacity: 1; }
}

/* Same rule the rest of the interface follows: the message stays, the
   movement goes. */
@media (prefers-reduced-motion: reduce) {
  #boot p { animation: none; }
}

/* `:has` rather than a sibling combinator: the splash comes *before* the
   mount point in the document — it has to, so it paints first — and CSS
   has no "preceded by" combinator. `#app:not(:empty) ~ #boot` would read
   as the obvious rule and never match once. */
#boot:has(~ #app:not(:empty)) {
  display: none;
}

/* While the palette is being swapped, nothing eases. Every themed colour
   moves at once, which is a change of palette rather than the change in the
   data CF-PWR-035 animates; and on the pipes the transition would not even
   finish, since the flow animation rewrites those paths every frame and
   restarts it. theme.js sets this for one frame (CF-THEME-012). */
:root[data-theme-switching] *,
:root[data-theme-switching] *::before,
:root[data-theme-switching] *::after {
  transition: none !important;
}

/* The Power page's host, which exists in this file only to hold the
   Flow height. */
.power-page {
  /* The height the Flow view is drawn for. Not the stage's height — the
     stage is the taller of the two view figures — but the room Flow lays
     itself out in, at the top of that stage.

     A length of its own rather than a figure inside one rule, because four
     things read it: the drawing (`heightFor` in flowView.js), the cards
     over it (`_placeCards`), `_viewHeight` which serves both, and now the
     stage's own height. They must agree to the pixel or the picture comes
     apart from its own labels, which is exactly what happened while the
     drawing was measuring its box instead (CF-HIST-065).

     Declared on the page rather than on `.power-stage`, which is the
     element that uses it: the stage's `height` now reads this to take the
     taller of the two views, and a property declared and read on one
     element is cyclic — it computes to nothing, not to its fallback, and
     took the stage to `0px`. Here it inherits down to the stage instead,
     which reads it without writing it. `--subject-shell-size` records the
     same trap further down this file.

     The stage reads it with no fallback, deliberately. A fallback there
     would be this clamp's floor written a second time, in a second place,
     and the two would part company the first time either moved — and it
     could never be reached anyway, since this rule declares the property
     unconditionally for every stage on the page.

     The room is measured from the stage's own top, not from the window's:
     whatever sits above it — the page's padding, a top bar — has already
     pushed it down, and `100dvh` counts from the top of the window. Taking
     the distance from the stage to the foot of the window and then
     removing the bottom bar is the only figure that is neither short nor
     double counted. It used to subtract a flat 3rem for the page's margin,
     which is a figure the page does not have: the margin is 2rem on a
     roomy screen and 0.25rem on a small one, and the stage sits inside it
     either way — the difference showed as a band of empty page under the
     picture, with the subjects clipped short of it.

     `100dvh` rather than `100vh`: on a phone the browser's own chrome
     comes and goes as the page scrolls, and `vh` is measured against the
     larger of the two, which would leave the picture taller than the
     window whenever the chrome was showing.

     Held between a floor and a ceiling, so a very short window still gets a
     usable picture and a very tall one does not stretch the hub into a
     picture with more air than drawing. The ceiling is in `rem` and so
     follows the interface scale, which is correct for it: it is a statement
     about how much drawing is worth having, and the drawing scales. It was
     once suspected of causing the overflow the stage's `max()` now fixes
     and it was not — at scale 1.5 the middle term wins at 1105px against a
     ceiling of 1128. */
  --flow-view-height: clamp(
    22rem,
    calc(100dvh - var(--stage-top, 0px) - var(--stage-bottom-inset, 0px) - var(--stage-foot)),
    47rem
  );
}

/* One drawing area holds the flows and the cards alike. A fixed height on
   purpose: sized by content, it would be sized by an SVG asking for 100%
   of it, and each resize would nudge the area, resize the SVG, and nudge
   the area again. */
.power-stage {
  position: relative;
  /* Width stated explicitly, as the height is. Everything inside is
     absolutely positioned, so a stage left to size itself collapses to
     zero and nothing is drawn at all.

     No maximum. There was one — 56rem — and it was removed deliberately:
     the room the window offers is the room the drawing gets. CF-PWR-060
     forbids the cap outright ("MUST NOT be capped at a size that leaves a
     large viewport substantially empty"), which this one did.
     A cap in `rem` also tracked the text size rather than the window, so
     zooming the interface *out* shrank the drawing while the window stayed
     put: at 40% it took 11% of the window's width, and the right of the
     screen sat empty. The measure limit that justifies capping a column of
     prose — the Settings page still has one — does not transfer to a
     chart, which is read by following a line along rather than by
     returning to the start of the next row. Where the drawing is too small
     to read, the interface zoom is the control for that. */
  width: 100%;

  /* The height is the History stack's — what the six rows and two axes
     come to — in *both* views, and it does not change when the view does.

     This is the one figure the two views share, and sharing it is the
     point. The stage used to be whichever view was showing, which meant it
     resized on every change; and a box that resizes while a view is on
     screen has no good moment to do it (the retired rule below records
     four attempts at finding one). Held at one height, there is no moment
     to find: the panes fade against a container that never moves, so the
     page's own scrollable height never changes, no scrollbar arrives
     partway through a fade, and there is no scroll position to put back
     afterwards.

     The taller of the two, and stated as that rather than assumed to be
     one of them. The stage has to contain the timelines stack, which does
     not shrink; Flow does not fill the stage — it is drawn for
     `--flow-view-height` and sits at the top of whatever it is given (see
     `heightFor` in flowView.js and `_placeCards`). A stage shorter than
     either view clips that view.

     This used to read "the taller of the two, necessarily" and took the
     timelines figure alone, on the reasoning that it was always the taller.
     CF-PWR-061 ended that: the stack is now drawn at the base size at every
     interface scale, so its figure stopped growing while Flow's — which is
     proportional and is *supposed* to grow, as that same requirement says
     in as many words — went on growing with the window and the root font.
     They cross at about scale 1.2; by 1.5 on a 1600×1200 window the stage
     stood at 804px around a picture drawn for 1105, and the House and EV
     cards were laid out 243px below the stage's own bottom edge.

     So the two figures are compared here rather than one of them being
     trusted. `max()` and not a media query or a script: both are already
     CSS lengths in the units they are needed in, and the comparison is the
     sentence above written down. Neither figure is restated to do it —
     which is the same rule that put `--history-stage-height` in the page
     instead of this file (see below), and the reason the ceiling in
     `--flow-view-height` was left alone: it is not what was overflowing.
     At a window tall enough for that ceiling to bind (about 1400px at the
     maximum scale) it caps Flow at 47rem, and the stage follows it there
     too.

     The price, stated plainly because it is a real one: whichever view is
     the shorter sits in a stage some tens of pixels taller than its own
     picture, so a window it used to fit exactly now scrolls by that much.
     Which view pays is not fixed — below the crossover it is Flow (47px at
     the size this was first measured), above it the timelines stack, which
     stops growing while Flow does not. The alternative is the stage
     changing size on a view change, which is what every version of this
     before it did, and which cost four separate visible faults. A short
     scroll in one view is the cheaper of the two, and unlike the resizing
     it is not a moving target: it is the same on every frame.

     Set by the page from the layout itself — the stack, both axes and the
     measured head — rather than written here (CF-HIST-020). It was 47rem,
     which happened to equal that total exactly, so widening the axes to
     stop the lower labels being clipped made the stack overflow the stage
     and clipped them again. The fallback is that old figure, for the
     moment before the first measurement. */
  height: max(var(--history-stage-height, 47rem), var(--flow-view-height));

  /* `--flow-view-height` is *not* declared here, though this is the
     element that uses it and where it lived until the stage began
     comparing the two view heights above. A custom property declared on an
     element cannot be read by that same element's own `height` without the
     value becoming cyclic — and a cyclic value computes to nothing at all,
     not to the fallback, which took the stage to `0px` and drew no picture
     whatever. The stylesheet already records this trap once, on
     `--subject-shell-size` further down; it is the same one.

     It is declared on `.power-page`, the host the stage sits in, and
     inherits down. See there for what it is and why. */

  /* The diameter of every subject shell. One number, because the shells
     are round and all the same: see `.subject-card`, which applies it to
     both axes.

     It is what an icon, its label and the padding around them need, with
     enough over for the hub-side reach to be spent inside the circle. It
     is deliberately not enlarged to fit the net's percentages or its EV
     badge — those are fitted into the shell instead, since sizing five
     subjects around what one of them displays is what made the shells
     unequal in the first place.

     Kept under its own name, `--subject-shell-base-size`, rather than
     stated only as `--subject-shell-size` here: the small-screen media
     query below blends that one down toward History's narrower figure as
     `--history-controls-opacity` rises, and a custom property cannot
     reference its own cascaded value across two rules for the same
     property without becoming cyclic — which computes to nothing at all,
     not to either rule's value, and took every shell on the page to zero
     with it the one time this was tried. The base figure needs a name the
     blend can read that is not the name it is also writing.

     The base figure itself is declared on `:root` rather than here, so that
     the subject panel — which sits outside the stage, on the body — can
     read it as the diameter to fall back on (CF-PWR-062). Every reader of
     it is inside this stage and inherits it just the same. It is only the
     *base* that moved: `--subject-shell-size` stays here, because the two
     panes and the small-screen rule each override it and each override
     belongs with the element it applies to. */
  --subject-shell-size: var(--subject-shell-base-size);
}

/* In History view the stage is a column of rows rather than a picture with
   a middle, and the rows need room the bus layout did not. The stage
   stands at that taller figure in both views (see `.power-stage`), and
   Flow is drawn for `--flow-view-height` within it rather than filling it. */

/* The view toggle used to sit here, in a row of its own above the picture.
   It has moved down beside the unit toggle, into the row that already
   holds every other choice about what the graphs show — where it is
   attached to what it governs, and where it costs no height of its own. */

/* Two buttons reading as one control: a single bordered group with a
   divider between them, rather than two separate buttons that happen to
   sit together. */
.mode-toggle {
  display: inline-flex;
  border: 1px solid var(--border-control);
  border-radius: 6px;
  overflow: hidden;
  background: var(--surface-card);
  /* The border is drawn inside the height, as it is on the square range
     buttons beside it. Left outside, the group came out two pixels taller
     than they are — its border sits on the group where theirs sits on each
     button — so the row stood at two heights and CF-HIST-069 asks for one.
     Two pixels, but the eye finds them: the icons no longer share a centre
     line. */
  box-sizing: border-box;
  height: var(--window-control-height);
  /* A gap of its own from the unit toggle beside it, smaller than the one
     that separates that pair from the range controls: a unit and a view
     are both "what am I looking at", where a range is "when". The nesting
     of the two gaps is what says which controls group with which — so
     this figure must stay below .value-mode-toggle's at every width, or
     the row says the unit toggle groups with the view toggle against the
     range buttons, which is the opposite of the truth. */
  margin-left: 0.45rem;
}

/* The snap-mode toggle shares .mode-toggle's box, border and height rules
   (CF-HIST-069) but not its margin: it is not a close pairing with the
   value or view toggles the way those two are with each other — it is its
   own group, gapped from the range controls beside it the way
   .value-mode-toggle is (CF-HIST-072), and the value toggle after it keeps
   its own smaller gap unchanged. */
.snap-mode-toggle {
  margin-left: 0.9rem;
}

.mode-toggle-button {
  display: flex;
  align-items: center;
  /* Fills its group rather than naming a height of its own. The group
     carries the height now that its border is drawn inside it, and a
     child still asking for the full figure would stand two pixels taller
     than the box it sits in. Filling it also keeps the pressable area the
     whole of what is drawn — a button inset from its own group would
     leave a hairline along the top and bottom that looks pressable and is
     not, which matters more to a finger than to a pointer. */
  align-self: stretch;
  padding: 0 var(--window-toggle-padding);
  border: none;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
  transition:
    background var(--flow-transition, 600ms) ease-in-out,
    color var(--flow-transition, 600ms) ease-in-out;
}

.mode-toggle-button + .mode-toggle-button {
  border-left: 1px solid var(--border-control);
}

.mode-toggle-button svg {
  /* Matched to the value-mode icons beside them. These were drawn larger
     when the control stood alone above the picture and had no neighbour
     to be measured against.

     Grown with the button that holds it: at 1.1rem in a target sized for
     a finger the mark sat in the middle of a field of empty button,
     which reads as a small icon someone forgot to scale rather than as a
     large control. It does not grow in step with the height — the extra
     room a touch target needs is meant to be reachable, not inked. */
  width: 1.25rem;
  height: 1.25rem;
  stroke: currentColor;
  /* The icons are drawn on a 100-unit grid rather than the 24 the rest of
     the interface uses, so the weight that reads as this line thickness is
     stated in those units. Raised from 6 when the icons shrank to sit
     beside the unit toggle: the same stroke reads as thin at this size,
     which is why the value-mode icons already carry 8. */
  stroke-width: 8;
  stroke-linecap: round;
  /* The timelines mark turns corners; without this they are mitred to
     points that read as spikes at button size. */
  stroke-linejoin: round;
}

/* Two kinds of mark in one icon: axes and the data line are drawn as
   strokes, the ring's centre as a solid. A blanket `fill: none` would
   erase the second kind, so it is named. It also stops a polyline
   filling the region its own ends enclose, which is what an unfilled
   `<polyline>` would otherwise do. */
.mode-toggle-button svg > * {
  fill: none;
}

.mode-toggle-button svg .mode-icon-solid {
  fill: currentColor;
  stroke: none;
}

/* The data line in the timelines mark, drawn a shade finer than the axes
   it sits between. Equal weights made the three strokes read as one
   tangle at button size; the axes carrying more weight is what lets the
   eye take the frame first and the line second, which is how a chart is
   actually read. */
.mode-toggle-button svg .mode-icon-trace {
  stroke-width: 5;
}

/* The selected view is the one lit, in the same blue an active flow uses,
   so the control belongs to the picture it controls. */
.mode-toggle-button.selected {
  background: var(--flow-bore);
  color: var(--text-reading-active);
}

.mode-toggle-button:focus-visible {
  outline: 2px solid var(--flow-casing);
  outline-offset: -2px;
}

/* A view that exists in the control but not yet in the application. Held
   at a weight that reads as "not on offer" rather than as "broken", and
   with the default cursor, since nothing happens on a click. */
.mode-toggle-button:disabled {
  opacity: 0.35;
  cursor: default;
}

/* The range summary: what is being shown, and the ways to change it
   (CF-HIST-036, CF-HIST-037). A range is chosen rarely and read
   constantly, so this is what earns permanent space and the controls do
   not.

   Positioned by the page over the histogram columns and just above the
   upper axis, so the label and the graphs it names read as one thing.
   Centring over the *stage* would push it right, since the right of the
   stage is sockets and cards rather than graph. */
/* One height for every control in the range row: the range buttons, the
   unit toggle and the view toggle. They are three different kinds of
   control — text buttons and two icon groups — and left to their own
   padding they came out three different heights, which read as a row that
   had failed to line up rather than as a row of choices.

   Sized for a finger, not for a pointer (CF-HIST-072). It was 1.6rem,
   which is a comfortable mouse target and a poor touch one: a fingertip
   covers rather more than 26px, so hitting the wanted control among
   several meant aiming. The same interface is meant to work on a phone
   and under a mouse, and the larger target is the one that serves both —
   a mouse loses nothing by being offered more room.

   Why not the 44px a phone platform asks for: the row has to fit across
   the narrowest screen the application supports, in one line, and 44 does
   not go. An extra row would cost more height than the larger targets are
   worth, on the one screen where height is scarcest. This is what fits,
   and it is the reason the reset control moved into the chooser
   (CF-HIST-071) — five buttons at this size do not fit either. */
:root {
  /* The diameter every subject shell is derived from. What it is sized for,
     and why it has a name of its own separate from `--subject-shell-size`,
     is written where that one is declared — on `.power-stage`, with the
     rules that read it.

     It sits here rather than there for one reason: the subject panel
     (CF-PWR-062) is on the body, outside the stage, and needs this figure
     as the diameter to fall back on when it has no pressed card to measure.
     Declared on the stage it would be invisible to the panel, and the panel
     would have to restate 6.1rem — a second copy of a length, which is the
     failure this file records three times over. Every other reader is
     inside the stage and inherits it from here unchanged. */
  --subject-shell-base-size: 6.1rem;
  /* The rest of what makes a shell look like a shell, beside its diameter:
     the air between its contents and its edge, the gap between icon and
     name, and the ring around it. Named because a subject's circle is drawn
     in two places now — on the page as a card, and on the subject panel's
     rim (CF-PWR-064) — and the second has to be the *same* circle, not one
     that resembles it. Written out twice they would part company the first
     time either was touched, and the panel's whole job is to be recognisably
     the thing that was pressed.

     The padding is two figures rather than a shorthand because a card
     rebuilds it per side — the side facing the hub carries the reach, and
     `calc()` cannot reach into a shorthand to add to one edge of it. Named
     per axis, every one of those rules reads the same figure the shorthand
     does. The panel's circle faces nothing and keeps the base as it stands. */
  --subject-shell-pad-y: 0.7rem;
  --subject-shell-pad-x: 1.1rem;
  --subject-shell-gap: 0.1rem;
  --subject-shell-border: 3px;
  --window-control-height: 2.25rem;
  /* How much air a segmented toggle puts either side of its icon. Stated
     here because the row's width is the binding constraint on a phone and
     this is the figure that gives, once the height above is settled: the
     square buttons cannot be narrowed without also being shortened. */
  --window-toggle-padding: 0.36rem;
  /* The air left under the flow picture, above whatever bar follows it.
     Enough that the lowest subject is not touching the bar, and no more:
     everything beyond this is empty page in the one view that wants
     height most. */
  --stage-foot: 0.5rem;
  /* Between the head's bands, and nowhere else. Small: enough that the
     controls, the range description and the axis below them do not read as
     crowded, and no more, since every pixel here is one the graphs do not
     get. */
  --head-band-gap: 0.35rem;
  /* The page's own side margin, either side of the scrolling stack. This
     is the desktop figure; the `max-width: 800px` block narrows it, where
     2rem a side is a sixth of a phone's width spent on empty page.

     Declared *here*, above that block, and not beside `--page-edge-gap`
     further down. Both would be `:root` at equal specificity, so the later
     one wins outright and a media query below it does nothing at all —
     which is exactly what happened: the override was written, matched, and
     was silently overridden by the base value 138 lines further on. Order
     is the whole mechanism for these two rules.

     Named rather than written at each use: the rail placement adds the
     bar's size to this same figure, and two copies of it disagree the
     moment either is touched. */
  --page-side-gap: 2rem;
}

/* The head of the History view: bands, stacked, pinned to the top edge
   (CF-HIST-064, CF-HIST-103).

   Normal flow rather than absolute placement *within* the head. Two
   elements placed absolutely in one space are neighbours, and every screen
   width then needs its own rule about which of them yields; stacked, they
   cannot overlap at all and no such rule exists.

   No gap above the first band — space over the controls is space taken
   from the graphs for nothing — and equal gaps between the bands.

   `fixed` rather than `sticky`. Sticky is not available here: everything
   inside `.power-stage` is absolutely positioned and the stage's height
   comes from `--history-stage-height`, so the head has no in-flow position
   to stick within. Fixed is also what the two pinned axis copies already
   do, and for the same reason — which is what lets the head and the axis
   beneath it be positioned by one rule rather than two that must agree.

   `left` and `width` are written from the stage's measured rect by the
   page, not declared here, so the description stays centred over the same
   graph columns it names once the head no longer sits inside the stage
   (CF-HIST-037). */
.power-head {
  position: fixed;
  top: 0;
  display: flex;
  flex-direction: column;
  gap: var(--head-band-gap);
  /* Above the pinned upper axis, which is 6. The head and that axis are
     the two halves of one block and never overlap, so between themselves
     the order does not matter — but the views menu (CF-HIST-080) hangs off
     the head and opens downward *across* the axis, and it cannot outrank
     the axis by itself: it is inside this head's stacking context, so its
     own z-index is resolved among the head's children and never against
     the head's siblings. The head is the element that has to win, and this
     is where that is decided. */
  z-index: 7;
  /* The band ends at the axis below it, which draws its own separating
     line. Padding here would be empty page between the controls and the
     times. */
  padding-top: var(--page-edge-gap, 0.35rem);
}

/* The opaque backing belongs to the timelines view alone. There the graphs
   scroll underneath the band and would read through it; in Flow the head
   holds nothing but the view toggle — everything else has faded to nothing
   (`--history-controls-opacity`) — and the picture is drawn from the top of
   the stage, so a band painted across the full width cuts the top off the
   highest subject. It did exactly that to `Net` the first time this was
   tried, which is a fault the previous transparent, in-stage head could not
   have: an overlap that costs nothing while it is see-through becomes a
   clip the moment it is painted.

   Carried on the *alpha* of one colour rather than by a class, and on the
   same variable the head's contents fade with. A class would have to be
   either `.history-showing` — the settled view, which turns the backing on
   at the fade's last frame, under a stack already at full strength — or a
   class for the view being headed for, which turns it on at the first
   frame, over a flow picture still in plain view. The band has to arrive at
   exactly the rate the thing it backs does, and that rate is already
   published as a number. `color-mix` rather than `opacity`: opacity here
   would fade the controls a second time, against their own already-faded
   paint. */
.power-head {
  background: color-mix(
    in srgb,
    var(--surface-page) calc(var(--history-controls-opacity, 1) * 100%),
    transparent
  );
}

/* The head and the top bar share the top edge, exactly as the pinned axes
   do with their bars. Measured, not stated, for the reason given there.

   The bar's own thickness is not where the bar *ends*: the page holds it
   `--page-edge-gap` clear of the window (see `body`'s padding), so it ends
   that much further down. Offsetting the head by the thickness alone put
   it 4px inside the bar, and the head's own top padding was then spent
   closing that error instead of separating the two — the controls came out
   flush against the bar with a fifth of a pixel between them. Both terms
   are needed: the gap above the bar, then the bar. The gap *below* the
   head is the head's own padding and is separate from both. */
:root[data-bar-placement='top'] .power-head {
  top: calc(var(--page-edge-gap) + var(--page-bar-size));
}

/* On a phone held upright the controls leave the head and stand on the page
   bar instead, a strip of their own directly above it (CF-HIST-122). That is
   where the thumb already is — the bar went to the bottom edge for the same
   reason — and the top of the window is given back to the pictures: Flow is
   drawn from the stage's own top, and the timelines' head is only the
   description and the upper axis.

   Still a child of the head in the document, so the menus that hang off the
   head, the fades and the pointer rules all keep working unchanged; only
   where the band is drawn moves. Fixed against the window like the head, so
   its `left` and `width` are written by the page from the stage's measured
   column, exactly as the head's are.

   The backing is the head's own, on the same variable and for the same
   reason: the graphs scroll under this strip in the timelines view and must
   not read through it, while in Flow it holds the view toggle alone over
   room the picture has already left free. Padding above is the air between
   the picture's foot and the row; below, the bar's own top border is the
   separation. */
:root[data-bar-placement='bottom'] .power-window {
  position: fixed;
  bottom: calc(var(--page-bar-size) + var(--page-edge-gap));
  padding: 0.3rem 0 0.25rem;
  background: color-mix(
    in srgb,
    var(--surface-page) calc(var(--history-controls-opacity, 1) * 100%),
    transparent
  );
}

/* The room that strip takes, counted where the bar's already is. The figure
   is the strip's measured height, published by the page as
   `--power-controls-size`: a control row's height follows the platform's
   font and the interface scale and cannot be stated here.

   Two things read it. Flow's room is the window less what stands below the
   stage (`--flow-view-height`), so the strip joins the bar in that sum and
   the lowest subject ends above the row rather than under it. And the page
   is padded by it at its foot, so the end of the stack — the lower axis —
   can still be scrolled clear of the strip. */
:root[data-bar-placement='bottom'] .power-page {
  --stage-bottom-inset: calc(var(--page-bar-size) + var(--power-controls-size, 0px));
  padding-bottom: var(--power-controls-size, 0px);
}

/* The controls, right-aligned: the row is pressed occasionally where the
   description above the graphs is read constantly, so the description is
   what earns the centre.

   Right-alignment is also what decides which end is lost when the row is
   too wide for the screen. The row overflows to the left and this band
   clips it there (CF-HIST-072), so the controls that survive are the ones
   at the right — the view toggle above all, which is the only way out of
   a view and must never be the one that goes.

   `overflow: hidden` on this band alone, not on the head: the head holds
   the description too, and hiding overflow there would cut off a long
   range description at the same edge, which is a different band with its
   own rule about what happens when it does not fit (CF-HIST-037). */
.power-window {
  display: flex;
  justify-content: flex-end;
  overflow: hidden;
}

/* The description, centred over the graphs — which is not the middle of
   the drawing area (CF-HIST-037).

   The graphs occupy a column with unequal margins: the axis labels take a
   strip on the left, the sockets and cards a wider one on the right.
   Centring the band in the full width therefore put the description well
   to the right of what it names. The band still spans the whole width, so
   a description longer than the column can use the rest rather than being
   truncated early; it is the padding that centres the text.

   The two figures come from the page, which takes them from the box the
   histograms are drawn in. They cannot be written here: the left one
   depends on the widest axis label, which is measured after drawing.

   The fallbacks are zero, so a first paint before any measurement centres
   on the drawing area — off by the same margin this rule exists to fix,
   but only until the first layout, and never wrong in a way that clips.

   Opacity rather than `hidden`: `hidden` collapsed this band to nothing
   until the exact frame the fade into timelines finished, then sprang it
   open at full height. `.power-head` sits under `position: absolute` and
   does not size the document itself, but `_publishHeadHeight` measures it
   to place the stage below, so that spring moved the stage the instant the
   description appeared — read as the timelines panel settling a few pixels
   lower right as the fade completed. Fading the band's opacity in step
   with `--history-controls-opacity` keeps its box, and so the head's
   measured height, constant throughout; only the paint moves. */
.power-range-label {
  padding-left: var(--graph-column-left, 0px);
  padding-right: var(--graph-column-right, 0px);
  text-align: center;
  pointer-events: none;
  opacity: var(--history-controls-opacity, 1);
}

/* A sockets-only drawing: the history view's copy, which carries the live
   readings beside the graphs and nothing else. The plumbing and the hub
   belong to the live picture and have no meaning in a stack of graphs, so
   they are not drawn rather than being drawn and hidden by opacity — an
   invisible pipe still costs a layer to paint every frame.

   The segments too: they are the moving parts inside a pipe, and a pipe
   that is not drawn has nothing for them to move along. Left visible they
   were the one part of the live picture still showing over the graphs.
   They need no rule here any more: a sockets-only drawing is built without
   the canvas they are painted on (flowView.js). */
.flow-view-sockets-only .flow-layer-casings,
.flow-view-sockets-only .flow-layer-fills,
.flow-view-sockets-only .flow-layer-hub {
  display: none;
}

/* The two views, stacked one over the other in the stage (CF-HIST-065).
   Each is drawn exactly as it would be if it were the only one, and the
   switch between them is a change of opacity.

   They were laid side by side in a strip that slid, when there were three
   of them ordered as time runs. A strip said something true then — the
   movement went the same way as the button that asked for it — and says
   nothing now that there are two views with no order between them.

   No CSS transition on the opacity: it is driven frame by frame from the
   animator, which owns the easing and the duration. A transition here
   would be a second animation fighting the first.

   Each pane fills the stage, and the stage is now one fixed height in both
   views (see `.power-stage`). Both panes therefore have the same box at
   every moment, which is what makes a fade a change of opacity and nothing
   else.

   Filling it does not mean drawing to fill it. Flow is laid out for
   `--flow-view-height` and sits at the top of the room it is given, so the
   spare height under it in a taller stage is simply empty. Giving the panes
   separate fixed heights instead — so that each pane were only as tall as
   its own view — was tried and is worse: the pane is what `overflow` is
   applied to, so a short pane clips its own drawing, and a pane that
   carries its view's height puts that height back into the layout, which
   is the resizing this arrangement exists to avoid. */
.view-pane {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  will-change: opacity;
}

/* The timelines pane is drawn at the base size whatever the interface
   scale is (CF-PWR-061).

   Its stack is a column of rows of a stated height, laid out in pixels
   that never see the scale, and the shells sit *in* those rows. Everything
   else here is in `rem` and so grows with the root font size — so on a
   large screen the shells grew, the rows did not, and every card was
   drawn overlapping the row beneath it. The sockets did the same, which is
   why they are asked for at their base length too (`historyLayout.js`).

   Undone by division rather than by restating the base figures in pixels.
   A `97.6px` here would be `6.1rem` written a second time, in a second
   unit, and the two would part company the first time either moved.

   `--ui-scale` is the shell's own figure, set on the document element from
   the same number it sets the root font size with, so this cannot drift
   from what the scale actually is. Its fallback is written into both
   `var()` calls rather than declared anywhere, so that a stylesheet loaded
   before any script has run computes to the base size — and so that the
   default sits with its only two readers rather than in a block far above
   that would have to be found to be trusted. The obvious home for it, the
   variables block at the top of this file, is not `:root` at all but
   `.power-stage`, which sits *between* the document element and this pane
   and so overrode the very value it was meant to back up.

   The flow pane is untouched and still scales in full. Its geometry is
   proportional in both directions, so what its cards grow into is room
   that grew with them. */
.view-pane-history {
  --subject-shell-size: calc(var(--subject-shell-base-size) / var(--ui-scale, 1));
  /* The socket readings are drawn inside arrows that no longer grow, so
     they have to stop growing too, or the text outgrows the outline it
     sits in. They are sized in `em` (see `.socket-live`) rather than
     `rem`, which is what lets this one division carry every one of them:
     `rem` would go on resolving against the root whatever this pane said.
     Anything else in the pane sized in `rem` is unaffected by this and
     still scales, which is correct — the labels beside the graphs are text
     on a page, not text inside a fixed-size arrow. */
  font-size: calc(1rem / var(--ui-scale, 1));
}

.power-stage {
  overflow: hidden;
}

/* Nothing is drawn here. The page assigns a view's height to this box and
   reads the result back, which is how a `clamp()` over `dvh` and `rem`
   held in a custom property is resolved to pixels at all — see
   `PowerPage`'s `heightProbe`. Zero width and out of the flow, so the box
   it is given costs nothing and covers nothing; `visibility: hidden`
   rather than `display: none`, which would make its height zero and defeat
   the whole point of it. */
.view-height-probe {
  position: absolute;
  top: 0;
  left: 0;
  width: 0;
  visibility: hidden;
  pointer-events: none;
}

/* The Flow view takes the room that is there rather than a height of its
   own — `--flow-view-height`, defined on `.power-stage` with the reasoning
   behind the figure.

   RETIRED. The stage no longer changes height at all — see `.power-stage`,
   which now stands at the taller of the two views in both. This rule is
   kept as a comment because the reasoning cost four attempts and the next
   person to think "the stage should just be whichever view is showing"
   should be able to read why it is not.

   The stage used to take each view's own height, and the question was only
   *when* to switch. Every answer was wrong, because there is no good moment
   to resize a box while either view can be seen:

   - `.history-showing`, the settled view, left the stage at Flow's height
     for the whole fade into Timelines and sprang to the stack's height on
     the last frame, under an arriving stack already at full strength.
   - `.history-visible`, "the pane has some opacity", changes when the
     timelines pane reaches zero — five sixths of the way through the fade,
     with the flow view already at full opacity. The height still moved
     under a picture in plain view.
   - `.history-bound`, the view being headed for, moved it to the change's
     first frame. That is the best moment there is, and it was still not
     good enough: the page's scrollable height changes with the stage, so a
     scrollbar appeared the instant a fade towards Timelines began and the
     flow view sat 48px short of the room it now had.

   The answer is not to time the change better but to have nothing to time.
   A container that never changes size cannot be seen changing size, and
   fading between two of its children then has no effect on layout at all —
   no scrollbar arriving mid-fade, and no scroll position to restore
   afterwards, because nothing ever scrolled. */

/* One finger on the graphs pans the timeline (CF-HIST-089), so the browser
   must not take that touch as a scroll of the page.

   It decides *before* any pointer event is delivered, so `preventDefault`
   in the handler is too late — the page has already begun scrolling under
   the finger. This is the only thing that tells it not to, the same reason
   `.layout-dragging` carries it.

   `pan-y` rather than `none`: the pan is horizontal — a timeline has one
   dimension and it is time — so the vertical axis is left to the browser
   and the page still scrolls under a finger moving up or down. On a phone
   the stack is taller than the screen, and taking both axes would strand
   the reader on whichever graph was in view.

   Only while the timelines are showing. The Flow view has no gesture and
   is scrolled like any other page. */
.power-page.history-showing .power-stage {
  touch-action: pan-y;
}

/* The range controls, the unit toggle and the snap toggle belong to the
   graphs and appear with them. The view toggle does not: it is the only way
   out of a view, so it is drawn whichever view is showing (CF-HIST-067).

   Every control that belongs to the graphs has to be listed in both rules
   below. The snap toggle was in neither and so stood at full strength over
   the live picture, which has nothing to snap: a list of selectors is a
   place a new control is silently left out of, and this one was.

   Their opacity follows `--history-controls-opacity`, which the script
   keeps equal to the timelines pane's own opacity — so these controls fade
   at the same rate the pane they belong to does, instead of snapping in
   once it has arrived. Neither `display` nor `visibility` toggles: taken
   out of the layout, the buttons would let the view toggle slide along the
   row and sit in a different place in each view. Opacity alone leaves the
   row's shape unchanged throughout; only the paint moves.

   `.window-label` is not listed here even though it carries the same
   description: it is reparented into `.power-range-label` (CF-HIST-064),
   which carries this same opacity variable on its own rule below, and a
   second `opacity` on the child would multiply against the parent's rather
   than replace it — fading the text to a quarter of the intended strength
   at the halfway point instead of a half.

   Pointer events are cut whenever the timelines view is not the settled
   one, mid-fade included, so a control fading in cannot catch a press
   meant for whatever is behind it. */
.power-page .window-icon,
.power-page .value-mode-toggle,
.power-page .snap-mode-toggle {
  opacity: var(--history-controls-opacity, 1);
}

.power-page:not(.history-showing) .window-icon,
.power-page:not(.history-showing) .value-mode-toggle,
.power-page:not(.history-showing) .snap-mode-toggle {
  pointer-events: none;
}

.window-summary {
  display: flex;
  /* The controls on one line, the range description under them on its own.
     A column, so the description is centred on the graphs rather than on
     whatever the buttons happen to come to. */
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
}

/* The controls that do not earn a permanent place in the row
   (CF-HIST-080): snap, previous and reset.

   Anchored under the button that opens it and to the same edge, so it
   appears where the finger already is. Right-aligned with the row it
   belongs to would put it under the view toggle instead, which is a
   different control entirely.

   Positioned against the head rather than against the controls row, even
   though the row is what it belongs to. The row's band clips its own
   overflow — that is what drops the leftmost control on a screen too
   narrow to hold them all (CF-HIST-072) — and a menu inside it is clipped
   by the same rule, which showed as a menu with only its first item
   visible. The offsets come from the page, which measures the button.

   Above the graphs it covers, and above the pinned axes, since a menu
   partly behind the drawing would be unreadable over a dense graph.

   The figure here cannot achieve that on its own and is not what does.
   This menu hangs off `.power-head`, which is itself positioned and
   layered, so it lives inside that head's stacking context and no z-index
   written here can lift it past one of the head's own siblings — raising
   it from 6 to 7 changed nothing at all. What puts it above the pinned
   upper axis is the *head* outranking that axis; see `.power-head`. Kept
   above the graphs, which is within its own context and is what this
   figure is for. */
.window-menu {
  position: absolute;
  z-index: 6;
  display: flex;
  flex-direction: column;
  min-width: 12rem;
  padding: 0.25rem;
  border: 1px solid var(--border-control);
  border-radius: 6px;
  background: var(--surface-card);
  box-shadow: 0 6px 16px rgb(0 0 0 / 0.45);
}

.window-menu[hidden] {
  display: none;
}

/* Words as well as icons. There is room for both here, and a control
   reached rarely is exactly the one whose icon has not been learned —
   which is the argument the row cannot afford and this can. */
.window-menu-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  /* Tall enough for a finger, as everything else is (CF-HIST-072). The
     row's own height, so a menu item is not a smaller target than the
     button that opened it. */
  min-height: var(--window-control-height);
  padding: 0 0.5rem;
  border: none;
  border-radius: 4px;
  background: none;
  color: var(--text-panel);
  font-size: 0.82rem;
  text-align: left;
  cursor: pointer;
}

.window-menu-item .window-icon-art {
  flex: none;
}

.window-menu-item:hover:not(:disabled) {
  background: var(--flow-bore);
  color: var(--text-reading-active);
}

/* Nothing to do, and said rather than discovered by pressing. Disabled
   rather than removed so the menu is the same shape every time it opens
   and the item being reached for is where it was last time. */
.window-menu-item:disabled {
  opacity: 0.4;
  cursor: default;
}

.window-menu-item:focus-visible {
  outline: 2px solid var(--flow-casing);
  outline-offset: -2px;
}

/* The open menu is marked on the button rather than by turning it. The
   mark used to be a chevron, which could rotate to point at what it had
   opened; three dots have no direction to turn, so the button takes the
   lit treatment its neighbours use for hover instead. */
.window-more[aria-expanded='true'],
.window-views[aria-expanded='true'] {
  color: var(--text-primary);
  border-color: var(--flow-casing);
}

/* The views menu (CF-HIST-092). A list of spans to go to, where the other
   menu holds controls that would not fit — so this one is narrower: its
   items are two or three words and there are no icons to leave room for.
   The tick occupies the slot the other menu gives an icon, which is what
   keeps the labels on one left edge whether ticked or not. */
.window-views-menu {
  min-width: 9rem;
}

.window-view-tick {
  flex: none;
  width: 1rem;
  text-align: center;
  /* Held even when there is nothing in it, so choosing a view does not
     shift every label sideways by the width of a tick. */
  visibility: hidden;
}

.window-view-tick::before {
  content: '✓';
}

.window-view-item.is-current .window-view-tick {
  visibility: visible;
}

/* The view in force reads as chosen rather than merely hovered: the same
   lit text as a hover, without the filled background, so that moving the
   pointer down the list still shows which item is under it. */
.window-view-item.is-current {
  color: var(--text-reading-active);
}

/* "Now and ahead" is not a calendar period where the five above it are
   (CF-HIST-104), and the rule is stated to hold of them rather than
   quietly dropped. A line says so: the set stays coherent and the
   exception stays visible, which a sixth item in the list would not do.
   Margin above as well as the border, so the line separates two groups
   rather than underlining the item before it. The item's height comes from
   `min-height` and its padding is horizontal only, so the space is added
   as margin and the border sits on the gap. */
.window-view-item.is-apart {
  margin-top: 0.3rem;
  border-top: 1px solid var(--border-control);
}

/* "Custom range" leads the views list (CF-HIST-110) and is the one item
   there that is not a view. It wears the clock it carried when it was a
   button of its own, in the tick's slot so every label still lines up —
   the tick is `visibility: hidden` rather than absent for exactly that
   reason, and an icon in the same slot inherits the alignment. Visible,
   unlike a tick, since it is not a state that comes and goes. */
.window-custom-icon {
  visibility: visible;
  display: flex;
  align-items: center;
  justify-content: center;
}

.window-custom-icon .window-icon-art {
  width: 1rem;
  height: 1rem;
}

/* A line below it: the mirror of `is-apart` at the other end of the list,
   separating the one chooser that can express anything from the five
   ready-made spans under it. */
.window-custom-item {
  margin-bottom: 0.3rem;
  border-bottom: 1px solid var(--border-control);
}

/* The toggles menu (CF-HIST-112). Each row is a heading over a pair of
   icon buttons rather than a line of text, so it is wider and more
   generously spaced than the views menu. */
.window-toggles-menu {
  min-width: 10rem;
  gap: 0.55rem;
  padding: 0.5rem;
}

.window-toggles-group {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

/* Small and quiet: it names the pair below it, and must not compete with
   the controls, which are what the reader opened the menu to press. */
.window-toggles-heading {
  font-size: 0.7rem;
  color: var(--text-muted);
  line-height: 1;
}

/* The three toggles were built for a right-aligned row and carry margins
   from it; in a column those are simply an indent. Cleared here rather
   than in each control, because the controls are unchanged — which is the
   point of hosting them rather than rebuilding them (see `togglesGroup`). */
.window-toggles-slot > .mode-toggle {
  margin: 0;
}

/* The dots are filled, not stroked. At the 1.7 stroke width the rest of
   this set uses, a ring of radius 1.35 closes into a blob; drawing them as
   fills keeps them dots at any size. */
.window-icon-dot {
  fill: currentColor;
  stroke: none;
}

/* Every control that governs the graphs, in one row: the range buttons,
   the unit toggle and the view toggle. They are all the same kind of
   thing — a choice about what is shown — and they were previously spread
   across two heights with the description wedged between them.

   Never wraps (CF-HIST-072). It used to, on the reasoning that a second
   row is more legible than a row running off the edge. That was wrong
   about which failure is worse: the second row is drawn from the graphs'
   height on exactly the screen with least of it, and it moves every
   control in the row — including the view toggle, which CF-HIST-067 says
   must sit in the same place in every view.

   Where the row will not fit it is clipped at its left edge instead. That
   end is chosen rather than the right because the row is right-aligned:
   the controls nearest the right are the ones drawn in every view and
   reached for most, and the leftmost is the way back, which is the one
   control here whose job is also done by the chooser two along from it.
   Losing sight of that on a screen too narrow to hold it costs less than
   moving all seven. */
.window-controls {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  justify-content: flex-end;
  gap: 0.25rem;
}

/* The controls are never squeezed to fit. Left shrinkable, a row a few
   pixels over its width would quietly give every button a fraction of its
   size back — which is how a row sized for a finger becomes one sized for
   a pointer without any rule saying so. They keep their size and the row
   overflows instead, which is what the clip above is for. */
.window-controls > * {
  flex: none;
}

.window-label {
  font-weight: 600;
  font-size: 0.85rem;
  color: var(--text-label);
  text-align: center;
  /* One line: a range long enough to wrap would push the block up into the
     graphs above it. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

/* One square button for every range control. Square because the row holds
   nothing but icons now, and a square target is both larger than the word
   it replaced and the same size as its neighbours — which is what lets the
   whole row line up. */
.window-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--window-control-height);
  height: var(--window-control-height);
  padding: 0;
  border: 1px solid var(--border-control);
  border-radius: 4px;
  background: var(--surface-card);
  color: var(--text-muted);
  cursor: pointer;
}

/* Grown with the button, and by less than the button grew — the room a
   finger needs is reachable area, not more ink. Held a shade under the
   toggles' 1.25rem: these are drawn on a 24-unit grid that reaches the
   edges of its box, where the toggles' icons are cropped to their
   drawing, so equal boxes would put more mark on screen here. */
.window-icon-art {
  width: 1.2rem;
  height: 1.2rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.window-icon:hover:not(:disabled) {
  color: var(--text-primary);
  border-color: var(--flow-casing);
}

/* Disabled rather than hidden with nowhere to go, so the row beside it
   does not jump about as ranges are chosen. Reset earns the same treatment
   for the same reason: the default range is where a session starts and
   returns to, so the control spends much of its life with nothing to do.
   The forward stepper spends most of a live session that way too, since a
   range that tracks the present has nowhere later to go.

   Multiplied against `--history-controls-opacity` with `calc()` rather than
   stated on its own: this and the fade are two independent dimmings of the
   same button, and a plain `opacity: 0.4` here would win outright over the
   fade by simply coming later in the file, leaving a disabled control at
   full strength — or worse, at 0.4 — while every other control was mid-fade
   beside it. */
.window-icon:disabled {
  opacity: calc(var(--history-controls-opacity, 1) * 0.4);
  cursor: default;
}

.window-icon:focus-visible {
  outline: 2px solid var(--flow-casing);
  outline-offset: 1px;
}

/* A control that is folded into the menu, or an item whose control is in
   the row, is out of the layout rather than merely invisible
   (CF-HIST-081). `visibility` would leave the row exactly as wide as it
   was and fold nothing, which is the whole point of measuring it.

   This is deliberately not the rule that hides controls belonging to
   another view: those are hidden *in place*, so the view toggle does not
   move between views (CF-HIST-069). Here the row is meant to close up.

   It must sit *after* `.window-icon` and `.window-menu-item`, both of
   which set a `display` of their own. `[hidden]` adds no specificity over
   a bare class, so an equal-specificity rule earlier in the file loses and
   the folded controls stay on screen — which is exactly what happened:
   the row measured a shortage, set `hidden` on three buttons, and drew all
   six anyway. */
.window-controls > .window-icon[hidden],
.window-menu-item[hidden] {
  display: none;
}

/* The range dialog. Only on screen while a range is being chosen, so it
   can spend room on giving each end its own labelled row. */
.range-dialog-backdrop {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  background: rgb(0 0 0 / 0.45);
}

.range-dialog-backdrop[hidden] {
  display: none;
}

.range-dialog {
  width: min(30rem, 100%);
  max-height: 100%;
  overflow-y: auto;
  padding: 1.1rem 1.2rem;
  border: 1px solid var(--border-control);
  border-radius: 10px;
  background: var(--surface-panel);
  color: var(--text-panel);
  box-shadow: 0 10px 40px rgb(0 0 0 / 0.4);
}

.range-title {
  margin: 0 0 0.8rem;
  font-size: 0.95rem;
  color: var(--text-primary);
}

/* The whole-day shortcuts that sat here are gone: they are the views menu
   in the controls row now (CF-HIST-092), where they are one press rather
   than three. */

.range-side {
  margin: 0 0 0.9rem;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--border-control);
  border-radius: 6px;
}

.range-side legend {
  padding: 0 0.35rem;
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--text-muted);
}

/* Every way of expressing an end is shown at once, so the shape of the
   choice is visible rather than hidden behind a dropdown selection. */
.range-option {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.25rem 0;
  font-size: 0.78rem;
  cursor: pointer;
}

.range-option select,
.range-option input[type='datetime-local'] {
  background: var(--surface-card);
  color: var(--text-panel);
  border: 1px solid var(--border-control);
  border-radius: 4px;
  padding: 0.25rem 0.35rem;
  font-size: 0.76rem;
  font-variant-numeric: tabular-nums;
  color-scheme: var(--control-scheme);
}

/* A shortcut past typing four zeros, sitting with the field it fills.
   Styled as a control rather than as one of the row's values: it does
   something to the field beside it instead of holding a value of its
   own. */
.range-from-midnight {
  background: var(--surface-card);
  color: var(--text-muted);
  border: 1px solid var(--border-control);
  border-radius: 4px;
  padding: 0.25rem 0.4rem;
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}

.range-from-midnight:hover:not(:disabled) {
  color: var(--text-panel);
}

/* The controls of an unselected row stay visible — they say what picking
   that row would offer — without looking active. */
.range-option select:disabled,
.range-option input:disabled,
.range-option button:disabled {
  opacity: 0.45;
}

.range-hint,
.range-now {
  color: var(--text-muted);
  font-size: 0.74rem;
}

/* What the current pair actually resolves to. Two ends chosen
   independently can surprise, and this is what makes them legible before
   they are applied. */
.range-preview {
  margin: 0 0 0.9rem;
  padding: 0.4rem 0.55rem;
  border-radius: 5px;
  background: var(--surface-card);
  font-size: 0.76rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
  min-height: 1.2rem;
}

.range-dialog.invalid .range-preview {
  color: var(--histogram-soc);
}

.range-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
}

.range-actions button {
  padding: 0.35rem 0.8rem;
  border: 1px solid var(--border-control);
  border-radius: 5px;
  background: var(--surface-card);
  color: var(--text-panel);
  font-size: 0.78rem;
  cursor: pointer;
}

.range-apply {
  background: var(--flow-bore);
  border-color: var(--flow-casing);
  color: var(--text-reading-active);
}

.range-apply:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.range-actions button:focus-visible {
  outline: 2px solid var(--flow-casing);
  outline-offset: 1px;
}

/* Above the subjects' shells, which reach under the sockets: the shell is
   backing, the plumbing sits on it. The icon and label are lifted back
   above this in turn, so they stay legible. */
.flow-view,
.history-view {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 1;
  pointer-events: none;
}

/* Cards are placed by the same fractions the sockets are computed from,
   so a card and its plumbing cannot drift apart. */
/* A subject is an icon, its name, and a rounded shell around both. The
   shell runs on past the icon towards the hub so that it passes behind the
   subject's sockets: that overlap is what makes an icon and its plumbing
   read as one object instead of a label sitting near some pipes. */
.subject-card {
  position: absolute;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--subject-shell-gap);
  white-space: nowrap;
  /* Every shell is the same size and round: one diameter, applied to both
     axes. A row of shells that differ in size reads as a row of things of
     differing importance, which is not what a subject's contents mean, and
     a shell that is round for one subject and oval for another says the
     same thing in a subtler way.

     Border-box, so the padding is inside the circle rather than added
     around it. That is what lets one number set both axes: with the
     padding outside, the horizontal and vertical padding differ — the
     hub-facing side carries the reach — and no pair of content dimensions
     comes out square.

     The size is decided by the icon and the label only. What else a
     subject displays is fitted into the shell rather than allowed to
     enlarge it: the net's price readout is taken out of flow (see
     `.subject-net-price`), for exactly the coupling this rule exists to
     break — sizing five shells around something only one of them carries.
     The net used to carry two percentages and an EV badge here, and both
     are now in its own panel (CF-PWR-108), which is what freed the space
     the price sits in. */
  box-sizing: border-box;
  width: var(--subject-shell-size);
  height: var(--subject-shell-size);
  padding: var(--subject-shell-pad-y) var(--subject-shell-pad-x);
  border-radius: 999px;
  background: var(--surface-card);
  /* The same weight as a socket's border, and the same hue as an active
     one — but held back in saturation. Matching the active blue exactly
     would make every shell look like it was carrying power; this keeps
     the shell and the fittings on it reading as one family without
     saying anything about flow. */
  border: var(--subject-shell-border) solid var(--border-card);
}

/* Pushed away from the hub by its own size so the anchor lands on the
   shell's hub-facing edge, then pulled back towards the hub by
   `--subject-shell-reach` so the shell runs on *under* the sockets. That
   overlap is what groups an icon with its plumbing.
   The extra padding on the same side keeps the icon clear of the sockets
   the shell now reaches beneath. */
/* Each card is pushed away from the hub by its own size, then pulled back
   towards it so its shell runs under its sockets. In History view there is
   no hub to face and every card ends centred on its row, so that
   displacement is scaled out as the view changes. `--pull` is how far the
   card is still offset from simply being centred on its point. */
/* How far a shell sits from the sockets on its hub side, as a share of the
   shell's own size — so the spacing keeps its proportions if the shells are
   ever resized.

   The two views need it on different axes and in opposite directions, so it
   is two terms rather than one.

   In the flow view a card faces the hub along its own side's axis — up,
   down or sideways — and its shell runs under the sockets there. The
   overlap was a touch deep, so the card backs away from the hub by
   `--socket-gap`, which is applied per side below and fades out as the view
   changes.

   In History every card sits in a row to the right of its own sockets,
   whatever side it had in the flow picture, so the axis is always
   horizontal and the card moves left to close the gap. `--socket-close`
   carries that, and fades in over the same transition. */
.subject-card {
  --socket-gap: calc(
    var(--subject-shell-size) * 0.1 * (1 - var(--history-progress, 0))
  );
  --socket-close: calc(
    var(--subject-shell-size) * 0.1 * var(--history-progress, 0)
  );
}

/* The reach extends the shell towards the hub so it passes under the
   sockets, and it must do that without making the shell any bigger — a
   subject whose hub lies sideways has to come out the same size and the
   same shape as one whose hub lies above. So the box keeps its fixed
   square size and the reach is spent inside it: padding on the hub-facing
   side pushes the contents clear of the sockets running beneath, and no
   dimension changes. Adding the reach to the size instead made the
   battery, which reaches sideways, half again as wide as the four that
   reach vertically.

   The contents therefore sit off-centre, away from the hub, which is the
   intent: the hub-side room belongs to the plumbing, not to the icon.

   Only part of the reach is spent that way. The sockets overlap the shell
   by the full reach, but they meet it at the circle's edge, where the
   curve has already carried the contents inward — pushing them clear by
   the whole reach as well would be paying twice for the same clearance,
   and it forced a diameter larger than the icons ever needed. */
.subject-card[data-side='top'] {
  --pull: calc((50% - var(--reach)) * (1 - var(--history-progress, 0)));
  transform: translate(calc(-50% - var(--socket-close)), calc(-50% - var(--pull) - var(--socket-gap)));
  padding-bottom: calc(var(--subject-shell-pad-y) + var(--reach) * 0.5);
}

.subject-card[data-side='bottom'] {
  --pull: calc((50% - var(--reach)) * (1 - var(--history-progress, 0)));
  transform: translate(calc(-50% - var(--socket-close)), calc(-50% + var(--pull) + var(--socket-gap)));
  padding-top: calc(var(--subject-shell-pad-y) + var(--reach) * 0.5);
}

/* The left-hand subject is the one case where the hub-side padding runs
   horizontally, and padding on one side only pushes the contents off the
   shell's centre line — which reads as a mistake next to four subjects
   whose contents are centred. So the padding is balanced: the same amount
   is added on the far side, and the contents sit in the middle of the
   circle like everyone else's.

   That costs nothing. The room the hub-side padding was making is still
   there — it is the shell's own extension towards the hub, not this
   padding, that carries the sockets — and centring leaves the label a few
   pixels clear of the socket edge, which is the only content that comes
   near it. */
.subject-card[data-side='left'] {
  --pull: calc((50% - var(--reach)) * (1 - var(--history-progress, 0)));
  transform: translate(calc(-50% - var(--pull) - var(--socket-gap) - var(--socket-close)), -50%);
  padding-right: calc(var(--subject-shell-pad-x) + var(--reach) * 0.5);
  padding-left: calc(var(--subject-shell-pad-x) + var(--reach) * 0.5);
}

/* The mirror of the left-hand case, for a subject sitting to the *right*
   of the hub.

   It had no rule at all while the layout was fixed, because no subject
   was ever placed there. Once a card could be dragged across the hub one
   could be, and without a rule it fell back to the default transform —
   which pulls a card the wrong way on this side, so its socket ended up
   level with the card's edge instead of its middle. That is the
   "sockets on the right do not connect" fault: the socket was where the
   geometry said, and the card was not. */
.subject-card[data-side='right'] {
  --pull: calc((50% - var(--reach)) * (1 - var(--history-progress, 0)));
  transform: translate(calc(-50% + var(--pull) + var(--socket-gap) + var(--socket-close)), -50%);
  padding-right: calc(var(--subject-shell-pad-x) + var(--reach) * 0.5);
  padding-left: calc(var(--subject-shell-pad-x) + var(--reach) * 0.5);
}

/* As the page moves into History view the shell no longer has sockets to
   reach under on the hub side — its sockets are beside it now — so the
   overlap padding is relaxed away over the same transition. `--history-progress`
   runs 0 to 1 and is set per card by the page, which is also what moves it,
   so the shape and the position change together. */
.subject-card[data-side='top'],
.subject-card[data-side='bottom'],
.subject-card[data-side='left'],
.subject-card[data-side='right'] {
  --reach: calc(var(--subject-shell-reach) * (1 - var(--history-progress, 0)));
}

.subject-card.unknown {
  opacity: 0.35;
}

/* Colour drained and the whole card pushed towards the page's own
   background, so it reads as faded rather than as merely different
   (CF-STALE-003). Which way "towards the background" is depends on the
   theme — darker on a dark page, lighter on a light one — so the
   brightness comes from a variable rather than being fixed at a value
   that would make a stale card the *boldest* thing on a white page. */
.subject-card.stale {
  filter: grayscale(1) brightness(var(--stale-brightness));
}

/* Lifted above the plumbing, which is itself above the shell — so the
   shell backs the sockets while the icon stays readable over everything. */
.subject-icon,
.subject-label,
.subject-net-row,
.subject-soc {
  position: relative;
  z-index: 2;
}

/* Height follows from the aspect ratio each icon sets, so the box is
   never taller than the drawing inside it. */
/* The contents are sized from the shell rather than in their own right,
   so that a shell drawn smaller carries a smaller icon and smaller text
   with it. Stated as fractions of `--subject-shell-size`, which is the one
   figure that changes.

   They were fixed lengths, and the History view's smaller shell therefore
   held an icon and a label sized for the larger one — the contents nearly
   filled the circle and the padding that keeps them off its edge was gone.
   Anything that has to shrink with the shell belongs here. */
.subject-icon {
  width: calc(var(--subject-shell-size) * 0.525);
  height: auto;
  overflow: visible;
}

/* Stroke-drawn, so one rule colours every icon and they all degrade the
   same way. */
.subject-icon-art {
  fill: none;
  stroke: var(--icon-stroke);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The charge bar fills the battery icon's hollow cell, behind the drawing
   so the outline stays crisp over it. */
.subject-soc-bar {
  fill: var(--soc-bar);
}

.subject-label {
  font-weight: 600;
  font-size: calc(var(--subject-shell-size) * 0.128);
  color: var(--text-label);
}

/* The battery's operating mode, above its icon (CF-PWR-072).

   Sized like the state of charge below rather than like the label, because
   it is the same kind of thing: a reading about the subject, not the
   subject's name. Giving it the label's weight would make two headings in
   one circle and leave the actual name looking like a caption.

   Set in the muted colour with a little letter-spacing. Three upper-case
   letters set solid read as a word one has to decode; spaced slightly they
   read as an abbreviation, which is what they are.

   It takes room from the circle rather than being given room: the contents
   are centred as a column, so a line added at the top pushes the rest down
   and the whole stays centred. That is why nothing here sets a height — the
   shell does not grow for it (CF-PWR-057), and the icon and label keep
   their arrangement. */
.subject-mode {
  position: relative;
  z-index: 2;
  font-weight: 600;
  font-size: calc(var(--subject-shell-size) * 0.105);
  letter-spacing: 0.04em;
  color: var(--text-muted);
  /* Empty where no mode was reported (CF-PWR-073). It keeps its line so the
     circle's contents do not jump when a mode arrives or goes away — the
     same reasoning as the self-use figures keeping their space. */
  min-height: 1em;
}

.subject-soc {
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  font-size: calc(var(--subject-shell-size) * 0.118);
}

/* The net's current price, left of its icon (CF-PWR-107).

   The row is a plain wrapper the width of the icon: the figure is taken out
   of flow and hung off its left side, so the icon sits exactly where every
   other subject's does and the shell is not stretched by what is written
   beside it. Laying it out in flow instead made the net's shell wider than
   the others and pushed the icon off centre — which is what happened to the
   two self-use percentages that stood here before, recorded because the same
   mistake is available again. */
.subject-net-row {
  /* The positioning context for the two figures. They are placed against
     the icon rather than against the card because the card's shape differs
     between the two views — the hub-side reach is horizontal in the flow
     view and absent in History — and a figure placed by the card would
     drift down into the shell's curve as that changed. The icon sits in
     the same place relative to its own drawing in both. */
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Set as the battery's state of charge is on its card: the two are read as
   the same kind of figure and should not look like different ones.

   A reading and not a control — there is nothing to press and nothing to
   choose, which is the difference from the percentages that stood here. So no
   cursor, no hover, no tooltip.

   Out of flow, and positioned against the icon row: the figure is placed by
   the shell it has to stay inside, and being absolute is what keeps a figure
   of any width — "5ct" or "-12ct" — from widening the shell or moving the
   icon.

   Tucked in beside the pylon's legs rather than held clear of the whole icon.
   The icon's box is as wide as its widest feature, which for the pylon is the
   top crossarm; below that the legs taper in and leave room inside the box on
   both sides. The band between the crossarm and the label is where that room
   is, and it is where this sits.

   **There are two of them** (CF-PWR-117): what a kWh drawn from the grid costs,
   on the left, and what a kWh fed back earns, on the right. The pylon's legs
   taper in on both sides, so the room the left figure was given was always
   there on the right too — this is the same band, used twice.

   They are set alike, in one rule, because they are two answers of equal
   standing to one question asked in two directions. A second rule differing
   only in size or weight would say one of them was the heading of the other. */
.subject-net-price {
  position: absolute;
  /* Level with the pylon's legs: below the crossarm, which is the icon's
     widest feature, and above the label. Measured from the icon's own
     bottom so it holds wherever the icon sits in the shell. */
  bottom: 0.15rem;
  display: inline-flex;
  align-items: center;
  /* The mark and the figure sit tight together, so each arrow reads as
     belonging to the number beside it rather than to the drawing between
     them. */
  gap: 0.08em;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  /* From the shell, like the label and the state of charge: it sits inside the
     same circle and has to shrink with it.
     Smaller than the state of charge, unlike at first: it shares the circle
     with the drawing rather than sitting under it, so at the label's weight it
     competed with the pylon for the eye. Asked for — "the '25ct' in smaller
     font" — and it is also what lets the figure fit beside the legs without
     reaching the shell's curve. */
  font-size: calc(var(--subject-shell-size) * 0.095);
  white-space: nowrap;
  z-index: 2;
}

/* **Anchored to the icon's own edges, not pushed outside them.**
   `right: 100%` put the figure wholly beyond the icon's box, where the
   shell's own width had to absorb it — so the drawing ended up sitting
   right of the circle's centre and the net read as the one misaligned
   subject. Reported that way: "the pylon is all shifted".
   The pylon is the thing that must stay centred, since five circles in a
   ring are read against each other. So each figure hangs inside the icon's
   box, in the space the legs taper away from, and the icon's own centre is
   untouched — which with a figure on each side is now also true by symmetry
   rather than only by arithmetic.

   The two are mirror images at the same inset, so neither side can quietly
   acquire more room than the other. Each is justified towards the drawing, so
   the pair grows outward from the pylon and the gap between figure and icon
   does not change with the length of the figure. */
.subject-net-price-import {
  right: 78%;
  justify-content: flex-end;
}

.subject-net-price-export {
  left: 78%;
  justify-content: flex-start;
}

/* Nothing to say where the payload published no price for this interval. The
   element keeps its space rather than being removed, so the card does not
   change width as prices come and go — a card that resized would move the
   flows drawn to it.

   Hidden on the whole element rather than on the value alone, so that an empty
   figure takes its direction mark with it. A lone arrow pointing at a blank is
   a claim that something is missing from a place nothing was promised. */
.subject-net-price.empty {
  visibility: hidden;
}

/* The direction mark: down for what the house takes, up for what it sends
   back (CF-PWR-117).

   **Shown only when both figures are** — `marked` is set on both or on
   neither. With one price on the card there is nothing to tell it apart from,
   so an arrow beside it would claim a distinction the backend has said it did
   not draw. That is the same reasoning that keeps `exportPrices` off the wire
   on market prices.

   It is hidden rather than removed so the figure does not shift sideways as
   the basis changes. Set a little smaller and lighter than the figure: it
   qualifies the number, and at the same weight two glyphs would read as part
   of it — "↓28ct" as a quantity rather than a marked one. */
.subject-net-price-mark {
  visibility: hidden;
  font-size: 0.85em;
  opacity: 0.75;
}

.subject-net-price.marked .subject-net-price-mark {
  visibility: visible;
}

/* The solar card's capacity figure (CF-PWR-132).

   Declared apart from `.subject-net-price` above rather than sharing its
   selector, because the two are different facts on different cards and a
   shared rule is a place one of them is changed for the other's sake. What
   they do share is deliberate and is the point of the arrangement: the same
   size, the same colour, and the same inset from the icon, so that a glance
   across the ring finds both small figures in the same place.

   **The size and the inset are read from the price's own declarations** —
   `font-size` and `right` repeat the figures on `.subject-net-price` and
   `.subject-net-price-import`. They are written out rather than shared
   through a variable because there are only two of them and a variable named
   for neither card would be the harder thing to read; but they are a
   duplicated figure, so a change to the price's size or inset belongs here
   in the same pass.

   Out of flow and hung inside the icon's own box, for the reason recorded on
   the prices: an in-flow figure is absorbed by the shell's width and pushes
   the drawing off the circle's centre — reported as "the pylon is all
   shifted". That is what honours the promise this figure was asked with,
   that the elements already in the circle stay exactly where they are. */
.subject-capacity {
  position: absolute;
  /* Inside the icon's box and raised, in the room the drawing leaves
     (CF-PWR-132).

     Placing it is a question about *this* icon's shape rather than about
     where the net's price sits, and it took two tries to get right. The net
     insets its figures to 78% because the pylon's legs taper and leave the
     corners of its box empty. Copied literally, that put "59%" over the
     solar panel's corner. Moved wholly outside the box instead, it cleared
     the drawing and landed on the circle's own border, which is what was
     reported next.

     The room is there; it is just not at the bottom. Read off the icon's
     own path: the panel is the trapezoid `M4 22 L7 14.5 L20 14.5 L17 22 Z`,
     so its left edge runs from x=4 at the foot to x=7 at the top, and above
     that the sun's leftmost ray reaches only x=6.7 in a 24-wide box. So
     everything left of x≈6.7 is empty for the whole upper two thirds of the
     icon and only the bottom corner jutts out — about 11px of clear space
     at the sizes this is drawn at, against 6px at the foot.

     Hence both moves the reader asked for, and they are one move: raising
     the figure out of the bottom corner is what lets it sit back inside the
     box without touching the drawing. `bottom: 38%` puts it in the panel's
     upper half, beside the slant rather than below it. */
  bottom: 38%;
  right: 80%;
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  font-size: calc(var(--subject-shell-size) * 0.095);
  white-space: nowrap;
  z-index: 2;
}

/* Nothing to say where the backend describes no panels, or gives them no
   rating (CF-PWR-079). The element keeps its space rather than being
   removed, so the card does not change width as the figure comes and goes —
   a card that resized would move the flows drawn to it. */
.subject-capacity.empty {
  visibility: hidden;
}

/* The two self-use figures, in the net's own panel (CF-PWR-108).

   **Named, which is the whole point of moving them off the card.** On the
   card they were two bare percentages flanking the icon, and which was which
   could only be learned from hover text — unavailable on the device this app
   is most used on. A panel has room for the words, so the figures are rows of
   name-and-value rather than numbers needing a legend.

   Side by side rather than stacked, because they are a pair to be compared: a
   house reads high on one and low on the other and that gap is the thing
   worth seeing. Equal columns say the two are answers of equal standing. */
.net-self-use {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  /* Carries the spacing the button used to, now that the button is inside it. */
  margin-bottom: 0.9rem;
}

/* `flex: 0 1 auto`, not `1 1 0`: the two figures share the row with the mode
   button now, and growing to fill would push it off the right edge. Each takes
   the width its name needs and no more. */
.net-self-use-figure {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  flex: 0 1 auto;
  min-width: 0;
}

.net-self-use-name {
  font-size: 0.72rem;
  color: var(--text-muted);
  /* The names are long — "self-consumption", "zelfvoorziening" — and two of
     them share a panel's width, so they wrap rather than widening the panel
     (CF-PWR-063 sizes it from its contents). */
  line-height: 1.25;
  /* Wrapping needs somewhere to wrap. The Dutch names are closed compounds
     with no dash, so a browser given "Zelfvoorziening" and too little room
     left it whole and let it spill over its neighbour — reported as the two
     names overlapping. The catalogue supplies a soft hyphen at the compound's
     seam, and `hyphens: manual` is what makes a browser honour it.
     Deliberately **not** `overflow-wrap: break-word`, which was tried here and
     read worse than the bug it fixed: given leave to break anywhere, the
     browser split "Zelfconsumptie" after "Zelfconsump" even with the good
     break available, because it takes the last character that fits rather
     than the best point. */
  hyphens: manual;
}

.net-self-use-value {
  font-size: 1.15rem;
  font-weight: 600;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}

/* "Not reported" is a sentence, not a value, so it is not set like one — the
   same rule the setting summary follows (CF-PWR-073). Where the totals a
   figure needs are absent it must not be shown as a percentage at all
   (CF-PWR-049), and this is what that looks like. */
.net-self-use-value.unknown {
  font-size: 0.85rem;
  font-weight: 400;
  color: var(--text-muted);
}

/* One control for the pair, because one EV setting governs both
   (CF-PWR-055). On the card this had to be a struck-through badge; here it
   says what it does in words. */
/* Beside the two figures rather than under them, and sized to its own words.
   It was a child of the panel's column, where `align-self: flex-start` was
   meant to keep it narrow and did not — a column flex stretches its children
   across, so the button ran the full width of the panel and read as a banner
   rather than a control. Reported as exactly that. In the figures' row it is a
   third flex item, pushed to the right end and shrink-wrapped by
   `flex: 0 0 auto`, which is what makes it small without depending on
   `align-self` at all. */
.net-self-use-mode {
  align-self: center;
  flex: 0 0 auto;
  margin-left: auto;
  padding: 0.2rem 0.55rem;
  border: 1px solid var(--border-control);
  border-radius: 999px;
  background: var(--surface-card);
  color: var(--text-muted);
  font: inherit;
  font-size: 0.75rem;
  cursor: pointer;
}

.net-self-use-mode:hover:not(:disabled),
.net-self-use-mode[aria-expanded='true'] {
  border-color: var(--flow-casing);
  color: var(--text-primary);
}

.net-self-use-mode:focus-visible {
  outline: 2px solid var(--flow-casing);
  outline-offset: 1px;
}

/* Nothing for a mode to apply to where neither percentage is on screen. */
.net-self-use-mode:disabled {
  opacity: 0.45;
  cursor: default;
}

/* The two figures above the prices graph: what today drew from the grid and
   what it fed back, each with what it came to (CF-PWR-144).

   Side by side and equal, because the reader compares them — that is the
   whole reason both are shown rather than the one net figure. `1fr 1fr`
   rather than a flex row so the pair does not shift as one figure grows a
   digit: two columns of fixed share put each label in the same place
   whatever the numbers are, which is what `.ev-charge-table` records as the
   reason for being a table. */
.net-money {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.9rem;
  margin: 0 0 1rem;
}

.net-money-figure {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.net-money-name {
  font-size: 0.62rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
}

/* The energy leads, the money follows it smaller: the question is "what did
   I draw", and what it cost is the qualifier. Both `dd`s, so the margin a
   browser puts on those has to be cleared here. */
.net-money-energy {
  margin: 0;
  font-size: 1.05rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-panel);
}

.net-money-cost {
  display: flex;
  gap: 0.3rem;
  align-items: baseline;
  margin: 0;
  font-size: 0.82rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}

/* A figure the backend has not answered for keeps its room rather than
   collapsing the row, so the tab does not change height while it loads. */
.net-money-energy.unknown,
.net-money-cost-value.unknown {
  color: var(--text-muted);
}

/* The month's total, above its bars. One figure rather than two: what the
   month comes to is a single net number (CF-PWR-146). */
.net-month-total {
  margin: 0 0 1rem;
}

.net-month-figure {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.net-month-total-label {
  font-size: 0.62rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
}

.net-month-total-value {
  margin: 0;
  font-size: 1.25rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-panel);
}

.net-month-total-value.unknown {
  font-size: 1.05rem;
  color: var(--text-muted);
}

/* One bar per day of the calendar month (CF-PWR-146).

   A fixed height, like the panel's other charts: the SVG scales its own
   coordinate space to whatever width the panel has, so only the height has
   to be stated here. */
.month-chart {
  width: 100%;
}

.month-chart-svg {
  display: block;
  width: 100%;
  height: 120px;
}

/* Import up, export down, in the same two hues the Timelines histogram uses
   — told apart by hue rather than by shade (CF-HIST-008), so the two halves
   of a day cannot be confused for one another. */
.month-chart-bar-import {
  fill: var(--histogram-import);
}

.month-chart-bar-export {
  fill: var(--histogram-export);
}

.month-chart-axis {
  stroke: var(--histogram-border);
  stroke-width: 1;
}

/* The day numbers along the foot. In `rem` like every other label in an SVG
   here, which works because the chart measures itself and sets a 1:1 view box
   — see `MonthChart._box`. Against a fixed view box scaled to fit, these
   would be stretched by whatever ratio the two differed by. */
.month-chart-day {
  fill: var(--text-muted);
  font-size: 0.58rem;
  font-variant-numeric: tabular-nums;
  opacity: 0.8;
}

/* What each half of the month chart reaches (CF-PWR-152). Set like the day
   numbers, since both are the chart's furniture rather than its data — and
   each takes the colour of the direction it labels, so a reader does not have
   to work out which figure belongs to which half. */
.month-chart-scale {
  font-size: 0.58rem;
  font-variant-numeric: tabular-nums;
  opacity: 0.8;
}

.month-chart-scale-import {
  fill: var(--histogram-import);
}

.month-chart-scale-export {
  fill: var(--histogram-export);
}

/* The subject panel: what opens when a subject's circle is pressed
   (CF-PWR-062). A modal, unlike the self-use chooser below: it is not one
   small choice made against something visible behind it but a place a
   subject says everything it could not fit in a circle, so what is behind
   it is not what is being read.

   A card is a `div` with a handler rather than a button, so it says so
   here. The pointer is what tells a reader the circle is pressable at all,
   since nothing about it looks like a control. */
.subject-card {
  cursor: pointer;
}

/* Except while the debug layout tool is on, where the same press drags the
   card (CF-DEBUG-014) — `layout-dragging` already sets `grab` cursors
   further up, and this only keeps this rule from fighting them. */
.layout-dragging .subject-card {
  cursor: inherit;
}

/* The card is reachable by keyboard, so it has to show when it is reached.
   The ring follows the shell's own shape rather than its box: an outline on
   a circle drawn as a square reads as a box round a circle, which is a
   second shape the picture does not have. */
.subject-card:focus-visible {
  outline: 2px solid var(--flow-casing);
  outline-offset: 2px;
  border-radius: 999px;
}

/* `--modal-shell-size` is the diameter of the circle that was pressed, set
   on this element by subjectModal.js from that card's own box. It is not
   read from `--subject-shell-size`: that property is declared three times
   over — once for the stage, once for the history pane, once again below
   the small-screen threshold (CF-PWR-058) — so which value applies depends
   on which card was pressed, and this element is outside the stage where
   none of them reaches it. The card knows; it is asked.

   The fallback is the base diameter, read from `:root` — which is where it
   is declared, and why. It applies only before a card has ever been laid
   out, which cannot happen through the interface: a card has to be on
   screen to be pressed. */
.subject-modal-backdrop {
  --shell: var(--modal-shell-size, var(--subject-shell-base-size));
  /* The widest a panel's contents may be drawn (CF-PWR-130).

     A chart inside the panel needs a *definite* width — the reason is on
     `.today-chart`: the panel is sized by its contents (CF-PWR-063), so a
     percentage there resolves against a container the chart is itself
     sizing, and comes out zero. But a figure in shells alone ignores the
     window, and on a phone it simply overflowed: the net's price graph lost
     most of `15 Sept` off the right, and the house's 24 hours showed six of
     them — each recoverable only by scrolling the panel body sideways. A
     graph read at a glance is not a graph anybody scrolls.

     So the cap is taken from the **viewport** rather than from the
     container. `100vw` is a definite length that no ancestor's width
     depends on, which is what breaks the circularity while still answering
     the question the percentage was trying to ask.

     Subtracted from it is everything between the window's edge and this
     content box, all of it fixed and all of it declared elsewhere in this
     file: the backdrop's own `1rem` either side (the `padding` below), the
     panel's `1px` border either side, and the panel's `1.2rem` padding
     either side. Those are declared on the child, `.subject-modal`, and
     cannot be read from here. Kept in step by hand is the honest
     description, so a change to the panel's padding must come here too —
     which is why the arithmetic is written out term by term rather than
     folded into one number.

     Stated once here rather than on each chart because three charts and the
     EV table share it, and a figure held in several places disagrees the
     first time one of them is touched. */
  --panel-content-width: calc(100vw - 2rem - 2px - 1.2rem - 1.2rem);
  position: fixed;
  inset: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: center;
  /* The circle straddles the panel's top edge, so the room it needs above
     that edge is part of what has to fit on screen. Half a shell plus the
     ordinary margin. */
  padding: calc(1rem + var(--shell) / 2) 1rem 1rem;
  background: rgb(0 0 0 / 0.45);
}

.subject-modal-backdrop[hidden] {
  display: none;
}

/* The panel (CF-PWR-063). Every figure bounding it is a multiple of the
   shell, so all of them keep their proportion to the circle on its rim at
   every screen width, and the multiples are the only figures stated here.

   `min(…, 100%)` on each bound keeps it on a phone: the panel takes what the
   window has when the window has less, and its contents scroll. The
   backdrop's padding is what keeps the circle on the rim from going off the
   top when that happens. */
.subject-modal {
  position: relative;
  box-sizing: border-box;
  /* Sized by what is in it (CF-PWR-063). The panel is a container for one
     subject's contents and those differ per subject — the battery's are a
     line, a switch and a row of six; another's will be a graph. A fixed size
     is a guess that is wrong for every subject but the one it was measured
     against, and it was wrong for the battery: five shells tall left two
     thirds of the panel empty.

     A floor keeps it from looking cramped where the contents are slight, and
     it is stated in shells so it stays in proportion to the circle on the rim
     at any interface scale. The ceiling is the window.

     `width: max-content` rather than `fit-content`: the contents' natural
     width is what should decide, and `fit-content` resolves against the
     available space, which here is the whole backdrop. Bounded by
     `max-width`, so a wide content still stops at the window. */
  width: max-content;
  min-width: min(calc(var(--shell) * 3), 100%);
  max-width: min(calc(var(--shell) * 6), 100%);
  min-height: calc(var(--shell) * 1.6);
  max-height: 100%;
  /* A column, so the body below can be given the height that is left over and
     scroll within it. As a plain block the body had no bounded box to scroll
     inside: `overflow-y: auto` on a box that grows with its contents never
     overflows, so a tall panel — the solar array is the first — simply ran
     past the bottom of the window with its last group cut off. */
  display: flex;
  flex-direction: column;
  /* The top padding clears the half of the circle that hangs inside, and
     with it the close button: the button sits in that corner, beside the
     circle, and its inset plus its size is less than half a shell plus the
     padding's own margin at every shell size. Nothing of a subject's
     contents is ever beside it, so the right padding equals the left.

     It used to be wider by the button's size plus its inset either side,
     on the reasoning that the first line of the contents ran underneath
     the button once panels were sized by their contents. That line starts
     below the top padding, so it does not; what the wider margin did was
     take width from every panel's contents. The net's panel was given equal
     margins first (CF-PWR-187), and every panel followed on 2026-10-05
     (CF-PWR-063) when the reader saw the rest had the same empty strip.

     Both of the button's figures are named here because the button's own
     rule below reads them. */
  --close-size: 1.7rem;
  --close-inset: 0.45rem;
  padding: calc(var(--shell) / 2 + 0.6rem) 1.2rem 1.2rem;
  /* What this panel's own chrome takes horizontally: the padding either
     side, and the border either side (`box-sizing: border-box` puts the
     border inside the `max-width` above, so it comes out of the same
     budget).

     Named here, once, because a graph inside the panel has to subtract it
     from the ceiling to know what width is actually left (CF-PWR-130). It
     was not subtracted, and that is precisely the defect: the house chart
     asked for 5.5 shells while the ceiling of 6 shells less this chrome
     left only 5.38, so the panel overflowed by 12px and scrolled sideways —
     which CF-PWR-063 forbids for a graph outright. Two places holding one
     figure disagree the first time either is touched (CLAUDE.md §Before you
     say you are done, item 5), so the graph rules below read this rather
     than restating the terms. */
  --panel-chrome-width: calc(1.2rem * 2 + 2px);
  border: 1px solid var(--border-control);
  border-radius: 14px;
  background: var(--surface-panel);
  color: var(--text-panel);
  box-shadow: 0 10px 40px rgb(0 0 0 / 0.4);
}

/* A subject whose contents have not been built yet (CF-PWR-062 requires it
   to open a panel regardless). There is nothing to size to, so it keeps the
   preset — the five-by-five that every panel used before any subject had
   contents. Without this an empty panel collapses to its minimum and reads
   as broken rather than as empty. */
.subject-modal.empty {
  width: min(calc(var(--shell) * 5), 100%);
  height: min(calc(var(--shell) * 5), 100%);
}

/* And it says so, centred in the room the preset gives it (CF-PWR-062). A
   preset-size box with nothing in it reads as content that failed to load,
   which is exactly the distinction opening a panel for every subject exists
   to make: "nothing here" is not "nothing happened". */
.subject-modal.empty .subject-modal-body {
  display: grid;
  place-items: center;
  /* The body is `flex: 0 1 auto` in the column above, so it is only as tall as
     its contents — one line of text — and centring within it put the words at
     the top of a mostly empty panel. Growing it to the room the preset gives
     is what makes "centred" mean centred in the panel. */
  flex: 1;
}

.subject-modal-placeholder {
  margin: 0;
  color: var(--text-muted);
  font-size: 0.9rem;
  text-align: center;
}

.subject-modal-placeholder[hidden] {
  display: none;
}

/* The subject's own circle, straddling the top edge: half above it, half
   inside. Centred horizontally, and pulled up by exactly half its own
   height so the edge runs through its middle whatever the shell's size.

   Its diameter, padding, radius, background, border and the column of icon
   over name are `.subject-card`'s own, so it is the circle that was pressed
   rather than something resembling it. What it is *not* is the same element:
   see subjectModal.js.

   `--subject-shell-size` is set here as well as `--shell` being used: the
   icon and the label inside are sized as fractions of that property (see
   `.subject-icon` and `.subject-label`), and this element is outside the
   stage that declares it. Setting it here is what makes them come out the
   size they do in a card, rather than collapsing to nothing for want of the
   property.

   It carries no reading — no state of charge, no percentages, no EV badge.
   Those are live figures and are on the card behind this panel; repeated
   here they would be the same figure displayed twice. This circle names a
   subject, it does not report on one. */
.subject-modal-badge {
  --subject-shell-size: var(--shell);
  position: absolute;
  top: 0;
  left: 50%;
  transform: translate(-50%, -50%);
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--subject-shell-gap);
  white-space: nowrap;
  width: var(--shell);
  height: var(--shell);
  padding: var(--subject-shell-pad-y) var(--subject-shell-pad-x);
  border-radius: 999px;
  background: var(--surface-card);
  border: var(--subject-shell-border) solid var(--border-card);
}

/* In the corner the reader's eye leaves a panel at, and small: it is the
   way out, not one of the things to do here. */
.subject-modal-close {
  position: absolute;
  /* Both from the panel, which reserves padding for exactly this box — see
     there. Two rules holding one size is how the contents end up running
     under it again. */
  top: var(--close-inset);
  right: var(--close-inset);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--close-size);
  height: var(--close-size);
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
}

.subject-modal-close svg {
  width: 1rem;
  height: 1rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
}

.subject-modal-close:hover {
  color: var(--text-primary);
  background: var(--surface-card);
}

.subject-modal-close:focus-visible {
  outline: 2px solid var(--flow-casing);
  outline-offset: 1px;
}

/* Empty for now: what each subject shows here is decided subject by
   subject. It scrolls rather than growing, so a panel filled with more
   than fits stays the size the layout gives it. */
/* Takes its height from what is in it, so the panel can do the same. It used
   to be `height: 100%`, which was right while the panel was a fixed box and
   is wrong now: a body stretched to a full height gives the panel no shorter
   height to shrink to.

   It still scrolls, for the case the panel has reached the window's height
   and the contents have not finished. */
/* Deliberately not a query container. It was made one so the mode row could
   ask how much room it had, and that is circular here: the panel is sized by
   its contents (CF-PWR-063), so a container query on this box asks the
   contents about a width the contents decide. See `.battery-modes`. */
.subject-modal-body {
  overflow-y: auto;
  /* Takes what is left of the panel and no more. `min-height: 0` is the part
     that is easy to miss: a flex item's floor is its content size, so without
     it the body refuses to shrink below its contents and pushes the panel past
     the window instead of scrolling — which is exactly what a tall panel did.

     `flex: 0 1 auto` rather than `1`: the body is sized by its contents
     (CF-PWR-063) and only gives way when there is not enough room. Growing it
     would stretch a short panel's contents down an empty box. */
  flex: 0 1 auto;
  min-height: 0;
}

/* One subject's contents. All of them are built and all but one hidden, so
   that contents holding state — the battery's override switch — survive the
   panel being closed and reopened. */
.subject-modal-section[hidden] {
  display: none;
}

/* The battery's mode used to have its own headline and label here. Both are
   the summary row's now (`.setting-summary`), shared with every other
   overridable setting — the battery was building its own arrangement of the
   same parts the two sliders were building separately. */

/* The override block: what the backend wants, what the override asks for,
   and the switch between them (CF-PWR-074 … CF-PWR-076). Shared by every
   panel with a server setting — the battery's modes, the solar cap, the
   charger's cap — so nothing here names any of them. */
.override-block {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

/* The pair of values, side by side so the difference between them is the
   thing seen rather than something to be worked out (CF-PWR-076). */
.override-values {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 1.1rem;
}

.override-value {
  display: flex;
  align-items: baseline;
  gap: 0.35rem;
}

/* `[hidden]` sets `display: none`, which a bare class rule beats on source
   order — so the override's label went on showing with no figure beside it
   while nothing was overridden. The same trap the mode row hit; stated here
   because `hidden` looks like it should be enough and is not. */
.override-value[hidden] {
  display: none;
}

.override-value-label {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}

.override-value-figure {
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}

/* Where the override departs from what the backend chose, the override is
   the one that is in force and reads as the live figure; the server's is
   held back, since it is now the counterfactual. Not marked at all where the
   two agree — an override set to the same value is running but is not a
   disagreement, and flagging it would cry wolf. */
.override-block.differs .override-server .override-value-figure {
  font-weight: 400;
  color: var(--text-muted);
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}

.override-block.differs .override-active .override-value-figure {
  color: var(--flow-casing);
}

/* The duration chooser (CF-PWR-082). This was a checkbox; the whole row was
   its label, so the text was a target as well as the box. A select carries its
   own hit area and its own label, so the row is now a label beside a control
   rather than a label wrapping one.

   Laid out in a row that wraps: the label and the chooser sit side by side
   where there is room, and the chooser drops beneath it where there is not,
   which is what a narrow panel gets without a media query. */
.override-choice {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem 0.55rem;
  cursor: pointer;
}

.override-choice-label {
  font-size: 0.9rem;
  color: var(--text-primary);
}

/* The chooser itself. `min-width: 0` so a long entry — "Until I turn it off"
   is the longest, and longer in Dutch — shrinks rather than pushing the panel
   wider: the panel is sized by its contents (CF-PWR-063), so a control that
   refused to shrink would decide the panel's width. */
.override-select {
  min-width: 0;
  padding: 0.2rem 0.35rem;
  border: 1px solid var(--border-control);
  border-radius: 6px;
  /* The card surface. In the dark theme this lifts the control off the panel
     behind it; in the light theme the two are both white, and it is the border
     and the dropdown arrow that delineate it. Checked in both. */
  background: var(--surface-card);
  color: var(--text-primary);
  font: inherit;
  font-size: 0.85rem;
  cursor: pointer;
}

.override-select:disabled {
  opacity: 0.5;
  cursor: default;
}

.override-select:focus-visible {
  outline: 2px solid var(--flow-casing);
  outline-offset: 1px;
}

/* Says what happens when the override ends, which the chooser alone does not:
   a list of durations says how long, and nothing about what follows. */
.override-hint {
  font-size: 0.78rem;
  line-height: 1.35;
  color: var(--text-muted);
}

/* The modes. The element carries `settings-choices` as well as this class —
   it is the segmented row the Settings page already uses, one border around
   the set, dividers between, rounded ends — and reuses it rather than
   describing a second one. What is added here is only what differs.

   **Both classes are load-bearing and the markup must carry both.** The
   buttons alone had `settings-choice` for a while and the container did not
   have `settings-choices`, which does not fail visibly enough to notice: the
   buttons keep their padding and colour, and the `+` divider rule matches
   whatever the container is, so the row still drew *lines between the
   options* — with no border around them, no rounded ends and no background.
   It read as a control missing its outline, which is exactly what it was.

   What differs is wrapping. The settings rows have a page's width; six modes
   do not fit across a panel on a phone, where the window caps how wide the
   panel may grow, and the shared rule wraps them. Wrapped, a segmented row
   comes apart: the divider before the
   first item of the second line has nothing to its left, so it hangs in the
   margin, and the single rounded border no longer encloses what is now two
   rows.

   So on wrapping it stops pretending to be one row. The outer border and the
   dividers are dropped and each option is given its own outline and a gap —
   which is a segmented control's honest form at two lines, and reads as one
   set of six rather than as two rows of a broken one.

   **Which form applies is decided by the viewport, not by measuring the row
   and not by a container query.** Two earlier attempts are worth recording,
   because each fails in a way that looks like it should work.

   Measuring came first: the panel compared the options' tops and set a class.
   This row is one of the contents the panel sizes itself to (CF-PWR-063), so
   the two settle against each other, and anything measured mid-settle is a
   layout on its way somewhere else — the row called itself wrapped on every
   opening. The correction then depended on a *second* measurement, from an
   observer that does not fire at all where no frames are produced (a hidden
   pane, a background tab — CLAUDE.md §Running the app), so the wrong first
   answer stuck exactly where it could not be seen.

   A container query on the panel body fails for the same reason in a subtler
   form: the panel's width comes from its contents, this row is the widest of
   them, so querying that width asks the row about a box the row itself
   decides. Narrow form is narrower, which lets the panel shrink, which keeps
   the query true — it settled at three lines in a panel that had room for
   one.

   The viewport is the one measure in this chain that nothing here influences.
   The row only ever wraps because the panel has hit its ceiling against a
   narrow window, so the window is what the question is really about, and a
   media query answers it during layout with no feedback and nothing to keep
   in step. The threshold is the width below which the panel can no longer
   give six options a line. */
.battery-modes {
  align-self: flex-start;
}

/* Both classes in the selector, not because the element needs identifying —
   `.battery-modes` alone reaches it — but for specificity. What is being
   undone here is `.settings-choices`, a single class like this one, and that
   rule is declared further down the file: on a tie, source order wins and the
   border came back. Naming both classes puts this above it wherever either
   moves. */
@media (max-width: 30rem) {
  .settings-choices.battery-modes {
    border: none;
    border-radius: 0;
    overflow: visible;
    background: none;
    gap: 0.3rem;
  }

  .settings-choices.battery-modes .battery-mode-choice {
    border: 1px solid var(--border-control);
    border-radius: 7px;
    background: var(--surface-page);
  }

  /* The divider between adjacent options, which only makes sense while they
     share one outline. */
  .settings-choices.battery-modes .settings-choice + .settings-choice {
    border-left: none;
  }
}

.battery-modes[hidden] {
  display: none;
}

/* Tighter than a settings row's, because six options have to fit across a
   panel rather than across a page — and because the panel is sized by its
   contents (CF-PWR-063), what this row asks for is what the panel becomes. */
.battery-mode-choice {
  position: relative;
  padding: 0.3rem 0.5rem;
  font-size: 0.8rem;
  letter-spacing: 0.02em;
}

/* The mode asked for and not yet reached (CF-PWR-182).

   A ring, in the override's own colour, drawn inside the button — **not** the
   selected mark, which stays on the mode the battery is actually in
   (CF-PWR-071). The two can be on screen at once and must read as two
   different statements: filled is "here", ringed is "on its way here".
   Dashed so that it is not mistaken for the keyboard focus outline, which is a
   solid line in the same colour.

   On a pseudo-element so the pulse fades the ring and not the mode's name. */
.battery-mode-choice.awaiting::after {
  content: '';
  position: absolute;
  inset: 2px;
  border: 2px dashed var(--flow-casing);
  border-radius: 5px;
  pointer-events: none;
  animation: mode-awaiting 1.1s ease-in-out infinite;
}

@keyframes mode-awaiting {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.3; }
}

/* A command still outstanding is still worth saying to someone who has asked
   not to see motion, so the ring is held rather than removed — the same
   treatment the history's pending mode gets. */
@media (prefers-reduced-motion: reduce) {
  .battery-mode-choice.awaiting::after {
    animation: none;
  }
}

/* Inert only while the setting is unreported, now that the modes are live
   whenever shown (CF-PWR-183). Held back rather than hidden: the row still
   says what the modes are, which is what makes the override's purpose
   legible without using it.

   The mode in force keeps its mark even here — dimmed with everything else,
   but still the one picked out — since "which mode" is exactly what a reader
   opens this panel to see. */
.settings-choices.disabled {
  opacity: 0.5;
}

.settings-choices.disabled .settings-choice {
  cursor: default;
}

/* The tabs a panel divides into (CF-PWR-077).

   Hidden by default, which is the wide arrangement for a panel whose sections
   are *parts* of one thing: nothing is put behind a press that could have been
   on screen. The media query near `.setting-slider-input:disabled` brings them
   back below the threshold.

   A panel whose sections are *alternatives* asks for `.always-tabbed` instead
   and keeps its tabs at every width — see the rules below and CF-PWR-077 as
   amended in revision 158. The distinction is what the sections are, not how
   wide the viewport is. */
.panel-tabs {
  display: none;
}

.panel-tab-panes {
  display: flex;
  flex-direction: column;
  gap: 1rem;

  /* How far a pane clears the rule the tab bar draws under itself
     (CF-PWR-153). Declared here, once, because *both* tabbed arrangements
     need it — the always-tabbed one and the narrow-screen media query — and
     they are deliberately not shared otherwise (see the note below on why
     those three declarations are repeated). A length is a different matter
     from a declaration: written out twice it is a figure held in two places,
     which disagree the first time either is touched. */
  --panel-tab-gap: 0.85rem;
}

/* A panel tabbed at every width (CF-PWR-077, CF-PWR-089).

   The same three declarations the media query applies, unconditionally. They
   are repeated rather than shared with it because the two cannot be combined
   without either making every panel always-tabbed or making the media query
   name this class — and a selector list spanning both would then have to be
   kept in step by hand in two places, which is the duplicated-figure trap in
   another form. Repeating three short declarations is the cheaper of the two.

   Stacking the panes in one grid cell is what CF-PWR-081 needs: the set
   occupies the largest of them in both directions, so the panel keeps one size
   as the tabs are pressed. `visibility` rather than `display` hides the
   unselected ones, which reserves the room without drawing anything and takes
   them out of the tab order — `display: none` is what made the panel jump. */
.panel-tabs.always-tabbed {
  display: flex;
  border-bottom: 1px solid var(--border-control);
}

.panel-tab-panes.always-tabbed {
  display: grid;
  /* Clear of the rule the tab bar draws under itself (CF-PWR-153).
     Without it the first thing in a pane — a figure's label on the net's
     tabs, a table heading on solar's — sits directly against that line and
     the two read as one block, which was reported as looking squashed.

     On the panes rather than on the tab bar's own `margin-bottom`: the bar
     is hidden at narrow widths on a panel that is not `always-tabbed`, and a
     margin under a hidden element is a gap under nothing. */
  padding-top: var(--panel-tab-gap);
}

.panel-tab-panes.always-tabbed > .panel-tab-pane {
  grid-area: 1 / 1;
}

.panel-tab-panes.always-tabbed > .panel-tab-pane:not(.selected) {
  visibility: hidden;
}

/* The group's own heading, which the tabs already name. Hidden for the same
   reason the media query hides it: with a tab bar above, the name appears
   twice. */
.panel-tab-panes.always-tabbed > .panel-tab-pane .solar-group-name {
  display: none;
}

.solar-panel {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.solar-tabs {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

/* The overview tab (CF-PWR-093): what the array is making now, then what each
   roof is contributing, then the day so far, then the cap. */
.solar-overview-totals {
  /* Held to the width of the chart below it (CF-PWR-130).

     The panes are stacked in one grid cell so the panel keeps one size as
     the tabs are pressed (CF-PWR-081, on `.panel-tab-panes.always-tabbed`).
     That is right, and it has a consequence here: the overview's cell is as
     wide as the *widest* pane, which is a roof's grid of panels — wider than
     the window once there are enough columns. The table inside this box is
     `width: 100%`, so without a cap it would spread to that off-screen
     width and carry its right-hand column out of view with it. That is the
     same fault the flex rows here had before the table replaced them, for
     the same reason, which is why the cap outlived them.

     Capping at the figure the chart uses keeps the columns under the
     reader's eye and leaves CF-PWR-081 alone: the cell is still sized by the
     roof grids, so nothing moves as the tabs are pressed. A grid of thirteen
     panels keeps its own width and scrolls on its own tab, which is where
     scrolling such a grid is reasonable. */
  max-width: min(calc(var(--shell, 5.5rem) * 4), var(--panel-content-width, 100vw));
}

/* The overview's figures, as a table (CF-PWR-135).

   `width: 100%` is the trap `.today-chart` records — a percentage inside a
   panel sized by its contents (CF-PWR-063) resolves against a container this
   element is helping to size. It is safe here for the reason it is safe on
   the EV charger's table: the pane has other contents with definite widths,
   the chart below being the plainest, so the container's width is decided
   without reference to this table. */
.solar-totals-table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 0.8rem;
}

/* The column headings are labels, not data: small and muted, in the same
   voice as the panel headings elsewhere, so the eye goes to the figures
   rather than to the words naming them. */
.solar-totals-table th[scope='col'] {
  padding: 0 0 0.3rem;
  /* Sized to fit the narrowest modal body rather than the desk — see the note
     on the table's width floor below. */
  font-size: 0.62rem;
  font-weight: 600;
  text-align: right;
  text-transform: uppercase;
  letter-spacing: 0;
  color: var(--text-muted);
  border-bottom: 1px solid var(--border-control);
  white-space: nowrap;
}

/* The roof names head their rows, so they read left. */
.solar-totals-table th[scope='row'] {
  padding: 0.3rem 0.35rem 0.3rem 0;
  font-weight: 400;
  text-align: left;
  white-space: nowrap;
}

.solar-totals-table td {
  padding: 0.3rem 0 0.3rem 0.35rem;
  text-align: right;
  /* Tabular figures: proportional digits set the same number of characters
     at different widths, so a column of them is ragged even right-aligned —
     and comparing down the column is what this table is for. */
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The array's own row, above the roofs that make it up. Set larger and in
   the primary colour because it is the answer to "how is solar doing"; the
   roofs are its parts, and reading them as equals would make the array's own
   figure hard to find among them. */
.solar-total-row th[scope='row'],
.solar-total-row td {
  /* Its weight and colour do the distinguishing; the extra size was what set
     the table's widest column — see the note below. */
  font-size: 0.92rem;
  font-weight: 600;
  color: var(--text-primary);
}

/* Separated from its parts by a rule rather than by a gap, so the table
   stays one block and the eye still runs down each column through it. */
.solar-total-row th,
.solar-total-row td {
  border-bottom: 1px solid var(--border-control);
  padding-bottom: 0.4rem;
}

.solar-group-total th[scope='row'],
.solar-group-total td {
  font-size: 0.78rem;
  color: var(--text-muted);
}

/* The first roof clears the rule above it. */
.solar-group-total:first-of-type th,
.solar-group-total:first-of-type td {
  padding-top: 0.45rem;
}

/* The totals table on a phone (CF-PWR-150).

   **Every cell in it is `nowrap`**, deliberately — a figure broken across two
   lines is unreadable and a wrapped column heading makes the row heights
   ragged. The consequence is that the four columns add up to a floor the
   table cannot go below: measured at 406px (79 + 92 + 120 + 115), against a
   panel 390px wide on the reader's iPhone 13 Pro. So the pane scrolled
   sideways by about fifteen pixels, which is exactly how it was reported:
   *"the totals panel in the Solar subject panel scrolls a few pixels (on
   mobile, anyway)"*.

   It is not a shortage of screen. Those pixels are the gutters and the
   figures being sized for a desk, so those are what give: the column gaps
   narrow, the headings tighten and the array's own row drops a little.
   Measured after: the table's floor falls from 406px to about 270px.

   **Not behind a viewport media query, and that is the whole lesson here.**
   The obvious rule — shrink below 480px — does not fire where the fault is.
   The modal is inset from the viewport and its body inset again, so at a
   500px viewport the table has 294px to live in and overflows while a
   `max-width: 480px` query is false. Measured, after writing exactly that
   rule and watching it not fire: viewport 500, body 294, overflow 8.

   The viewport is the wrong question. What constrains this table is the
   modal body, so the figures are simply set at the size that fits the
   narrowest body the modal ever has. They are a touch smaller on a desk than
   they were, which costs nothing — this is a table of four short figures, not
   a reading surface — and it removes a breakpoint that would have been wrong
   again at the next inset.

   This is the same trap `.today-chart` records one step along: a panel sized
   by its contents cannot be asked how much room its contents have
   (CF-PWR-063). Asking the *viewport* instead looks like an escape and is
   not, because the panel is not the viewport.

   The `nowrap` stays. Shrinking what can shrink is the fix; letting a figure
   wrap would trade a scroll nobody minds much for a number nobody can read.

   The values this describes are set on the rules above rather than repeated
   here: the gutters on `th[scope='row']`/`td`, the heading size and tracking
   on `th[scope='col']`, and the two row sizes on `.solar-total-row` and
   `.solar-group-total`. Restating them in a second block is how a figure
   held in two places starts disagreeing with itself. */

/* The day's own chart (CF-PWR-090).

   Given a definite size rather than measuring the panel around it. The panel
   is sized by its contents (CF-PWR-063), so a content asking how much room it
   has is asking about a width it itself decides — the circularity recorded
   beside `.battery-modes`, built wrong twice before.

   Deliberately short. The overview tab would otherwise be much the tallest,
   and CF-PWR-081 makes every other tab match it — so a tall chart here is
   empty space under every roof. Enough to read the shape of a day is enough.
   Stated in shell diameters, so it keeps its proportion to the rest of the
   panel at any interface scale. */
.today-chart {
  position: relative;
  /* A definite width, and **no percentage cap**. Inside a `max-content` panel
     (CF-PWR-063) a percentage resolves against a container whose width this
     element is supposed to be deciding, so it comes out zero and takes the
     modal with it — the whole panel collapsed to 63px. `max-width: 100%` is the
     same trap wearing a different hat: it clamps against that same zero.
     The modal's own `max-width` is what keeps this inside a narrow window, and
     it is applied to the panel rather than to its contents.

     Capped at the window as well (CF-PWR-130): `--panel-content-width` is a
     viewport figure, not a percentage, so it is definite and carries none of
     the circularity described above. */
  width: min(calc(var(--shell, 5.5rem) * 4), var(--panel-content-width, 100vw));
  height: calc(var(--shell, 5.5rem) * 1.15);
  margin-bottom: 0.8rem;
}

/* The solar overview's own chart (CF-PWR-090, CF-PWR-134).

   Taller than the bare `.today-chart` above, because it now draws axes and
   they need room that is not the drawing: `TodayChart._plotInset` takes a
   band off the bottom for the hours and a head off the top, and at the
   sparkline's height what was left was too short to read a day's shape in.
   The extra is the two bands, near enough, so the drawing itself keeps the
   height it had rather than being squeezed by its own labels.

   Still the shortest chart of the three, and deliberately: CF-PWR-081 sizes
   the panel by its largest tab, so every roof's grid of panels sits under
   whatever height is taken here. A day's shape is legible well before a
   chart is tall, and the roofs pay for anything beyond that. */
.solar-today.today-chart {
  height: calc(var(--shell, 5.5rem) * 1.5);
}

/* The net's panel: a heading, the price graph, and a line saying why the
   right-hand half may be empty. */
.net-panel {
  display: flex;
  flex-direction: column;
}

/* The battery's panel: the mode setting, and under it how long the battery
   has left at the rate it is running (CF-PWR-137). */
.battery-panel {
  display: flex;
  flex-direction: column;
}

/* The remaining-time line.

   Muted and small, in the same voice as the settings' own hints: it is a
   derived figure the reader did not ask for and is not being invited to act
   on, so it must not compete with the mode above it.

   The top margin is on this element rather than under `.setting-groups`,
   which is shared by three panels and must not all grow a trailing gap — the
   same reasoning as `.ev-history`'s.

   No height is reserved for it. The line is absent far more often than it is
   present (an idle battery, and every backend until the field ships), and
   holding a blank line's worth of space for it would put a gap on the panel
   in the commonest case to avoid a reflow in the rarer one. */
.battery-eta {
  margin: 0.8rem 0 0;
  font-size: 0.82rem;
  line-height: 1.4;
  color: var(--text-muted);
}

/* Every heading inside a subject panel, in one rule. It was written out
   once per panel, and the EV panel's was left out altogether — so its
   "Recent charges" drew as a browser-default `h3`, twice the size of the
   others and in the body colour, among headings that are small muted
   capitals. A list of selectors is a place a new panel is silently left
   out of, which is the same trap the controls-row rules record; here the
   cost is only that a new panel has to be added to the list, and the list
   is at least one place rather than four.

   The EV panel no longer appears here: its history gained tabs
   (CF-PWR-155), whose labels name the two tables, so the heading that used
   to sit above them went with them. */
.net-panel-heading,
.house-panel-heading {
  margin: 0 0 0.5rem;
  font-size: 0.78rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}

/* The EV's history is the only panel section with something above it — the
   other panels open with their tabs — so it is the only one that has to be
   held off what it follows. Without this the charging-rate setting and the
   tab bar sat about six pixels apart, which read as one block rather than as
   a setting and then a history of a different kind of thing.

   On the history rather than under the setting group: the space belongs to
   whatever starts a new section, and `.setting-groups` is shared by three
   panels that must not all grow a trailing gap.

   It was on `.ev-panel-heading` until the history gained its tabs
   (CF-PWR-155), which took that heading away — the tab labels name the two
   tables now, and a heading above them would name the pair twice. */
.ev-history {
  margin-top: 1.1rem;
}

/* The charge plan (CF-PWR-171 … CF-PWR-179), above the setting. Held off
   what follows it on the same terms as the history: the space belongs to the
   section that starts after it, and a hidden plan must leave no gap, so it is
   a bottom margin on the plan rather than a top one on the settings. */
.ev-plan {
  margin-bottom: 1.1rem;
}

.ev-plan[hidden] {
  display: none;
}

.ev-plan-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.4rem 0.7rem;
}

/* The same small muted capitals as every other heading in a subject panel. */
.ev-plan-title {
  font-size: 0.78rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}

.ev-plan-status {
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--text-primary);
}

/* "Observing only" and "suspended" say that what is drawn is not being done.
   Marked like an override's pill, so a reader skimming the panel is not left
   to infer from the absence of charging that the plan is broken. */
.ev-plan-note {
  margin: 0.4rem 0 0;
  padding: 0.15rem 0.45rem;
  border-left: 3px solid var(--flow-casing);
  font-size: 0.8rem;
  line-height: 1.35;
  color: var(--text-primary);
}

.ev-plan-note[hidden] {
  display: none;
}

/* Label and figure in two columns, so the figures line up down the rows the
   way the charges' columns do. */
.ev-plan-facts {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.2rem 0.8rem;
  margin: 0.5rem 0 0;
  font-size: 0.85rem;
}

.ev-plan-row {
  display: contents;
}

.ev-plan-label {
  color: var(--text-muted);
}

.ev-plan-value {
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}

.ev-plan-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.6rem;
}

/* `.setting-disclose` pushes itself to the far end of a summary row; here the
   buttons are a row of their own and sit together. */
.ev-plan-actions .setting-disclose,
.ev-plan-form .setting-disclose {
  margin-left: 0;
}

.ev-plan-actions .setting-disclose[hidden] {
  display: none;
}

.ev-plan-form {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.5rem 0.8rem;
  margin-top: 0.6rem;
}

.ev-plan-form[hidden] {
  display: none;
}

.ev-plan-field {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  font-size: 0.78rem;
  color: var(--text-muted);
}

.ev-plan-field input {
  width: 7rem;
  background: var(--surface-card);
  color: var(--text-panel);
  border: 1px solid var(--border-control);
  border-radius: 4px;
  padding: 0.25rem 0.35rem;
  font: inherit;
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
  color-scheme: var(--control-scheme);
}

.ev-plan-form-problem {
  flex-basis: 100%;
  font-size: 0.8rem;
  color: var(--text-primary);
}

.ev-plan-form-problem[hidden] {
  display: none;
}

/* The battery's charge plan (CF-PWR-189 … CF-PWR-194), above the mode. Held
   off the mode on the terms of `.ev-plan`: the space is a bottom margin on
   the plan, so a hidden plan leaves no gap. */
.battery-plan {
  margin-bottom: 1.1rem;
}

.battery-plan[hidden],
.battery-plan-reason[hidden],
.battery-plan-chart[hidden],
.battery-plan-facts[hidden],
.battery-plan-assumptions[hidden],
.battery-plan-reserve-label[hidden] {
  display: none;
}

.battery-plan-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.4rem 0.7rem;
}

/* The same small muted capitals as every other heading in a subject panel. */
.battery-plan-title {
  font-size: 0.78rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}

.battery-plan-status {
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--text-primary);
}

/* The backend's sentence (CF-PWR-190): the part of the section a reader is
   meant to judge before pressing a mode, so it is in the body colour and a
   size above the figures — unlike the mode's own reason line, which is muted
   because it describes a house working as intended. Wraps as far as it
   needs; never truncated. Constrained to the graph's width so a long sentence
   does not widen the panel past what the graph below it needs. */
.battery-plan-reason {
  margin: 0.45rem 0 0;
  max-width: min(calc(var(--shell, 5.5rem) * 4), var(--panel-content-width, 100vw));
  font-size: 0.9rem;
  line-height: 1.4;
  color: var(--text-primary);
}

/* The graph of the expected state of charge (CF-PWR-191). A definite width
   capped at the window, on `.today-chart`'s terms (CF-PWR-130): a percentage
   inside a panel sized by its contents resolves to zero. The bottom padding
   is the axis's row, whose height is named once because the reserve's label
   is placed against the drawing above it (`batteryPlanView.js`). */
.battery-plan-chart {
  --battery-plan-axis: 1.1rem;
  position: relative;
  width: min(calc(var(--shell, 5.5rem) * 4), var(--panel-content-width, 100vw));
  margin-top: 0.6rem;
  padding-bottom: var(--battery-plan-axis);
}

/* Stretched to the box: the drawing is lines and rectangles only, so
   stretching distorts nothing, and the text that would stretch is HTML laid
   over it. */
.battery-plan-svg {
  display: block;
  width: 100%;
  height: calc(var(--shell, 5.5rem) * 0.95);
  overflow: visible;
  border-bottom: 1px solid var(--border-control);
}

.battery-plan-run {
  fill: var(--flow-casing);
  opacity: 0.22;
}

.battery-plan-grid {
  stroke: var(--border-control);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

.battery-plan-area {
  fill: var(--soc-bar);
  opacity: 0.18;
}

.battery-plan-line {
  fill: none;
  stroke: var(--soc-bar);
  stroke-width: 2;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}

.battery-plan-reserve {
  stroke: var(--text-muted);
  stroke-width: 1;
  stroke-dasharray: 4 3;
  vector-effect: non-scaling-stroke;
}

.battery-plan-scale,
.battery-plan-reserve-label,
.battery-plan-mark {
  position: absolute;
  font-size: 0.68rem;
  line-height: 1;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  pointer-events: none;
  white-space: nowrap;
}

.battery-plan-scale {
  left: 0.15rem;
}

.battery-plan-scale-top {
  top: 0.15rem;
}

/* Above the axis row, inside the drawing. */
.battery-plan-scale-bottom {
  bottom: calc(var(--battery-plan-axis) + 0.2rem);
}

/* Sits just above its own line, at the right, where the line it names ends. */
.battery-plan-reserve-label {
  right: 0.15rem;
  transform: translateY(calc(-100% - 0.1rem));
}

.battery-plan-axis {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--battery-plan-axis);
}

.battery-plan-mark {
  top: 0.25rem;
}

/* The estimates the plan rests on (CF-PWR-194): after the figures, quiet. */
.battery-plan-assumptions {
  margin: 0.45rem 0 0;
  max-width: min(calc(var(--shell, 5.5rem) * 4), var(--panel-content-width, 100vw));
  font-size: 0.76rem;
  line-height: 1.4;
  color: var(--text-muted);
}

.net-panel-hint {
  font-size: 0.78rem;
  line-height: 1.35;
  color: var(--text-muted);
}

/* Spent intervals: one flat grey whatever they cost (CF-PWR-138). Mixed from
   the panel's text colour so it is a grey in every theme without a palette
   entry of its own. */
.net-prices .price-bar-past {
  fill: color-mix(in srgb, var(--text-panel) 28%, transparent);
}

/* The present (CF-PWR-138): the panel's text colour — white on the dark
   themes — at full strength, rather than the Timelines row's muted seam
   colour, because here it is what makes "now" obvious among grey bars. */
.net-prices .histogram-now {
  stroke: var(--text-panel);
  stroke-width: 1.5;
  opacity: 1;
}

/* The name over each of the two graphs (CF-PWR-187). */
.price-day-name {
  display: block;
  margin-bottom: 0.15rem;
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--text-muted);
}

/* The house's panel: a heading and the last 24 hours (CF-PWR-109). */
.house-panel {
  display: flex;
  flex-direction: column;
}

/* Sized like the net's price graph rather than solar's sparkline: it is the
   panel's only content, so nothing else has to match its height and there is
   no reason to keep it small — the constraint on solar's is CF-PWR-081, which
   does not apply to an untabbed panel.

   Definite, and with no percentage cap, for the reason recorded on
   `.today-chart`: inside a `max-content` panel both a percentage width and a
   percentage max-width resolve against a container this element is itself
   sizing, and both come out zero. */
.house-day-chart .today-chart {
  /* Narrowed to the window where the window is narrower (CF-PWR-130), for
     the reason on `--panel-content-width`. Without it a phone showed the
     first six of the twenty-four hours and the rest had to be scrolled to,
     which is not a day at a glance.

     Narrowed to the *panel's* ceiling as well, which is the third term. The
     panel stops at 6 shells (`.subject-modal`'s `max-width`) and its own
     chrome comes out of that, so a graph asking for 5.5 shells asked for
     more than the panel could ever give it and overflowed by 12px — a
     sideways scroll on a graph, which CF-PWR-063 forbids. The ceiling is
     read from the same two custom properties that state it rather than
     restated, so moving either moves this with it. */
  width: min(
    calc(var(--shell, 5.5rem) * 5.5),
    calc(var(--shell, 5.5rem) * 6 - var(--panel-chrome-width, 0px)),
    var(--panel-content-width, 100vw)
  );
  /* Shorter than the single chart that preceded it, because there are two of
     them now and the panel is bounded by the window's height (CF-PWR-063).
     Still tall enough for the axis labels plus a day's shape. */
  height: calc(var(--shell, 5.5rem) * 1.25);
  margin-bottom: 0;
}

/* The two days, one above the other (CF-PWR-110). Both rows carry the same
   24 hours on the same axis, so they are read down a column as well as
   across — which is the reason for stacking them rather than tabbing them. */
.house-panel {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}

.house-day {
  display: flex;
  flex-direction: column;
}

/* The day's name on the left, its two figures on the right, on one line.
   Above the chart rather than beside it: the chart is as wide as the panel
   allows (CF-PWR-130), so there is no room beside it that would not be taken
   out of the graph. */
.house-day-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.8rem;
  margin-bottom: 0.25rem;
}

.house-day-heading {
  margin: 0;
  font-size: 0.78rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}

/* A description list, because that is what it is: two labelled figures. The
   label sits above its figure so the pair reads as one item and the two items
   do not run together into a single line of four words. */
.house-day-figures {
  display: flex;
  gap: 1.1rem;
  margin: 0;
}

.house-day-figure {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
}

.house-day-figure dt {
  font-size: 0.62rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
}

.house-day-figure dd {
  margin: 0;
  font-size: 0.92rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-panel);
}

/* A figure the backend has not answered for keeps its room rather than
   collapsing the row, so the two days stay the same height while one is
   still loading. */
.house-day-value.unknown {
  color: var(--text-muted);
}

/* The EV charger's recent charges (CF-PWR-114).

   A real table, because it is one: two columns of the same kind of thing
   down every row, and the reader compares down a column rather than
   reading across. It was three spans in a flex row, which left each row
   to be as wide as its own contents and so put the energies at a
   different place on every line — the thing a table exists to stop.

   `width: 100%` inside a panel sized by its contents (CF-PWR-063) is the
   trap `.today-chart` records: a percentage resolves against a container
   this element is itself helping to size. It is safe *here* and nowhere
   near that chart's case, because the panel has other contents that are
   definite — the setting group and the heading — so the container's width
   is decided without reference to this table. `max-content` on the table
   would be better still if it were alone, and it is not alone.

   `border-collapse` so the row rules are single lines rather than doubled
   between adjacent rows. */
.ev-sessions-table,
.ev-months-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.82rem;
  /* Clears the note underneath, which is the only thing that follows it. */
  margin-bottom: 0.2rem;
}

/* The monthly totals are the same table with a different span (CF-PWR-155),
   so they share every rule below rather than restating them. Two copies of a
   column's alignment and insets would disagree the first time either was
   touched, which is the duplication this stylesheet has been bitten by
   before — and the two tables sit one tab apart, where a drift between them
   is read as one table being subtly wrong rather than as two rules
   diverging. The selectors below name both. */
.ev-sessions-table th,
.ev-months-table th {
  /* The column headings are labels, not data: smaller and muted, in the
     same voice as the panel heading above them, so the eye goes to the
     figures rather than to the words naming them. */
  padding: 0 0 0.3rem;
  font-size: 0.72rem;
  font-weight: 600;
  text-align: left;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
  border-bottom: 1px solid var(--border-control);
}

.ev-sessions-table td,
.ev-months-table td {
  padding: 0.32rem 0 0.3rem;
  /* A hairline between rows rather than a full border, so twelve charges
     read as one block with separable lines rather than as twelve boxes.
     On the last row it is the block's own bottom edge, which is wanted:
     it closes the table off from the note beneath it. */
  border-bottom: 1px solid var(--border-control);
  white-space: nowrap;
}

/* The energy column is a quantity, so it is set to the right and the
   digits line up under each other — which is the comparison the reader is
   making, and the reason this is a table at all. Stated on both the
   heading and the cells so the column reads as one column. */
.ev-sessions-table th.ev-session-energy-cell,
.ev-sessions-table td.ev-session-energy,
.ev-sessions-table th.ev-session-cost-cell,
.ev-sessions-table td.ev-session-cost,
.ev-sessions-table th.ev-session-rate-cell,
.ev-sessions-table td.ev-session-rate,
.ev-months-table th.ev-session-energy-cell,
.ev-months-table td.ev-session-energy,
.ev-months-table th.ev-session-cost-cell,
.ev-months-table td.ev-session-cost,
.ev-months-table th.ev-session-rate-cell,
.ev-months-table td.ev-session-rate {
  text-align: right;
  /* Tabular figures: proportional digits put the same number of
     characters at different widths, so a column of them is ragged even
     when it is right-aligned. */
  font-variant-numeric: tabular-nums;
}

/* The three figure columns are read against each other down the table, so
   they are given room to breathe apart rather than running together at the
   right edge. Only a left inset, so each column stays hard against its own
   right edge and the digits keep lining up. */
.ev-sessions-table th.ev-session-cost-cell,
.ev-sessions-table td.ev-session-cost,
.ev-sessions-table th.ev-session-rate-cell,
.ev-sessions-table td.ev-session-rate,
.ev-sessions-table th.ev-session-energy-cell,
.ev-sessions-table td.ev-session-energy,
.ev-months-table th.ev-session-cost-cell,
.ev-months-table td.ev-session-cost,
.ev-months-table th.ev-session-rate-cell,
.ev-months-table td.ev-session-rate,
.ev-months-table th.ev-session-energy-cell,
.ev-months-table td.ev-session-energy {
  padding-left: 0.7rem;
}

/* The rate is the quietest of the three: it is derived from the two beside
   it (CF-PWR-133), and at equal weight three figures per row read as three
   equally important answers when the reader is comparing one. */
.ev-sessions-table td.ev-session-rate,
.ev-months-table td.ev-session-rate {
  color: var(--text-muted);
}

/* The clock column is the one that goes when there is no room for five
   (CF-PWR-133).

   The reader's own call, and their reasoning: *"it doesn't happen very often
   that cars are charged multiple times on a day"* — so the date identifies
   the charge on its own, and the time is the column whose loss costs least.
   The cost and the rate are what the columns were added for and stay.

   Hidden rather than dropped from the markup, so the same table serves both
   widths and nothing has to be rebuilt when the window is resized or the
   phone turned. The threshold is the one the panel's own tabs use, so a
   panel does not change arrangement at two different widths. */
@media (max-width: 30rem) {
  .ev-sessions-table th.ev-session-time-cell,
  .ev-sessions-table td.ev-session-time {
    display: none;
  }

  /* The months' first cell is held on one line and sized to its contents at
     every width, not only this one, so that rule lives outside this query —
     see `.ev-months-table td.ev-session-when` below. It was stated in both
     places until the later one was found to be overriding this copy outright,
     which is the duplicated-figure trap this stylesheet keeps meeting: two
     places holding one value disagree the first time either is touched.

     What remains here is what genuinely belongs to a phone's width. */

  /* The figure columns give up their inset here: at this width the three of
     them are what the table is for, and 0.7rem apiece is room the rate needs
     more. Their right alignment is what keeps them separable. */
  .ev-months-table th.ev-session-cost-cell,
  .ev-months-table td.ev-session-cost,
  .ev-months-table th.ev-session-rate-cell,
  .ev-months-table td.ev-session-rate,
  .ev-months-table th.ev-session-energy-cell,
  .ev-months-table td.ev-session-energy {
    padding-left: 0.35rem;
  }

  /* There was a rule here placing "so far" on its own line under the month.
     It is gone with the word: the month still accumulating now carries three
     dots on the end of its own text (CF-PWR-155), which cannot wrap away from
     it and cannot add a line, so the first row is the same height as the
     rest. That was the reader's objection to the word in either position. */
}

/* The date column takes what is left, so the energies sit against the
   panel's right edge rather than floating in the middle of it. */
.ev-sessions-table td.ev-session-when {
  width: 100%;
  /* The one cell allowed to wrap: a long locale date on a narrow phone
     has somewhere to go, and the energy column keeps its place. */
  white-space: normal;
}

/* The months' first cell does **not** wrap, at any width (CF-PWR-155).

   A month carries its year — "Sept 2026" — so it is half again as wide as the
   charges' bare day, and it is a single name rather than a date that can break
   after its day. Allowed to wrap it takes two lines, which makes that row
   taller than the rest; the month still accumulating takes two lines for
   certain, because its ellipsis is what tips it over the width.

   That is the fault the reader reported, and the ellipsis alone did not fix
   it: this rule and the media query below both said `nowrap`, and this one —
   being later at equal specificity — was quietly winning with `normal`. The
   row measured 42px against its neighbours' 26px with nothing visibly wrapped,
   the second line holding only the dots. Measured, not guessed: removing the
   dots returned the row to 26px while removing the italic changed nothing.

   So the months are excluded from the rule above rather than fighting it in a
   media query. `width: auto` with it, so the column takes only what the name
   needs and hands the rest to the three figure columns. */
.ev-months-table td.ev-session-when {
  width: auto;
  white-space: nowrap;
}

/* The months' total row (CF-PWR-156). Set apart from the months above it by a
   rule and by weight, because it is a different kind of statement — a sum of
   the column rather than another month — and a reader running an eye up the
   column must not mistake it for one.

   `border-top` doubled against the last row's own `border-bottom` is what the
   heavier line is made of, which is wanted here: it closes the table. */
.ev-months-table tfoot td {
  padding-top: 0.42rem;
  border-top: 1px solid var(--border-control);
  border-bottom: none;
  font-weight: 600;
}

/* The total's rate stays quiet, exactly as each month's does: it is derived
   from the two figures beside it rather than being a third answer. Stated
   after the rule above so it wins the weight. */
.ev-months-table tfoot td.ev-session-rate {
  font-weight: 400;
}

/* "charging", beside the date it belongs to rather than in a column of
   its own that would be empty on every finished charge. Set apart from
   the date by weight and colour rather than by a badge: it is a note on
   the row, and CF-PWR-112 asks only that a running charge be shown as
   running. */
.ev-session-running {
  margin-left: 0.4rem;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-label);
}

/* A running charge's energy is what it has drawn so far and not a total
   (CF-PWR-112). The title says so on a pointer; this says so at a glance,
   without adding a word the finished rows would then be missing. */
.ev-session.running td,
.ev-month.running td {
  font-style: italic;
}

/* The net's two price graphs, today above tomorrow (CF-PWR-187). Wider and
   taller than solar's sparkline, because each carries more: a day of
   quarter-hours, a vertical axis, and a band above the bars for the cheapest
   and dearest figures (CF-COST-017). Squeezed to the solar chart's size the
   bars become slivers and the labels collide.

   Each is the height the one graph across both days had. Two of them, with
   the day's figures above, still fit an iPhone 13 in Safari with its toolbars
   showing — 390×664, where the panel measured 530px and nothing scrolled — so
   neither was made shorter. That makes the prices tab the tallest of the
   net's three, and CF-PWR-081 sizes the panel by it. */
.net-prices .today-chart {
  /* Definite, and with no percentage cap, for the reason on `.today-chart`
     above: inside a `max-content` panel both a percentage width and a
     percentage max-width resolve against a container this element is itself
     sizing, and both come out zero. */
  /* Narrowed to the window where the window is narrower (CF-PWR-130). Both
     terms are definite lengths, so `min` is safe here in a way a percentage
     is not — see `--panel-content-width` on the backdrop. Without it the
     graph kept its 5.5 shells on a phone and two thirds of tomorrow sat off
     the right edge.

     The panel's own ceiling is the third term, for the reason recorded on
     `.house-day-chart .today-chart`: 6 shells less the panel's chrome is less
     than the 5.5 asked for here, so this graph overflowed the panel by the
     same 12px. It showed less plainly than the house's because this chart's
     right-hand stretch is usually the unpublished part of tomorrow and so
     carries no bars to notice missing. */
  width: min(
    calc(var(--shell, 5.5rem) * 5.5),
    calc(var(--shell, 5.5rem) * 6 - var(--panel-chrome-width, 0px)),
    var(--panel-content-width, 100vw)
  );
  height: calc(var(--shell, 5.5rem) * 1.8);
  margin-bottom: 0.35rem;
}

/* The price graph does not scroll the panel behind it (CF-PWR-116).

   `.today-chart-svg` already said this and it was not enough, for a
   reason worth writing down: **`touch-action` is not inherited.** It is
   consulted on the element the touch actually lands on, and inside this
   SVG that is never the `<svg>` — it is a `rect.price-bar`, or one of the
   groups around it, every one of which computed `auto`. So the rule on
   the SVG governed only the gaps between the bars, and a finger put on a
   bar — which is the whole graph, and exactly where a reader puts it —
   scrolled the panel body underneath. Measured in the browser rather than
   reasoned about: the hit element under the middle of the chart is a
   `rect`, and the first ancestor with `none` was three levels above it.

   So the rule has to reach the descendants, and `*` is the honest way to
   say "wherever the touch lands inside here". The wrapper is included
   because the readout is its child and is positioned over the drawing.

   The SVG's own rule is left where it is rather than folded into this
   one: it is there for a different fault — the pointer stream being taken
   away mid-gesture, which is the "tooltip vanishes when I slide fast"
   report recorded on it — and removing it would quietly re-open that. */
.net-prices .today-chart,
.net-prices .today-chart * {
  touch-action: none;
}

.today-chart-svg {
  display: block;
  width: 100%;
  height: 100%;
  /* A glance, not an instrument (CF-PWR-091). Pointing at it reads a figure;
     dragging must not select text, which is what a chart under a finger on a
     phone would otherwise do.
     `none` rather than `pan-y`: the chart sits in a panel body that scrolls,
     and `pan-y` is a standing invitation for the browser to reinterpret a
     drag across the chart as a scroll of that body — taking the pointer
     stream away mid-gesture and firing `pointercancel`, which cleared the
     readout. That is the "tooltip vanishes when I slide fast" fault. The
     chart is a few lines tall and the panel around it is not, so a reader
     scrolling has plenty of surface that is not this element. */
  touch-action: none;
  user-select: none;
}

/* The readout, over the chart it belongs to. Positioned by the pointer's
   fraction across the chart and translated back by half its own width, so it
   is centred on where the reader is pointing without its width having to be
   known in advance. `pointer-events: none` or it would take the pointer from
   the chart underneath and the readout would flicker as it moved. */
.today-chart-readout {
  position: absolute;
  /* Just inside the chart's own top rather than above it. Above, it sat over
     whatever the panel put there — on the net's panel it covered the heading,
     hiding the word "tomorrow" exactly when the reader was pointing at
     tomorrow. Inside, it overlaps only the graph's own empty upper corner. */
  top: 2px;
  transform: translateX(-50%);
  padding: 0.15rem 0.4rem;
  border-radius: 4px;
  background: var(--surface-bar);
  color: var(--text-primary);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  pointer-events: none;
}

.today-chart-readout[hidden] {
  display: none;
}

/* The hour marks along a panel chart's foot (CF-PWR-114). Quieter than the
   price labels beside them: the price is what is being read, the time only
   says where. */
.panel-time-label {
  fill: var(--text-muted);
  font-size: 0.62rem;
  font-variant-numeric: tabular-nums;
  opacity: 0.8;
}

/* Which interval the readout is describing (CF-PWR-114). On the price graph it
   runs down the middle of the bar being read rather than under the cursor, so
   it names a quarter-hour instead of a pixel. `pointer-events: none` or it
   would take the pointer from the chart and the readout would flicker as it
   moved. */
.panel-chart-marker {
  stroke: var(--text-primary);
  stroke-width: 1;
  opacity: 0.55;
  pointer-events: none;
}

/* Where a readout's line meets what the graph drew — a bar's top, a line's
   height (CF-GEN-019). White in every theme, as asked for; the thin dark ring
   is what keeps it visible where it sits on a light background, which a line
   graph's dot half does. Shared by every graph drawn in SVG; the Timelines
   stack draws the same dot as `.scrub-dot`. */
.readout-dot {
  fill: #fff;
  stroke: rgb(0 0 0 / 0.55);
  stroke-width: 1;
  pointer-events: none;
}

.solar-group-name {
  margin: 0 0 0.4rem;
  font-size: 0.78rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}

/* A panel cell is sized by the figures it carries, not by the room available.

   Wide enough for its widest reading and no wider: 2.9rem of text — measured,
   the worst case being a four-digit "1089 Wh" or a "10.5 kWh" — plus the
   cell's own padding either side. Twice the text's own height tall, which is
   two lines of figures and as much again of fill to read the level against.

   Both figures are held here rather than in the rules below because each is
   used twice — the width by the grid's track and the cell, the height by the
   cell — and two copies of one number disagree the first time either moves.

   One roof. The column count is the group's own, set from the payload — the
   client holds no layout (CF-PWR-079), so the grid is shaped by what arrived
   rather than by anything written here.

   Tracks are the cell's own width rather than `1fr`, so a roof of three
   columns in a narrow panel draws three small panels instead of three cells
   stretched to a third of the width each — which, with a portrait aspect
   ratio governing the height, is what made the array far taller than the
   panel and clipped its last column. `justify-content` keeps the array
   together at the leading edge now that it no longer fills the row.

   Each track is `auto` — as wide as the cells in it, which size themselves
   from the figures below — because a roof can now mix the two shapes
   (CF-PWR-104 as amended in revision 312): a column holding a panel lying
   across is wider than one of upright panels. `auto` still never takes a
   share of the room, since `justify-content: start` stops the tracks
   stretching; what is forbidden is `fr`.

   About a third taller than wide: enough for an upright panel and one lying
   across to be told apart at a glance, which the nearly square 3.6rem cell
   they replaced did not allow. */
.solar-grid {
  --solar-cell-text: 2.9rem;
  --solar-cell-pad: 0.25rem;
  --solar-cell-height: 4.5rem;
  --solar-cell-width: calc(var(--solar-cell-text) + 2 * var(--solar-cell-pad));
  display: grid;
  grid-template-columns: repeat(var(--solar-columns, 3), auto);
  justify-content: start;
  justify-items: center;
  align-items: center;
  gap: 0.3rem;
}

/* One panel. Upright unless it faces east or west, because the panels are:
   a shape that matches what is on the roof is easier to compare with it than
   a square would be. The shape comes from the two figures above rather than
   from an `aspect-ratio`, which derived the height from whatever width the
   track happened to have and so could not be bounded.

   `position: relative` for the fill, which is absolutely placed within it so
   that its height can be a percentage of the cell without disturbing the
   figures sitting over it. */
.solar-cell {
  position: relative;
  overflow: hidden;
  width: var(--solar-cell-width);
  height: var(--solar-cell-height);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding: var(--solar-cell-pad);
  border: 1px solid var(--border-control);
  border-radius: 4px;
  background: var(--surface-page);
}

/* A panel facing east or west, seen from above with north at the top: its
   short sides are left and right, so it lies across (CF-PWR-078). The upright
   panel turned a quarter, each figure taking the other's place (CF-PWR-104),
   so a group is drawn at exactly two sizes. */
.solar-cell.solar-cell-across {
  width: var(--solar-cell-height);
  height: var(--solar-cell-width);
}

/* The side a panel faces is its low edge, drawn heavier so the facing reads
   at night, with no fill to show it (CF-PWR-078). A panel whose facing was
   not reported marks no side: that would be a claim about the roof.

   A strip laid over the edge rather than a wider border, which would make
   the cell larger than the panel turned a quarter (CF-PWR-104) and so not
   one of the group's two sizes. Above the fill, which would otherwise cover
   it on exactly the side the fill rises from. */
.solar-cell[class*='solar-cell-faces-']::after {
  content: '';
  position: absolute;
  z-index: 2;
  background: var(--text-muted);
}

.solar-cell-faces-north::after {
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
}

.solar-cell-faces-east::after {
  top: 0;
  bottom: 0;
  right: 0;
  width: 3px;
}

.solar-cell-faces-south::after {
  left: 0;
  right: 0;
  bottom: 0;
  height: 3px;
}

.solar-cell-faces-west::after {
  top: 0;
  bottom: 0;
  left: 0;
  width: 3px;
}

/* Filled in proportion to how much of its own maximum the panel is making
   (CF-PWR-078), from the side it faces: a level rises from the low edge.
   From the bottom by default, which is a south-facing panel and also one
   whose facing nobody reported. Anchored to that edge rather than given the
   opposite one, so the level alone moves it — which is what makes it read as
   a level rising rather than a block resizing. */
.solar-cell-fill {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--solar-level, 0%);
  background: linear-gradient(
    to top,
    color-mix(in srgb, var(--solar-fill) 85%, transparent),
    color-mix(in srgb, var(--solar-fill) 45%, transparent)
  );
  transition:
    height 400ms ease,
    width 400ms ease;
}

.solar-cell-faces-north .solar-cell-fill {
  top: 0;
  bottom: auto;
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--solar-fill) 85%, transparent),
    color-mix(in srgb, var(--solar-fill) 45%, transparent)
  );
}

.solar-cell-faces-east .solar-cell-fill,
.solar-cell-faces-west .solar-cell-fill {
  top: 0;
  bottom: 0;
  height: auto;
  width: var(--solar-level, 0%);
}

.solar-cell-faces-east .solar-cell-fill {
  left: auto;
  right: 0;
  background: linear-gradient(
    to left,
    color-mix(in srgb, var(--solar-fill) 85%, transparent),
    color-mix(in srgb, var(--solar-fill) 45%, transparent)
  );
}

.solar-cell-faces-west .solar-cell-fill {
  left: 0;
  right: auto;
  background: linear-gradient(
    to right,
    color-mix(in srgb, var(--solar-fill) 85%, transparent),
    color-mix(in srgb, var(--solar-fill) 45%, transparent)
  );
}

/* Over the fill, so the figures stay legible whatever the level behind them
   (CF-PWR-078: the numbers remain regardless of the fill). */
.solar-cell-figures {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.05rem;
  text-align: center;
  line-height: 1.2;
}

.solar-cell-power {
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}

.solar-cell-energy {
  font-size: 0.62rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* A panel producing nothing. Dimmed rather than blank: it is still a panel
   and still has a place on the roof, and the reader is being shown that it
   is there and idle rather than that it is missing. */
.solar-cell.idle .solar-cell-power {
  color: var(--text-muted);
}

/* A position the backend described no panel at. Drawn as a hole so the array
   keeps its shape — a gap in a roof is a fact about the roof. */
.solar-cell-absent {
  border-style: dashed;
  opacity: 0.35;
  background: none;
  /* The size of its slot rather than of an upright panel, so a hole beside
     panels lying across is their shape and not a third one. */
  width: auto;
  height: auto;
  min-height: 1rem;
  justify-self: stretch;
  align-self: stretch;
}

/* Something on the roof that is not a panel — the A/C's outdoor unit
   (CF-PWR-186). Smaller than its slot, so it does not read as a panel; no
   figures and no fill, since it produces nothing. Sized as a share of the
   slot, which the panels beside it have already sized, with a floor so a
   fixture alone in a column still shows its name. */
.solar-fixture {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 70%;
  height: 55%;
  min-width: 2.4rem;
  min-height: 1.6rem;
  box-sizing: border-box;
  border: 1px solid var(--border-control);
  border-radius: 3px;
  background: color-mix(in srgb, var(--text-muted) 14%, transparent);
}

.solar-fixture-name {
  font-size: 0.62rem;
  color: var(--text-muted);
}

.solar-empty {
  font-size: 0.82rem;
  line-height: 1.4;
  color: var(--text-muted);
}

/* One overridable setting: a summary row, and a panel that opens beneath it
   (CF-PWR-086). Shared by the battery's mode, the solar cap and the charger's
   cap — the three used to build three arrangements of the same parts. */
.setting-groups {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.setting-group + .setting-group {
  padding-top: 0.6rem;
  border-top: 1px solid var(--border-control);
}

/* The row that is always there. It answers what the setting is and what it is
   set to, which is what almost everyone opening the panel came for, and is
   what makes the disclosure a disclosure rather than a hiding place
   (CF-PWR-075 as amended). */
.setting-summary {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.4rem 0.7rem;
}

.setting-summary-name {
  font-size: 0.9rem;
  color: var(--text-primary);
}

.setting-summary-value {
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}

/* "Not reported" is a sentence, not a value — so it is not set like one.
   Without this it is shown in the same weight as `NOM` or `3.6 kW`, which
   reads as the setting being in a state of that name (CF-PWR-073). */
.setting-summary-value.unknown {
  font-weight: 400;
  color: var(--text-muted);
}

/* What the backend chose, superseded by an override (CF-PWR-105).

   Struck through rather than dimmed. Dimming the same figure reads as "less
   certain"; a line through it reads as "not this one any more", which is what
   is true. Shown only where the override actually departs from it, so an
   override pinning a setting to the value it already had does not draw a
   strike through a figure nothing disagrees with. */
.setting-summary-server {
  font-size: 0.9rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  text-decoration: line-through;
  /* The strike is the whole point, so it must survive a theme where the muted
     colour is close to the background. */
  text-decoration-thickness: 1px;
}

.setting-summary-server[hidden] {
  display: none;
}

/* Why the backend holds its value (CF-PWR-121), on its own line beneath the
   summary. Quieter than the value and plain: no warning colour, no icon, no
   border — the commonest reason describes a house working as intended, and
   styling it like the failure callout below would report every correct
   decision as a fault (CF-PWR-122). Wraps as far as it needs; never
   truncated (SPEC-POWER.md §Length). */
.setting-reason {
  margin: 0.3rem 0 0;
  font-size: 0.8rem;
  line-height: 1.4;
  color: var(--text-muted);
}

.setting-reason[hidden],
.setting-reason-lead[hidden] {
  display: none;
}

/* "Would otherwise be NOM:" — the client's own words, set apart from the
   backend's sentence that follows them only by a space and a little weight. */
.setting-reason-lead {
  margin-right: 0.25em;
  color: var(--text-label);
}

/* The override, its countdown and the control that changes it, as one thing
   (CF-PWR-106).

   Bordered so the three read as a single statement — this is overridden, for
   this long, change it here — rather than as three unrelated items on a row.
   Before this the mark sat beside the value, the countdown was inside the
   disclosure where nobody saw it without opening something, and the control
   was pushed to the far end of the row away from both. */
.setting-override-group {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-left: auto;
  padding: 0.2rem 0.3rem 0.2rem 0.35rem;
  border: 1px solid var(--flow-casing);
  border-radius: 999px;
}

.setting-override-group[hidden] {
  display: none;
}

.setting-override-remaining {
  font-size: 0.72rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.setting-override-remaining[hidden] {
  display: none;
}

/* Inside the bordered group, so it carries no border of its own — two nested
   outlines on one control read as a control inside a control. */
.setting-disclose-inline {
  margin-left: 0;
  padding: 0.1rem 0.45rem;
  border-color: transparent;
  background: none;
  font-size: 0.75rem;
  text-decoration: underline;
}

.setting-disclose-inline:hover:not(:disabled) {
  border-color: transparent;
  color: var(--flow-casing);
}

.setting-disclose-plain[hidden] {
  display: none;
}

/* That an override is running, said without anything being opened
   (CF-PWR-087). The reader who comes to find out why the house is behaving
   oddly must not have to press a control to be told. */
.setting-summary-mark {
  padding: 0.05rem 0.35rem;
  border-radius: 999px;
  background: var(--flow-casing);
  color: var(--surface-panel);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.setting-summary-mark[hidden] {
  display: none;
}

/* Pushed to the far end, so the button is in the same place on every row
   however long the setting's name is. */
.setting-disclose {
  margin-left: auto;
  padding: 0.2rem 0.6rem;
  border: 1px solid var(--border-control);
  border-radius: 6px;
  background: var(--surface-card);
  color: var(--text-primary);
  font: inherit;
  font-size: 0.82rem;
  cursor: pointer;
}

.setting-disclose:hover:not(:disabled) {
  border-color: var(--flow-casing);
}

.setting-disclose:disabled {
  opacity: 0.45;
  cursor: default;
}

.setting-disclose:focus-visible {
  outline: 2px solid var(--flow-casing);
  outline-offset: 1px;
}

/* The controls that open. **Over the panel, not in it** (CF-PWR-107).

   This was in the flow, pushing what followed down, on the reasoning that the
   controls belong to the row above and an overlay would detach them. Two things
   overruled that. They are a temporary input — a duration, a value, done — and
   something that opens in the column reads as a new permanent part of the
   panel. And CF-PWR-081 stacks a tabbed panel's panes in one grid cell, so
   anything growing inside a pane grows the shared cell and shoves the selected
   tab's contents; harmless while the solar panel only tabbed on a narrow
   screen, live at every width once its tabs became unconditional
   (CF-PWR-077, revision 158).

   The element keeps its own border and background because it is now the
   popover's whole contents — see `.override-popover-panel`, which holds only
   the frame around it. It is moved between the row and the popover rather than
   rebuilt, so the chooser's duration and the slider's position survive. */
.setting-detail {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  padding: 0.7rem;
  border-radius: 8px;
  background: var(--surface-card);
}

/* `display: none` rather than the attribute's default, because the flex rule
   above would otherwise win on specificity and show it anyway — the trap
   recorded further up this file, hit twice before. */
.setting-detail[hidden] {
  display: none;
}

.setting-detail-hint {
  font-size: 0.78rem;
  line-height: 1.35;
  color: var(--text-muted);
}

/* Why an override did not take (CF-PWR-099 … CF-PWR-102).

   Under the summary and outside the disclosure, so a problem arriving on the
   websocket is seen whether or not anything is open — a message inside a
   collapsed panel is a message nobody is shown.

   The border is on the leading edge alone rather than all round: the box
   belongs to the setting above it, and a full outline would read as a
   separate card. The `[hidden]` rule is stated because `display: flex` on a
   bare class beats `[hidden]`'s own `display: none` on source order — the
   same trap `.override-value` documents a few hundred lines up, and the
   reason a dismissed callout would otherwise go on taking up its space. */
.failure-callout {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  margin-top: 0.4rem;
  padding: 0.45rem 0.55rem;
  border-left: 3px solid var(--failure-mark);
  border-radius: 0 4px 4px 0;
  background: var(--failure-surface);
}

.failure-callout[hidden] {
  display: none;
}

.failure-message {
  flex: 1;
  margin: 0;
  font-size: 0.8rem;
  line-height: 1.35;
  color: var(--failure-text);
  /* The backend's own message can be a long sentence naming several wire
     constants, and it arrives unbroken. Without this it would widen the
     panel rather than wrap inside it. */
  overflow-wrap: anywhere;
}

/* Dismissing is the only way this goes away (CF-PWR-102), so the control
   has to be genuinely reachable: a real button, big enough to hit on a
   phone, and it keeps its focus ring. */
.failure-dismiss {
  flex: none;
  width: 1.6rem;
  height: 1.6rem;
  padding: 0;
  border: none;
  border-radius: 4px;
  background: transparent;
  color: var(--failure-text);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
}

.failure-dismiss:hover {
  background: color-mix(in srgb, var(--failure-mark) 25%, transparent);
}

/* The value control for a numeric setting: the slider and its figure. */
.setting-slider {
  display: flex;
  align-items: center;
  gap: 0.7rem;
}

.setting-slider-value {
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
  /* Wide enough for the longest figure this shows, so the handle does not
     shift sideways as the number's width changes under it. */
  min-width: 4.5rem;
  text-align: right;
}

/* While the handle is being dragged the figure is the reader's own position,
   not anything the backend has confirmed (CF-PWR-071). Marked so the two are
   not mistaken for each other in the moment before the command is sent. */
.setting-slider-value.pending {
  color: var(--flow-casing);
}

.setting-slider-input {
  flex: 1;
  min-width: 0;
  accent-color: var(--flow-casing);
}

/* Inert until the override is engaged (CF-PWR-075), and visibly so — the
   track still shows the scale and where the value sits on it, which is what
   tells the reader what the override would let them do. */
.setting-slider-input:disabled {
  opacity: 0.45;
  cursor: default;
}

/* Below the threshold the panel cannot show its groups side by side, so they
   become tabs (CF-PWR-077). Both arrangements are in the markup and this
   chooses; nothing appears or disappears, so a reader moving between a phone
   and a desk finds the same panel laid out differently.

   Keyed to the viewport rather than to the panel, for the reason recorded at
   `.battery-modes`: the panel is sized by its contents, so asking it how much
   room its contents have is circular. */
@media (max-width: 30rem) {
  .panel-tabs {
    display: flex;
    border-bottom: 1px solid var(--border-control);
  }

  .panel-tab {
    padding: 0.4rem 0.7rem;
    border: none;
    border-bottom: 2px solid transparent;
    background: none;
    color: var(--text-muted);
    font: inherit;
    font-size: 0.85rem;
    cursor: pointer;
  }

  .panel-tab.selected {
    color: var(--text-primary);
    border-bottom-color: var(--flow-casing);
  }

  .panel-tab:focus-visible {
    outline: 2px solid var(--flow-casing);
    outline-offset: -2px;
  }

  /* Every pane is stacked in one grid cell, so the set occupies the largest
     of them and the panel stops resizing as tabs are pressed (CF-PWR-081).

     `display: none` on the unselected panes was the obvious way to show one
     at a time, and it is what made the panel jump: a hidden pane takes no
     room, so the panel was sized to whichever tab happened to be selected and
     changed shape under the reader's finger. Stacking keeps them all in the
     layout and hides them by `visibility`, which reserves the room without
     drawing anything.

     A pane is dimensioned by the largest across the set in *both* directions
     — that is what one grid cell means — so a subject whose tabs differ in
     width is as steady as one whose tabs differ in height. */
  .panel-tab-panes {
    display: grid;
    /* Clear of the tab bar's rule (CF-PWR-153). Both arrangements draw that
       line, so both need the gap under it, or the same panel looks squashed
       at one width and not the other. The length itself is declared once,
       on `.panel-tab-panes` above. */
    padding-top: var(--panel-tab-gap);
  }

  .panel-tab-pane {
    grid-area: 1 / 1;
  }

  /* Hidden but still occupying its cell. `visibility` rather than `display`
     for the reason above; it also takes the pane's contents out of the tab
     order and away from assistive technology, which `opacity` would not. */
  .panel-tab-pane:not(.selected) {
    visibility: hidden;
  }

  .panel-tab-pane .solar-group-name {
    display: none;
  }
}

/* The self-use chooser. A popover rather than a modal: it switches between
   two ready answers, and the percentage it changes is behind it and should
   stay visible while it does (CF-PWR-052). */
.self-use-popover[hidden] {
  display: none;
}

/* The override controls, over the panel that owns them (CF-PWR-107). Same
   positioning contract as the self-use chooser — `fixed`, placed in viewport
   coordinates by `AnchoredPopover`.

   **Above `.subject-modal-backdrop`, which is 40.** Stated as 41 rather than
   left to tie: both are children of `body`, so an equal z-index is resolved by
   document order, and this popover happens to be appended later only because
   the modal is built during startup. That is a fact about construction order,
   not a layering decision, and it would reverse the first time either moved. */
.override-popover[hidden] {
  display: none;
}

.override-popover-panel {
  position: fixed;
  z-index: 41;
  max-width: min(22rem, calc(100vw - 2rem));
  border: 1px solid var(--border-control);
  border-radius: 10px;
  background: var(--surface-panel);
  color: var(--text-panel);
  box-shadow: 0 10px 28px rgb(0 0 0 / 0.4);
}

/* Focused programmatically when the popover opens, since the first thing
   inside is a label rather than a control. The outline would otherwise appear
   on a panel the reader did not tab to, which reads as an error. */
.override-popover-panel:focus {
  outline: none;
}

.override-popover-panel:focus-visible {
  outline: 2px solid var(--flow-casing);
  outline-offset: 2px;
}

/* `fixed`, not `absolute`. Its parent is `body`, which clips (it is the
   window's height and does not scroll), so an absolutely-positioned panel
   whose place is below the fold is clipped away to nothing — which is what
   happened: the panel came out zero-high at the foot of the page instead of
   under the readout that opened it.

   Safe here for the same reason the page bar no longer needs `fixed`:
   nothing between this and the window scrolls, so viewport coordinates and
   page coordinates are the same, and the figures `_place` computes from
   `getBoundingClientRect` are already in the right frame. */
.self-use-popover-panel {
  position: fixed;
  /* **Above `.subject-modal-backdrop`, which is 40**, because the chooser is
     opened from inside the net's panel now that the figures live there
     (CF-PWR-108). At 30 it opened *behind* the backdrop: invisible, and the
     modal's own outside-press handler treated the press as a press outside
     itself and shut the panel. Matched to the override popover's 41 for the
     same reason — both are opened from within a subject panel. */
  z-index: 41;
  padding: 0.55rem 0.6rem;
  border: 1px solid var(--border-control);
  border-radius: 8px;
  background: var(--surface-panel);
  color: var(--text-panel);
  box-shadow: 0 8px 24px rgb(0 0 0 / 0.35);
}

.self-use-popover-title {
  margin: 0;
  font-size: 0.7rem;
  font-weight: 600;
  color: var(--text-muted);
}

/* States what the choice reaches, since it is opened from one of three
   controls and would otherwise look like it applied to that one alone
   (CF-PWR-055). Held narrow so it wraps to two short lines rather than
   stretching the panel wider than the buttons under it. */
.self-use-popover-note {
  margin: 0.15rem 0 0.45rem;
  max-width: 15rem;
  font-size: 0.68rem;
  line-height: 1.35;
  color: var(--text-muted);
  opacity: 0.8;
  white-space: normal;
}

/* The two options sit as one group with a divider, the same way the view
   toggle does: they are two states of one setting, not two commands. */
.self-use-choices {
  display: inline-flex;
  border: 1px solid var(--border-control);
  border-radius: 6px;
  overflow: hidden;
}

.self-use-choice {
  padding: 0.3rem 0.6rem;
  border: 0;
  background: var(--surface-card);
  color: var(--text-muted);
  font-family: inherit;
  font-size: 0.74rem;
  white-space: nowrap;
  cursor: pointer;
}

.self-use-choice + .self-use-choice {
  border-left: 1px solid var(--border-control);
}

.self-use-choice:hover:not(.active) {
  color: var(--text-panel);
}

/* The one in force. Carried by fill rather than by a tick, so which mode is
   active is legible at a glance from the shape of the control. */
.self-use-choice.active {
  background: var(--flow-bore);
  color: var(--text-reading-active);
}

.self-use-choice:focus-visible {
  outline: 2px solid var(--flow-casing);
  outline-offset: -2px;
}

/* Pipes. Stroke widths are set per pipe from the power it carries, so
   none is given here.

   Everything that changes while the picture is live is transitioned: the
   colour as a flow starts or stops, the width as it grows, and the shape
   itself when a subject reverses and its flow moves to the other socket.
   Without that the move is a jump, which reads as a glitch rather than as
   the direction having changed. */
.flow-stream {
  fill: none;
  stroke-linecap: round;
  /* Only colour eases here. Neither the geometry nor the opacity may:
     both are animated frame by frame in flowAnimation.js, which supplies
     a fresh value every tick (CF-PWR-037), and a CSS transition would be
     chasing a target that has already moved — two easings fighting over
     one property, with the slower one winning. */
  transition: stroke var(--flow-transition, 600ms) ease-in-out;
}

.flow-casing {
  stroke: var(--pipe-casing);
}

.flow-fill {
  stroke: var(--pipe-bore);
}

.flow-casing.flow-active {
  stroke: var(--flow-casing);
}

.flow-fill.flow-active {
  stroke: var(--flow-bore);
}

/* Sockets take the pipes' treatment, so they read as fittings on the same
   plumbing. An empty socket stays visible: it is what shows the
   connection exists and which way it would carry power. */
.socket {
  fill: var(--socket-fill);
  stroke: var(--socket-stroke);
  stroke-width: 3;
  stroke-linejoin: round;
  /* Lights up and dims with the flow that plugs into it, over the same
     span the pipe takes to move. */
  transition:
    fill var(--flow-transition, 600ms) ease-in-out,
    stroke var(--flow-transition, 600ms) ease-in-out;
}

.socket.flow-active {
  fill: var(--flow-bore);
  stroke: var(--flow-casing);
}

/* The hub takes the same treatment, so it reads as part of the same
   plumbing rather than an icon sitting on top of it. */
.hub-casing {
  fill: var(--pipe-casing);
}

.hub-fill {
  fill: var(--pipe-bore);
}

.hub-casing.flow-active {
  fill: var(--flow-casing);
}

.hub-fill.flow-active {
  fill: var(--flow-bore);
}

/* The pipe's interior is a run of pointy segments. Even segments match
   the pipe's own fill, odd ones are a shade darker: deliberately low
   contrast, so the movement and its direction read without the pattern
   drawing the eye.

   They are painted on a canvas standing between the two SVGs of the
   picture — pipes below, hub and sockets above — rather than drawn as SVG
   shapes; flowView.js says why. The canvas takes its two shades from
   `--flow-segment-even` and `--flow-segment-odd`, read off its own
   computed style, so the themes reach it exactly as they reach the SVG.

   Same box and same stacking as `.flow-view`, so document order alone
   puts it between the two. */
.flow-segments {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 1;
  pointer-events: none;
}

/* All in-picture text is centred on the point it is given. */
.flow-view text {
  text-anchor: middle;
  dominant-baseline: central;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}

.hub-total {
  fill: var(--text-hub-total);
  font-size: 0.95rem;
  font-weight: 600;
}

/* Live power leads; today's total sits under it, quieter. */
/* An idle reading has to stay legible against the dark socket without
   competing with an active one — hence a light grey that is clearly not
   the near-white the live figures use. */
/* Both readings are sized in `em`, not `rem`, so that one pane can hold
   them at a different size from the other. The timelines pane divides its
   own `font-size` by the interface scale (CF-PWR-061) because its sockets
   do not grow with it; in `rem` these would have gone on growing inside an
   arrow that had stopped, and outgrown the outline they sit in. The flow
   pane sets no font size of its own, so there `em` is the root's `rem` and
   these are the sizes they always were. */
.socket-live {
  fill: var(--text-reading-idle);
  font-size: 0.82em;
  font-weight: 600;
  transition: fill var(--flow-transition, 600ms) ease-in-out;
}

.socket-live.flow-active {
  fill: var(--text-reading-active);
}

/* The energy total, and the money line beneath it on the net's sockets
   (CF-PWR-151).

   One rule for both, because the reader asked for exactly that — the money
   line "can be the same font and colour as the total power". Sharing the
   declaration rather than repeating its two values is what stops the pair
   drifting apart the first time either is touched. */
.socket-total,
.socket-cost {
  fill: var(--text-reading-total);
  font-size: 0.72em;
}

/* A reading is set in three pieces so it fits inside a socket: the whole
   number at full size, the decimal raised and small — which lets the
   decimal point go entirely — and the unit small, since it is the part
   the eye can most readily skip. */
.reading-fraction {
  font-size: 0.68em;
}

.reading-unit {
  font-size: 0.72em;
}

/* A currency symbol leads its number and is part of the figure, not a unit
   appended to it — so it is set with the digits rather than shrunk like a
   trailing "kWh". Held a shade back, since the symbol is the same on every
   reading and the number is what differs. */
.reading-prefix {
  font-size: 0.86em;
  opacity: 0.8;
}

/* Histograms. The frame is what shows a subject has a history at all,
   including before any has arrived, so it is drawn whether or not there
   are columns in it. */
.histogram-frame {
  fill: var(--histogram-surface);
  stroke: var(--histogram-border);
  stroke-width: 1;
  rx: 4;
}

/* The irradiance band above the solar row (CF-PWR-126). One rect per run of
   equal light, its fill computed per stripe — the value *is* the colour
   here, so there is no class that could carry it. What is here is only what
   every stripe shares.

   Clipped to the frame's own rounded corners so the band reads as one strip
   rather than as a row of rectangles laid over a rounded box. */
.irradiance-stripe {
  shape-rendering: crispEdges;
}

/* A stretch the backend holds no forecast for (API-CONTRACT §4.4.1). Not
   black: "the sun will not shine" and "nothing is known" are different
   statements, and drawing the second as the first is the one misreading
   that section names outright. It takes the row's own surface instead, so
   an unknown stretch reads as a hole in the band rather than as night. */
.irradiance-unknown {
  fill: var(--histogram-surface);
}

/* What has not happened yet (CF-HIST-085): a wash in the page's own colour
   over the predicted stretch, taking the colour out of the series beneath
   without drawing any of them a second time in grey.

   Not pointer-transparent by accident — `pointer-events: none` is what
   keeps the hover readout working across the forecast. The wash covers the
   graph, and without this it would swallow every press and hover on the
   right-hand part of the stack, which is the part somebody switched to
   this window to interrogate. */
.histogram-forecast-wash {
  fill: var(--forecast-wash);
  opacity: var(--forecast-wash-alpha);
  pointer-events: none;
}

/* A stretch panned to but not yet fetched (CF-HIST-090).

   It pulses, which is the whole point: a gap that has been asked for and a
   gap the backend has nothing for are both flat, so the difference has to
   be motion rather than colour. "Coming" is a thing in progress and reads
   as one; "nothing here" is still.

   Distinct from the forecast wash beside it — that stretch has data and is
   merely uncertain, where this one has none yet. */
.histogram-awaited-wash {
  fill: var(--forecast-wash);
  pointer-events: none;
  animation: awaiting-timeline 1.4s ease-in-out infinite;
}

@keyframes awaiting-timeline {
  0%, 100% { opacity: calc(var(--forecast-wash-alpha) * 0.55); }
  50% { opacity: calc(var(--forecast-wash-alpha) * 1.5); }
}

/* Someone who has asked not to see motion still needs to know the stretch
   is awaited rather than empty, so the pulse becomes a steady wash. */
@media (prefers-reduced-motion: reduce) {
  .histogram-awaited-wash {
    animation: none;
    opacity: calc(var(--forecast-wash-alpha) * 1.2);
  }
}

/* The seam where measurement stops and estimate begins (CF-HIST-086).

   Solid, and the only solid vertical on the graph. It was dashed, on the
   reasoning that a solid rule at this weight reads as structure where this
   is a statement about the data — which is true in isolation and wrong
   beside its neighbours: the day marks are dashed too, so the one line
   that says how far the facts reach looked like one more piece of calendar
   furniture among several. Distinguishing them by dash *length* would be
   asking the reader to measure; distinguishing them by kind is immediate.

   Brighter than the day lines for the same reason it is solid: those are
   furniture and this is the mark the graph is read against. */
.histogram-boundary {
  stroke: var(--forecast-boundary);
  stroke-width: 1;
  opacity: 0.85;
  pointer-events: none;
}

/* Hidden by an attribute the drawing sets, not by a width the drawing
   sets.

   A presentation attribute loses to *any* CSS declaration, so a rule
   giving `stroke-width` a value here silently overrode a `stroke-width="0"`
   written onto the element — the line went on being drawn at 1px while the
   attribute said 0, and the boundary stayed on screen over windows that
   had none. It looked like a stale repaint for some time, because the
   markup read correctly and only the computed style disagreed.

   `hidden` is the same mechanism the folded controls use, and it cannot be
   overridden by a width. */
.histogram-boundary[hidden] {
  display: none;
}

/* The price row's `histogram-now` carries no rule of its own: it marks the
   present with exactly the treatment above. On the subject rows one line
   is both the present and the edge of what is known; on the price row they
   are two instants, and giving them different treatments would say they
   were different kinds of mark when what differs is only that the price
   row knows further ahead. */

/* A series is a filled area under a bright line. The line carries the
   shape of the day; the fill under it gives that shape enough weight to
   read from across the room (CF-HIST-021). The two directions take
   different colours so a subject that does both is never ambiguous about
   which half was which (CF-HIST-008). */
.histogram-line {
  fill: none;
  stroke-width: 1.6;
  stroke-linejoin: round;
  stroke-linecap: round;
}

.histogram-line-import {
  stroke: var(--histogram-import);
}

.histogram-line-export {
  stroke: var(--histogram-export);
}

/* The lower edge of each column's range. Quiet: it says where a column
   started, while the bright line says how far it reached. */
.histogram-trough {
  fill: none;
  stroke-width: 1;
  opacity: 0.45;
}

.histogram-trough-import {
  stroke: var(--histogram-import);
}

.histogram-trough-export {
  stroke: var(--histogram-export);
}

.histogram-area {
  stroke: none;
}

/* Solid where the line is, gone by the baseline. */
.histogram-stop-near {
  stop-opacity: 0.55;
}

.histogram-stop-far {
  stop-opacity: 0;
}

.histogram-stop-import {
  stop-color: var(--histogram-import);
}

.histogram-stop-export {
  stop-color: var(--histogram-export);
}

/* State of charge: a line alone, no fill, on its own 0..100% axis. Dashed
   and in a colour neither direction uses, so it never reads as a third
   quantity flowing (CF-HIST-022). */
.histogram-soc {
  fill: none;
  stroke: var(--histogram-soc);
  stroke-width: 1.3;
  stroke-dasharray: 4 3;
  stroke-linejoin: round;
  opacity: 0.9;
}

/* The line dividing import from export on a subject that does both. */
.histogram-baseline {
  stroke: var(--histogram-border);
  stroke-width: 1;
}

/* Midnight. Dashed and quiet: it is a reference mark, not data
   (CF-HIST-007). */
.histogram-day-line {
  stroke: var(--histogram-day-line);
  stroke-width: 1;
  stroke-dasharray: 3 3;
}

/* The round values between the baseline and the maximum (CF-HIST-009).
   Solid where the day line is dashed — it runs the other way, so the two
   never read as one mark — and fainter than the baseline, which is the
   line these are measured from and has to stay the stronger of the two. */
.histogram-grid {
  stroke: var(--histogram-border);
  stroke-width: 1;
  opacity: 0.35;
}

/* The per-subject maximum (CF-HIST-009), against the axis it describes. */
.histogram-axis-label {
  fill: var(--text-reading-total);
  font-size: 0.68rem;
  text-anchor: end;
  font-variant-numeric: tabular-nums;
}

/* Where the data ran past the clip. A clipped peak is drawn flat, and flat
   is a shape the data might genuinely have, so the cut is marked rather
   than left to be inferred (CF-HIST-024). */
.histogram-clip-edge {
  fill: none;
  stroke-width: 2.5;
  stroke-dasharray: 2 3;
  stroke-linecap: round;
}

.histogram-clip-edge-import {
  stroke: var(--histogram-import);
}

.histogram-clip-edge-export {
  stroke: var(--histogram-export);
}

/* A maximum the data ran past reads "≥ 4 kW", so it is never taken for
   the real peak. */
.histogram-axis-clipped {
  font-style: italic;
}

/* The hover readout (CF-HIST-026). The line crosses every graph, because
   the question worth asking here is comparative. */
.scrub-line {
  position: absolute;
  width: 1px;
  background: var(--scrub-line);
  pointer-events: none;
  z-index: 4;
}

/* Where the line meets the row being read (CF-GEN-019): `.readout-dot`, drawn
   in HTML because the line it sits on is. */
.scrub-dots {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 4;
  pointer-events: none;
}

.scrub-dot {
  position: absolute;
  width: 7px;
  height: 7px;
  margin: -3.5px 0 0 -3.5px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 0 0 1px rgb(0 0 0 / 0.55);
  pointer-events: none;
}

.scrub-tooltip {
  position: absolute;
  z-index: 5;
  pointer-events: none;
  padding: 0.4rem 0.55rem;
  border: 1px solid var(--border-control);
  border-radius: 6px;
  background: var(--surface-panel);
  color: var(--text-panel);
  font-size: 0.7rem;
  white-space: nowrap;
  box-shadow: 0 2px 10px rgb(0 0 0 / 0.35);
}

.scrub-time {
  font-weight: 600;
  font-size: 0.78rem;
  color: var(--text-primary);
  margin-bottom: 0.2rem;
  font-variant-numeric: tabular-nums;
}

/* The period has not finished (CF-HIST-116).

   Lighter and unemphasised against the time it follows: it qualifies that
   period rather than naming a second one, and at the same weight it would
   read as part of the timestamp. Muted is the same colour the row labels
   beneath it use, which is what a qualifier should look like here. */
.scrub-running {
  font-weight: 400;
  color: var(--text-muted);
}

.scrub-tooltip table {
  border-collapse: collapse;
}

.scrub-tooltip th {
  text-align: left;
  font-weight: 400;
  color: var(--text-muted);
  padding-right: 0.6rem;
}

.scrub-tooltip td {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* The readout names each figure in the colour its graph uses, so a number
   and the area it came from are the same thing. */
.scrub-in {
  color: var(--histogram-import);
}

.scrub-out {
  color: var(--histogram-export);
}

.scrub-soc {
  color: var(--histogram-soc);
}

.scrub-idle {
  color: var(--text-reading-idle);
}

/* The span being dragged over, across every graph at once — the selection
   is of a stretch of time, which all of them share (CF-HIST-038). */
.scrub-band {
  position: absolute;
  z-index: 3;
  pointer-events: none;
  background: var(--flow-bore);
  opacity: 0.28;
  border-left: 1px solid var(--scrub-line);
  border-right: 1px solid var(--scrub-line);
}

/* The lower axis pinned to the bottom of the window, for a screen too
   short to show the whole stack (CF-HIST-020). Hidden until then: on a
   laptop the axis in the drawing is already visible and this would be a
   duplicate lying over the graphs. */
/* Its `left` and `width` are set from the stage's measured position rather
   than declared here, so the sticky copy lands on exactly the same pixels
   as the axis in the drawing. Centring it in the viewport instead — with
   its own padding and max-width guessing at where the stage is — puts its
   labels at a different horizontal position from the graphs they label,
   which is worse than having no sticky axis at all. */
/* `visibility` rather than `display` for showing and hiding this band:
   both are fixed to the viewport, and toggling `display` on one of them on
   every scroll event — this one shows and hides as the page scrolls past
   the real axis — made an unrelated fixed element elsewhere on the page
   (the top bar) paint a scroll or two behind where it actually was, for as
   long as the page kept moving. Chromium has to rebuild more of its
   compositing tree for a `display` change than for a `visibility` one, and
   doing that on every scroll frame is what left the bar's own layer
   stale. `visibility: hidden` costs nothing extra here: this band was
   already `pointer-events: none`, so being hit-testable while invisible
   changes nothing a reader could act on. */
.history-sticky-axis {
  position: fixed;
  bottom: 0;
  visibility: hidden;
  padding: 0.15rem 0 0.3rem;
  background: var(--surface-page);
  border-top: 1px solid var(--border-control);
  z-index: 6;
  pointer-events: none;
}

.history-sticky-axis.visible {
  visibility: visible;
}

/* The upper axis, pinned directly beneath the pinned head and forming the
   foot of that block (CF-HIST-020, CF-HIST-103).

   It no longer appears only on a short screen. The head above it is pinned
   in every view (CF-HIST-103) and the axis is the last of the three bands
   the user asked to stay put, so it is shown whenever the timelines are
   the settled view and hidden otherwise — there is no scroll threshold
   left to be on the wrong side of.

   Its `top` is written by the page from the pinned head's measured height,
   for the reason the head's own `left`/`width` are: the head is controls
   and text, whose height follows the platform's font and the interface
   scale, and cannot be stated in a stylesheet.

   The border goes on the other side, since this one is a lid rather than
   a floor: what it separates is above it. */
.history-sticky-axis-top {
  bottom: auto;
  /* Nothing below the labels. The drawing already ends at its axis line,
     and this band sits directly above the graphs it labels — any padding
     here is empty page between the times and the data. */
  padding: 0.3rem 0 0;
  border-top: none;
  border-bottom: 1px solid var(--border-control);
}

.history-sticky-axis svg {
  display: block;
  overflow: visible;
}

/* The one time axis under the whole stack (CF-HIST-020). */
.axis-tick {
  stroke: var(--histogram-border);
  stroke-width: 1;
}

.axis-tick-day {
  stroke: var(--histogram-day-line);
}

.axis-label {
  fill: var(--text-reading-total);
  font-size: 0.68rem;
  text-anchor: middle;
  font-variant-numeric: tabular-nums;
}

/* Midnight is where the date changes — the one label worth picking out of
   the row. */
.axis-label-day {
  fill: var(--text-label);
  font-weight: 600;
}

/* The debug scenario panel. Deliberately plain and out of the way: it is
   a development aid, not part of the application (CF-DEBUG-008). */
/* The debug controls, inside the settings page's Developer section.
   Nothing here floats: the panel was `position: fixed` in a corner until
   revision 121, which meant a handle permanently over every page — see
   SPEC.md §Debug scenario panel for why that went.

   `.debug-panel` is the host the settings page hands over; it needs a rule
   only for the separation from the switch above it. */
.debug-panel {
  margin-top: 0.9rem;
  padding-top: 0.9rem;
  border-top: 1px solid var(--border-control);
  font-size: 0.8rem;
  color: var(--text-panel);
}

.debug-body {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

/* Says where the tool it belongs to actually acts, since the switch and the
   picture it applies to are now on different pages. */
.debug-hint {
  margin: -0.1rem 0 0.2rem;
  color: var(--text-muted);
}

.debug-toggle {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  cursor: pointer;
}

/* Both of the panel's dropdowns take the same treatment. `color-scheme`
   is what makes the native control itself — the popup list, the drop
   arrow — follow the theme; without it a light theme keeps a dark popup
   on a dark OS, since the widget is drawn by the platform rather than by
   these rules. */
.debug-scenario,
.debug-select {
  background: var(--surface-card);
  color: var(--text-panel);
  border: 1px solid var(--border-control);
  border-radius: 4px;
  padding: 0.25rem;
  font-size: 0.8rem;
  color-scheme: var(--control-scheme);
}

.debug-scenario:disabled {
  opacity: 0.45;
}

/* The layout tool (CF-DEBUG-014). While it is on, the cards and the hub
   are handles rather than pictures — so they say so under the pointer, and
   nothing inside them takes the drag instead. */
.layout-dragging {
  cursor: grab;
  /* The browser decides whether a touch pans the page *before* any
     pointer event is delivered, so `preventDefault` in the handler is too
     late — the page had already started scrolling under the drag, which
     is what put a scrollbar on screen mid-drag. This is the only thing
     that tells it not to.

     `none` rather than `pan-y`: the picture is dragged in both axes, and
     leaving either one to the browser means half of every diagonal drag
     scrolls the page instead. It applies only while the tool is on. */
  touch-action: none;
  /* A drag that crosses a label would otherwise select the text in it and
     leave the picture behind a blue highlight. */
  user-select: none;
}

.layout-dragging .subject-card,
.layout-dragging .flow-layer-hub {
  cursor: grab;
}

/* While something is actually being dragged, the cursor says so wherever
   it goes — the pointer is captured, so it is often outside the item. */
.layout-dragging.layout-dragging-active,
.layout-dragging.layout-dragging-active .subject-card {
  cursor: grabbing;
}

.layout-dragging .subject-card * {
  /* The card is the handle, not the button inside it: a drag that started
     on the self-use badge would select text or fire the badge instead. */
  pointer-events: none;
}

.debug-layout-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
}

.debug-layout-actions button {
  background: var(--surface-card);
  color: var(--text-panel);
  border: 1px solid var(--border-control);
  border-radius: 4px;
  padding: 0.2rem 0.4rem;
  font-size: 0.72rem;
  cursor: pointer;
}

.debug-layout-actions button:hover {
  border-color: var(--flow-casing);
}

.debug-note {
  margin: 0.2rem 0 0;
  font-size: 0.68rem;
  color: var(--text-muted);
  min-height: 1em;
}

/* Bars, at resolutions where one sample is a period rather than a moment
   (CF-HIST-058, CF-HIST-059). Filled solid rather than with the line
   rendering's gradient: under a line the shape is the edge and the fill
   beneath it is weight, but a bar's whole body is the quantity, and one
   fading towards the axis would read as a bar half as tall. */
.histogram-bars {
  stroke: none;
  opacity: 0.85;
}

.histogram-bars-import {
  fill: var(--histogram-import);
}

.histogram-bars-export {
  fill: var(--histogram-export);
}

/* The period still being assembled (CF-HIST-088).

   Its figure is real but incomplete — the element is a mean over the whole
   interval and only part of that interval has happened — so the bar stands
   short of where it will finish. At full strength that is indistinguishable
   from a genuinely quiet period, which is the misreading the dimming exists
   to prevent: it says "this one is not finished" without inviting a height
   to be read off it.

   Dimmed rather than hidden. On a coarse window hiding it would leave the
   newest column missing for up to a quarter of an hour, which reads as data
   failing to arrive rather than as a period in progress. Set against the
   0.85 the finished bars carry, low enough to be plainly a different
   statement and high enough to still show the period has begun. */
.histogram-bars-partial {
  opacity: 0.34;
}

/* The price row of the History stack (CF-COST-014 .. CF-COST-019).

   Each bar carries its own fill, set from the price it stands for, so the
   colour is not listed here: it is continuous between the cheapest and
   dearest of whatever window is on screen and no fixed set of classes can
   express it (see priceView.js). What belongs here is everything that is
   the same for every bar. */
.price-bar {
  stroke: none;
  opacity: 0.9;
}

/* The price row's vertical axis (CF-COST-020): a few round prices with a
   gridline each, so a bar's top can be read off rather than estimated.
   Quiet enough to be a scale rather than a grid competing with the bars. */
.price-grid {
  stroke: var(--histogram-day-line);
  stroke-width: 1;
  opacity: 0.5;
}

/* Zero is picked out: it is the one price on this row that means something
   on its own, since below it you were paid rather than charged. */
.price-grid-zero {
  stroke: var(--histogram-border);
  opacity: 1;
}

/* Set to match the subject rows' own axis labels, so the two columns of
   figures down the left of the stack read as one. */
.price-axis-label {
  fill: var(--text-reading-total);
  font-size: 0.62rem;
  text-anchor: end;
  font-variant-numeric: tabular-nums;
}

/* The two prices that anchor the colour scale. Set small and quiet: they
   are there to calibrate the row, not to be read before it. */
.price-label {
  fill: var(--text-label);
  font-size: 0.62rem;
  font-variant-numeric: tabular-nums;
  text-anchor: middle;
  paint-order: stroke;
  /* Carried over the bar it names where the row is tight, so the figure
     stays legible against a saturated fill rather than disappearing into
     it. */
  stroke: var(--surface-page);
  stroke-width: 2.5px;
  stroke-linejoin: round;
}

/* The price card has no sockets and no readings, so its shell holds only
   the mark and the label — but it is the same shell as every other, since
   a row that looked different would read as a different kind of thing
   rather than as one more row of the same stack.

   Its opacity is set from the view transition's own progress, frame by
   frame, so there is deliberately no CSS transition on it: a transition
   would be restarted on every frame by the new value and so would never
   arrive, leaving the card stranded part-faded over the flow picture. The
   movement it needs is already in the progress it is given. */
.subject-card-price {
  /* Nothing to hover or click on a card that carries no controls, and at
     zero opacity it must not intercept what is behind it. */
  pointer-events: none;
}

/* Whether the prices carry tax (CF-COST-011). Smaller and quieter than the
   row's own name: it qualifies the figures rather than naming the row, and
   a reader who has not changed the setting should be able to ignore it.

   Sized from `--subject-shell-size` like the label above it, so a smaller
   shell carries a smaller marker - the fault §Framework describes, where a
   fixed length drawn for the large shell crowded the small one.

   Empty until a payload states a basis, and an empty span occupies no room,
   so nothing reserves space for a marker that may never come. */
.subject-price-basis {
  position: relative;
  z-index: 2;
  font-size: calc(var(--subject-shell-size) * 0.1);
  color: var(--text-label);
  opacity: 0.75;
  white-space: nowrap;
}

/* Joins a price label in the reserved band to the bar it names. In the band
   the figure is no longer beside its bar, and on a row of ninety-six bars a
   figure that names no particular one is no answer at all. Quiet: it is a
   pointer, not data. */
.price-label-stem {
  stroke: var(--text-reading-total);
  stroke-width: 1;
  opacity: 0.55;
}

/* The value-mode switch: energy or money (CF-COST-021).

   Built from the view control's parts so the two read as one family, and
   deliberately smaller: that one picks between whole arrangements of the
   page, this one between two labellings of a single arrangement. */
.value-mode-toggle {
  display: inline-flex;
  border: 1px solid var(--border-control);
  border-radius: 6px;
  overflow: hidden;
  background: var(--surface-card);
  /* Inside the height, for the reason given on .mode-toggle. */
  box-sizing: border-box;
  height: var(--window-control-height);
  /* Held clear of the snap-mode toggle beside it (revision 93 put that
     toggle between this one and the range controls). The two decide
     different things — whether the window is kept to the calendar, and
     which unit it reads in — and controls that touch read as one control
     with several parts.

     Narrower than the 1.6rem it was. The gap only has to be plainly
     larger than the 0.25rem between the range buttons for the grouping to
     read, and the controls either side of it grew: on the narrowest
     screen this is width the row cannot spare, and a gap that pushed the
     view toggle off the edge would break CF-HIST-067 to make a point
     about grouping. */
  margin-left: 0.9rem;
}

.value-mode-button {
  display: flex;
  align-items: center;
  /* Fills its group, for the reason given on .mode-toggle-button. */
  align-self: stretch;
  padding: 0 var(--window-toggle-padding);
  border: none;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
  transition:
    background var(--flow-transition, 600ms) ease-in-out,
    color var(--flow-transition, 600ms) ease-in-out;
}

.value-mode-button svg {
  /* The same figure the view icons beside them carry, and grown for the
     same reason — see .mode-toggle-button svg. */
  width: 1.25rem;
  height: 1.25rem;
  fill: none;
  stroke: currentColor;
  /* Drawn on the same 100-unit grid the view icons use, so the weight is
     stated in those units. A little heavier than theirs, because at this
     size the same stroke reads as thin. */
  stroke-width: 8;
  stroke-linecap: round;
}

/* The bolt is a filled shape rather than a stroked path: at this size a
   stroked zigzag closes up into a blob. */
.value-mode-button svg .value-mode-icon-solid {
  fill: currentColor;
  stroke: none;
}

.value-mode-button.selected {
  background: var(--flow-bore);
  color: var(--text-reading-active);
}

/* While the answer to a mode change is in flight (CF-COST-024).

   On the graphs rather than on the button, because it is the graphs that
   are about to change and the graphs that are, for that moment, showing
   something other than what was asked for. They keep their data: it is
   still the correct answer to the previous question. */
.history-layer.value-mode-pending,
svg.value-mode-pending {
  animation: value-mode-waiting 1.1s ease-in-out infinite;
}

@keyframes value-mode-waiting {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.45; }
}

/* Someone who has asked not to see motion still needs to know the request
   is outstanding, so the pulse becomes a steady dimming rather than
   nothing at all. */
@media (prefers-reduced-motion: reduce) {
  .history-layer.value-mode-pending,
  svg.value-mode-pending {
    animation: none;
    opacity: 0.6;
  }
}

/* The zero line's own figure on a two-directional graph (CF-COST-025).
   Quieter than the two extents it sits between: those are the readings,
   this is the datum they are measured from. */
.histogram-axis-zero {
  opacity: 0.65;
}

/* A gridline's figure on a subject's panel (CF-PWR-114). As quiet as the
   zero's: reference marks, read only when a bar is being measured. */
.histogram-grid-label {
  opacity: 0.65;
}

/* A price in the hover readout. Neutral rather than tinted by direction:
   a price has no direction, and the bar's own colour already says where it
   sits between the window's cheapest and dearest. */
.scrub-price {
  color: var(--text-label);
}

/* A screen too narrow to afford its comforts (CF-PWR-058).

   The breakpoint is stated twice — here and as SMALL_SCREEN_PX in
   historyLayout.js — because a media query cannot read a module and the
   layout cannot read a media query. They have to agree: a stack laid out
   for sockets that this rule had already hidden would leave a column of
   nothing where the readings were. A test holds the two to the same
   figure. */
@media (max-width: 800px) {
  /* The page's own margin is the cheapest width on the screen to reclaim:
     it carries nothing. Not removed altogether, so the drawing does not
     sit flush against the bezel. */
  body {
    padding: 0.25rem;
  }

  /* And the stack's own side margin with it, for the same reason and on
     the same screens. `body`'s padding was already reclaimed here while
     this, the larger of the two, was not — 2rem a side is 64px, a sixth of
     a 400px phone, spent on empty page either side of the graphs.
     Set on the root rather than on `.page-stack` so the rail placement,
     which adds the bar's size to this figure, narrows with it instead of
     keeping the desktop margin next to a bar. */
  :root {
    --page-side-gap: 0.5rem;
  }

  /* The sockets and their readings, dropped together — but only in the
     History view.

     There the argument holds: what they carry is the live figure and the
     span total, the live figure is on the card beside them, and a graph
     squeezed to half its width has lost the shape that is the whole reason
     to draw it. In the Flow view none of that applies. Nothing is
     competing for the width, so hiding them buys nothing — and a socket is
     where its flow plugs in, so without it every pipe ends in mid-air
     short of the subject it belongs to.

     Hidden rather than removed: they are still positioned, and still there
     the moment the screen is wide enough again — or the moment the view
     changes back.

     Scoped to the timelines pane's own drawing rather than keyed to
     `.history-showing`. The settled class only turns on once the change has
     finished, so these layers stayed drawn for the whole of a fade and
     disappeared on its last frame — a line left lying above the Net row
     after everything around it had gone. Keyed to the pane, the pane that
     must not show them never shows them, at any point in a change or
     outside one. It is the same correction as the shell size below: a
     per-view difference belongs to the view, not to a moment. */
  .view-pane-history .flow-layer-fittings,
  .view-pane-history .flow-layer-labels {
    display: none;
  }

  /* The shells give up a quarter of their diameter — in the History view
     only.

     They are sized for an icon, a label and the depth a socket reaches
     into them. In History the sockets are not drawn, so that last part is
     unused and the size that was right beside them is larger than this
     screen can justify. In Flow the sockets are drawn at their own fixed
     size, and a shell shrunk out from under them leaves the arrows
     overlapping the cards and each other — so this is never asked of Flow
     at all.

     Set on `.view-pane-history` rather than on the shared `.power-stage`:
     `--subject-shell-size` inherits to both panes' cards from wherever it
     is set, and stated on the stage it reached Flow's cards too, which had
     no reason to change size for a fade that is not about them. Scoped to
     History's pane, only History's cards ever see this figure; Flow's stay
     at the base size throughout.

     Unconditional — no class, no moment, nothing to do with the change.
     The two panes hold entirely separate cards, so this pane's cards can
     simply *be* this size, always, and Flow's can always be the base size.
     Neither ever changes; a fade is a change of opacity over two boxes that
     were already the size they will still be afterwards.

     Every previous version tied it to something about the change, and each
     was visible in its own way. Tied to `--history-controls-opacity` the
     cards slid continuously, 98px to 74px, shrinking while the fade ran.
     Tied to `.history-showing`, the settled view, the step landed on the
     last frame under a pane long since at full strength. Tied to
     `.history-bound`, the view being headed for, it landed on the *first*
     frame — while the timelines pane was still fully opaque, so pressing
     the flow button visibly enlarged the graphs' subjects before anything
     had begun to fade. That last one also brought back the net's second
     percentage, which the smaller card clips and the larger one does not.

     The mistake all three share is treating a per-view constant as
     something that has to be *switched* between views. It does not: each
     view has its own cards, so each view's cards just have their own size.
     This is the same correction the stage height needed one level up, and
     it is worth stating as a rule — if a value differs between the views
     and each view owns the element it applies to, give each element its own
     value and there is no moment to get wrong.

     The division by `--ui-scale` is the pane's own unscaling (CF-PWR-061)
     carried through this override, not a second decision. It has to be
     repeated because this rule replaces the size outright rather than
     adjusting it, and a narrow window can still be tall enough to be
     scaled — 800px across and 1150 down reaches 1.1, which is exactly the
     screen this rule exists for and would have put the shells back over
     their rows. The subtraction is applied to the base and the whole
     result divided, so the shell gives up the same share of itself here as
     it does at any other scale. */
  .view-pane-history {
    --subject-shell-size: calc(
      (var(--subject-shell-base-size) - 1.5rem) / var(--ui-scale, 1)
    );
  }


  /* The range description already sits above its controls at every width;
     here the row beneath it closes up, since a small screen has less to
     spare between buttons. */
  .window-summary {
    gap: 0.35rem;
  }

  /* Closer to the controls beside it than on a wide screen, where the gap
     was there to say the two are separate. That still has to read, but a
     third of a line of text is enough at this size.

     Both gaps close, keeping the wider one wider: this is the width where
     the row is tightest, and it is also where losing the nesting would
     matter most, since it is where the controls are packed closest
     together and the grouping has least else to go on. */
  .snap-mode-toggle {
    margin-left: 0.6rem;
  }

  .value-mode-toggle {
    margin-left: 0.6rem;
  }

  .mode-toggle {
    margin-left: 0.3rem;
  }
}

/* ---------------------------------------------------------------------------
   The page bar and the shell it sits in (SPEC.md §Overview, CF-NAV-002,
   CF-NAV-003).
   --------------------------------------------------------------------------- */

/* Everything the bar leaves is what a page gets. Stated as one custom
   property, set by the bar itself from its own measured size, because two
   things need the figure and neither can measure it: these rules, which
   have to keep the page off the bar, and the sticky time axis, which pins
   itself to the window's bottom edge from script. A number written twice
   is a number that will disagree the first time a button's padding
   changes, and the axis would then sit under the bar. */
:root {
  --page-bar-size: 0px;
}

/* The strip of page left around the shell at the window's edges. Written
   here rather than left to the browser's own `body` padding, because two
   things need the figure: `body`, which draws it, and the sticky time axis,
   which is fixed to the *window* and has to clear both this and the bar to
   land on top of the bar rather than 4px into it. */
:root {
  --page-edge-gap: 4px;
}

/* A page button's own height, and half of it. The bottom bar adds the half
   below its buttons so the row clears the iPhone's home indicator — the
   drag line for the app switcher, which runs across the very bottom of the
   screen and was catching thumbs aimed at the buttons.
   Held as a variable because three rules need the figure: the button's own
   `min-height`, the bottom bar's extra padding, and nothing may state it
   twice — a button that grew while the padding did not would put the row
   back over the line. */
:root {
  --page-button-size: 2.75rem;
  --page-button-half: calc(var(--page-button-size) / 2);
}

.page-bar {
  /* Not `fixed` at the top and bottom edges: the bar is a flex item in a
     shell that does not scroll, which holds it at its edge without asking
     any engine to agree about what `fixed` means inside a scrolling
     ancestor. The rail re-declares `position: fixed` for itself, where it
     is wanted for a different reason — see `.page-bar-rail`. */
  position: relative;
  display: grid;
  gap: 0.25rem;
  /* A step off the page rather than the page's own colour, so the band
     reads as a bar before its rule is noticed. The step is small on
     purpose: much more and it stops being an edge of the page and starts
     being a panel laid on top of one. */
  background: var(--surface-bar);
  z-index: 8;
}

.page-button {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.15rem;
  padding: 0.3rem 0.35rem;
  border: 1px solid transparent;
  border-radius: 8px;
  background: none;
  color: var(--text-muted);
  font: inherit;
  cursor: pointer;
  /* A target a thumb can hit without aiming. Below this the bar is a row
     of things that have to be tapped twice, which is worse than a bar
     that costs a little more height. */
  min-width: var(--page-button-size);
  min-height: var(--page-button-size);
}

.page-icon {
  display: block;
  width: 1.5rem;
  height: 1.5rem;
}

/* Stroke-based like the subject icons, so one rule sets the weight and
   the colour for both families and they degrade alike. */
.page-icon-art {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.page-button-label {
  font-size: 0.65rem;
  line-height: 1;
  white-space: nowrap;
}

.page-button:hover:not(.selected) {
  background: var(--surface-card);
  color: var(--text-primary);
}

.page-button.selected {
  background: var(--surface-card);
  border-color: var(--border-control);
  color: var(--text-primary);
}

.page-button:focus-visible {
  outline: 2px solid var(--flow-casing);
  outline-offset: 1px;
}

/* A page with no content yet. Dimmed rather than disabled: the page
   does exist and is reachable, it is simply empty, and a disabled button
   would say the opposite — that this part of the app is switched off. */
.page-button.unbuilt:not(.selected) {
  opacity: 0.55;
}

/* --- Bottom bar: a phone held upright (CF-NAV-003) ------------------------ */

.page-bar-bottom {
  grid-auto-flow: column;
  /* Every button the same width, so the row cannot shift under a thumb
     when one label is longer than another. */
  grid-auto-columns: 1fr;
  gap: 0;
  /* Top padding unchanged, so the buttons stay exactly where they were
     relative to the top of the bar; all the extra height goes below them.
     Half a button's worth on top of the safe-area inset — the inset alone
     leaves the row flush against the iPhone's home indicator, so a thumb
     reaching for the bottom buttons drags the app switcher instead. */
  padding: 0.15rem 0.15rem
    calc(0.15rem + var(--page-button-half) + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--border-control);
}

/* Seven buttons across a phone leave no room for a word under each. The
   icon is what is read at this size anyway; the name is still on the
   button for a screen reader, just not drawn. */
.page-bar-bottom .page-button-label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.page-bar-bottom .page-button {
  min-width: 0;
  border-radius: 6px;
}

/* --- Left rail: a phone on its side (CF-NAV-003) -------------------------- */

/* The rail is the one placement still taken out of the flow. A flex item in
   a column cannot sit down the side of its sibling, and the shell it is
   fixed within does not scroll, so `fixed` here is safe in a way it was not
   when the bar was inside the scrolling box. The stack keeps clear of it
   with a padding rather than by layout — see the rail's inset rule. */
.page-bar-rail {
  position: fixed;
  left: 0;
  top: 0;
  bottom: 0;
  grid-auto-flow: row;
  /* The buttons share the whole height between them, the way the bottom
     bar's share the whole width. Distributed rather than centred in a
     column of the same length: a rail whose buttons huddle in the middle
     reads as a floating strip that happens to be tall, where one filling
     the edge reads as the edge itself — which is what it is.

     `minmax(0, 1fr)` rather than `1fr`, so a set too tall for the screen
     shrinks its rows instead of overflowing the bar it is inside. */
  grid-auto-rows: minmax(0, 1fr);
  gap: 0;
  padding: 0.15rem calc(0.15rem + env(safe-area-inset-left, 0px)) 0.15rem 0.15rem;
  border-right: 1px solid var(--border-control);
}

/* Height is the scarce axis here, so the labels go for the same reason
   they do on the bottom bar — seven of them stacked is most of a
   landscape phone's height. */
.page-bar-rail .page-button-label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.page-bar-rail .page-button {
  min-width: 2.5rem;
  /* The row already sets the height, and a minimum taller than the share
     each row gets would push the last button off a short screen — the one
     screen this placement exists for. */
  min-height: 0;
  border-radius: 6px;
}

/* --- Top bar: laptop and tablet (CF-NAV-003) ------------------------------ */

/* A strip along the top edge, but only as wide as its buttons need. The
   two edge bars fill their edge because on those screens nothing else
   wants the room; here something does, so the bar takes what it needs and
   the rest of the width is left for whatever the page puts there.

   The buttons sit in one row rather than in a block. A block in the corner
   was the earlier arrangement and it cost the page a band as tall as three
   buttons before its content began — two-thirds of it air, since a
   three-by-three grid holding seven pages leaves five cells unused. */
.page-bar-top {
  top: 0;
  /* Spans the edge even though its buttons do not. The bar is a band the
     page sits below, exactly as the bottom and left bars are, and a band
     that stopped where its last button did would leave the rule under it
     ending in mid-air — which is what made this one read as loose buttons
     floating over the content rather than as a bar.

     The buttons still take only the width they need (CF-NAV-012); it is
     the surface behind them that reaches both edges. */
  grid-auto-flow: column;
  /* The last column takes the leftover width so the buttons stay packed to
     the left rather than spreading across the whole edge. */
  grid-auto-columns: min-content;
  grid-template-columns: auto;
  justify-content: start;
  gap: 0.2rem;
  /* The top inset matters here for the same reason the bottom one does on
     the bottom bar. Launched from the home screen the page runs under the
     status bar (`apple-mobile-web-app-status-bar-style: black-translucent`
     in index.html), so without this the clock and the battery sit on top
     of the first row of buttons. In a browser tab the inset is zero and
     this is the padding it always was. */
  padding: calc(0.25rem + env(safe-area-inset-top, 0px)) 0.4rem 0.25rem;
  /* The same rule the other two bars draw against their content. It is
     what separates the bar from the page, and its absence was the whole
     complaint. */
  border-bottom: 1px solid var(--border-control);
}

/* The icon and its word side by side, with enough padding that the row
   does not read as cramped. The word is worth drawing at this size —
   there is width for it, and it is what makes an unfamiliar icon legible
   on the first visit rather than the third. */
.page-bar-top .page-button {
  gap: 0.4rem;
  padding: 0.4rem 0.7rem;
  min-height: 2.4rem;
}

.page-bar-top .page-button-label {
  font-size: 0.8rem;
}

/* --- The sample-data marker (CF-DATA-029, CF-DATA-030) -------------------- */

/* Two marks for one state, which is what the requirement asks for: the chip
   names it in words, the bar's own edge carries a colour so it reads at a
   glance without the words being read. Both live on the bar because the bar
   is the only furniture on every page that never scrolls away — and because
   a screenshot of the app almost always includes it, which is the case
   CF-DATA-029 exists for.

   Hidden unless the bar is marked, so a live session pays nothing for it. */
.sample-chip {
  display: none;
}

.page-bar.sample-data .sample-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.2rem 0.55rem;
  border: 1px solid var(--sample-edge);
  border-radius: 999px;
  background: var(--surface-card);
  color: var(--text-label);
  font-size: 0.68rem;
  line-height: 1.2;
  white-space: nowrap;
  /* Not a control (CF-DATA-030) — nothing here can change the source, so it
     must not invite a press. */
  cursor: default;
  user-select: none;
}

/* The dot repeats the edge's colour on the chip itself, so the two marks
   read as one statement rather than two unrelated decorations. */
.page-bar.sample-data .sample-chip::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--sample-edge);
  flex: none;
}

/* The accent recolours the rule the bar already draws rather than adding a
   second line. A new line would change the bar's height, and the sticky time
   axis is positioned from that measured height (CF-NAV-015) — so an extra
   pixel here would move the axis on every page, for mock readers only. */
.page-bar-bottom.sample-data {
  border-top-color: var(--sample-edge);
}

.page-bar-rail.sample-data {
  border-right-color: var(--sample-edge);
}

.page-bar-top.sample-data {
  border-bottom-color: var(--sample-edge);
}

/* The chip sits past the buttons on the top bar, at the far end of the band
   that already spans the edge. */
.page-bar-top.sample-data .sample-chip {
  margin-left: auto;
  margin-right: 0.4rem;
  justify-self: end;
  align-self: center;
}

/* The bottom bar shares its width evenly between seven buttons and has no
   room for a word beside them; the rail has no width at all. Both keep the
   coloured edge, and the chip stays in the accessibility tree for a screen
   reader — the same arrangement the button labels already use at these
   sizes. */
.page-bar-bottom.sample-data .sample-chip,
.page-bar-rail.sample-data .sample-chip {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  padding: 0;
  border: 0;
}

/* --- The shell ------------------------------------------------------------ */

.app-shell {
  position: relative;
  /* Fills the window and clips, so the stack inside it is the only thing
     that can scroll. Column, so the stack takes whatever height is left
     once the bar has had its own — see `.page-stack`. */
  display: flex;
  flex-direction: column;
  height: 100%;
  overflow: hidden;
}

/* The one scrolling box in the application. Everything the pages draw is
   inside it; the bar is not, which is what stops the bar scrolling.
   `min-height: 0` because a flex item's default floor is its content, and
   without it the stack would grow to fit its pages rather than scrolling
   them.

   The page's own margin lives here rather than on `body`, so the scrollbar
   sits at the edge of the panel that is actually scrolling instead of down
   the far side of the window. Generous left and right, where a wide window
   has width to spare, and thin top and bottom, where it does not: the
   drawing area is what the height is for, and a margin above it is height
   the graphs never get.

   A narrow screen trims the sides too (see the small-screen rules); this is
   the vertical half of the same argument, and it applies at every width. A
   short window is short whether or not it is also narrow — a phone on its
   side is wide — so keying this on width alone left a band of empty page
   above the picture on exactly the screens with the least height to spare.

   Sideways scrolling is clipped, never reachable: the layout fits the width
   it is given and the timelines are panned rather than scrolled
   (CF-HIST-089). Without it a zoom flashed a horizontal scrollbar in and
   out on every step of the gesture, each flash costing a full relayout. */
.page-stack {
  flex: 1;
  min-height: 0;
  overflow-x: hidden;
  overflow-y: auto;
  padding: 0.35rem var(--page-side-gap);
}

/* The Flow view does not scroll (CF-HIST-065).

   It has nothing below the fold: the picture is drawn for
   `--flow-view-height`, which is itself the room the window leaves, and
   the cards are placed within it. What made the page scrollable there is
   the stage, which stands at the taller of the two views in *both* so that
   it never changes size — the price that rule states plainly and that the
   retired rule above `.power-page.history-showing .power-stage` explains
   at length. Below the crossover it is Flow that pays, by some tens of
   pixels of empty stage under a complete picture.

   Clipped rather than resized. Every attempt at giving the stage each
   view's own height failed on *when* to change it, four different ways;
   this touches no height at all, so the stage is as fixed as it was and
   there is no moment to find. What goes away is only the ability to
   scroll to the empty part.

   `:has()` because the class that says which view is settled sits on the
   page, and the box that scrolls is the page's own container two levels
   up. The alternative is the shell mirroring the page's view state onto
   itself — a second copy of a fact, kept in step by hand.

   Only for the *settled* Flow view. During a fade `history-showing` is off
   while the timelines are still on screen, and clipping then would take
   the scroll away from a stack that still needs it; `settledOnTimelines`
   turns the class on at the end of the change, so the window stays
   scrollable through the whole transition and is clipped once Flow alone
   is showing. The timelines view is untouched and scrolls as it did.

   `:not([hidden])` because a page that is not showing stays in the
   document — the shell hides it rather than removing it, so its state
   survives being navigated away from. Without that clause the Power page
   sitting hidden behind Settings would go on clipping this box, and
   Settings — a column long enough to need it — could not be scrolled at
   all.

   `:not(.bars-showing)` for the Bars view (CF-PWR-158), which is neither of
   the two and is a column of graphs read by scrolling. Under it the
   animator stands on Flow, so `history-showing` is off — and without this
   clause the view could not be scrolled past its first two graphs. */
.page-stack:has(> .power-page:not([hidden]):not(.history-showing):not(.bars-showing)) {
  overflow-y: hidden;
}

/* The bar is out of the flow in every placement, so the host is only
   there to hold it in the document. In the corner placement it is the
   positioned ancestor the block is measured from, which is why the shell
   is relative rather than the bar being fixed there too: a fixed corner
   block would stay put while the page scrolled under it and would then
   sit over whatever scrolled up to meet it. */
.page-bar-host {
  position: static;
}

/* A hidden page keeps its place in the document but is not drawn. Removed
   from the flow rather than merely transparent: an element still taking
   layout would keep the page as tall as the tallest page ever visited. */
.page-panel[hidden] {
  display: none;
}

/* What the bar leaves. The bar is a sibling of the stack inside a column
   flex box, so at the top and bottom edges it takes its own height out of
   the stack's simply by being there — nothing needs reserving, and a
   padding here would be air stacked on air. `order` puts it on the right
   edge; the stack is `order: 0` by default.

   The rail is the exception and still needs an inset: it is `fixed` down
   the left edge rather than in the column, so the stack would otherwise
   run underneath it. */
:root[data-bar-placement='bottom'] .page-bar-host {
  order: 1;
}

/* What the bar takes from *below* the stage, which is what the flow view
   has to keep clear of. Only the bottom placement takes any: a top bar is
   already counted in `--stage-top`, since it pushed the stage down, and a
   rail takes width rather than height. Subtracting the bar's size in every
   placement left the picture short by a bar's height on the two where the
   bar is not underneath it. */
:root[data-bar-placement='bottom'] {
  --stage-bottom-inset: var(--page-bar-size);
}

:root[data-bar-placement='rail'] .page-stack {
  padding-left: calc(var(--page-side-gap) + var(--page-bar-size));
}

/* The top bar takes the top edge. It is first in the column already, so
   the page begins below it without anything being reserved — see the
   bottom placement above for why there is no padding here. */
:root[data-bar-placement='top'] .page-bar-host {
  order: 0;
}

/* Nothing here for the pinned upper axis any more. It sits under the
   pinned head (CF-HIST-103), which is the element that clears the top bar
   now, and its own `top` is written from that head's measured height — so
   a second rule offsetting it by the bar as well would count the bar
   twice. See `.power-head` and `.history-sticky-axis-top`. */

/* The sticky time axis pins to the window's bottom edge and so does the
   bottom bar. Lifted by exactly the bar's own measured height, which is
   the whole reason that figure is published rather than written here as a
   guess.

   Still needed now the bar is in the flow rather than over the page: the
   axis is `fixed` to the *window*, so it is unaware of the shell's column
   and would sit behind the bar without this.

   The page's own bottom padding is in the sum as well, since the bar sits
   inside it: measured from the window's edge, the axis would otherwise
   overlap the top of the bar by exactly that padding. */
:root[data-bar-placement='bottom'] .history-sticky-axis:not(.history-sticky-axis-top) {
  bottom: calc(var(--page-bar-size) + var(--page-edge-gap));
}

/* On the Power page the range controls stand on the bar too (CF-HIST-122),
   so the pinned axis rides on them rather than on the bar. The page counts
   the same strip when it decides whether the drawn axis is still in view. */
:root[data-bar-placement='bottom'] .power-page .history-sticky-axis:not(.history-sticky-axis-top) {
  bottom: calc(var(--page-bar-size) + var(--page-edge-gap) + var(--power-controls-size, 0px));
}

/* --- Placeholder pages ---------------------------------------------------- */

.placeholder-page {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  /* Tall enough to read as a page rather than as a line of text that
     failed to load, without reaching for the full viewport height, which
     on a short screen would push it under the fold. */
  min-height: 60vh;
  text-align: center;
}

.placeholder-title {
  margin: 0;
  font-size: 1.6rem;
  font-weight: 600;
  color: var(--text-primary);
}

.placeholder-note {
  margin: 0;
  font-size: 0.95rem;
  color: var(--text-muted);
}

/* --- Settings page -------------------------------------------------------- */

.settings-page {
  max-width: 36rem;
  margin: 0 auto;
}

/* The title and the version badge share a line, the badge at the right
   (CF-CFG-013). Baseline-aligned rather than centred, so the small text
   sits on the title's baseline instead of floating against its cap height.
   Wrapping is allowed: on a narrow phone the badge drops below the title
   rather than squeezing it. */
.settings-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem 1rem;
  flex-wrap: wrap;
  margin: 0 0 1.2rem;
}

.settings-title {
  margin: 0;
  font-size: 1.4rem;
  font-weight: 600;
  color: var(--text-primary);
}

/* Deliberately quiet. This is a thing to look up when something seems
   wrong, not to read on every visit; it earns its place by being reliably
   *findable*, which is what pins it to a corner rather than what makes it
   loud. Same size as `.settings-hint`, which is the page's other
   say-it-once text. */
.settings-versions {
  font-size: 0.78rem;
  line-height: 1.35;
  color: var(--text-muted);
  text-align: right;
}

/* Two columns, so the figures line up under one another however wide the
   names are. Comparing the running version against the served one is the
   whole purpose, and a ragged pair is harder to compare than a column. */
.settings-version-grid {
  display: grid;
  grid-template-columns: auto auto;
  gap: 0 0.5rem;
  justify-content: end;
}

/* Tabular figures for the same reason: a proportional "1" is half the
   width of a "0", which throws the alignment the grid just made. */
.settings-version-value {
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}

.settings-version-note {
  margin-top: 0.15rem;
  color: var(--text-primary);
}

.settings-group {
  border: 1px solid var(--border-control);
  border-radius: 10px;
  padding: 0.9rem 1rem;
  background: var(--surface-card);
}

/* Between one group and the next, rather than under every group: a margin
   on the group itself would also push the closing note away from the last
   one, and the note belongs with what it follows.

   There was no rule here at all until a second group existed — the page
   had one group from the day it was built, so a missing gap between groups
   could not show. */
.settings-group + .settings-group {
  margin-top: 1rem;
}

.settings-group-title {
  margin: 0 0 0.7rem;
  font-size: 0.8rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}

.settings-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.7rem;
}

/* Space between rows, not above the first one, which sits under its
   section title and wants to stay with it. Written as an adjacent-sibling
   rule rather than a margin on every row so that stays true however many
   rows a section grows to, and however many sections the page gains.

   Without it a row's hint ran straight into the next row's label — the
   theme's "Kept on this device only." nearly touching "Language" — and the
   two read as one block rather than as two settings. */
.settings-row + .settings-row {
  margin-top: 1.1rem;
}

.settings-row-text {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.settings-label {
  font-size: 0.95rem;
  color: var(--text-primary);
}

.settings-hint {
  font-size: 0.78rem;
  color: var(--text-muted);
}

/* One bordered group with dividers, the same shape the view toggle uses,
   so a set of exclusive choices looks the same wherever it appears. */
.settings-choices {
  display: inline-flex;
  flex-wrap: wrap;
  border: 1px solid var(--border-control);
  border-radius: 7px;
  overflow: hidden;
  background: var(--surface-page);
}

.settings-choice {
  padding: 0.35rem 0.7rem;
  border: none;
  background: none;
  color: var(--text-muted);
  font: inherit;
  font-size: 0.85rem;
  cursor: pointer;
}

.settings-choice + .settings-choice {
  border-left: 1px solid var(--border-control);
}

.settings-choice:hover:not(.selected) {
  background: var(--surface-card);
  color: var(--text-primary);
}

.settings-choice.selected {
  background: var(--surface-card);
  color: var(--text-primary);
  font-weight: 600;
}

.settings-choice:focus-visible {
  outline: 2px solid var(--flow-casing);
  outline-offset: -2px;
}

/* A switch, for the one setting on this page that is on or off rather than
   one of several values (CF-HIST-111). Its neighbours are groups of two or
   more buttons, and rendering a yes/no that way says "choose between these
   two things" where what is meant is "turn this on". */
.settings-switch {
  flex: none;
  padding: 0.3rem;
  border: none;
  background: none;
  cursor: pointer;
  /* Matches the height a row of choices takes, so a row carrying a switch
     sits at the same height as the rows around it. */
  min-height: 2rem;
}

.settings-switch-track {
  display: block;
  position: relative;
  width: 2.4rem;
  height: 1.35rem;
  border: 1px solid var(--border-control);
  border-radius: 999px;
  background: var(--surface-page);
  transition: background 0.12s ease;
}

.settings-switch-knob {
  position: absolute;
  top: 0.15rem;
  left: 0.15rem;
  width: 0.95rem;
  height: 0.95rem;
  border-radius: 50%;
  background: var(--text-muted);
  transition:
    transform 0.12s ease,
    background 0.12s ease;
}

.settings-switch.on .settings-switch-track {
  background: var(--surface-card);
}

/* Travel is the track's width less the knob and both insets, written as
   one sum rather than as a measured figure so the two cannot disagree when
   either changes. */
.settings-switch.on .settings-switch-knob {
  transform: translateX(calc(2.4rem - 0.95rem - 0.3rem - 2px));
  background: var(--text-primary);
}

.settings-switch:disabled {
  opacity: 0.5;
  cursor: default;
}

.settings-switch:focus-visible {
  outline: 2px solid var(--flow-casing);
  outline-offset: 2px;
  border-radius: 999px;
}

.settings-note {
  margin: 1rem 0 0;
  font-size: 0.8rem;
  color: var(--text-muted);
}

/* The developer section, below the closing note rather than among the
   settings — it is the one group on this page that is not for the reader.
   Set apart by a rule and by distance rather than by being hidden, since
   the wording of the row is what keeps it out of the way (CF-DEBUG-008).

   `margin-top: auto` would push it to the bottom of a flex column; this
   page is not one, so the separation is stated as a margin. */
.settings-group-developer {
  margin-top: 2.5rem;
  padding-top: 1rem;
  border-top: 1px solid var(--border-control);
  opacity: 0.75;
}

/* Restored on interaction, so a section deliberately played down does not
   also become hard to read for whoever actually wants it. */
.settings-group-developer:hover,
.settings-group-developer:focus-within {
  opacity: 1;
}

/* A switch rather than a row of buttons: this is one thing that is on or
   off, where the choosers above pick one of several. */
.settings-switch {
  display: inline-flex;
  align-items: center;
  cursor: pointer;
}

.settings-switch input {
  /* Off-screen rather than `display: none`, which would take it out of the
     accessibility tree and off the keyboard's path. The track beside it is
     what is drawn, and `:checked`/`:focus-visible` reach it as siblings. */
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

.settings-switch-track {
  position: relative;
  display: block;
  width: 2.6rem;
  height: 1.5rem;
  border: 1px solid var(--border-control);
  border-radius: 0.75rem;
  background: var(--surface-page);
  transition: background 0.15s ease;
}

.settings-switch-track::after {
  content: '';
  position: absolute;
  top: 0.15rem;
  left: 0.15rem;
  width: 1.1rem;
  height: 1.1rem;
  border-radius: 50%;
  background: var(--text-muted);
  transition: transform 0.15s ease, background 0.15s ease;
}

/* On is carried by colour as well as by position, so the state is legible
   at a glance rather than only by comparing the knob against the track.
   `--flow-casing` is the interface's accent and is defined in every theme,
   high contrast included. */
.settings-switch input:checked + .settings-switch-track {
  background: var(--flow-casing);
  border-color: var(--flow-casing);
}

.settings-switch input:checked + .settings-switch-track::after {
  transform: translateX(1.1rem);
  /* The page's own background rather than a fixed white, so the knob stays
     a hole punched in the accent in all three themes. */
  background: var(--surface-page);
}

.settings-switch input:focus-visible + .settings-switch-track {
  outline: 2px solid var(--flow-casing);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .settings-switch-track,
  .settings-switch-track::after {
    transition: none;
  }
}

/* The backend is unreachable (CF-OFF-003, CF-OFF-004).

   A strip at the top of the window over an interface that stays drawn and
   fully usable. It deliberately covers nothing and blocks nothing: this
   replaced an opaque full-screen cover in revision 184, because that cover
   cost every launch several seconds of waiting page in front of an interface
   that was ready the whole time.

   What answers the case the cover was written for — stale readings left
   legible — is CF-STALE-006 rather than anything here: the length of the
   outage is added to every subject's age, so each goes stale on its own and
   wears the treatment CF-STALE-003 defines.

   Fixed to the top of the *window*, not placed in the layout, and not riding
   along with the page bar: the bar moves between the bottom edge, a left rail
   and the top with the shape of the window, so an indicator that followed it
   would sit somewhere different on a phone from on a desktop. Fixed also
   means it never reflows the page when it appears or disappears, which at a
   launch is exactly when everything else is settling.

   It does have to *clear* the bar where the bar has taken the top edge,
   though — found in the browser, where the pill was drawn across the A/C and
   Settings buttons. The figure is the bar's own published thickness rather
   than a copy of it (CF-NAV-015): the bar republishes it whenever a webfont
   reflows the labels or the scale is zoomed, and a second copy here would be
   the duplicated figure that has already cost this project a clipped axis and
   a stale test height. Only the top placement is answered, because that is
   the only edge the pill is on; `barInset`'s other two are not this rule's
   business.

   `pointer-events: none` because it is not a control and must not take a
   press meant for what is behind it (CF-OFF-006). */
.connection-indicator {
  position: fixed;
  top: max(0.5rem, env(safe-area-inset-top));
  left: 50%;
  transform: translateX(-50%);
  z-index: 100;
  pointer-events: none;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  max-width: calc(100vw - 2rem);
  padding: 0.35rem 0.75rem;
  border: 1px solid var(--border-control);
  border-radius: 999px;
  background: var(--surface-page);
  color: var(--text-primary);
  font-size: 0.8rem;
  line-height: 1.3;
  white-space: nowrap;
  overflow: hidden;
  /* Opaque, and lifted off what it sits over: it passes across graphs and
     cards of every colour, and a translucent pill would be unreadable over
     some of them. */
  box-shadow: 0 2px 8px rgb(0 0 0 / 25%);
}

.connection-indicator[hidden] {
  display: none;
}

/* Below the bar where the bar has the top edge, so the two do not overlap.
   The bar's own gap above it is already in the published thickness, so only
   a small separation is added here. */
:root[data-bar-placement='top'] .connection-indicator {
  top: calc(max(0.5rem, env(safe-area-inset-top)) + var(--page-bar-size, 0px));
}

/* Turning, because a still indicator reads as a stuck one and invites a
   reload of an application that is working correctly (CF-OFF-007). */
.connection-indicator-spinner {
  flex-shrink: 0;
  width: 0.85rem;
  height: 0.85rem;
  border: 2px solid var(--border-control);
  border-top-color: var(--flow-casing);
  border-radius: 50%;
  animation: connection-spin 1s linear infinite;
}

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

/* A reader who has asked for less motion still needs to see that the client
   is alive, so the indicator pulses instead of stopping entirely. */
@media (prefers-reduced-motion: reduce) {
  .connection-indicator-spinner {
    animation: connection-pulse 2s ease-in-out infinite;
  }
}

@keyframes connection-pulse {
  0%, 100% { opacity: 0.35; }
  50% { opacity: 1; }
}

/* Muted and after the message, so a glance reads "reconnecting" and only a
   second look costs the reader the duration. Empty for the first seconds of
   an outage (ELAPSED_AFTER_MS), which is every ordinary launch. */
.connection-indicator-elapsed {
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
}

.connection-indicator-elapsed:not(:empty)::before {
  content: '·';
  margin-right: 0.4rem;
}

/* A new version is being served (CF-OFF-010, CF-OFF-011). Above the overlay,
   because an incompatible client shows both at once: the backend it cannot
   talk to is also unreachable. */
.version-dialog {
  position: fixed;
  inset: 0;
  z-index: 110;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem;
  background: color-mix(in srgb, var(--surface-page) 82%, transparent);
}

.version-dialog[hidden] {
  display: none;
}

.version-dialog-panel {
  max-width: 24rem;
  padding: 1.2rem 1.3rem;
  border: 1px solid var(--border-control);
  border-radius: 10px;
  background: var(--surface-card);
  color: var(--text-primary);
  box-shadow: 0 10px 30px rgb(0 0 0 / 45%);
}

.version-dialog-message {
  margin: 0 0 1rem;
  font-size: 0.95rem;
  line-height: 1.45;
}

/*
 * The seconds left before an unanswered offer restarts (CF-OFF-021).
 *
 * Quieter than the message above it: it reports what is about to happen
 * rather than asking anything, and the question is the part that wants
 * reading first. The message's own bottom margin is what separates them,
 * so this only has to close the gap to the buttons.
 */
.version-dialog-countdown {
  margin: -0.6rem 0 1rem;
  font-size: 0.85rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.version-dialog-actions {
  display: flex;
  gap: 0.6rem;
  justify-content: flex-end;
}

.version-dialog-actions button {
  padding: 0.45rem 0.9rem;
  border: 1px solid var(--border-control);
  border-radius: 7px;
  background: var(--surface-panel);
  color: var(--text-primary);
  font: inherit;
  cursor: pointer;
}

.version-dialog-actions button.primary {
  border-color: var(--flow-casing);
  background: var(--flow-bore);
}

.version-dialog-actions button:focus-visible {
  outline: 2px solid var(--flow-casing);
  outline-offset: 2px;
}

/* --- The Overview page and its widgets (CF-OVR-001 … CF-OVR-008) --- */

.overview-page {
  padding: 1rem;
}

/* Everything inside a widget is sized as a fraction of the widget itself,
   not in rem. The arrangement was drawn at one square and judged there, and
   the same arrangement is meant to hold on a phone's home screen at a
   different size — so what must be preserved is the *proportion* between
   the icon, the figures and the row, not any particular pixel count. Sizing
   in rem would keep the type fixed while the square changed, which is the
   one thing that would make it a different design.

   The size itself is declared **here**, on the widget, and not on the page.
   It is the widget's own property — a square of a known size is what the
   thing is — and putting it here means a widget is well formed wherever it
   is placed. A widget that inherited its size from an ancestor and did not
   find one would compute every length against nothing, and `width` would
   fall back to `auto`: for a flex child that means "as wide as the page".
   That is not hypothetical. It happened while this was being checked and
   produced a battery icon a thousand pixels wide.

   A page wanting a different size sets `--overview-widget-size` on a rule
   of its own; because the grid's tracks read the same property, the two
   cannot disagree.

   **The figure is chosen so two widgets fit across a phone**, which is the
   narrowest screen this page has to work on and so the one that settles it.

   The width available is less than the viewport and the difference is not
   guessable, which is why this figure was measured rather than derived. On a
   375px phone: the scrollbar takes 8, the page stack's own padding 16, and
   this page's padding 32, leaving 319px of grid. One 1rem gap leaves 151px
   for each of two widgets.

   **10rem, capped at half the page's own width less the gap.** 10rem was
   tried first, from arithmetic against the bare viewport, and stacked the
   widgets one per row on that 375px phone — the arithmetic was right and the
   assumption underneath it was not. It became 9rem, which fitted and looked
   small on the reader's phone; asked for again on 2026-09-23 as fitting the
   mobile page better. So it is 10rem wherever two of those fit, and exactly
   two across wherever they do not, rather than one guess that is too big on
   one phone or too small on another.

   The cap reads the page's width through a container query unit, so it is
   what the page actually offers rather than the viewport — the mistake the
   paragraph above records. Outside the Overview (a host drawing a widget on
   its own) there is no container, `cqi` falls back to the viewport, and the
   cap only bites on a screen too narrow for two anyway. Every length inside
   is a proportion of this one, so a widget shrunk by the cap is the same
   arrangement, smaller. */
.overview-page {
  container-type: inline-size;
}

.power-widget,
.overview-grid {
  --overview-widget-size: min(10rem, (100cqi - 1rem) / 2);
}

.power-widget {
  --widget-icon: calc(var(--overview-widget-size) * 0.123);
  --widget-value: calc(var(--overview-widget-size) * 0.098);
  --widget-small: calc(var(--overview-widget-size) * 0.062);
  --widget-mode: calc(var(--overview-widget-size) * 0.055);
}

/* Fixed-width tracks rather than `1fr`, so the widgets keep the square
   proportion they were designed at instead of stretching to whatever width
   the window has. The same arrangement is meant to hold on a phone's home
   screen, and a stretched widget is a different design wearing the same
   name. `auto-fill` rather than `auto-fit`: with one widget on the page
   `auto-fit` would collapse the empty tracks, so the single widget would
   sit somewhere other than where the first of six will. */
.overview-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, var(--overview-widget-size));
  gap: 1rem;
  /* The bulge stands 0.19 of a widget above each widget's top edge (its
     radius, 0.13, plus its centre's rise, 0.06), so a second row of widgets
     needs that much between them or its bulges would sit on the row above.
     The flat 1rem the columns keep was short of it even before the bulge
     grew. */
  row-gap: calc(var(--overview-widget-size) * 0.26);
  justify-content: start;
  /* The same 0.19 above the first row, or the page's own padding clips it —
     and a little over, because the connection indicator is fixed to the top
     of the window and would otherwise sit across the first row's badges
     while the backend is away (CF-OFF-003). */
  padding-top: calc(var(--overview-widget-size) * 0.26);
}

/* Square, because that is the shape the arrangement was designed at and the
   shape a home-screen widget is.

   `position: relative` so the badge can be placed against this edge, and the
   button resets because the widget is a `<button>` when pressing it opens a
   page (CF-OVR-013) — a control the platform would otherwise draw its own
   border, background and centred text on. `text-align: left` is one of
   those: a button centres its contents, which would move every row. */
.power-widget {
  position: relative;
  box-sizing: border-box;
  width: var(--overview-widget-size);
  height: var(--overview-widget-size);
  border: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  padding: calc(var(--overview-widget-size) * 0.052);
  display: flex;
  flex-direction: column;
  gap: calc(var(--overview-widget-size) * 0.026);
  border-radius: calc(var(--overview-widget-size) * 0.129);
  /* Its own surface, stepped further from the page than a card is — see
     `--surface-widget` in theme.css. The bulge and its fillets below use the
     same one, or the badge would show as a patch of another colour. */
  background: var(--surface-widget);
  /* Digits line up down the column, so a row does not shift sideways as a
     figure gains or loses a character. */
  font-variant-numeric: tabular-nums;
}

/* Only where pressing it does something. A widget drawn as a reading alone
   — which is what a host outside this page gets (CF-OVR-014) — must not
   claim to be pressable. */
.power-widget[aria-label] {
  cursor: pointer;
}

.power-widget:focus-visible {
  outline: 2px solid var(--flow-casing);
  outline-offset: 2px;
}

/* The page's own icon, in a bulge of the widget's top edge (CF-OVR-013).

   **The card's outline swells up to make room for it**, in the card's own
   colour and with no ring, so it reads as part of the widget's shape rather
   than as a circle laid on top. It was the second until 2026-09-23: a circle
   centred on the edge, ringed in the page's colour, whose lower half sat
   over the first row — in the way of anything written there, and worst for
   a row with a centred title. The reader: *"like the background top border
   bulges up so there is room for the logo"*.

   The geometry, all as fractions of the widget (CF-OVR-002):
   - the circle's radius is 0.13 and its centre 0.06 **above** the edge, so
     0.07 of it hangs inside the card — and the card's top padding
     (`.power-widget:has(.power-widget-badge)`) is 0.07 plus a gap, so no
     row can reach it;
   - where it meets the edge, a fillet of radius 0.04 on each side
     (`::before`, `::after`), tangent to the edge and to the circle, turns
     the join into a curve. Its centre sits 0.1688 either side of the
     circle's: √((0.13 + 0.04)² − (0.06 − 0.04)²). Change one of these and
     that figure changes with it.

   **`z-index` stays**, although nothing now overlaps: at equal stacking
   level a positioned row paints above a static badge, and the A/C widget
   sliced the old badge's bottom off exactly that way on 2026-09-20. Kept on
   the shared rule so no widget has to remember it. */
.power-widget-badge {
  --badge-radius: calc(var(--overview-widget-size) * 0.13);
  --badge-rise: calc(var(--overview-widget-size) * 0.06);
  --badge-fillet: calc(var(--overview-widget-size) * 0.04);
  --badge-size: calc(var(--badge-radius) * 2);
  position: absolute;
  z-index: 1;
  top: calc(-1 * (var(--badge-rise) + var(--badge-radius)));
  left: calc(50% - var(--badge-radius));
  width: var(--badge-size);
  height: var(--badge-size);
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--surface-widget);
}

/* The two fillets. Each is a small box standing on the card's edge, filled
   with the card's colour except for a quarter-disc of the fillet's radius
   cut out of its outer top corner — so what is left is the concave curve
   from the edge up into the circle.

   The box runs from inside the circle — 0.1 from its centre, where the
   circle is at least 0.115 wide at every height the box covers — to the
   fillet's centre at 0.1688. Starting it at 0.125 was tried and left a
   pixel of page showing at the foot of the join, where the circle narrows
   to 0.115. The icon reaches only 0.075 from the centre, so the box never
   covers it and needs no stacking of its own. It is a little taller than the fillet (0.046 against 0.04)
   because the fillet meets the circle 0.0047 above its own centre. */
.power-widget-badge::before,
.power-widget-badge::after {
  content: '';
  position: absolute;
  top: calc(var(--badge-rise) + var(--badge-radius) - var(--overview-widget-size) * 0.046);
  width: calc(var(--overview-widget-size) * 0.0688);
  height: calc(var(--overview-widget-size) * 0.046);
  pointer-events: none;
}

.power-widget-badge::after {
  left: calc(var(--badge-radius) + var(--overview-widget-size) * 0.1);
  background: radial-gradient(
    circle var(--badge-fillet) at 100% calc(var(--overview-widget-size) * 0.006),
    transparent calc(var(--badge-fillet) - 0.5px),
    var(--surface-widget) var(--badge-fillet)
  );
}

.power-widget-badge::before {
  right: calc(var(--badge-radius) + var(--overview-widget-size) * 0.1);
  background: radial-gradient(
    circle var(--badge-fillet) at 0% calc(var(--overview-widget-size) * 0.006),
    transparent calc(var(--badge-fillet) - 0.5px),
    var(--surface-widget) var(--badge-fillet)
  );
}

.power-widget-badge .page-icon {
  width: calc(var(--badge-size) * 0.58);
  height: auto;
  display: block;
}

.power-widget-badge .page-icon-art {
  fill: none;
  stroke: var(--icon-stroke);
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The rows start below the part of the bulge that hangs inside the card —
   0.07 of the widget — plus a gap, so the icon never sits over the first
   row (CF-OVR-013). Applied to the widget rather than to the first row,
   because a widget drawn without a badge must not carry the space one would
   have taken. */
.power-widget:has(.power-widget-badge) {
  padding-top: calc(var(--overview-widget-size) * 0.105);
}

/* One band colour for every row. It says "four rows", not "which subject" —
   the icon and the figure's own colour carry identity, as they do on the
   Power page. `color-mix` rather than `opacity`, so the text on the band
   keeps its full contrast. */
.power-widget-row {
  flex: 1 1 0;
  min-height: 0;
  display: flex;
  align-items: center;
  gap: calc(var(--overview-widget-size) * 0.045);
  padding: 0 calc(var(--overview-widget-size) * 0.045);
  border-radius: calc(var(--overview-widget-size) * 0.052);
  background: color-mix(in srgb, var(--soc-bar) 17%, transparent);
}

/* Sized here rather than inheriting the Power page's shell-relative rule,
   which would resolve against a variable this page does not set. */
.power-widget .subject-icon {
  width: var(--widget-icon);
  flex: 0 0 auto;
}

.power-widget-main {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 0;
}

/* Pushed to the trailing edge, so a qualifier sits away from the figure it
   qualifies rather than crowding it. */
.power-widget-aside {
  margin-left: auto;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
}

/* The whole number at full size, as the flow view sets its readings. */
.power-widget-value {
  font-weight: 700;
  font-size: var(--widget-value);
  line-height: 1;
  letter-spacing: -0.015em;
  color: var(--text-reading-active);
  white-space: nowrap;
}

/* Raised and smaller, which removes the decimal point entirely — the same
   device the sockets use, and for the same reason. */
.power-widget-fraction {
  font-size: 0.62em;
  vertical-align: 0.46em;
}

/* The least surprising part of a reading, and the first the eye skips. */
.power-widget-unit {
  font-size: 0.58em;
  font-weight: 600;
  margin-left: 0.08em;
}

/* The Net row's money — what today's import cost (CF-OVR-015). Named for the
   quantity rather than for the slot: it held a price per kWh until
   2026-09-18, and a class called `-price` on a figure that is a cost is the
   sort of thing the next reader has to check the markup to disbelieve. */
.power-widget-money {
  font-weight: 600;
  font-size: var(--widget-small);
  line-height: 1;
  color: var(--text-reading-total);
  white-space: nowrap;
}

/* Boxed rather than coloured: the mode is a name, not a quantity, and
   giving it a colour would put it in the same language as the figures. */
.power-widget-mode {
  font-weight: 700;
  font-size: var(--widget-mode);
  letter-spacing: 0.05em;
  line-height: 1;
  padding: 0.15rem 0.25rem;
  border: 1px solid var(--border-control);
  border-radius: 0.25rem;
  color: var(--text-reading-total);
}

/* Each subject's own colour on its figure, which is where the Power page
   carries identity. The net takes the import blue: this widget shows
   imported energy, so the figure is always an import. */
.power-widget-row[data-subject='net'] .power-widget-value {
  color: var(--histogram-import);
}

.power-widget-row[data-subject='solarPanels'] .power-widget-value {
  color: var(--solar-fill);
}

.power-widget-row[data-subject='battery'] .power-widget-value {
  color: var(--soc-bar);
}

/* Nothing where a figure is unknown, rather than a zero: a subject that has
   not reported and one that moved no energy are different facts
   (CF-PWR-079, CF-PWR-108). The row keeps its height either way. */
.power-widget-value.unknown,
.power-widget-money.unknown {
  visibility: hidden;
}

/* ---------------------------------------------------------------------------
   The A/C widget (CF-OVR-016 … CF-OVR-020).

   The same square and the same rows as the Power widget, which is why it
   carries that class too — what differs is that there are two rows rather
   than four, each naming its unit instead of drawing a subject icon, and
   each stacking its figure under that name rather than beside it.
   --------------------------------------------------------------------------- */

/* Two rows sharing the height four would have shared, so the widget is as
   full as the Power one rather than half empty with the rows crowded at the
   top. `1fr` each, exactly as the Power widget's four are.

   **Stacked, not in a line** (CF-OVR-020). The row ran along one line until
   2026-09-20 — name, figure, countdown — which fitted, and left the right
   half of every row empty: two rows of furniture carrying one short word and
   one short number each. Stacking lets the figure take the width and grow,
   which is the thing being read.

   `position: relative` because the mode mark and the countdown are placed
   against the row's own edges rather than flowed: both must sit clear of a
   figure that is centred on the row, and a centred thing cannot be centred
   if a sibling in the flow is pushing it. */
.aircon-widget .power-widget-row {
  flex: 1 1 0;
  position: relative;
  padding: calc(var(--overview-widget-size) * 0.03) calc(var(--overview-widget-size) * 0.045);
  flex-direction: column;
  align-items: stretch;
  justify-content: center;
  gap: 0;
}

/* The figure's line. Centred on the row, which is what the mark and the
   countdown are taken out of the flow to protect. */
.aircon-widget-figures {
  display: block;
  text-align: center;
  flex: 0 0 auto;
  min-width: 0;
  /* The line keeps its height whatever it holds — a figure, the word "Off",
     or nothing at all where the page has not spoken (CF-OVR-004). Otherwise
     the two rows change size as a unit is switched, which moves the other.

     Stated as the figure's own size rather than as a separate figure of its
     own. A hand-picked `min-height` was tried and was wrong by 1.4px: "Off"
     is set smaller than a temperature, so it fell back to the floor while a
     temperature stood above it, and the line moved between the two states —
     the exact thing the floor is here to prevent. A number derived from the
     size it must clear cannot drift away from it. */
  --aircon-widget-figure: calc(var(--overview-widget-size) * 0.135);
  min-height: calc(var(--aircon-widget-figure) * 1.13);
}

/* The unit's name where the Power widget puts a subject icon. Two dormers
   cannot be told apart by a picture — they are the same equipment in two
   rooms — so the name is the identity here.

   Over the figure rather than beside it, and centred on it: the pair reads as
   one label-and-value, which is what the stack is for. */
.aircon-widget-name {
  flex: 0 0 auto;
  font-size: var(--widget-small);
  color: var(--text-label);
  white-space: nowrap;
  text-align: center;
  line-height: 1.05;
}

/* The target, at half again the size it had in the line layout — the room
   the stack freed up, spent on the thing the widget exists to show.

   The size is `--aircon-widget-figure`, declared on the line above, so the
   figure and the floor that reserves room for it are one value in one place. */
.aircon-widget .power-widget-value {
  font-size: var(--aircon-widget-figure);
  color: var(--aircon-mercury);
}

/* Which way the unit is working (CF-OVR-020). The same two colours the
   thermometer uses for a room off either end of its scale — this is the
   same pair of ideas (too cold, too warm) one step along, and a third red
   and a fourth blue would be two more colours meaning what these already
   mean. */
.aircon-widget-row.heating .power-widget-value {
  color: var(--aircon-over);
}

.aircon-widget-row.cooling .power-widget-value {
  color: var(--aircon-under);
}

/* "Off", where a temperature would be. Smaller than a figure and in the
   totals grey rather than a reading colour: it is a state, not a quantity,
   and giving it a figure's weight would make an idle unit the loudest thing
   in the widget. */
.power-widget-value.aircon-widget-off {
  font-size: calc(var(--overview-widget-size) * 0.095);
  font-weight: 600;
  letter-spacing: 0.06em;
  color: var(--text-reading-total);
}

/* The mode mark, against the row's trailing edge and vertically centred on
   it. Out of the flow so the figure stays centred on the row rather than on
   whatever space the mark leaves. */
.aircon-widget-mark {
  position: absolute;
  right: calc(var(--overview-widget-size) * 0.055);
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  pointer-events: none;
}

/* A flame is a silhouette and a crystal is not: outlined, the flame hollows
   out and reads as a comma; filled, the crystal is a blob. So they differ in
   technique and are matched on weight instead — see `airconIcons.js`. */
.aircon-mode-mark {
  width: calc(var(--overview-widget-size) * 0.105);
  height: auto;
  display: block;
}

.aircon-mode-mark.heating {
  fill: var(--aircon-over);
  stroke: none;
}

.aircon-mode-mark.cooling {
  fill: none;
  stroke: var(--aircon-under);
  stroke-width: 2.1;
  stroke-linecap: round;
  stroke-linejoin: round;
  /* Six arms against the flame's one mass, so the same box reads heavier.
     Drawn smaller to match it optically rather than dimensionally. */
  width: calc(var(--overview-widget-size) * 0.095);
}

/* The countdown sits in the row's leading corner, out of the flow.
   The trailing edge belongs to the mode mark, and a unit that is both
   heating and scheduled would otherwise stack the two on top of each other.
   Out of the flow for the same reason the mark is: the figure between them
   is centred on the row. */
.aircon-widget .power-widget-aside {
  position: absolute;
  top: calc(var(--overview-widget-size) * 0.035);
  left: calc(var(--overview-widget-size) * 0.045);
  margin-left: 0;
}

/* The countdown to the next scheduled command. A bordered mark like the
   battery's mode, because it is the same kind of thing: a qualifier beside a
   figure rather than a figure itself. */
.aircon-widget-command {
  font-size: var(--widget-mode);
  line-height: 1;
  padding: 0.15em 0.4em;
  border: 1px solid var(--border-control);
  border-radius: 999px;
  color: var(--text-muted);
  white-space: nowrap;
}

/* A unit that is off, drawn as the page draws it (CF-AC-014): colour drained
   and stepped back, so the two agree about what "not running" looks like.
   The row keeps its height, so the widget does not move as a unit is
   switched. */
.aircon-widget-row.off {
  filter: saturate(0.12) opacity(0.5);
}

/* ---------------------------------------------------------------------------
   The Lights widget (CF-OVR-021 … CF-OVR-025).

   Three rows, each its words at the leading edge and "a of b" at the
   trailing one, and a battery warning under them only when it is true. The
   warning takes no band: it is not a fourth count but a note about the three,
   and giving it a row's weight would make an ordinary day look like one with
   something to report.

   **One line per row, not the A/C widget's stack.** The stack was built first
   and measured on the page: three stacked rows and a warning left each band
   too short for two lines, and the first row's words, centred, ran under the
   badge. Along one line the words and the figure fill the row between them,
   which is what the A/C lesson was actually about — a row half empty, not a
   row on one line.
   --------------------------------------------------------------------------- */

/* A little tighter than the shared row, which is what lets the Dutch words
   sit beside "10 / 10" at phone size with no ellipsis — measured, with a
   pixel to spare. */
.lights-widget .power-widget-row {
  gap: calc(var(--overview-widget-size) * 0.035);
  padding: 0 calc(var(--overview-widget-size) * 0.04);
}

/* The row's words, where the Power widget puts a subject icon. */
.lights-widget-name {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--widget-small);
  color: var(--text-label);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  line-height: 1.1;
}

/* At the trailing edge, and never squeezed: the figure is what is read, so
   the words give way first. */
.lights-widget-figures {
  flex: 0 0 auto;
  white-space: nowrap;
  line-height: 1;
}

/* The total is the count's scale, not a second figure (CF-OVR-004): stepped
   back in size and colour so the count is what is read. */
.lights-widget-total {
  font-size: var(--widget-small);
  font-weight: 600;
  color: var(--text-reading-total);
}

.lights-widget-figures.unknown {
  visibility: hidden;
}

/* Identity on the figure, as CF-OVR-003 puts it: a lit lamp's own colour for
   the lamps, the sensor's for the sensors — the colours the plan draws them
   in. The buttons have no colour on the plan, not being on it, and keep the
   ordinary reading colour. */
.lights-widget-row[data-count='lights'] .power-widget-value {
  color: var(--plan-lamp-on);
}

.lights-widget-row[data-count='sensors'] .power-widget-value {
  color: var(--plan-sensor);
}

/* The Gas widget borrows the Lights widget's rows (gasWidget.js). Today's
   cost carries the gas page's own orange, the colour of its bars; the rest
   keep the ordinary reading colour (CF-OVR-003). */
.gas-widget-row[data-row='today'] .power-widget-value {
  color: var(--gas-bar);
}

.water-widget-row[data-row='today'] .power-widget-value {
  color: var(--water-bar);
}

/* The Water widget's figures are invented until the backend sends water
   (CF-WAT-017), and a widget has no room for the page's notice. So a quiet
   line beneath the rows says so: not a warning, which would make it look
   like a fault, but plain enough to stop the figures being believed. */
.water-widget-demo {
  flex: 0 0 auto;
  font-size: var(--widget-small);
  font-style: italic;
  line-height: 1.1;
  text-align: center;
  color: var(--text-muted);
}

.water-widget-demo[hidden] {
  display: none;
}

/* How long ago a measuring widget's figures were updated, once they are stale
   (CF-OVR-012). One line beneath the rows and nothing on the rows themselves:
   the rows shrink by its height rather than being faded, which on a square of
   single figures would read as switched off rather than out of date. Plain
   rather than a warning colour — an outage is not a fault in the house. One
   line always. Measured 2026-09-29 on a 375px window, where the line has
   136px: "Updated 23h 59m ago" takes 103, "23h 59m geleden bijgewerkt" 131,
   and every figure below 100 hours fits in both languages (the digits are
   tabular). Past four days the Dutch line loses its last pixel to the
   ellipsis, which is a guard rather than a layout. Every length here scales
   with the widget, so the ratio holds on a narrower phone. */
.widget-stale {
  flex: 0 0 auto;
  font-size: var(--widget-small);
  line-height: 1.1;
  text-align: center;
  color: var(--text-label);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.widget-stale[hidden] {
  display: none;
}

.lights-widget-warning {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: calc(var(--overview-widget-size) * 0.025);
  font-size: var(--widget-mode);
  font-weight: 600;
  line-height: 1.1;
  text-align: center;
  color: var(--failure-text);
}

.lights-widget-warning[hidden] {
  display: none;
}

.lights-widget-warning .device-selector-battery-icon {
  width: calc(var(--overview-widget-size) * 0.1);
  height: calc(var(--overview-widget-size) * 0.1);
  flex: 0 0 auto;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
}

/* ---------------------------------------------------------------------------
   The A/C page (SPEC-AIRCON.md, CF-AC-001 … CF-AC-046).

   A strip of unit tabs, and beneath it (or above it) the one unit showing:
   its switch and figures, its thermometer, and its timer and weekly
   schedule. Portrait stacks the timer under the thermometer; landscape puts
   it beside — the same parts, placed for the axis the screen has most of.
   --------------------------------------------------------------------------- */

/* The page fills the shell's pane, so the tabs can stay put at their edge
   while the unit beneath them scrolls. */
.aircon-page {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
  gap: 0.5rem;
}

.aircon-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 0 0.3rem 0.6rem;
}

/* ---- The unit tabs (CF-AC-037 … CF-AC-039) ----
   They share the width equally between however many units there are, and
   sit at the edge the page bar is not on: at the top where the bar is along
   the bottom or down the left, at the bottom where the bar is along the top.
   Read off the placement the bar publishes on the document, so there is no
   second opinion about where the bar is. */
.aircon-units {
  display: flex;
  align-items: stretch;
  flex: none;
  order: 0;
  padding: 0.3rem 0.3rem 0;
}

:root[data-bar-placement='top'] .aircon-units {
  order: 2;
  padding: 0 0.3rem 0.3rem;
}

.aircon-tab {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.05rem;
  padding: 0.4rem 0.3rem 0.45rem;
  font: inherit;
  color: var(--text-muted);
  background: var(--surface-page);
  border: 1px solid var(--border-control);
  border-radius: 10px;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}

.aircon-tab:hover:not(.selected) {
  background: var(--surface-card);
}

.aircon-tab.selected {
  background: var(--surface-card);
  border-color: var(--flow-casing);
  box-shadow: inset 0 0 0 1px var(--flow-casing);
  color: var(--text-primary);
}

.aircon-tab:focus-visible {
  outline: 2px solid var(--flow-casing);
  outline-offset: 2px;
}

.aircon-tab-name {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--text-label);
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The target, the figure a tab is read for. */
.aircon-tab-target {
  display: flex;
  align-items: center;
  gap: 0.2rem;
  font-size: 1.3rem;
  font-weight: 600;
  line-height: 1.15;
  color: var(--text-reading-active);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Warming or cooling, as the Overview widget draws it (CF-OVR-020): the same
   two colours, and the mark beside so the colour is not the only signal. */
.aircon-tab-target.heating {
  color: var(--aircon-over);
}

.aircon-tab-target.cooling {
  color: var(--aircon-under);
}

.aircon-tab-mark:empty {
  display: none;
}

.aircon-tab-mark .aircon-mode-mark {
  width: 0.85rem;
  height: 0.85rem;
  display: block;
}

.aircon-tab-now {
  font-size: 0.72rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.aircon-tab-now.over {
  color: var(--aircon-over);
}

.aircon-tab-now.under {
  color: var(--aircon-under);
}

.aircon-tab-now.stale {
  opacity: 0.45;
}

/* An off unit's tab says "Off" in the quieter colour, and steps back the way
   the widget's row does (CF-AC-014). */
.aircon-tab.off .aircon-tab-target,
.aircon-tab.off .aircon-tab-now {
  color: var(--text-reading-idle);
  filter: saturate(0.12);
}

/* ---- One unit ---- */
.aircon-unit {
  background: var(--surface-card);
  border: var(--subject-shell-border, 1px) solid var(--border-card);
  border-radius: 10px;
  padding: 0.9rem 0.8rem 1rem;
  display: grid;
  gap: 1.2rem;
  /* The box the flames and the draught are drawn in, and clipped to. */
  position: relative;
  overflow: hidden;
  isolation: isolate;
}

/* The controls above the background, which is out of the grid's flow. */
.aircon-unit > .aircon-gauge,
.aircon-unit > .aircon-plans,
.aircon-unit > .aircon-failure {
  position: relative;
  z-index: 1;
}

.aircon-unit > .aircon-failure[hidden] {
  display: none;
}

/* Text kept readable over the flames or the draught (CF-AC-048). The halo
   is the theme's: none on a dark panel, a soft white glow on a light one,
   where dark text would otherwise sit straight on a deep red. Only while an
   effect is showing, so a still panel's text is untouched. */
.aircon-unit[data-ambience='heating'] :is(.aircon-readout, .aircon-plans),
.aircon-unit[data-ambience='cooling'] :is(.aircon-readout, .aircon-plans) {
  text-shadow: var(--aircon-text-halo);
  --text-muted: var(--aircon-muted-over-effect);
}

/* ---- Flames and a draught (CF-AC-048) ----
   Behind a unit's controls, by which way it is working. The strength comes
   from the theme (--aircon-ambience), because a glow that leaves the
   controls readable on a dark panel washes them out on a light one. */
.aircon-ambience {
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: inherit;
}

.aircon-flames,
.aircon-draught,
.aircon-ambience-edge {
  position: absolute;
  inset: 0;
  display: none;
}

.aircon-ambience[data-mode='heating'] .aircon-flames,
.aircon-ambience[data-mode='cooling'] .aircon-draught,
.aircon-ambience[data-mode='heating'] .aircon-ambience-edge,
.aircon-ambience[data-mode='cooling'] .aircon-ambience-edge {
  display: block;
}

.aircon-flames {
  opacity: var(--aircon-ambience);
}

/* The glow the tongues rise out of. */
.aircon-flame-glow {
  position: absolute;
  inset: auto 0 0 0;
  height: 70px;
  background: radial-gradient(ellipse 80% 100% at 50% 100%,
    color-mix(in srgb, var(--aircon-over) 45%, transparent), transparent 70%);
}

/* One tongue: a soft teardrop, brightest at its root, flickering from its
   base. Heights, speeds and offsets are set per tongue in script. */
.aircon-flame {
  position: absolute;
  bottom: -34px;
  width: 20%;
  border-radius: 50% 50% 40% 40%;
  background: radial-gradient(ellipse at 50% 100%,
    var(--aircon-flame-core),
    var(--aircon-flame-body) 40%,
    color-mix(in srgb, var(--aircon-over) 15%, transparent) 62%,
    transparent 72%);
  filter: blur(7px);
  transform-origin: 50% 100%;
  animation: aircon-flicker 1.3s ease-in-out infinite alternate;
}

@keyframes aircon-flicker {
  0% { transform: translateY(0) scale(1, 1); }
  50% { transform: translateY(-10px) scale(0.88, 1.18); }
  100% { transform: translateY(3px) scale(1.1, 0.92); }
}

/* The cold side: a faint tint from the vent's edge, and streaks of air
   crossing it. */
.aircon-draught {
  opacity: var(--aircon-ambience);
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--aircon-under) 14%, transparent), transparent 60%);
}

.aircon-draught-line {
  position: absolute;
  left: 0;
  height: 10px;
  border-radius: 50%;
  background: linear-gradient(90deg, transparent,
    color-mix(in srgb, var(--aircon-under) 45%, var(--aircon-draught-core)), transparent);
  filter: blur(4px);
  /* Unseen until a crossing moves it: each crossing is an animation of its
     own, started from script (airconAmbience.js says why it is not a
     repeating keyframe animation here). */
  opacity: 0;
}

/* The edge: a band of colour flowing round the panel, just inside its
   border — the panel clips anything drawn on the border itself. */
.aircon-ambience-edge {
  border-radius: inherit;
  padding: 2px;
  background-size: 300% 100%;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  opacity: 0.85;
  animation: aircon-edge-flow 6s linear infinite;
}

.aircon-ambience[data-mode='heating'] .aircon-ambience-edge {
  background-image: linear-gradient(90deg, var(--aircon-over), var(--aircon-flame-bright),
    color-mix(in srgb, var(--aircon-over) 60%, #000), var(--aircon-over));
}

.aircon-ambience[data-mode='cooling'] .aircon-ambience-edge {
  background-image: linear-gradient(90deg, var(--aircon-under), #cfe6ff,
    color-mix(in srgb, var(--aircon-under) 60%, #000), var(--aircon-under));
  animation-duration: 8s;
}

@keyframes aircon-edge-flow {
  from { background-position: 0 0; }
  to { background-position: 300% 0; }
}

/* A reader who has asked for less motion gets the colours standing still:
   the panel still says warming or cooling, it just does not move. */
@media (prefers-reduced-motion: reduce) {
  .aircon-flame,
  .aircon-ambience-edge {
    animation: none;
  }

  /* Nothing is animated, so the streaks stand still, spread down the panel. */
  .aircon-draught-line {
    opacity: 0.5;
    width: 60%;
    transform: translateX(30%);
  }

  .aircon-draught-line:nth-child(3n + 1) { top: 15%; }
  .aircon-draught-line:nth-child(3n + 2) { top: 48%; transform: translateX(-10%); }
  .aircon-draught-line:nth-child(3n) { top: 80%; transform: translateX(55%); }
}

.aircon-unit[hidden] {
  display: none;
}

/* Portrait: the gauge across the top, the timer and schedule beneath it at
   the full width of the page. The gap between them is deliberate: the bulb
   used to sit right on the tabs, and a switch touching another control is
   one pressed by mistake. */
.aircon-vertical .aircon-unit {
  grid-template-columns: minmax(0, 1fr);
}

/* Landscape: the timer and schedule beside the gauge — a landscape phone has
   width to spare and no height, so they go where the width is. */
.aircon-horizontal .aircon-unit {
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  align-items: start;
  column-gap: 1.4rem;
}

/* A refused command sits under the thermometer it came from in both
   orientations (CF-AC-050): here that is the left column, beneath the gauge,
   with the timer and schedule running down beside both. */
.aircon-horizontal .aircon-unit > .aircon-gauge {
  grid-column: 1;
  grid-row: 1;
}

.aircon-horizontal .aircon-unit > .aircon-failure {
  grid-column: 1;
  grid-row: 2;
}

.aircon-horizontal .aircon-unit > .aircon-plans {
  grid-column: 2;
  grid-row: 1 / span 2;
}

/* The gauge: the thermometer on its side, its bulb the switch, and the
   target beyond its far end. The thermometer takes whatever width the
   target leaves and is drawn to that length (the slot is measured, and must
   therefore not be sized by the drawing inside it). */
.aircon-gauge {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding-bottom: 0.2rem;
}

.aircon-therm-slot {
  flex: 1 1 0;
  min-width: 0;
}

/* Switched off: the colour drains and the gauge steps back. Greyscale alone
   left an off unit as loud as a running one, which reads as "something is
   wrong here" rather than "this is not running" (CF-AC-014).

   The glass and the figures only. The bulb is the switch and the way back
   on, so it has to stay findable; the timer and the schedule are about
   later, not now, and a schedule that will switch the unit on at seven is
   not made doubtful by the unit being off at six. */
.aircon-unit.off .aircon-therm-body,
.aircon-unit.off .aircon-readout {
  filter: saturate(0.12);
  opacity: 0.5;
}

/* The bulb, which is the switch: the look the round power button had, at
   the size it had. */
.aircon-bulb {
  cursor: pointer;
}

.aircon-bulb circle {
  fill: var(--surface-panel);
  stroke: var(--border-control);
  stroke-width: 1.5;
  transition: fill 0.15s, stroke 0.15s;
}

.aircon-bulb path {
  fill: none;
  stroke: var(--text-muted);
  stroke-width: 2.6;
  stroke-linecap: round;
  transition: stroke 0.15s;
}

.aircon-bulb[aria-pressed='true'] circle {
  fill: var(--flow-bore);
  stroke: var(--flow-casing);
}

.aircon-bulb[aria-pressed='true'] path {
  stroke: var(--text-reading-active);
}

.aircon-bulb:focus-visible {
  outline: none;
}

.aircon-bulb:focus-visible circle {
  stroke: var(--flow-casing);
  stroke-width: 3;
}

/* The target, with the word above it so it is not read as the room's
   temperature, and the room beneath once there is a reading (CF-AC-003). */
.aircon-readout {
  flex: none;
  line-height: 1.2;
  text-align: center;
}

.aircon-target-label {
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
}

.aircon-target {
  font-size: 1.5rem;
  font-weight: 600;
  color: var(--text-reading-active);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.aircon-now {
  font-size: 0.75rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* A room outside the settable scale says so beside the target as well as in
   the colour of the bar (CF-AC-010, CF-AC-011). */
.aircon-now.over {
  color: var(--aircon-over);
}

.aircon-now.under {
  color: var(--aircon-under);
}

/* A reading too old to present as current (CF-AC-015). Faded rather than
   removed: it is still the last thing known about the room, and taking it
   away would lose that as well as moving the layout under the reader.

   The figure alone fades — not the target beside it, which is the client's
   own state and has not gone off because the backend stopped answering.
   `opacity` rather than a colour, so the out-of-range red and blue keep
   saying which way the room had gone. */
.aircon-now.stale {
  opacity: 0.45;
}

.aircon-unit.off .aircon-target {
  color: var(--text-reading-idle);
}

.aircon-therm {
  display: block;
  max-width: none;
  /* The knob is dragged along one axis, so the browser must not claim the
     gesture for scrolling before the handler sees it. */
  touch-action: none;
  overflow: visible;
}

.aircon-knob {
  cursor: grab;
}

.aircon-knob:focus-visible {
  outline: 2px solid var(--flow-casing);
  outline-offset: 2px;
}

/* ---- The link (CF-AC-030) ----
   Between two neighbouring tabs, on a rule joining them. No text label: the
   icon, the ring and the rule state it three times over. */
.aircon-link {
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  flex: none;
  padding: 0 0.25rem;
}

.aircon-link::before,
.aircon-link::after {
  content: '';
  position: absolute;
  top: 50%;
  height: 0;
  width: 0.25rem;
  transform: translateY(-50%);
  border: 0 dashed var(--border-control);
  border-top-width: 2px;
}

.aircon-link::before {
  left: 0;
}

.aircon-link::after {
  right: 0;
}

.aircon-link.linked::before,
.aircon-link.linked::after {
  border-style: solid;
  border-color: var(--flow-casing);
}

.aircon-link-button {
  position: relative;
  z-index: 1;
  width: 2.1rem;
  height: 2.1rem;
  border-radius: 999px;
  padding: 0;
  cursor: pointer;
  background: var(--surface-card);
  border: 2px solid var(--border-control);
  display: grid;
  place-items: center;
  transition: background 0.15s, border-color 0.15s;
}

.aircon-link-button svg {
  width: 1.1rem;
  height: 1.1rem;
  fill: none;
  stroke: var(--text-muted);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: stroke 0.15s;
}

.aircon-link-button[aria-pressed='true'] {
  background: var(--flow-bore);
  border-color: var(--flow-casing);
}

.aircon-link-button[aria-pressed='true'] svg {
  stroke: var(--text-reading-active);
}

.aircon-link-button:focus-visible {
  outline: 2px solid var(--flow-casing);
  outline-offset: 3px;
}

/* ---- Timer and schedule (CF-AC-040) ---- */
.aircon-plans {
  min-width: 0;
}

.aircon-plan-tabs {
  display: flex;
  width: 100%;
}

.aircon-plan-tab {
  flex: 1;
  min-height: 2.3rem;
}

.aircon-schedule[hidden],
.aircon-weekly[hidden] {
  display: none;
}

.aircon-schedule,
.aircon-weekly {
  width: 100%;
  margin-top: 0.3rem;
}

/* One line each, always. An earlier version wrapped because the chrome, not
   the text, was too wide: at 12px "23:45" is 34px and "20,0°" is 31px — 65px
   of text in what was then a 134px row — while two borders, four paddings,
   two carets, an arrow glyph and a trash button spent the other 185. So the
   text stayed and the chrome went. */
.aircon-row {
  display: flex;
  align-items: stretch;
  gap: 0.25rem;
  margin-top: 0.4rem;
}

/* The chips of one command or one period as a single segmented control: one
   border around them, a divider between, and an equal share each so all are
   the same finger target. */
.aircon-pair {
  display: flex;
  align-items: stretch;
  flex: 1 1 auto;
  min-width: 0;
  background: var(--surface-panel);
  border: 1px solid var(--border-control);
  border-radius: 6px;
  overflow: hidden;
}

.aircon-pair:hover {
  border-color: var(--flow-casing);
}

.aircon-chip {
  font: inherit;
  font-size: 0.85rem;
  cursor: pointer;
  background: none;
  color: var(--text-primary);
  border: 0;
  border-radius: 0;
  padding: 0.45rem 0.25rem;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 1 1 0;
}

.aircon-chip:hover {
  background: var(--surface-card);
}

.aircon-chip:focus-visible {
  outline: 2px solid var(--flow-casing);
  outline-offset: -2px;
}

.aircon-chip + .aircon-chip {
  border-left: 1px solid var(--border-control);
}

.aircon-what.is-off {
  color: var(--text-muted);
}

.aircon-remove {
  flex: none;
  width: 1.8rem;
  padding: 0;
  cursor: pointer;
  background: none;
  border: 0;
  border-radius: 5px;
  display: grid;
  place-items: center;
}

.aircon-remove svg {
  width: 0.95rem;
  height: 0.95rem;
  fill: none;
  stroke: var(--text-muted);
  stroke-width: 2;
  stroke-linecap: round;
}

.aircon-remove:hover svg {
  stroke: var(--aircon-over);
}

.aircon-add {
  margin-top: 0.5rem;
  width: 100%;
  font: inherit;
  font-size: 0.85rem;
  cursor: pointer;
  background: none;
  color: var(--text-muted);
  border: 1px dashed var(--border-control);
  border-radius: 6px;
  padding: 0.45rem 0.5rem;
}

.aircon-add:hover {
  border-color: var(--flow-casing);
  color: var(--text-primary);
}

/* ---- The weekly schedule (CF-AC-041 … CF-AC-045) ---- */

/* What the schedule is asking for now, in a sentence. */
.aircon-weekly-now {
  margin: 0.5rem 0.1rem 0.2rem;
  font-size: 0.82rem;
  color: var(--text-muted);
}

.aircon-period {
  margin-top: 0.5rem;
  padding: 0.45rem 0.45rem 0.4rem;
  border: 1px solid var(--border-control);
  border-radius: 8px;
  background: var(--surface-page);
}

.aircon-period .aircon-row {
  margin-top: 0.4rem;
}

/* The period in force now: the accent down its leading edge. */
.aircon-period.in-force {
  border-color: var(--flow-casing);
  box-shadow: inset 3px 0 0 var(--flow-casing);
}

/* Two periods claiming the same time (CF-AC-044), marked on both. The
   failure colour, since it is something the reader has to put right. */
.aircon-period.overlaps {
  border-color: var(--failure-mark);
}

.aircon-period-note {
  margin: 0.35rem 0.1rem 0;
  font-size: 0.75rem;
  color: var(--failure-mark);
}

/* A unit the backend cannot reach (CF-AC-054): the word and one sentence in
   place of the thermometer and the plans. Quiet, since it is a standing fact
   about the unit rather than a fault. */
.aircon-unavailable {
  padding: 2.5rem 1rem;
  text-align: center;
  /* Across the whole card in landscape too, where the card has two columns. */
  grid-column: 1 / -1;
}

.aircon-unavailable[hidden],
.aircon-gauge[hidden],
.aircon-plans[hidden] {
  display: none;
}

.aircon-unavailable-title {
  margin: 0;
  font-size: 1.25rem;
  color: var(--text-muted);
}

.aircon-unavailable-note {
  margin: 0.5rem 0 0;
  font-size: 0.85rem;
  color: var(--text-muted);
}

/* A tab for a unit that cannot be reached, drained as an off one is. */
.aircon-tab.unavailable .aircon-tab-target {
  color: var(--text-reading-idle);
}

/* A link beside an unreachable unit cannot be pressed. */
.aircon-link.unusable .aircon-link-button {
  opacity: 0.35;
  cursor: default;
}

/* Seven day buttons across the period's width, Monday first. */
.aircon-days {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 0.2rem;
}

.aircon-day {
  font: inherit;
  font-size: 0.76rem;
  cursor: pointer;
  padding: 0.35rem 0;
  min-width: 0;
  background: var(--surface-panel);
  color: var(--text-muted);
  border: 1px solid var(--border-control);
  border-radius: 6px;
}

.aircon-day[aria-pressed='true'] {
  background: var(--flow-bore);
  border-color: var(--flow-casing);
  color: var(--text-primary);
  font-weight: 600;
}

.aircon-day:focus-visible {
  outline: 2px solid var(--flow-casing);
  outline-offset: 1px;
}

/* An end at or before the start runs into the next morning (CF-AC-041). */
.aircon-next-day {
  font-size: 0.65rem;
  margin-left: 0.15rem;
  color: var(--text-muted);
  align-self: flex-start;
}

/* ---- The popups (CF-AC-023 … CF-AC-029) ---- */
.aircon-pop-backdrop {
  position: fixed;
  inset: 0;
  z-index: 40;
  background: rgba(0, 0, 0, 0.35);
}

.aircon-pop {
  position: fixed;
  z-index: 41;
  background: var(--surface-card);
  border: 1px solid var(--border-card);
  border-radius: 10px;
  padding: 0.55rem;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
  /* The height is capped by the placement code where there is not enough
     room above the row; two opinions here would disagree. */
  overflow: auto;
  /* Narrow enough for a phone, and the fractional grids inside size
     themselves against it. */
  width: 15rem;
  max-width: calc(100vw - 1.5rem);
}

.aircon-pop-label {
  margin: 0.1rem 0.25rem 0.4rem;
  font-size: 0.72rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

.aircon-pop-grid {
  display: grid;
  gap: 0.3rem;
}

.aircon-pop-delays {
  grid-template-columns: repeat(4, 1fr);
}

.aircon-pop-commands {
  grid-template-columns: repeat(3, 1fr);
}

/* The hour as two digit rows — three tens then ten units — rather than 24
   targets, which is too many to hit on a phone. The units sit as two rows of
   five, which gives a bigger target than one row of ten. */
.aircon-pop-tens {
  grid-template-columns: repeat(3, 2.6rem);
  margin-bottom: 0.3rem;
}

.aircon-pop-units {
  grid-template-columns: repeat(5, 1fr);
  gap: 0.25rem;
}

.aircon-pop-minutes {
  grid-template-columns: repeat(4, 1fr);
}

.aircon-pop-option {
  font: inherit;
  font-size: 0.82rem;
  cursor: pointer;
  padding: 0.4rem 0.2rem;
  background: var(--surface-panel);
  color: var(--text-primary);
  border: 1px solid var(--border-control);
  border-radius: 6px;
  font-variant-numeric: tabular-nums;
}

.aircon-pop-units .aircon-pop-option {
  padding: 0.42rem 0.1rem;
}

.aircon-pop-option:hover {
  border-color: var(--flow-casing);
}

.aircon-pop-option[aria-pressed='true'] {
  background: var(--flow-bore);
  border-color: var(--flow-casing);
}

/* An hour that does not exist, such as 2 then 7. Still in place, so the grid
   does not reflow under a finger already on its way down (CF-AC-025). */
.aircon-pop-option:disabled {
  opacity: 0.25;
  cursor: default;
}

.aircon-pop-option:disabled:hover {
  border-color: var(--border-control);
}

.aircon-pop-off {
  grid-column: 1 / -1;
  color: var(--text-muted);
}

.aircon-pop-tabs {
  display: flex;
  gap: 0.3rem;
  margin-bottom: 0.45rem;
}

.aircon-pop-tab {
  flex: 1;
  font: inherit;
  font-size: 0.76rem;
  cursor: pointer;
  padding: 0.28rem;
  background: var(--surface-panel);
  color: var(--text-muted);
  border: 1px solid var(--border-control);
  border-radius: 6px;
}

.aircon-pop-tab[aria-pressed='true'] {
  background: var(--flow-casing);
  color: #fff;
  border-color: var(--flow-casing);
}

.aircon-pop-done {
  width: 100%;
  margin-top: 0.5rem;
  font: inherit;
  font-size: 0.82rem;
  cursor: pointer;
  padding: 0.4rem;
  background: var(--flow-casing);
  color: #fff;
  border: 1px solid var(--flow-casing);
  border-radius: 6px;
}

/* --- The Lights page (SPEC-LIGHTS.md) ---

   The map and nothing else: it fills the pane, and both controls sit over its
   bottom-right corner rather than in rows of their own. A row of chrome costs
   the same height on every storey to show six short words, and the picture is
   the page. */
.lights-page {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  /* The page fills the shell's pane rather than growing with its contents:
     the plan is scaled to fit, so a page that grew would scale the plan to
     fit a box that the plan itself had sized. */
  height: 100%;
  min-height: 0;
}

/* The view toggle and the floor selector, as one cluster in the map's
   bottom-right corner: the corner a thumb already reaches on a phone held in
   one hand (SPEC-LIGHTS.md §The floor selector sits at the bottom right). The
   floors sit furthest right because they are pressed most often. */
.lights-controls {
  position: absolute;
  right: 0;
  bottom: 0;
  display: flex;
  gap: 0.4rem;
  align-items: flex-end;
  /* Over the plan rather than beside it: the map is the page, and a bar that
     took a row of its own would take that height from every storey to show
     six short words. */
  z-index: 1;
}

/* Both selectors are `.settings-choices`, the client's text-label segmented
   control — see the battery's mode row. Only the sizing is this page's, since
   these sit over a drawing rather than in a settings column. */
.lights-views .settings-choice,
.lights-floors .settings-choice {
  font-size: 0.78rem;
  /* Enough of a target for a thumb. The text is small, so the padding is what
     carries the size rather than the font. */
  min-height: 2.4rem;
  padding: 0.3rem 0.6rem;
  white-space: nowrap;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* The views are glyphs, so they need no width beyond the icon and can give
   what they save to the floors. */
.lights-views .settings-choice {
  padding: 0.3rem 0.5rem;
}

.lights-view-icon {
  width: 1.05rem;
  height: 1.05rem;
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Wider than the views, because these are the control pressed most and were
   reported hard to hit on a phone. A thumb is the least precise pointer
   there is, and these are the buttons it aims at most often. */
.lights-floors .settings-choice {
  min-width: 2.6rem;
  padding: 0.3rem 0.5rem;
}

.lights-floor-icon {
  width: 1rem;
  height: 1rem;
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
}

/* A thumb may slide along the bar to correct a mispress, which means the
   press must not also be interpreted as a scroll or a text selection. */
.lights-floors {
  touch-action: none;
  user-select: none;
}

/* Which one is selected, made obvious. The shared `.settings-choice.selected`
   is a slightly lighter background and bolder text, which is enough on the
   settings page — a plain surface, read close up — and was reported hard to
   see here, where the control is small, sits over a drawing and is looked at
   from arm's length.

   **The whole border is lit, and there is no shadow.** A first attempt used
   an inset shadow for a pressed look and was reported still not visible
   enough; a shadow reads as depth, which is a weak signal at this size and
   against a dark ground. A ring right round the button is unmissable and
   does not depend on telling two greys apart.

   Drawn with an inset box-shadow rather than `border-color`, because the
   buttons share single-pixel dividers with their neighbours: colouring the
   border moves nothing but leaves the ring broken wherever a divider is
   drawn on top of it. An inset spread paints inside the button's own box,
   so the ring is complete whatever the group does at its edges. */
.lights-controls .settings-choice.selected {
  background: var(--surface-panel);
  color: var(--text-primary);
  box-shadow: inset 0 0 0 2px var(--flow-casing);
  /* Above its neighbours, so the ring is never half-covered by the next
     button's divider. */
  position: relative;
  z-index: 1;
}

/* The cluster sits over the map, so it needs its own ground rather than the
   page's: `.settings-choices` is transparent to the surface behind it, and
   here that surface is a floor plan. */
.lights-controls .settings-choices {
  background: var(--surface-card);
}

/* The map, and the room name that a press puts under it. Positioned, so the
   floor selector can sit in its bottom-right corner. */
.lights-stage {
  position: relative;
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

.lights-plan {
  /* The drawing is the shape of the house, not of the box it is given, so it
     is fitted whole rather than cropped or stretched. */
  height: 100%;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Clear of the corner controls, which are drawn over this. Reported: on
     the ground floor the erker reaches the bottom of the drawing and was
     touching the buttons. The controls are about 2.5rem tall, and this is
     that plus a gap. */
  box-sizing: border-box;
  padding-bottom: 3.25rem;
}

.lights-plan svg {
  height: 100%;
  width: 100%;
}

/* What was last pressed, named. A placeholder for switching, so that the map
   can be judged as a map before there is anything on it to switch.

   **Shown in control alone.** It shares the bottom-left corner with the edit
   view's device chooser, and the two were painted one over the other because
   the CSS here claimed they could never both be showing. They can: pressing
   a device in edit named it through this element as well. The claim was
   written as a comment rather than as a rule, so nothing checked it — which
   is why it is now a selector. */
.lights-page[data-view='control'] .lights-picked {
  position: absolute;
  left: 0;
  bottom: 0;
  font-size: 0.85rem;
  color: var(--text-muted);
  pointer-events: none;
}

.lights-page:not([data-view='control']) .lights-picked,
.lights-picked.is-empty {
  display: none;
}

/* The floors run right to left, ground floor rightmost, so that the storey a
   reader starts from is the one nearest the thumb and moving left moves up
   the house. Reversed here rather than in the markup, because the markup's
   order is the reading order a screen reader follows: top storey first, as
   the list itself is written. */
.lights-floors {
  flex-direction: row-reverse;
}

/* row-reverse flips which edge each divider belongs to. Without this the
   dividers double up on one side and the group's own border is drawn over. */
.lights-floors .settings-choice + .settings-choice {
  border-left: 0;
  border-right: 1px solid var(--border-control);
}

/* All three storeys at once, the same drawings as a single storey, each with
   a button that goes to that storey (SPEC-LIGHTS.md §The overview shows the
   storeys together). */
/* Two by two, or one row of three: `_fitOverview` measures both against the
   room the stage has and sets `--overview-columns` to whichever draws the
   storeys larger (CF-LIT-146). On a phone the plans are tall against a
   narrow screen and two by two wins; on a wide window it is the row.

   Two by two leaves the fourth quadrant empty. With three storeys there is
   no arrangement that fills a rectangle, and leaving the gap is better than
   stretching one plan to fill it — they are drawn to one scale, and a storey
   drawn larger than its neighbours would read as a bigger storey. Equal
   `1fr` cells are what keeps the scale one in either arrangement. */
.lights-overview {
  display: grid;
  grid-template-columns: repeat(var(--overview-columns, 2), 1fr);
  grid-auto-rows: 1fr;
  gap: 0.5rem;
  align-items: stretch;
  justify-items: stretch;
  width: 100%;
  height: 100%;
  /* Room for the controls, which sit over this as they do over a plan.
     `border-box`, or the padding is added to the full height and the row
     overflows the stage by exactly that much — which it did, pushing the
     plans up out of the top and the captions down under the controls. */
  box-sizing: border-box;
  /* Clear of the connection indicator above and the controls below. The
     indicator runs the full width of the page and is drawn over everything,
     so the first row starts below it. Less room is needed than when the
     storey names sat at the top of each cell, but not none: the plans
     themselves would otherwise run under it. */
  padding-top: 1.2rem;
  padding-bottom: 3rem;
}

/* A plain box holding two layers: the storey's button underneath, and the
   drawing with its name on top. Positioned so the button can fill it. */
.lights-overview-cell {
  position: relative;
  /* Equal cells whatever the plans' own proportions: three storeys of one
     house, drawn to one scale, read as a section through the building. */
  min-width: 0;
  min-height: 0;
  padding: 0.3rem;
  border: 1px solid transparent;
  color: var(--text-muted);
}

/* The storey's button, the whole cell, *behind* the drawing rather than
   around it. The devices on the drawing can be controls themselves
   (CF-LIT-147), and a control inside a button is two targets claiming one
   press. The drawing above is transparent to the pointer, so a press that
   reaches no pressable device lands here and selects the storey
   (CF-LIT-031). It draws the cell's hover and focus edge, being the thing
   hovered and focused. */
.lights-overview-open {
  position: absolute;
  inset: -1px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 8px;
  background: none;
  cursor: pointer;
}

.lights-overview-open:hover,
.lights-overview-open:focus-visible {
  border-color: var(--border-control);
  outline: none;
}

/* The real layout box: the plan takes what is left, the caption takes what
   it needs. `1fr auto` says which of the two is elastic and ends the
   argument — a flex child holding an SVG would negotiate its own height
   from the drawing's aspect ratio instead.

   **Not a button, and not inside one.** WebKit lays a button's children out
   in an anonymous internal block, so a grid declared on a button is not the
   box its children sit in: on iOS the two rows once collapsed and the
   caption was painted over the plan's bottom wall, while Firefox in its own
   iPhone mode was correct (CF-LIT-082). A plain element is a reliable grid
   container in every engine.

   Above the button in paint order, and transparent to the pointer so the
   button still takes every press the devices do not. */
.lights-overview-cell-inner {
  position: relative;
  display: grid;
  grid-template-rows: 1fr auto;
  justify-items: center;
  gap: 0.25rem;
  width: 100%;
  height: 100%;
  min-width: 0;
  min-height: 0;
  pointer-events: none;
}

/* The drawing fills its row and keeps its shape. `min-height: 0` is what
   lets a grid row shrink below its content's intrinsic size — without it
   the SVG's own aspect ratio sets a floor and the row grows past the
   cell. */
.lights-overview-cell svg {
  width: 100%;
  height: 100%;
  min-height: 0;
  object-fit: contain;
  /* A press on the drawing is a press on the storey's button beneath it.
     Without this the attic's translated box also took presses aimed at the
     caption beneath it — see the translate rule below. */
  pointer-events: none;
}

/* The devices' press targets take the pointer of their own accord
   (`plan.css`), which on the overview they may do only where they are drawn
   large enough to hit (CF-LIT-147). `is-pressable` is set from the measured
   size of a target, not from the width of the window: below it a mis-press
   would switch a light in another room, so the press goes to the storey. */
.lights-overview .plan-device-target,
.lights-overview .plan-device-aim {
  pointer-events: none;
}

.lights-overview.is-pressable .plan-device-target {
  pointer-events: auto;
}

/* The attic hangs lower, so the storeys line up along the back of the house
   rather than floating at their own heights.

   All three plans share one coordinate box, and in it the ground and first
   floors both end at y=11600 while the attic stops at 9680 — measured, not
   estimated. That 1920 of 11800 is the 16% below, and it is what makes the
   overview read as a section through the building instead of three
   unrelated pictures.

   `translate` moves the painted drawing but not the box it is laid out in,
   so the attic's plan ends 16% below its own row and overlaps the caption
   beneath it — measured, its box reached 380px while its label began at
   343. Nothing is hidden, because the walls are painted well above that,
   but the box was taking presses aimed at the label. Reaching for a
   matching bottom margin does not work: a percentage margin resolves
   against the *width*, and the grid row absorbs it either way.

   The drawing is made transparent to the pointer instead (see the rule
   above), which is what it should always have been: a press anywhere on it
   that is not on a pressable device selects that storey (CF-LIT-031). */
.lights-overview-cell[data-floor='tweede-verdieping'] svg {
  transform: translateY(16%);
}

.lights-overview-label {
  font-size: 0.65rem;
  line-height: 1.4;
  text-align: center;
  /* The storey names are long enough to wrap in a third of a phone; cut
     rather than wrapped, since the drawing above already says which is
     which and two lines would push the plans smaller. */
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* In the overview nothing is placed, so the chooser has no storey to place
   onto and is hidden — the same reasoning that hides it outside Edit. */
.lights-page.is-overview .lights-device {
  display: none;
}

/* The Edit view's device chooser, bottom-left of the map: the same edge as
   the view and floor controls but the far end of it, so a thumb reaching for
   those cannot catch this instead (SPEC-LIGHTS.md §Choosing a device to
   place).

   **Not the top**, which is where the connection indicator sits. Measured
   rather than assumed: at phone width the indicator runs the full width of
   the page from 8px to 39px, and a chooser at the top-left was drawn
   underneath it with only its second line showing. The bottom edge is also
   simply the better place, for the reason the floor selector is there.

   It shares the corner with `.lights-picked`, which names a pressed room.
   That line is scoped to the control view by a selector, so the corner is
   not contested — and it is a *selector* because this comment used to say
   the two could never both show, which was false. A name left from a press
   in control survived into edit and the two were painted one over the
   other. An invariant worth relying on is worth stating in a rule. */
.lights-device {
  position: absolute;
  left: 0;
  bottom: 0;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.25rem;
  /* Ends where the corner cluster begins. That figure is published by the
     cluster itself rather than guessed: a percentage was tried at 45% and
     still overlapped it by 11px at phone width, because the two answer to
     nothing in common — the cluster is sized by its own labels, which change
     with the language. */
  right: calc(var(--lights-cluster-width, 14rem) + 0.5rem);
}

/* Shown in Edit alone. The other two views do not place anything, and a
   chooser they ignored would be a control over the map costing the map room
   for nothing. */
.lights-page:not([data-view='edit']) .lights-device {
  display: none;
}

.device-selector-button {
  font: inherit;
  font-size: 0.78rem;
  min-height: 2.1rem;
  padding: 0.3rem 0.6rem;
  border: 1px solid var(--border-control);
  border-radius: 8px;
  background: var(--surface-card);
  color: var(--text-panel);
  cursor: pointer;
  text-align: left;
  /* A long device name is cut rather than allowed to push the button across
     the plan: the full name is in the list this button opens. */
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.device-selector-button.has-selection {
  border-color: var(--accent, var(--border-control));
}

/* What is selected, named under the button as asked, with where it is. */
.device-selector-chosen {
  font-size: 0.7rem;
  color: var(--text-muted);
  padding-left: 0.15rem;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.device-selector-popover[hidden] {
  display: none;
}

.device-selector-popover-panel {
  position: fixed;
  /* Matched to the other two popovers, which are opened from inside a panel
     and have to clear `.subject-modal-backdrop` at 40. This one is opened
     over a map rather than a panel, but sharing the figure is what keeps
     three popovers from each picking their own and tying. */
  z-index: 41;
  width: min(19rem, calc(100vw - 2rem));
  padding: 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
  border: 1px solid var(--border-control);
  border-radius: 8px;
  background: var(--surface-panel);
  color: var(--text-panel);
  box-shadow: 0 8px 24px rgb(0 0 0 / 0.35);
}

.device-selector-tab {
  flex: 1;
}

/* The house has 33 lights, so the list scrolls rather than growing past the
   screen. Bounded in viewport units as well as rems: the panel is positioned
   against the window, and a list taller than the window would put its last
   entries where nothing can reach them. */
.device-selector-list {
  display: flex;
  flex-direction: column;
  max-height: min(18rem, 50vh);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

.device-selector-option {
  font: inherit;
  font-size: 0.8rem;
  display: flex;
  justify-content: space-between;
  /* Centred, not on a baseline. A baseline aligns the *text* of the name
     and the storey with each other, which is right — but the symbol beside
     the row is a drawing with no baseline of its own, so the two ended up
     visibly out of line, the icon sitting low against the name. Reported
     from the app. */
  align-items: center;
  gap: 0.6rem;
  /* Comfortably tappable: this is a list a reader works down on a phone. */
  min-height: 2.2rem;
  /* No padding on the left: the symbol is already a 2.2rem target with its
     own air, and this was added to it — which is where the gap between the
     icon and the name came from. */
  padding: 0.35rem 0.4rem 0.35rem 0;
  border: 0;
  border-radius: 6px;
  background: none;
  color: inherit;
  cursor: pointer;
  text-align: left;
}

.device-selector-option:hover {
  background: var(--surface-card);
}

.device-selector-option[aria-selected='true'] {
  background: var(--surface-card);
  font-weight: 600;
}

.device-selector-option-name {
  /* Takes the row's slack, so the storey and the figures after it sit
     together at the end rather than the storey floating in the middle. */
  margin-right: auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Where the device is. Kept from shrinking so the names line up against it
   rather than against a ragged column. */
.device-selector-option-floor {
  flex: none;
  font-size: 0.7rem;
  color: var(--text-muted);
}

/* Not placed anywhere. Dimmer still than a storey name, because it is the
   absence of an answer rather than a different one. */
.device-selector-option-floor.is-unplaced {
  opacity: 0.65;
  font-style: italic;
}

.device-selector-empty {
  margin: 0;
  padding: 0.5rem 0.4rem;
  font-size: 0.75rem;
  color: var(--text-muted);
}

/* A row: a switch, then the name and where it is. Two controls rather than
   one, because they do different things — the switch turns the thing on and
   the rest opens it (SPEC-LIGHTS.md §The list is the same rows without a
   map). */
.device-selector-row {
  display: flex;
  align-items: center;
  gap: 0.15rem;
  /* How much room the switch takes. Held once, because the rows that have
     no switch reserve the same width so every name starts in the same
     place — two figures would part company the first time either moved. */
  --device-power-width: 1.8rem;
}

.device-selector-row .device-selector-option {
  flex: 1 1 auto;
  min-width: 0;
}

/* The lamp symbol, the same one the map draws, so a reader who has learned
   it on a plan reads the list without learning a second. */
.device-selector-power {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Tall enough for a thumb, narrow enough that the name starts near the
     symbol rather than a centimetre away. The height is the target; the
     width was carrying air the row did not need. */
  width: var(--device-power-width, 1.8rem);
  height: 2.2rem;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--plan-lamp, #3a424a);
  cursor: pointer;
}

.device-selector-power:hover {
  background: var(--surface-card);
}

.device-selector-power.is-on {
  color: var(--plan-lamp-on, #f5c542);
}

/* Not known: neither lit nor dark, which is a third appearance rather than
   a default (CF-LIT-035). */
.device-selector-power.is-unknown {
  opacity: 0.45;
}

.device-selector-power-icon {
  width: 1.15rem;
  height: 1.15rem;
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
}

/* Where a switch would be, for a row that has nothing to switch, so every
   name starts at the same place. */
.device-selector-power-gap {
  flex: none;
  width: var(--device-power-width, 1.8rem);
}

/* A button's row opens nothing, so it must not look as though it would. It
   is a div rather than a button, and a div measures its min-height without
   the padding where a button includes it — so the box model is stated, or
   these rows come out a third taller than every other row in the list. */
.device-selector-option.is-static {
  box-sizing: border-box;
  cursor: default;
}

.device-selector-option.is-static:hover {
  background: none;
}

/* A sensor's temperature and a battery's charge, at the row's end. The
   storey column sits before them and is allowed to be empty, so the figures
   stay in one column down the list whatever the rows before them say. */
.device-selector-figures {
  flex: none;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-size: 0.7rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* Each reading keeps one width down the list, right-aligned, so the figures
   form columns rather than shifting with every "0.0 lux" against "16 lux".
   Sized for the widest the house produces — "19.9 °C", "123 lux". */
.device-selector-temperature,
.device-selector-lux {
  text-align: right;
  white-space: nowrap;
}

.device-selector-temperature {
  min-width: 2.9rem;
}

.device-selector-lux {
  min-width: 3rem;
}

/* The glyph alone; its figure is the hover text. It ends every row that has
   one, so the batteries form their own column at the right edge. */
.device-selector-battery {
  display: inline-flex;
  align-items: center;
  cursor: default;
}

/* A little larger than the other glyphs on the row, because its fill is now
   the only figure a reader sees without hovering. */
.device-selector-battery-icon {
  width: 1.3rem;
  height: 1.3rem;
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
}

.device-selector-battery-fill {
  fill: currentColor;
  stroke: none;
}

/* The bridge says this battery wants changing. Coloured so the ones to
   change are the ones that stand out; a battery that is merely partly used
   stays muted like every other figure on the row. */
.device-selector-battery.is-low,
.device-selector-battery.is-critical {
  color: var(--failure-text);
}

.device-selector-battery.is-critical {
  font-weight: 600;
}

/* The list view: the same tabs, filter and rows as the chooser, on the whole
   page instead of in a panel (SPEC-LIGHTS.md §The list is the same rows
   without a map). One class builds both, so this only has to place them. */
.lights-list {
  position: absolute;
  inset: 0;
  display: none;
  flex-direction: column;
  gap: 0.5rem;
  /* Clear of the connection indicator at the top — it runs the full width
     from 8px to 39px, and the tabs were drawn underneath it — and of the
     view and floor controls at the bottom, which stay reachable here. */
  padding: 2.6rem 0.5rem 3.5rem;
  box-sizing: border-box;
  background: var(--surface-page);
  /* Above the plan, which stays in the document so that returning to a map
     view does not refetch it. Below `.lights-controls`, which is the one
     thing that must stay pressable over the list. */
  z-index: 1;
}

/* Over the list, since the view and floor buttons are how a reader leaves
   it. The list's own ground would otherwise cover them. */
.lights-page[data-view='list'] .lights-controls {
  z-index: 2;
}

.lights-page[data-view='list'] .lights-list {
  display: flex;
}

/* The plan is pointless in the list view and the controls are not: the view
   and floor buttons stay reachable over the list. */
.lights-page[data-view='list'] .lights-plan,
.lights-page[data-view='list'] .lights-picked {
  visibility: hidden;
}

/* On the page the list takes the height it is given rather than a capped
   panel's, which is the point of having it. */
.lights-list .device-selector-list {
  flex: 1 1 auto;
  max-height: none;
  min-height: 0;
}

/* The tabs keep their own height. Inside a flex column they are otherwise
   squashed to whatever is left over, which took the tab bar down to sixteen
   pixels and cut its labels in half. */
.lights-list .device-selector-tabs {
  flex: none;
}

.lights-list .device-selector-tab {
  min-height: 2.4rem;
  /* Five tabs on a 375px phone: at the control's usual side padding the
     fifth wrapped onto a row of its own. The labels are short in both
     languages, so the air beside them is what gives. */
  padding-left: 0.25rem;
  padding-right: 0.25rem;
  /* Sized from the label rather than shared equally: "Sensoren" in bold is
     wider than a fifth of a phone, and "Overig" has room to give it. */
  flex: 1 1 auto;
}

.lights-list .device-selector-tabs {
  flex-wrap: nowrap;
}

/* The automations view: what the house does by itself, drawn over the map
   exactly as the list view is (SPEC-LIGHTS.md §The automations are read here
   and written elsewhere).

   Much smaller than it was. The editor it used to hold — a trigger's fields,
   a chain of steps, a target per action — is gone, and with it the whole
   argument about fitting four controls on a phone line: a rule is now read
   here and written into CasaFlux's own configuration. */
.lights-automations {
  position: absolute;
  inset: 0;
  display: none;
  flex-direction: column;
  gap: 0.5rem;
  /* The same clearances the list keeps: the connection indicator above, the
     view and floor controls below. */
  padding: 2.6rem 0.5rem 3.5rem;
  box-sizing: border-box;
  background: var(--surface-page);
  /* The *list* scrolls, not the view. Scrolling the view took the note at
     the foot of it out of sight, and slid it across the cards on the way:
     it is the last child of a scrolling box rather than a fixed footer. */
  overflow: hidden;
  z-index: 1;
}

.lights-page[data-view='automations'] .lights-automations {
  display: flex;
}

.lights-page[data-view='automations'] .lights-controls {
  z-index: 2;
}

.lights-page[data-view='automations'] .lights-plan,
.lights-page[data-view='automations'] .lights-picked {
  visibility: hidden;
}

.automations-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  flex: none;
}

.automations-title {
  margin: 0;
  font-size: 1rem;
  font-weight: 600;
  color: var(--text-primary);
}

.automations-note,
.automations-empty {
  margin: 0;
  flex: none;
  font-size: 0.8rem;
  line-height: 1.4;
  color: var(--text-muted);
}

/* The note sits under the scrolling list and must not be transparent: a card
   sliding beneath it showed through the words. A rule above it says where
   the list ends, and the page's own colour hides what is passing under. */
.automations-note {
  background: var(--surface-page);
  border-top: 1px solid var(--border-card);
  padding-top: 0.4rem;
}

/* The tabs: buttons, sensors, other (CF-LIT-137). They replace the two
   headed sections the list used to have, which still left one long scroll.
   Full width and a thumb's height, like the device list's tabs, and kept
   from being squashed by the scrolling list below them. */
.automations-tabs {
  display: flex;
  flex: none;
}

.automations-tabs[hidden] {
  display: none;
}

.automations-tab {
  flex: 1;
  min-height: 2.4rem;
}

/* A tab holding a rule CasaFlux could not use (CF-LIT-139): a dot in the
   failure colour, so a broken rule on a tab nobody has opened still shows. */
.automations-tab.has-broken::after {
  content: '';
  display: inline-block;
  width: 0.45rem;
  height: 0.45rem;
  margin-left: 0.35rem;
  border-radius: 50%;
  background: var(--failure-mark);
  vertical-align: middle;
}

.automations-list {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  flex: 1 1 auto;
  min-height: 0;
  /* This is the part that scrolls, so the heading above and the note below
     stay where they are. */
  overflow-y: auto;
}

.automation {
  border: 1px solid var(--border-card);
  border-radius: 0.5rem;
  background: var(--surface-card);
  /* Less above and below than beside: the head's own targets are already a
     thumb tall, and a closed card is one line in a column a reader scans. */
  padding: 0.25rem 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.automation.is-open {
  padding-bottom: 0.5rem;
}

/* A disabled rule is a working rule that is not running, so it is dimmed
   rather than hidden — the holiday rules exist all year. */
.automation-off {
  opacity: 0.55;
}

.automation-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.automation-name {
  margin: 0;
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--text-primary);
  flex: 1 1 auto;
  min-width: 0;
}

/* The part of the head that opens the card (CF-LIT-138): everything but the
   switch, so the target is as wide as the card and a thumb cannot miss it. */
.automation-toggle {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  width: 100%;
  min-height: 2.2rem;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

/* A chevron pointing right when closed and down when open — the shape every
   platform uses for "there is more under this". */
.automation-chevron {
  flex: none;
  width: 0.45rem;
  height: 0.45rem;
  border-right: 2px solid var(--text-muted);
  border-bottom: 2px solid var(--text-muted);
  transform: rotate(-45deg);
  margin: 0 0.15rem 0 0.1rem;
}

.automation.is-open .automation-chevron {
  transform: rotate(45deg);
}

.automation-body {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.automation-body[hidden] {
  display: none;
}

.automation-enable {
  border: 1px solid var(--border-card);
  border-radius: 0.35rem;
  background: var(--surface-page);
  color: var(--text-muted);
  font: inherit;
  font-size: 0.8rem;
  min-width: 2.8rem;
  min-height: 2.2rem;
  padding: 0 0.5rem;
  flex: none;
  cursor: pointer;
}

.automation-enable[aria-pressed='true'] {
  color: var(--text-primary);
  border-color: var(--text-primary);
}

/* What the rule does, in its own words. This is the view's whole job now,
   so it is given the room a summary deserves rather than a truncated line:
   a rule written in a language this client does not parse can still say
   what it does, and this is where it says it (CF-LIT-119). */
.automation-summary {
  margin: 0;
  font-size: 0.85rem;
  line-height: 1.45;
  color: var(--text-muted);
}

/* The bands of one grouped rule (CF-LIT-125).
 *
 * Listed rather than run together into a paragraph: they are genuinely
 * different behaviours, and a reader checking what the hall does at two in
 * the morning wants to find that sentence rather than read past two others.
 * The rule they belong to carries the name and the switch. */
.automation-bands {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.automation-band {
  margin: 0;
  font-size: 0.85rem;
  line-height: 1.45;
  color: var(--text-muted);
  /* A rule the eye can follow back to the group's own switch. */
  padding-left: 0.5rem;
  border-left: 2px solid var(--border-card);
}

/* A band switched off while its neighbours run. The group's own switch
   cannot produce this, so it is worth saying rather than leaving a reader
   with a description of something that is not happening. */
.automation-band-off {
  opacity: 0.5;
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}

/* A rule with no description at all. Distinguished from one that has a
   short description, because the two mean different things. */
.automation-undescribed {
  font-style: italic;
}

/* A rule CasaFlux could not use, said out loud. The failure that matters
   here is the silent one: a rule that is simply missing looks exactly like
   a rule nobody wrote (CF-LIT-122). */
.automation-broken {
  border-color: var(--failure-mark);
}

.automation-error {
  margin: 0;
  font-size: 0.8rem;
  line-height: 1.4;
  color: var(--failure-text);
  background: var(--failure-surface);
  border-radius: 0.35rem;
  padding: 0.3rem 0.4rem;
}

/* One device's own controls, opened beside it. */
.device-dialog[hidden] {
  display: none;
}

.device-dialog-panel {
  position: fixed;
  /* The same figure the other popovers use, for the same reason: three
     panels each picking their own would tie, and a tie between siblings is
     resolved by document order rather than by intent. */
  z-index: 41;
  width: min(17rem, calc(100vw - 2rem));
  padding: 0.6rem;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  border: 1px solid var(--border-control);
  border-radius: 8px;
  background: var(--surface-panel);
  color: var(--text-panel);
  box-shadow: 0 8px 24px rgb(0 0 0 / 0.35);
}

/* The name and the close button on one line, the button hard right. */
.device-dialog-head {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.device-dialog-name {
  margin: 0;
  /* Takes the room the button does not, so a long name ellipsises rather
     than pushing the close button off the panel. */
  flex: 1 1 auto;
  min-width: 0;
  font-size: 0.85rem;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Renaming, in the edit view only. Sits between the name and the close
   button, so the row reads name → change it → dismiss. Quieter than the
   close, which is the control every panel has. */
.device-dialog-rename {
  flex: none;
  display: grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
}

.device-dialog-rename svg {
  width: 0.85rem;
  height: 0.85rem;
  stroke: currentcolor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
  fill: none;
}

.device-dialog-rename:hover,
.device-dialog-rename:focus-visible {
  background: var(--surface-card);
  color: var(--text-panel);
  outline: none;
}

.device-dialog-close {
  flex: none;
  /* A press target that a thumb can hit, around a smaller drawn cross: the
     panel is small and the button sits at its very corner, where an
     undersized target is missed outwards — onto the map behind, which is
     exactly the press this button exists to avoid. */
  display: grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
}

.device-dialog-close svg {
  width: 0.85rem;
  height: 0.85rem;
  stroke: currentcolor;
  stroke-width: 2.2;
  stroke-linecap: round;
  fill: none;
}

.device-dialog-close:hover,
.device-dialog-close:focus-visible {
  background: var(--surface-card);
  color: var(--text-panel);
  outline: none;
}

.device-dialog-body {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}

.device-dialog-toggle {
  font: inherit;
  font-size: 0.8rem;
  min-height: 2.4rem;
  border: 1px solid var(--border-control);
  border-radius: 8px;
  background: var(--surface-card);
  color: var(--text-panel);
  cursor: pointer;
}

/* Lit, so the control shows the state it is about to leave as well as the
   one it offers. */
.device-dialog-toggle.is-on {
  border-color: var(--plan-lamp-on, #f5c542);
  color: var(--plan-lamp-on, #f5c542);
}

.device-dialog-slider {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0.2rem 0.5rem;
  font-size: 0.75rem;
  color: var(--text-muted);
}

.device-dialog-slider input {
  grid-column: 1 / -1;
  width: 100%;
  margin: 0;
  accent-color: var(--plan-lamp-on, #f5c542);
}

.device-dialog-slider-value {
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}

/* A sensor's readings. A definition list, because that is what they are —
   a name and the thing it names. */
.device-dialog-readings {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.2rem 0.6rem;
  margin: 0;
  font-size: 0.78rem;
}

.device-dialog-readings dt {
  color: var(--text-muted);
}

.device-dialog-readings dd {
  margin: 0;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.device-dialog-groups h3 {
  margin: 0 0 0.3rem;
  font-size: 0.7rem;
  font-weight: 600;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* One row per group, which is what the reader chose where a light is in
   several. Not pressable yet: group editing comes later, and a row that
   looked like a control and did nothing would be a promise unkept. */
/* A group row: its switch turns the whole group, its name opens that
   group's own controls in this same panel. */
.device-dialog-group {
  display: flex;
  align-items: center;
  gap: 0.2rem;
  border-radius: 6px;
  background: var(--surface-card);
}

.device-dialog-group + .device-dialog-group {
  margin-top: 0.2rem;
}

.device-dialog-group-power {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--plan-lamp, #3a424a);
  cursor: pointer;
}

.device-dialog-group-power.is-on {
  color: var(--plan-lamp-on, #f5c542);
}

.device-dialog-group-power.is-unknown {
  opacity: 0.45;
}

.device-dialog-group-power .device-selector-power-icon {
  width: 1.05rem;
  height: 1.05rem;
}

.device-dialog-group-name {
  font: inherit;
  font-size: 0.78rem;
  flex: 1 1 auto;
  min-width: 0;
  padding: 0.35rem 0.4rem 0.35rem 0;
  border: 0;
  background: none;
  color: inherit;
  cursor: pointer;
  text-align: left;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.device-dialog-group-name:hover {
  text-decoration: underline;
}

/* Taking something out of something: a trashcan at the end of its row.
   One rule for all three — a light leaving a group from either side, and a
   device leaving its storey — because they are one idea and three drawings
   would be three chances to differ.

   Quieter than the switch beside it: removal is the rarer act and should
   not compete for the eye with turning a light on. */
/* Deleting the whole group, below its members (CF-LIT-092).
 *
 * Worded rather than drawn as a trashcan, deliberately: the bins just above
 * it each remove one light, and two controls that look alike must not mean
 * different sizes of thing. The rule beneath the member list is what says
 * "everything above this belongs to the group; this acts on the group". */
.device-dialog-delete {
  display: flex;
  justify-content: flex-end;
  margin-top: 0.3rem;
  padding-top: 0.4rem;
  border-top: 1px solid var(--border-card);
}

.device-dialog-delete-group {
  border: 1px solid var(--border-card);
  border-radius: 0.35rem;
  background: none;
  color: var(--text-muted);
  font: inherit;
  font-size: 0.8rem;
  /* A destructive control, so it is a comfortable target rather than a
     small one a thumb can catch by accident. */
  min-height: 2.2rem;
  padding: 0 0.6rem;
  cursor: pointer;
}

.device-dialog-bin {
  flex: none;
  display: grid;
  place-items: center;
  width: 1.6rem;
  height: 1.6rem;
  margin-right: 0.15rem;
  padding: 0;
  border: 0;
  border-radius: 5px;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
}

.device-dialog-bin svg {
  width: 0.8rem;
  height: 0.8rem;
  stroke: currentcolor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
  fill: none;
}

.device-dialog-bin:hover,
.device-dialog-bin:focus-visible {
  color: var(--text-panel);
  outline: none;
}

/* Where a device is placed, and the control that unplaces it. */
.device-dialog-placement {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding-left: 0.4rem;
  border-radius: 6px;
  background: var(--surface-card);
  font-size: 0.76rem;
  color: var(--text-muted);
}

.device-dialog-placement-where {
  flex: 1 1 auto;
  min-width: 0;
  padding: 0.35rem 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Not placed is a real state, not a missing value, so it is said in words
   and marked as the exception rather than left to look like a blank. */
.device-dialog-placement-where.is-unplaced {
  font-style: italic;
}

.device-dialog-group-add {
  margin-top: 0.3rem;
}

.device-dialog-group-add-button {
  font: inherit;
  font-size: 0.74rem;
  width: 100%;
  padding: 0.35rem 0.4rem;
  border: 1px dashed var(--border-control);
  border-radius: 6px;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
  text-align: left;
}

.device-dialog-group-add-button:hover,
.device-dialog-group-add-button:focus-visible {
  color: var(--text-panel);
  outline: none;
}

/* The groups it could join, opened in place rather than as a second panel
   (CF-LIT-060's argument). Capped and scrollable: a house may have many
   groups and the panel must not grow past the screen. */
.device-dialog-group-choices {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  max-height: 9rem;
  margin-top: 0.25rem;
  overflow-y: auto;
}

.device-dialog-group-choices[hidden] {
  display: none;
}

.device-dialog-group-choice {
  font: inherit;
  font-size: 0.76rem;
  padding: 0.3rem 0.4rem;
  border: 0;
  border-radius: 5px;
  background: var(--surface-card);
  color: inherit;
  cursor: pointer;
  text-align: left;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.device-dialog-group-choice.is-new {
  font-style: italic;
  color: var(--text-muted);
}

.device-dialog-group-choice:hover,
.device-dialog-group-choice:focus-visible {
  background: var(--surface-bar);
  outline: none;
}

/* Why a group change did not take. The same mark the override callout uses
   (`--failure-mark`), so a refusal looks the same wherever it is shown. */
.device-dialog-problem {
  margin: 0;
  padding-left: 0.45rem;
  border-left: 3px solid var(--failure-mark);
  font-size: 0.72rem;
  color: var(--text-panel);
}

.device-dialog-empty,
.device-dialog-note {
  margin: 0;
  font-size: 0.72rem;
  color: var(--text-muted);
  font-style: italic;
}

/* ---- The Gas page (SPEC-GAS.md, CF-GAS-001 … CF-GAS-015) ----
   One column, capped at a reading width: a flame and today's cost at the
   top with the price in the corner and the days beneath, then the three
   graphs. The page scrolls with the stack like any other; nothing is pinned,
   because there is nothing here a reader has to keep in view while reading
   something else. */
.gas-page-body {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  max-width: 44rem;
  margin: 0 auto;
  padding: 0.4rem 0.2rem 1rem;
}

/* The flame spans the head's full height on the left; today sits beside it
   with the price at the far right, and the days run under today and the
   price together. */
.gas-head {
  display: grid;
  grid-template-columns: 6.5rem 1fr auto;
  grid-template-areas:
    'flame today price'
    'flame days  days';
  column-gap: 0.9rem;
  row-gap: 0.5rem;
  align-items: start;
}

.gas-flame-box {
  grid-area: flame;
  align-self: stretch;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  min-height: 9rem;
}

/* The flame box (gasFlame.js). Two flames can burn in it: the shader flame,
   "Flame in the wind", and the candle it falls back to where WebGL is
   missing.

   The candle is drawn small and scaled up here, which softens its edges;
   its glow is the gradient behind it, swelling and settling with the
   flame's height, which it publishes as `--gas-flame-lift`.

   The shader flame draws its own halo, so the glow is switched off for it.
   That halo is sized for the wide scene the flame was written for and so
   fills the whole box; its edges are faded out here, or the page would show
   a faintly red rectangle where the box ends. */
.gas-flame-shader::before {
  display: none;
}

/* A dark window for the flame where the page is light (theme.css says why);
   transparent, and so invisible, on the dark theme. */
.gas-flame-shader {
  background: var(--gas-flame-backdrop);
  border-radius: 0.8rem;
}

/* A press on the shader flame slows it or restores it (CF-GAS-019). The
   pointer says so on a desktop; a finger needs no telling. No tap
   highlight: a grey flash over a flame looks like a fault. */
.gas-flame-shader[role='button'] {
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.gas-flame-shader[role='button']:focus-visible {
  outline: 2px solid var(--flow-casing);
  outline-offset: 2px;
}

.gas-flame-shader .gas-flame-canvas {
  -webkit-mask-image: radial-gradient(ellipse 50% 50% at 50% 50%, #000 62%, transparent 100%);
  mask-image: radial-gradient(ellipse 50% 50% at 50% 50%, #000 62%, transparent 100%);
}

.gas-flame {
  position: relative;
  width: 100%;
  aspect-ratio: 84 / 140;
  max-height: 14rem;
}

.gas-flame::before {
  content: '';
  position: absolute;
  inset: 18% -18% -6%;
  background: radial-gradient(closest-side at 50% 62%, var(--gas-flame-glow), transparent);
  transform: scale(calc(0.85 + 0.2 * var(--gas-flame-lift, 1)));
  transform-origin: 50% 90%;
  pointer-events: none;
}

.gas-flame-canvas {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  image-rendering: auto;
}

.gas-today {
  grid-area: today;
  display: flex;
  flex-direction: column;
  gap: 0.05rem;
}

.gas-today-label {
  font-size: 0.85rem;
  color: var(--text-label);
}

.gas-today-value {
  font-size: 2.3rem;
  font-weight: 600;
  line-height: 1.1;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}

.gas-today-note {
  font-size: 0.72rem;
  color: var(--text-muted);
}

/* The price of a m³ now: small, in the corner, a figure to glance at rather
   than to read (CF-GAS-006). A widget of its own so it does not read as a
   line of the day list. */
.gas-price {
  grid-area: price;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0.35rem 0.6rem;
  border-radius: 0.5rem;
  background: var(--surface-widget);
  border: 1px solid var(--border-control);
}

.gas-price-value {
  font-size: 1.25rem;
  font-weight: 600;
  line-height: 1.1;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}

.gas-price-unit {
  font-size: 0.65rem;
  color: var(--text-muted);
}

.gas-days {
  grid-area: days;
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  /* Name and cost, the costs kept near their names rather than pushed to
     the far edge, where on a wide window the eye would have to cross the
     page to pair them. */
  grid-template-columns: max-content max-content;
  justify-content: start;
  column-gap: 1.6rem;
  row-gap: 0.18rem;
  font-size: 0.85rem;
}

/* The columns line up down the list: each row takes the list's columns
   rather than sizing its own, so the figures form a column. */
.gas-day {
  display: contents;
}

.gas-day-name {
  color: var(--text-label);
}

.gas-day-cost {
  text-align: right;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}

.gas-day-empty .gas-day-cost {
  color: var(--text-muted);
}

.gas-chart-section {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

.gas-chart-title {
  margin: 0;
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--text-label);
}

.gas-chart-legend {
  font-size: 0.68rem;
  font-weight: 400;
  color: var(--text-muted);
}

.gas-chart {
  height: 9.5rem;
}

/* Years or winters (CF-GAS-017): two words as a pair of buttons, the
   chosen one lit as the Power page's view toggle lights its choice. The
   pair stands where the graph's title would, because it *is* the title —
   "per year" or "per winter" is exactly what it chooses. */
.gas-span-toggle {
  display: inline-flex;
  border: 1px solid var(--border-control);
  border-radius: 6px;
  overflow: hidden;
  background: var(--surface-card);
}

.gas-span-button {
  border: none;
  background: none;
  font: inherit;
  font-size: 0.85rem;
  padding: 0.3rem 0.7rem;
  color: var(--text-muted);
  cursor: pointer;
}

.gas-span-button + .gas-span-button {
  border-left: 1px solid var(--border-control);
}

.gas-span-button.selected {
  background: var(--flow-bore);
  color: var(--text-reading-active);
}

.gas-span-button:focus-visible {
  outline: 2px solid var(--flow-casing);
  outline-offset: -2px;
}

/* The popup over a bar (CF-GAS-016). Positioned by the graph against its own
   box, above the bar it names. */
.gas-chart-host {
  position: relative;
  /* A finger resting on a bar should not start selecting its figures. */
  -webkit-user-select: none;
  user-select: none;
}

.gas-chart-popup {
  position: absolute;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0.3rem 0.55rem;
  border-radius: 0.4rem;
  background: var(--surface-widget);
  border: 1px solid var(--border-card);
  box-shadow: 0 2px 8px rgb(0 0 0 / 0.3);
  pointer-events: none;
  white-space: nowrap;
}

.gas-chart-popup[hidden] {
  display: none;
}

.gas-chart-popup-title {
  font-size: 0.68rem;
  color: var(--text-muted);
}

.gas-chart-popup-volume {
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}

.gas-chart-bar-shown {
  stroke: var(--text-primary);
  stroke-width: 1.5;
  stroke-dasharray: none;
}

/* Today's hours (CF-GAS-018): no figures over the bars and no price row, so
   a little shorter than the others for the same height of bar. */
.gas-chart.gas-chart-hours {
  height: 8rem;
}

/* The one figure the hourly graph carries: what its full height is worth. */
.gas-chart-scale {
  font-size: 0.62rem;
  fill: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.gas-chart-scale-line {
  stroke: var(--histogram-day-line);
  stroke-width: 1;
  stroke-dasharray: 2 3;
  opacity: 0.6;
}

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

/* A finger moving up or down over a bar graph scrolls the page; one moving
   sideways carries the popup from bar to bar (CF-GEN-019). On the shapes as
   well as the SVG, for the reason recorded on `.net-prices .today-chart`. */
.gas-chart-svg,
.gas-chart-svg * {
  touch-action: pan-y;
}

.gas-chart-axis {
  stroke: var(--histogram-day-line);
  stroke-width: 1;
}

.gas-chart-bar {
  fill: var(--gas-bar);
}

/* The period still accumulating (CF-GAS-010): an outline filled faintly, so
   it reads as a bar that is not finished rather than a small finished one. */
.gas-chart-bar-current {
  fill-opacity: 0.35;
  stroke: var(--gas-bar);
  stroke-width: 1;
  stroke-dasharray: 3 2;
}

.gas-chart-value {
  font-size: 0.62rem;
  fill: var(--text-primary);
  font-variant-numeric: tabular-nums;
}

.gas-chart-value-current {
  fill: var(--text-muted);
}

.gas-chart-label {
  font-size: 0.62rem;
  fill: var(--text-label);
}

.gas-chart-label-empty {
  fill: var(--text-muted);
  opacity: 0.6;
}

.gas-chart-price {
  font-size: 0.58rem;
  fill: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* The backend has been away past the threshold (CF-GAS-014): the figures
   are last known rather than current, and are dimmed the way every other
   page dims a stale reading. */
.gas-stale .gas-today-value,
.gas-stale .gas-price-value {
  filter: brightness(var(--stale-brightness));
  opacity: 0.75;
}

/* ---- The Water page (SPEC-WATER.md, CF-WAT-001 … CF-WAT-017) ----
   Laid out as the Gas page is, and taking its column (`.gas-page-body`) and
   its graphs' classes whole: the graphs are the same component
   (gasBarChart.js), and only their colour is the page's own. */

/* Over the page while its figures are made on the device (CF-WAT-002): plain
   enough to be read, quiet enough not to be mistaken for a fault. */
.water-demo {
  margin: 0;
  padding: 0.3rem 0.6rem;
  border-radius: 0.4rem;
  border: 1px dashed var(--border-control);
  font-size: 0.72rem;
  color: var(--text-muted);
  text-align: center;
}

/* The tap spans the head's height on the left; today sits beside it, and
   the days run beneath today. */
.water-head {
  display: grid;
  grid-template-columns: 6.5rem 1fr;
  grid-template-areas:
    'tap today'
    'tap days';
  column-gap: 0.9rem;
  row-gap: 0.5rem;
  align-items: start;
}

.water-tap-box {
  grid-area: tap;
  align-self: stretch;
  min-height: 10rem;
  /* The pipe is cut by the left edge and the stream by the bottom one: the
     box is a window onto a tap on a wall. */
  overflow: hidden;
  position: relative;
}

.water-tap-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.water-tap-metal > * {
  stroke: var(--water-metal-edge);
}

/* The water, drawn by the shader (waterStream.js) under the metal. It
   covers the whole box, so the stream reaches the bottom edge whatever the
   box's height; the shader frames it by the same view as the SVG. */
.water-tap-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

.water-today {
  grid-area: today;
  display: flex;
  flex-direction: column;
  gap: 0.05rem;
}

.water-today-label {
  font-size: 0.85rem;
  color: var(--text-label);
}

.water-today-value {
  font-size: 2.3rem;
  font-weight: 600;
  line-height: 1.1;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}

.water-today-note {
  font-size: 0.72rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* Name, litres and cost, each a column down the list, kept near one another
   as the Gas page's days are. */
.water-days {
  grid-area: days;
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: max-content max-content max-content;
  justify-content: start;
  column-gap: 1.2rem;
  row-gap: 0.18rem;
  font-size: 0.85rem;
}

.water-day {
  display: contents;
}

.water-day-name {
  color: var(--text-label);
}

.water-day-litres,
.water-day-cost {
  text-align: right;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}

.water-day-cost {
  color: var(--text-muted);
}

.water-day-empty .water-day-litres {
  color: var(--text-muted);
}

/* Today's spikes (CF-WAT-009): a plain table, figures right-aligned in
   columns so a reader runs down them as down a bill. */
.water-spikes-section {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.water-spikes {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
}

.water-spikes[hidden] {
  display: none;
}

.water-spikes th {
  font-size: 0.7rem;
  font-weight: 400;
  color: var(--text-muted);
  text-align: right;
  padding: 0 0.3rem 0.2rem;
  border-bottom: 1px solid var(--border-control);
}

.water-spikes td {
  padding: 0.22rem 0.3rem;
  text-align: right;
  color: var(--text-primary);
  border-bottom: 1px solid var(--border-control);
}

.water-spikes th.water-spike-time,
.water-spikes td.water-spike-time {
  text-align: left;
  color: var(--text-label);
}

.water-spikes th.water-spike-time {
  color: var(--text-muted);
}

.water-spikes td.water-spike-total {
  font-weight: 600;
}

.water-spikes td.water-spike-unknown {
  color: var(--text-muted);
}

/* A spike still flowing: its figures are so far, and quieter for it. */
.water-spike-open td {
  color: var(--text-muted);
  font-style: italic;
}

.water-spikes-empty,
.water-spikes-unknown {
  margin: 0;
  font-size: 0.72rem;
  color: var(--text-muted);
}

.water-spikes-empty[hidden],
.water-spikes-unknown[hidden] {
  display: none;
}

/* The graphs in the page's own colour. */
.water-page .gas-chart-bar {
  fill: var(--water-bar);
}

.water-page .gas-chart-bar-current {
  stroke: var(--water-bar);
}

.water-page .gas-chart-bar-shown {
  stroke: var(--text-primary);
}

/* A bar that is less than the meter's whole record — a year from the water
   company's statements, or a period with days the meter missed (CF-WAT-013):
   drawn, as the reader asked, but fainter, with the reason in its popup and
   a line beneath the graph. Faint rather than outlined, because the
   outline already means "still running". */
.gas-chart-bar-marked {
  fill-opacity: 0.45;
}

.water-chart-note {
  margin: 0.15rem 0 0;
  font-size: 0.68rem;
  color: var(--text-muted);
}

.water-chart-note[hidden] {
  display: none;
}

.water-stale .water-today-value {
  filter: brightness(var(--stale-brightness));
  opacity: 0.75;
}

/* ---- The Power page's Bars view (SPEC-POWER.md §The Bars view,
   CF-PWR-158 … CF-PWR-170) ----
   Laid out as the Gas and Water pages are, taking their column
   (`.gas-page-body`) and their graphs' classes whole, behind a row of subject
   tabs at the edge the page bar is not on.

   It is shown over the stage rather than faded against it (viewMode.js says
   why): while it is up the stage keeps only its controls row, whose view
   toggle is the way back (CF-HIST-067), and its drawing collapses to nothing
   so the page is only as long as the bars. */
.power-bars-pane {
  display: none;
}

.power-page.bars-showing .power-bars-pane {
  display: flex;
  flex-direction: column;
  /* The head is fixed over the top of the window and nothing in the page's
     flow makes room for it; `--power-head-size` is its measured height. */
  padding-top: var(--power-head-size, 0px);
}

.power-page.bars-showing .power-stage {
  height: 0;
  /* The head is fixed and inside the stage; a collapsed stage must not
     clip it. */
  overflow: visible;
}

.power-page.bars-showing .view-pane {
  visibility: hidden;
}

/* Where the controls row stands on the bottom bar (CF-HIST-122) the Bars
   view scrolls beneath it, as the timelines do, so it needs the opaque
   backing they give it. Flow's transparent strip is right only because
   Flow's picture ends above it. */
:root[data-bar-placement='bottom'] .power-page.bars-showing .power-window {
  background: var(--surface-page);
}

/* Elsewhere the controls row is in the head at the top, and the view
   scrolls under the head in the same way. */
:root:not([data-bar-placement='bottom']) .power-page.bars-showing .power-head {
  background: var(--surface-page);
}

/* The range description belongs to the timelines; with the stage collapsed
   its band would only push the Bars view down. */
.power-page.bars-showing .power-range-label {
  display: none;
}

/* A view arriving from or to the Bars view fades in once, at the pace the
   stage's own cross-fade keeps (MODE_TRANSITION_MS). */
.power-arriving {
  animation: power-arrive 380ms cubic-bezier(0.33, 1, 0.68, 1);
}

@keyframes power-arrive {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* ---- The subject tabs (CF-PWR-159) ----
   One per subject, sharing the width equally, at the edge the page bar is
   not on — the A/C page's unit tabs' rule (CF-AC-039): at the top where the
   bar is along the bottom or down the left, at the bottom where it is along
   the top. They stay put while the view scrolls beneath them. */
.power-bars-tabs {
  position: sticky;
  top: var(--power-head-size, 0px);
  z-index: 3;
  display: flex;
  gap: 0.3rem;
  order: 0;
  padding: 0.3rem 0.3rem;
  background: var(--surface-page);
  /* The scrolling box keeps a little padding between its edge and anything
     pinned in it, where the page would show scrolling past. A solid shadow
     covers that strip in the page's own colour without restating the
     padding's figure here. */
  box-shadow: 0 -0.6rem 0 var(--surface-page);
}

:root[data-bar-placement='top'] .power-bars-tabs {
  order: 2;
  top: auto;
  bottom: 0;
  box-shadow: 0 0.6rem 0 var(--surface-page);
}

/* With the bar at the bottom the head holds no controls — they stand on the
   bar (CF-HIST-122) — so the tabs stand at the top of the scrolling box
   rather than under the head's empty padding. */
:root[data-bar-placement='bottom'] .power-bars-tabs {
  top: 0;
}

.power-bars-body {
  order: 1;
  width: 100%;
  box-sizing: border-box;
}

.power-bars-tab {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.1rem;
  padding: 0.35rem 0.2rem 0.4rem;
  font: inherit;
  color: var(--text-muted);
  background: var(--surface-page);
  border: 1px solid var(--border-control);
  border-radius: 10px;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}

.power-bars-tab:hover:not(.selected) {
  background: var(--surface-card);
}

.power-bars-tab.selected {
  background: var(--surface-card);
  border-color: var(--flow-casing);
  box-shadow: inset 0 0 0 1px var(--flow-casing);
  color: var(--text-primary);
}

.power-bars-tab:focus-visible {
  outline: 2px solid var(--flow-casing);
  outline-offset: 2px;
}

.power-bars-tab .subject-icon {
  width: 1.6rem;
  height: 1.3rem;
}

.power-bars-tab-name {
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-label);
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Today's figure on each tab, so subjects compare without a press. Holds its
   line when empty, so a tab does not change height as the figure arrives. */
.power-bars-tab-today {
  min-height: 1em;
  font-size: 0.68rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ---- The head: emblem, today, and the week before it (CF-PWR-160) ---- */
.power-bars-head {
  display: grid;
  grid-template-columns: 6.5rem 1fr;
  grid-template-areas:
    'emblem today'
    'emblem days';
  column-gap: 0.9rem;
  row-gap: 0.5rem;
  align-items: start;
}

.power-emblem-box {
  grid-area: emblem;
  align-self: stretch;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 8rem;
}

.power-bars-today {
  grid-area: today;
  display: flex;
  flex-direction: column;
  gap: 0.05rem;
}

.power-bars-today-label {
  font-size: 0.85rem;
  color: var(--text-label);
}

.power-bars-today-value {
  font-size: 2.3rem;
  font-weight: 600;
  line-height: 1.1;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}

.power-bars-today-note {
  font-size: 0.72rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* Name, energy and cost, each a column down the list, as the Water page's
   days are. */
.power-bars-days {
  grid-area: days;
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: max-content max-content max-content;
  justify-content: start;
  column-gap: 1.2rem;
  row-gap: 0.18rem;
  font-size: 0.85rem;
}

.power-bars-day {
  display: contents;
}

.power-bars-day-name {
  color: var(--text-label);
}

.power-bars-day-energy,
.power-bars-day-cost {
  text-align: right;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}

.power-bars-day-cost {
  color: var(--text-muted);
}

.power-bars-day-empty .power-bars-day-energy {
  color: var(--text-muted);
}

/* What the figure is, for this subject (CF-PWR-161). */
.power-bars-what {
  margin: 0;
  font-size: 0.75rem;
  color: var(--text-muted);
}

.power-bars-note {
  margin: 0.15rem 0 0;
  font-size: 0.68rem;
  color: var(--text-muted);
}

.power-bars-note[hidden] {
  display: none;
}

/* The bars in the colour the timelines draw that direction in: what flows
   into a subject — the net's import, the house, the car — in the import
   colour, and what flows out of one — the battery, the array — in the
   export colour. */
.power-bars[data-direction='in'] .gas-chart-bar {
  fill: var(--histogram-import);
}

.power-bars[data-direction='in'] .gas-chart-bar-current {
  stroke: var(--histogram-import);
}

.power-bars[data-direction='out'] .gas-chart-bar {
  fill: var(--histogram-export);
}

.power-bars[data-direction='out'] .gas-chart-bar-current {
  stroke: var(--histogram-export);
}

.power-bars .gas-chart-bar-shown {
  stroke: var(--text-primary);
}

/* A place with nothing recorded (CF-PWR-164): a short dash at the floor. */
.gas-chart-placeholder {
  stroke: var(--text-muted);
  stroke-width: 1.5;
  stroke-linecap: round;
  opacity: 0.6;
}

.power-bars-stale .power-bars-today-value {
  filter: brightness(var(--stale-brightness));
  opacity: 0.75;
}

/* ---- The emblem (powerEmblem.js, CF-PWR-169) ----
   The subject's icon, large, with one or two parts of it moving slowly. */
.power-emblem-svg {
  width: 100%;
  height: auto;
  overflow: hidden;
}

.power-emblem-art {
  fill: none;
  stroke: var(--icon-stroke);
  stroke-width: 1.1;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* A cable: a faint line, with what it carries running along it as dashes. */
.power-emblem-cable {
  fill: none;
  stroke: var(--icon-stroke);
  stroke-width: 0.45;
  opacity: 0.35;
}

.power-emblem-flow {
  fill: none;
  stroke: var(--histogram-import);
  stroke-width: 0.9;
  stroke-linecap: round;
  stroke-dasharray: 0.6 2.4;
  animation: power-emblem-flow 1.8s linear infinite;
}

.power-emblem-flow-out {
  stroke: var(--histogram-export);
}

/* Toward the path's end, which each drawing starts outside and ends at the
   subject — or, for the battery, starts at the terminal and ends outside. */
@keyframes power-emblem-flow {
  from { stroke-dashoffset: 3; }
  to { stroke-dashoffset: 0; }
}

.power-emblem-sun {
  fill: color-mix(in srgb, var(--emblem-warm) 30%, transparent);
  stroke: var(--emblem-warm);
  stroke-width: 1.1;
  animation: power-emblem-glow 5s ease-in-out infinite;
}

.power-emblem-rays {
  fill: none;
  stroke: var(--emblem-warm);
  stroke-width: 1.1;
  stroke-linecap: round;
  transform-box: view-box;
  transform-origin: 12px 6.5px;
  animation: power-emblem-turn 24s linear infinite;
}

@keyframes power-emblem-turn {
  to { transform: rotate(360deg); }
}

@keyframes power-emblem-glow {
  0%, 100% { opacity: 0.85; }
  50% { opacity: 1; }
}

/* A glint crossing the panel, then a long pause before the next. */
.power-emblem-glint {
  fill: var(--emblem-warm);
  opacity: 0.35;
  transform: skewX(-20deg) translateX(0);
  animation: power-emblem-glint 7s ease-in-out infinite;
}

@keyframes power-emblem-glint {
  0% { transform: skewX(-20deg) translateX(0); }
  30%, 100% { transform: skewX(-20deg) translateX(30px); }
}

/* The battery's charge running down slowly and filling back: the view shows
   what it discharged. From the left edge of its cell, as the card's gauge
   fills. */
.power-emblem-charge {
  fill: var(--soc-bar);
  transform-box: fill-box;
  transform-origin: left center;
  animation: power-emblem-drain 9s ease-in-out infinite;
}

@keyframes power-emblem-drain {
  0% { transform: scaleX(0.9); }
  75% { transform: scaleX(0.25); }
  100% { transform: scaleX(0.9); }
}

/* The house's windows, lit at slightly different moments. */
.power-emblem-window {
  fill: var(--emblem-warm);
  animation: power-emblem-window 6s ease-in-out infinite;
}

.power-emblem-window-late {
  animation-delay: -2.5s;
}

@keyframes power-emblem-window {
  0%, 100% { opacity: 0.35; }
  50% { opacity: 0.95; }
}

/* The charging bolt over the car, brightening and fading. */
.power-emblem-bolt {
  fill: var(--emblem-warm);
  stroke: none;
  animation: power-emblem-glow-strong 2.4s ease-in-out infinite;
}

@keyframes power-emblem-glow-strong {
  0%, 100% { opacity: 0.35; }
  50% { opacity: 1; }
}

/* Held paused always: powerEmblem.js moves each animation's clock along
   itself, a set number of times a second, and stops moving it while the
   view is not showing (SPEC.md §Pages). Left to run, these repainted the
   emblem on every display frame — 120 a second on a ProMotion Mac — and
   cost half a core for a few pixels' movement a second. `!important`
   because every `animation` shorthand above resets the play state. */
.power-emblem * {
  animation-play-state: paused !important;
}

/* …and for a reader who has asked for less motion, still at all times —
   each drawing is still its subject's icon standing still. */
@media (prefers-reduced-motion: reduce) {
  .power-emblem * {
    animation: none !important;
  }
}
