# Visual Redesign — "Ride Computer" Dark Theme — Design Date: 2026-08-16 Status: Draft for user review ## 1. Goal Replace the current light, generic admin-template look with a distinctive dark "bike computer cockpit" theme grounded in the app's own subject matter (indoor cycling sync, Garmin telemetry). The redesign is a shared foundation: it must land before the planned dashboard summary tiles and live-update UX polish, so those can be built directly in the new visual language instead of needing a second pass. This spec covers only the visual system and the already-shipped next-sync indicator's evolution into a live countdown. It does not add new pages, new data, or new business logic. ## 2. Scope ### In scope - New CSS design-token system (color, spacing reuse, borders) replacing the current light theme in `app/web/static/style.css`. - Cockpit-style topbar (dark, brand mark, nav, live sync countdown). - Restyled shared components: cards, buttons, badges, tables, forms, empty states — applied globally via shared CSS classes so every existing template (login, account-login, dashboard, account detail/edit, users detail/new/edit, system, sync result fragment) picks it up without structural rewrites. - Typographic treatment: uppercase tracked labels for headings/section labels (extends the existing table-header pattern), tabular monospace numerals for all stats/timestamps/counters. - Evolving the existing static next-sync timestamp (`app/web/static/app.js`, `base.html`) into a live, client-ticking countdown. - Accessible focus states (visible lime outline) and `prefers-reduced-motion` handling for the one animated element (the countdown) and card entrance. ### Out of scope (separate follow-up specs) - Dashboard summary/stat tiles (next phase, builds on this theme). - Live sync results without full page reload (UX-polish phase). - Any light-mode / theme-toggle support (explicitly rejected by user — dark only). - New charts/statistics views. - Any change to routes, models, or sync/business logic. ## 3. Design tokens ### Color | Token | Hex | Usage | |---|---|---| | `--bg` | `#12151A` | Page background | | `--surface` | `#1A1F27` | Cards, topbar, table zebra-free rows | | `--surface-raised` | `#232935` | Hover states, inputs | | `--border` | `#2A3038` | Hairline dividers/card borders | | `--text` | `#E7EAF0` | Primary text | | `--text-muted` | `#8B93A3` | Secondary text, labels, hints | | `--accent` | `#C8FF4D` | Electric lime — signature countdown, primary buttons, focus rings, "healthy" status | Semantic status colors (each with a ~14% opacity tint of the same hue over `--surface` for badge backgrounds, matching the existing `--*-bg` variable pattern already in `style.css`): | State | Hex | Meaning | |---|---|---| | Success / healthy | `--accent` `#C8FF4D` | Reuses the signature accent — a healthy sync *is* the good state the accent celebrates | | Warning / degraded | `#FFB454` | Amber | | Danger / action required / failed | `#FF5F6D` | Coral | | Info / syncing / running | `#5FD4FF` | Cyan | | Neutral / disabled | `#5A6472` | Slate | These map 1:1 onto the existing `--success`, `--warning`, `--danger`, `--info`, `--neutral` variable names already used by `.badge-*` classes in `style.css` — only their values change, not the class structure, so templates need no edits for badges. ### Typography - Body/UI face: unchanged system stack (`-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif`) — no new font loads, works offline in a self-hosted container. - Monospace face for all numeric data (stats, timestamps, the countdown, table numeric columns): `ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace`, with `font-variant-numeric: tabular-nums`. - Headings and structural labels (`h1`/`h2`, `.badge`, table `th`, the new topbar labels): uppercase, `letter-spacing: 0.06em`, extending the tracked uppercase style `table th` already has today — applied consistently instead of only in tables. ### Layout - Topbar becomes the cockpit header: `--surface` background, hairline bottom border, brand mark left, nav + live countdown grouped right (existing `.topbar-right` wrapper from the next-sync work is reused). - Cards: hairline `--border` outline, `--surface` background, no drop shadow (shadows read poorly on dark; hairlines carry the "device bezel" feel instead). Radius stays at the existing `--radius: 10px`, unchanged. - Buttons: primary uses `--accent` background with dark text (for contrast against the light lime); secondary keeps outline/ghost style against `--surface`. - Focus-visible: 2px `--accent` outline on all interactive elements (links, buttons, inputs) — dark backgrounds need this to stay accessible since the current subtle browser default focus ring is hard to see on `--surface`. ### Motion - The live countdown ticks once per second (text content update only, no layout shift). - Cards fade/slide in ~150ms on initial page load, `translateY(4px) → 0`. - Both respect `prefers-reduced-motion: reduce` (countdown text still updates, since it's informational, not decorative; the card entrance animation is skipped entirely). ## 4. Signature element: live sync countdown `app/web/static/app.js` currently does a one-time UTC→local conversion of the `time[data-utc]` element on `DOMContentLoaded`. It's extended to: 1. On load, read `data-utc` as the target instant. 2. If the target is in the future, start a `setInterval` (1s) that computes the remaining duration and renders it as `HH:MM:SS` (or `MM:SS` under an hour) in monospace, e.g. `NEXT SYNC ▸ 00:12:04`. 3. If the target is in the past (page left open past the sync time, or `next_tick` not yet known on first boot), render `due now` instead of a negative countdown. 4. The `title` attribute keeps showing the absolute local time (via `toLocaleString()`) and the original UTC instant, so hovering still gives an absolute reference — this preserves today's behavior as a fallback/aid. 5. `data-utc` stays the templating contract between server and client (same attribute the current tests assert on), so no server-side route or test changes are needed for this evolution — only `app.js` behavior and the surrounding CSS/markup in `base.html` change. No server-side change: `next_sync_tick()` in `app/web/routes.py` and the `base.html` template variable wiring stay as they are; only the visual presentation and `app.js` ticking logic change. ## 5. Testing This is a CSS/JS-presentation change with one markup adjustment (countdown wrapper element/label in `base.html`). Existing server-rendered tests assert on `data-utc="..."` substrings and text content, not on CSS classes or exact visual output, so no test breakage is expected. No new automated test is meaningful for pure CSS token values; the existing `tests/web/test_next_sync_display.py` continues to guard the server-side contract (the attribute and value), and manual verification (screenshot) is used to confirm the visual outcome, consistent with how the next-sync feature was verified. ## 6. Rollout Single pass across `style.css`, `base.html`, `app.js`. No template restructuring needed beyond `base.html`'s topbar, since all other templates already consume the shared `.card`, `.badge`, `button`/`.btn`, `table`, and `form.stacked-form` classes this spec restyles centrally.