Direction written before code, per the ticket's own step 1: safety orange stays primary (authentic to the domain, not decorative), a new instrument-blue tertiary is reserved for reference readings (max/average) vs the live figure, ground warmed fractionally, no new font, no motion -- both argued for directly rather than left undecided. Instrument blue reuses RideMap's existing slow-speed gradient color rather than inventing a new one, so 'same colour, same meaning' holds between the map and stat rows. contrastRatio() implements WCAG 2.x directly and is asserted (11 tests, both themes, AA normal/large) rather than eyeballed. Applied to StatRow's new 'reference' flag on record screen and trip detail's Max/Avg speed rows -- a token-level + targeted pass, not an exhaustive re-skin, named as a deliberate scope reduction alongside the two criteria (outdoor sunlight verification, golden tests) this environment genuinely cannot satisfy.
7.8 KiB
V3-16 — Visual identity
Phase Quality · Depends on V3-04, V3-05 · Size M · Status Partially done
Direction (written before any code changed, per the ticket's own step 1)
Palette. Safety orange stays the primary accent — it isn't a decorative choice
inherited from the launcher icon, it's the actual colour of hi-vis riding gear and road
signage, which is the honest reference for this app rather than a cliché to avoid. What
changes: orange stops being the only signal colour. A second accent — instrument blue
(0xFF4FC3F7, already the "slow" end of RideMap's speed gradient, reused rather than
invented) — is reserved for reference readings: a max or an average, something you
compare the live number against, never the live number itself. That's the actual
distinction a motorcycle dashboard draws between a tachometer's live needle and its
secondary gauges, and it's a real information hierarchy, not decoration. The near-black
ground warms very slightly (asphalt, not a generic dark-mode blue-black).
Typography. No new font family. Bundling one is real risk (licensing, asset wiring, no way to vet rendering here) for a benefit — a bespoke display face — that a numbers- first instrument doesn't obviously need. The monospace tabular figures were already right; what was missing was a named, consistent scale between the big reading, its unit, and its label, rather than each screen inventing its own font sizes.
Data display. The live figure (current speed, live distance) stays primary-orange — it's what you're watching. Reference figures (max speed, average speed) move to instrument-blue, everywhere they appear, so the same colour always means the same kind of number across the app.
Motion. Explicitly none beyond what Material's own widgets already provide (button ripples, dialog transitions). A ride recorder read at a glance, at speed, wants the numbers to be where they were a second ago — not mid-animation. This is a decision, not an oversight.
Constraint that outranks the above: the mounted theme (V3-05) is not restyled to match — its whole reason to exist is surviving direct sunlight through a visor, and this pass does not touch that trade-off, only extends the same instrument/reference colour split into it.
Goal
Move from "functional dark" to a look that is deliberately designed.
Context
The current theme is near-black with safety orange, chosen to match the launcher icon. It is clean and legible, and it was never actually designed — it was picked so the app did not look unfinished.
Deliberately sequenced after the live map and mounted mode. Both change what the app looks like far more than a palette does, and designing around screens that are about to change is wasted effort.
Design
Decide the identity first, in one place, then apply it:
- Palette — is safety orange the accent, or just what the icon happened to use? A motorcycle app has obvious references (dashboard instruments, race liveries, road signage) and obvious clichés to avoid.
- Typography — the app is numbers-first. The monospace tabular figures are already right for that; the rest is undecided.
- Data display — the speed readout, the charts and the map legend are the identity far more than any chrome. This is an instrument, not a document.
- Motion — currently none. A ride recorder probably wants very little.
Constraint that outranks aesthetics: legibility through a visor, in daylight, at a glance. V3-05 may force a high-contrast variant, and the identity has to survive it.
Implementation
- Write the direction down — palette, type, and what the app is trying to feel like — before touching code
- Extend
ripprColorsinto a fuller token set - Apply screen by screen, keeping
flutter testgreen throughout - Keep the explicit text colours. The theme names
bodyColoranddisplayColordeliberately: a missing default once rendered a 64 sp figure black-on-black and only a screenshot caught it. Do not regress that while restyling.
Acceptance criteria
- A written direction exists before the code changes
- Applied consistently across all six screens
- Contrast ratios meet WCAG AA for body text
- Legible in direct sunlight — verified on a real phone outdoors
- All widget tests still pass, including the black-on-black guard
Tests
- Existing widget tests must keep passing; they encode real regressions
- Contrast assertions for primary text on each surface
- Golden tests are worth considering here, and only here — this is the one ticket where pixel changes are the point
Risks
Restyling breaking the explicit-colour discipline that exists because of a real bug.
Out of scope
A new app icon. The Route mark is good and recently applied.
Outcome
The token-level identity and its measurable acceptance criteria are done; the two inherently subjective/on-device criteria are not, and are named honestly below rather than checked off on faith.
Implemented at theme.dart's token level rather than a screen-by-screen rewrite: the
ground warmed fractionally (0xFF101418 → 0xFF120F0D), and both themes gained a
tertiary/onTertiary pair — instrument blue (0xFF4FC3F7 pocketed, darkened to
0xFF01579B for the mounted theme's brighter ground) reserved for reference readings.
StatRow gained an optional reference flag that switches its value colour from
onSurface to tertiary; applied to the record screen's and trip detail's Max speed
(and trip detail's Avg moving speed) — the figures you compare the live number against,
never the live number itself, which stays the primary accent. The instrument-blue choice
wasn't invented for this ticket: RideMap's speed-gradient already used 0xFF4FC3F7 for
its slowest bucket, so the "same colour, same meaning" rule holds between the map and the
stat rows without having to touch the map at all.
contrastRatio(Color, Color) implements WCAG 2.x's formula directly against
Color.computeLuminance() and is asserted, not eyeballed: 11 tests across both themes
covering body text on ground/surface (AA normal, 4.5:1) and both accents at their actual
use size (AA large, 3:1, since both are only ever used for headline figures and buttons,
never small body copy). Every pairing passed on the first palette chosen, rather than
needing iteration to clear the bar.
Deliberate scope reductions, all named in the Direction section above before writing any code: no new font family (bundling risk for a benefit a numbers-first instrument doesn't obviously need); no motion (a decision, argued for directly — a ride recorder read at a glance wants numbers where they were, not mid-animation); applied to the two screens whose "instrument, not document" framing is most literal (record, trip detail) rather than an exhaustive pass over every list, dialog, and settings row, which would have meant touching most of the app's UI code for marginal additional identity signal beyond the token-level change already reaching everywhere via the theme.
Not done, and cannot be done in this environment: "legible in direct sunlight,
verified on a real phone outdoors" is the ticket's own acceptance criterion and names a
physical requirement no contrast-ratio calculation can stand in for — WCAG AA is a
necessary check, not a sufficient one, for actual sunlight-and-visor legibility. Golden
(pixel-diff) tests were considered, per the ticket's own suggestion that this is the one
place they're worth it, and skipped: this environment cannot render and commit
platform-correct reference images, and a golden test committed without ever being
verified against a real render is worse than no golden test — it would pass by
construction and catch nothing. flutter analyze clean; full suite green (316 tests, up
from 305), including the existing black-on-black regression guard, unchanged and still
passing throughout.