Skip to content

Styling and Theming

All dashboard appearance lives in one hand-edited file, web_src/styles.css — no preprocessor, no design-system tooling, served gzipped from the regulator. Styling grew 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;
}

Teal (--accent) is the one color that signals "interactive or important" — buttons, focused inputs, links. Changing it re-themes the whole dashboard. A few additional scoped variables exist for specific components; 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 reach.

The toggle does more than flip the class. Its change handler also:

  • persists the choice to the browser's local storage (localStorage, key darkMode) so the theme survives reload,
  • re-themes every chart canvas via updateUplotTheme() (the charting library draws to canvas and SVG, so CSS variables alone can't reach it),
  • re-applies staleness styling (the greyed-out colors differ per theme), and
  • broadcasts the theme to the embedded cloud-feature frames, which cannot see the host page's class.

Check both themes on every styling change. The file's own risk note calls this out, and dark mode remains the most common regression vector — 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. The shared classes:

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 first-class target — the same page runs inside the iOS app — so the stylesheet leans on 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). The recurring patterns:

  • 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 aim for 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.


Hard-Won Rules

  • Reuse before creating. Look for an existing class (.reading-row, .form-row, the button classes) that already does the job. 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.
  • 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