Process, other apps and the five costliest lessons
5. Process
Section titled “5. Process”34. Check every page in both themes, before and after merging.
- Problem. Several of the above bugs (invisible skeletons, washed-out teal text, glaring pills) were visible only in dark and only on some pages. Unit tests with no real colour compositing cannot see them.
- Rule. Every screen is reviewed in light and dark, at phone and desktop width, including empty, error and loading states. Automated checks: axe in unit tests catches structure only; the browser (Chromium) accessibility suite checks WCAG 2.1 AA including real contrast and runs on light and dark journeys. A violation is a red build with no allow-list.
- Source. INVARIANTS “accessibility floor”; decision 32; app
2d606d4e1(28 light/dark journeys).
35. Write the contrast math as tests that read the real tokens.
- Problem. A palette spec in prose and a palette in CSS drift apart.
- Rule. A test reads the token file, strips comments, and asserts every text ink at 4.5:1 on every neutral and
soft background, every fill label at rest/hover/active at 4.5:1, focus and outline at 3:1, and that each
neutral is a documented mix (provenance). Source guards ban alpha text, alpha tints, fill-as-text, obsolete
fonts and off-palette hex outside the brand file. Keep
:root {and:root.dark {blocks literal so the parser can read them. - Enforced.
backend/tests/test_frontend_theme_contrast.py,frontend/src/styles/darkTokens.test.ts,frontend/src/lib/brandColours.test.ts. - Source. Design spec section I; app
d619267b1.
36. One token source, generated into every consumer. Never hard-code colours in a second place.
- Problem. The hosted design site hard-coded colours in its own stylesheet and kept showing an old dark palette after the app moved to solid black. The design repo’s own spec and PDF also lagged the code (the PDF still predates decision 299, and the spec’s ratios had to be recomputed after 316).
- Rule.
tokens/tokens.jsonandtokens/tokens.cssare the source. The site, docs tables, README swatches and mobile token files are generated from them (or checked against them), never retyped. After any palette change, recompute every ratio and list old to new values in the changelog so other apps can sync. Check that the app and the design repo agree (the 2026-11-13 check found all 55 dark tokens identical). - Source. Decision 363; design repo
aa251ed,8060ba0,a1869a2.
37. Name tokens by role and keep the names stable when values change.
- Rule.
bg,surface,panel,accent-inkand the rest describe jobs, not colours. When dark was redesigned three times in a month, the token names never changed, so screens needed no edits. If a rename is unavoidable (for examplesecondarytobrand), map every call site; a blind rename hides differences such as an ink that was fixed in one family and adaptive in another. - Source. Decisions 275, 300, 316 (“token names stay”); app
d619267b1.
38. Keep a “do not regress” list, and add to it the moment something breaks.
- Rule. Each lesson above that has an enforcement is also a regression check. When a bug is found only in one theme, add the assertion that would have caught it before fixing the screen.
- Source. App
docs/INVARIANTS.md.
39. Define an animation’s states and check its frames, not only its curves.
- Problem. The logo box animation had smooth curves and passing tests, yet in dark black holes opened in the side of the box while the flaps moved, and in light the closed box showed two orange faces. Each piece was right on its own: the dark master cuts its faces short under the front flaps (a cap filled them, but only near shut); a flap’s colour switched at a chosen angle instead of when it turned edge-on; the rising flap was painted under the lid it stands in front of; and four flaps folding together passed through each other. Nobody had written down what a correct frame looks like, so nothing failed.
- Rule. Write the states down first (rest = the static logo exactly; shut = one solid shape, one orange face;
colour changes only edge-on; paint order = depth order) and test them on rendered frames in both themes: exact
frames (pause, rewind with a negative
animation-delay), the whole cycle at 60 fps, on a loud page colour so transparency shows. A shape drawn in two masters (light and reversed) must be checked in both: what one master covers, the other may leave open. - Enforced.
scripts/check-box-frames.mjs(headless Edge); appboxOpen.frames.test.ts. - Source. Decision 381; design repo
motion/box-open/README.md“Known-good states”.
Current rule (v2.0.1, 2026-11-13): SW-389 (2026-11-13), also recorded in lesson 40, supersedes lesson 39’s one-orange-face and face-recolouring rules: four solid regions, two orange and two green, in both themes throughout movement. Keep the no-hole, depth-order and exact-rest checks. (1A-N19) — supersedes the line above; see docs/current-rules.md#cr-logo-01.
For Android, iOS and sibling apps
Section titled “For Android, iOS and sibling apps”- Use the hex values in
tokens/tokens.json. Define two colour sets (light and dark) with the same role names. Do not use the platform’s automatic dark inversion. - Follow the pairing table: text on
panel, labels on fills,outlinefor control edges,focusfor focus. - Teal as a fill needs a label; teal as text or icon in dark is
accent-ink. - Status chips use the solid fill with the dark label in both themes; large banners use soft plus ink in dark.
- Draw the mascot from
mascot/dark/and the logo from the reversed set on dark surfaces. - Honour the platform’s reduce-motion setting; the loader shows its still frame. No hover animation on touch.
- Verify with the platform’s contrast tooling in both themes on every screen, as in lesson 34.
The five lessons that cost the most
Section titled “The five lessons that cost the most”- Dark is one neutral black family; tints and mixed families fight the page (lesson 1).
- Do not use a fill as text; dark text and icons need the
*-inktokens (lessons 5, 10). - Controls need an
outlineedge;lineis decoration on black (lesson 3). - Tenant colours must be derived and proven in both themes, and fixed rather than rejected (lessons 14, 15).
- One generated token source, plus tests that read it, stops every consumer drifting (lessons 35, 36).
40. Test every flap’s identity, not just the number of colours.
- Problem. The earlier closed-lid fix hid two flaps and recoloured reverse faces. A total colour count alone could pass while the orange flap changed to green, or the dark mark became entirely cream.
- Rule. Follow the same named flap through every frame. The closed lid must expose four substantial regions in the logo’s arrangement: two orange and two green in both themes. Check region positions as well as counts; mono and forced-colours variants still need four separate regions. Retain the no-hole and depth-order checks.
- Enforced. App
boxOpen.frames.test.tsand designscripts/check-box-frames.mjs; generate CSS, markup, the inline splash and the design copies together. This corrects lesson 39’s one-orange-face shut-state rule. - Source. SW-389, superseding that part of SW-381.
41. Preserve an approved animation family separately when only another family changes.
- Problem. SW-389 applied coloured flaps to the reversed mark as well as the light mark. The owner then specified a physical outer-pair lid for coloured marks and explicitly restored black-and-white to v2.0.0. Sharing the new choreography or recolouring the reversed artwork would silently change the wrong family.
- Rule. Track flap identity and stacking order, then scope the new animation to the intended artwork. For SW-412 the closed coloured lid has two orange outer regions and no visible green; black-and-white retains the released geometry, fills and animation. Pin approved output to the release’s actual hashes, keep matrix sampling stepped, and test reverse choreography, moving-frame holes and exact static rest.
- Enforced. App
boxOpen.frames.test.tsandboxOpen.sync.test.ts; designscripts/check-box-frames.mjswith its additional browser-independent--rastermode. Export CSS, three artwork layers and React sources together. Raster proofs supplement the actual browser cascade/compositor check. - Source. SW-412 and its immediate owner correction — superseding lesson 40’s four-visible-regions and coloured-dark rules. Earlier lesson text is retained as history.
42. A fixed surface takes its colours from itself, never from the page theme.
- Problem. Demo cards switch theme with
.sw-scope.dark, but the box-open kit readshtml.dark, so a light card on a dark page drew its loader, inline marks and logo cream-on-cream (blank), and a dark card on a light page drew the coloured mark. The same leak hid focus rings: variables such as--sw-focusare computed once on:root, so a cream card or mascot frame on a dark page got the dark theme’s citron ring (1.03:1), and a page-coloured Play button flipped to black on a white logo stage. - Rule. Anything whose appearance is fixed (a themed demo card, a forced light or dark surface, a logo stage, a
mascot frame, a colour card or chip, a page image) declares its own ground: the kit’s
g-light/g-darkon marks, and the card’s own tokens for the focus ring, hover and panel. Anything that should follow the theme must not be pinned. Check every page in both page themes, not only the theme the card was designed in. - Enforced.
scripts/check-box-grounds.mjs(every non-mono mark shows exactly one artwork, the right one for its surface, in both page themes and through every demo theme switch) andscripts/check-site-qa.mjs(fixed: the same element on a fixed surface renders identically in both page themes;focus: every focus indicator >= 3:1 against what is behind it). - Source. Owner screenshot and “make sure fixed places are not affected”, 13 November 2026 (lane DS-QA, v2.1.1).
43. Write docs through UTF-8 only, and check for lost characters.
- Problem. A v2.1.0 batch was written through a non-UTF-8 path: every em dash, en dash, multiplication sign, middle
dot and curly quote became
?(SW-412 ? Orange,64?352 ms,base ? .75,?unmodified copies?), across 13 sources and every page generated from them. Link and content checks passed, because the text was still there. - Rule. Write files with an explicit UTF-8 encoding (PowerShell 5.1 defaults to the ANSI code page). A repair of such characters restores the intended character; it is not a rewording.
- Enforced.
scripts/check-encoding.mjs, run by the site build (npm run build->check:integration). - Source. DS-QA audit, v2.1.1.
Part of Design lessons learned, published from docs/lessons-learned.md. Edit it there.