Skip to content

CSS Styling

web_src/styles.css (~3350 lines) handles all dashboard styling — single file, served gzipped. Mobile-responsive via media queries; dark mode is a separate .dark-mode class toggled on <body>.

Style additions are intentionally additive — there is no design system and no preprocessor. Reuse existing classes (.reading-row, .form-row, .btn-primary, etc.) whenever possible.


CSS Custom Properties (:root)

:root {
    --primary:     #333333;   /* dark gray — main UI lines */
    --accent:      #00a19a;   /* teal — links, focused inputs, primary buttons */
    --bg-light:    #f5f5f5;   /* page background (light mode) */
    --text-dark:   #333333;   /* body text */
    --text-light:  #ffffff;   /* on-dark text */
    --card-light:  #ffffff;   /* card surface */
    --border:      #dddddd;   /* hairline borders */
    --reading:     #333333;   /* live-value text color */
    --radius:      4px;       /* border-radius default */
}

A second :root block ~line 287 overrides several of these under .dark-mode — see the dark-mode section below.

--accent is the single brand color; everything that should "stand out" uses it (buttons, focused borders, charge-stage chip backgrounds in some states). Changing --accent re-themes the entire dashboard.


Component Class Catalog

Recurring component classes, alphabetical-ish by family:

Buttons

Class Use
.btn-primary Submit / save buttons. Teal background
.btn-secondary Cancel / less-important actions
.btn-danger Destructive actions (factory reset, delete data)
.main-tab, .main-tabs Top-level tab navigation
.tab (.active), .tabs Sub-tab strip
.alt-tab-btn, .alt-tab-btn-primary, .alt-tab-btn-secondary Settings → Alternator sub-sub-tabs

The button hierarchy is: .main-tab (largest, top) → .tab (sub) → .alt-tab-btn (sub-sub).

Reading rows (live display)

Class Use
.reading-row One value display: label + number + unit
.reading-label Left-side label text
.reading-value The number itself (styled with --reading color)
.reading-unit Unit suffix (V, A, °F, etc.)
.metric-value, .metric-value-compact Larger variants for headline metrics
.counter-value Integer counters (event counts, runtime hours)
.reading-duo Two label/number/unit triples on one line (e.g. Batt + SOC)
.duo-lbl, .duo-num, .duo-unit Sub-elements inside .reading-duo

Forms

Class Use
.form-row A label + input + Set button unit (the most-replicated pattern)
.form-label Left side, includes the echo span
.form-input Right side, contains the <form> element
.password_field Hidden password input (every settings form has one)
.password-form, .password-form-wrapper, .password-input-group The login form at the top
.show-checkbox-container, .show-checkbox, .show-label "Show password" toggle
.switch, .slider iOS-style toggle (alternator-enable, accel-enable, etc.)
.tooltip, .tooltip-box, .tooltip.active Tap-to-reveal info bubbles next to settings labels

Status indicators

Class Use
.field-status-active Green field-state chip — field ON
.field-status-inactive Grey — field OFF
.field-status-rampdown Orange — shutdown in progress
.field-status-waiting-cloud Yellow — held off awaiting core0Busy clear
.field-status-manual Cyan — manual mode
.field-status-fault Red — fault state
.charge-stage (base) + .charge-stage-bulk / -absorption / -float / -idle / -maintain / -target-v / -manual / -test / -unknown / -hidden Charge-stage chip color coding
.ignition-on, .ignition-off Green / grey ignition indicator
.lock-status-locked Yellow banner styling (settings locked, protections disabled warning)
.alarm-active Red blink — used for any alarm-style indicator

Containers

Class Use
.permanent-header Always-visible top bar
.header-brand-row, .header-wordmark, .header-field-cluster Brand + field-status row inside header
.sensor-control-row, .sensor-readings-tier, .alternator-control-tier Sensor chips + alternator-enable strip
.settings-access-tier Settings-locked / password row
.tab-content, .sub-tab-content, .alt-tab-content The three nesting levels of tab content; hidden by default, .active to show
.section-title, .tab-section Section dividers inside a tab
.chart-container uPlot mount points
.learning-table-container, .learning-table-wrap, .learning-table-xscroll RPM table editing UI (legacy "learning" name, but still used)
.section-header, .section-body Collapsible-section helper pattern
.history-container Tuning-log table containers

Plot/chart-specific

