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-rowalready 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¶
- HTML structure that the CSS styles → HTML Structure
- JS-driven class toggles (
.active,.dark-mode, etc.) → JavaScript Logic - gzip compression workflow → Configuration & Development → Build / Flash Workflow