Skip to content

Styling and Theming

All dashboard styling is in one hand-edited file, web_src/styles.css — no preprocessor, no design-system tooling, served gzipped from the XREG-010. Styling was added ad hoc (the file's own header comment says so), so the working rules are: reuse existing classes before inventing new ones, and check dark mode on every change.


Design Tokens

A small set of CSS custom properties at the top of the file defines the palette; nearly everything else derives from them:

:root {
  --primary: #333333;     /* dark gray — main UI lines */
  --accent: #00a19a;      /* teal — the single brand color */
  --bg-light: #f5f5f5;    /* page background */
  --text-dark: #333333;
  --text-light: #ffffff;
  --card-light: #ffffff;  /* card surfaces */
  --border: #dddddd;
  --reading: #333333;     /* live-value text */
  --radius: 4px;
  --text-muted: #5f6368;  /* secondary text */
  --accent-fill: #00807a; /* filled pills carrying white text */
}

Teal (--accent) is the one color that signals "interactive or important" — buttons, focused inputs, links. Changing it re-themes the whole dashboard. Filled pills with white text use the darker --accent-fill in light mode, because white on #00a19a is below 4.5:1 contrast; dark mode puts dark text on those pills and sets --accent-fill back to the brighter teal. A few additional scoped variables exist for specific components, such as the commissioning checklist badge colors (--cx-*), which are a deliberate per-theme pair; see styles.css for the current set.


Dark Mode

Dark mode is a single class — dark-mode — toggled on <body>. A .dark-mode block redefines the custom properties (near-black backgrounds, dark card surfaces, muted text), so most components restyle themselves through the variables alone. A set of follow-up override rules handles elements with hardcoded colors that the variables don't apply to.

The toggle does more than flip the class. Its onchange (inline on the checkbox in index.html, not registered in script.js) also:

  • persists the choice to the browser's local storage (localStorage, key darkMode) so the theme survives reload,
  • re-themes every chart canvas via refreshAllPlotThemes() (the charting library draws to canvas and SVG, so CSS variables alone can't restyle it),
  • re-applies staleness styling via updateAllStalenessStyles() (the grayed-out colors differ per theme), and
  • broadcasts the theme to the embedded cloud-feature frames via broadcastThemeToCloudIframes(), since their documents cannot read the host page's class.

Check both themes on every styling change. The file's own risk note states this, and dark mode remains the most common source of regressions — many components carry hand-tuned overrides precisely because they were forgotten once.


The Unified Button System

Every action button shares one tonal-plus-shadow system (the "Soft Tonal + depth" restyle, marked as such in a comment in styles.css): translucent tinted background, colored text, subtle shadow, and a one-pixel press translation on :active.

Class Use
.btn-primary (also applied to bare input[type="submit"]) Save / Set / confirm actions — teal tonal
.btn-secondary Neutral or less-important actions
.btn-danger Destructive actions
.btn-danger-ghost Destructive but lower-emphasis variant
.btn-sm Compact size modifier

A few specialized families build on the same look — logging controls (.btn-log-*) and the segmented charge-rate toggle (.cap-mode-*, an iOS-style segmented control; the name is unrelated to the Capacitor app shell). Navigation chips (tabs and sub-tabs) are styled separately from action buttons.

The rule: never inline-style a button. Every button gets one of the shared classes; if none fits, extend the system in styles.css rather than scattering one-off styles in index.html.


Responsive Layout

Mobile is a primary target — the same page runs inside the iOS and Android apps — so the stylesheet uses width-based media queries rather than any grid framework. Breakpoints exist from compact-phone widths up through tablet and desktop (multiple @media (max-width: ...) blocks; see styles.css for the current set).

  • Multi-column form rows collapse to single column at phone widths.
  • The header's sensor-chip strip wraps, and header controls reflow to their own rows.
  • Charts cap their width on large screens and go full-width on phones.

Touch rules, applied throughout:

  • Tappable elements are sized to the iOS-recommended minimum target size (44 px), sometimes achieved with padding around a smaller visual element.
  • No hover-only interactions — tooltips toggle on tap, and a capability query (@media not ((hover: hover) and (pointer: fine))) adjusts behavior on touch devices.
  • Inputs keep a minimum 16 px font size; anything smaller triggers iOS auto-zoom on focus, which breaks the layout.

iOS Safe Areas and Scroll Behavior

The body padding reserves room for the iPhone notch and home indicator using the system-provided insets:

body {
  padding-top: max(0.5rem, env(safe-area-inset-top));
  padding-bottom: max(0.5rem, env(safe-area-inset-bottom));
  padding-left: max(0.5rem, env(safe-area-inset-left));
  padding-right: max(0.5rem, env(safe-area-inset-right));
}

env(safe-area-inset-*) resolves to the device's reserved-edge sizes (zero on desktop). A few fixed-position elements add the inset to their own offsets. Scrolling is tuned for iOS as well: momentum scrolling is enabled (-webkit-overflow-scrolling: touch) and the pull-to-refresh gesture is suppressed (overscroll-behavior-y: contain) — without that, a quick swipe at the top of a long settings form reloads the page.


Rules

  • Reuse before creating. Look for an existing class (.metric-row, .form-row, the button classes) that already provides the needed style. The file's own header asks for this to limit bloat.
  • Check dark mode on every change.
  • No external assets. Every byte serves from the regulator, including in access-point mode with no internet — no CDN fonts, icons, or scripts.
  • Gray out a disabled feature's settings; hide only readouts that can never show a value. A setting whose feature is switched off carries a .gate-* class and stays visible, dimmed, and inert, with a .gate-note-* in its card naming the switch that turns it back on. A live readout that could only ever show a dash carries .req-* and is hidden. Body classes set from the settings echo (no-batt-current, no-soc-src, no-extra-temp, ap-mode, cloud-off) drive both, and the rules are explained at the top of styles.css.
  • No preprocessor, no build chain. Direct CSS only; the only processing is gzip.
  • Edit web_src/styles.css only. data/styles.css.gz is generated output (the gzip step recompresses web_src/ before flashing) and is overwritten on every build.

Cross-references