uPlot is styled via its own .u-* classes (.u-legend, .u-cursor, etc.) which are part of uPlot.min.css. The dashboard's own additions are mostly in:

Class Use
.chart-container Outer wrapper, fixed-height responsive
.u-legend.dark-mode-legend Override legend colors in dark mode
.matrix-cell, .matrix-cell-empty, .matrix-cell-populated, .matrix-cell-ref Field-bucketer matrix cells
.matrix-red-dot Live-position marker overlay
.sparkline-container Efficiency-history mini chart

Dark Mode

Toggled by adding class="dark-mode" to <body>. The dark-mode block (~line 287) re-defines the CSS custom properties:

body.dark-mode {
    --primary:     #e0e0e0;
    --accent:      #4dd2cd;     /* lighter teal for AA contrast on dark */
    --bg-light:    #1a1a1a;
    --text-dark:   #e0e0e0;
    --card-light:  #2a2a2a;
    --border:      #404040;
    --reading:     #ffffff;
}

Most components inherit correctly via the variables. A handful of override rules handle elements that don't:

.dark-mode .telemetry-label { color: var(--text-dark); }
.dark-mode .telemetry-value { color: var(--reading); }
.dark-mode input[type="text"], .dark-mode input[type="number"] { background-color: var(--card-light);  }
.dark-mode .charge-stage-bulk { background-color: ; }  /* status colors brightened */
.dark-mode .u-legend.dark-mode-legend { color: var(--text-dark); }
/* etc. */

uPlot's canvases re-render themselves on theme change — updateUplotTheme(plot) in script.js re-applies series colors and grid colors after the body class flip.

Toggle is mirrored to localStorage so the page restores last theme on reload.

Always check both themes when adding a new component. The "AI_RISKS" comment at the top of styles.css says this explicitly — dark mode is the most-common regression vector.


Responsive Breakpoints

@media (max-width: 320px)  { /* compact phone */ }
@media (max-width: 420px)  { /* phone */ }
@media (max-width: 480px)  { /* phone landscape */ }
@media (max-width: 768px)  { /* tablet portrait */ }
@media (min-width: 560px)  { /* */ }
@media (min-width: 768px)  { /* tablet+ */ }
@media (min-width: 769px) and (max-width: 1000px) { /* small desktop */ }
@media (min-width: 1024px) { /* desktop */ }

Several form-row layouts collapse to single-column at narrow widths. The sensor-chip strip wraps to multiple lines. The alternator-enable toggle moves from header right to its own row below the chips below ~ 768 px.

Touch targets are sized for thumbs — minimum 44 px tall on tappable elements. The "Show" checkbox next to password fields and the tooltip ℹ️ icons are sized accordingly.


iOS Safe-Area Padding

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));
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-y: contain;     /* disable pull-to-refresh */
}

env(safe-area-inset-*) pulls the iOS notch / home-indicator inset values. -webkit-overflow-scrolling: touch enables momentum scrolling. overscroll-behavior-y: contain disables the iOS Safari pull-to-refresh gesture, which otherwise reloads the page from a quick swipe at the top of a long form.

When wrapped in Capacitor, the page is loaded from http://alternator.local. Capacitor's status bar plugin tells iOS to overlay the page; the safe-area inset values then reserve room for the time / battery indicator above the wordmark.

Font size minimum is 16 px on <input> elements — anything smaller triggers iOS auto-zoom on focus, which is jarring and breaks the layout.


Auto-Generated data/styles.css.gz

compress_web.sh regenerates this. Never edit data/styles.css or data/styles.css.gz by hand — they will be overwritten on the next flashFactory / flashOTA. Always edit web_src/styles.css.


Hard-Won Rules

From styles.css top-of-file comments and accumulated experience:

  • Style was added ad-hoc — there is no consistent design system. Look at existing similar UI before adding new patterns.
  • Re-use before creating — if .reading-row already does what you want, use it. If you need to add three new classes for a one-off, refactor instead.
  • Check dark mode on every change. The accumulated technical debt here is real — many components have hand-tuned overrides because variables alone don't cover them.
  • Touch sizing. Mobile is a first-class target via Capacitor. Phone-thumb sized targets, no hover-only interactions, tap-to-reveal tooltips.
  • No external assets. Every byte is served from the ESP32. No CDN-hosted fonts, icons, or JS libraries — that would break the AP-mode use case (no internet).
  • No CSS preprocessor. Direct CSS only. No SCSS, no PostCSS, no build chain beyond gzip.

Cross-references