V3-16: visual identity (token-level pass)

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.
This commit is contained in:
2026-08-17 19:50:08 -05:00
parent e4ffafe9e0
commit 1bcc6f1c5a
7 changed files with 209 additions and 10 deletions

View File

@@ -32,7 +32,7 @@ backup are v4 — see [../BACKLOG.md](../BACKLOG.md).
| [V3-13](V3-13-real-ride-measurements.md) | Real-ride measurements | M | **riding** | Not started |
| [V3-14](V3-14-gpx-interop.md) | GPX interoperability | S | V3-01 | Partially done (code only; needs real-device verification) |
| [V3-15](V3-15-auto-pause.md) | Auto-pause | M | V3-13 *(gated)* | Not started |
| [V3-16](V3-16-visual-identity.md) | Visual identity | M | V3-04, V3-05 | Not started |
| [V3-16](V3-16-visual-identity.md) | Visual identity | M | V3-04, V3-05 | Partially done (token-level identity shipped; needs outdoor device verification) |
## Dependencies

View File

@@ -1,6 +1,40 @@
# V3-16 — Visual identity
**Phase** Quality · **Depends on** V3-04, V3-05 · **Size** M · **Status** Not started
**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.
@@ -56,3 +90,48 @@ Restyling breaking the explicit-colour discipline that exists because of a real
## 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.