Design Review Process (#457)

The design system and the shipped app drift apart in both directions: PRs introduce ad-hoc values the system should have provided, and PRs invent good patterns the system should adopt. This process closes the loop in both directions. It complements design_system_workflow.md (which covers how a design gets built); this covers how shipped design stays coherent.

The two-way street

  • Drift down - a PR hardcodes a color, radius, breakpoint, or spacing value instead of using a token, or restyles an existing component slightly differently. Outcome: a design-drift issue, fixed either directly (token swap) or through the design loop when the fix needs visual judgment.
  • Promotion up - the same new pattern appears in two or more PRs (a chip, a table register, an empty state). Outcome: promote it - name it, move the CSS to a component file, add tokens if needed, document it in docs/technical/css_best_practices.md, and refactor the copies onto it.

Cadence

Per-PR (cheap, always) - the reviewer (Sean, Copilot, or Claude on its own PRs) checks the design checklist below for any PR touching app/assets/stylesheets/ or view files. Claude PR descriptions should state any deliberate deviation from the checklist and why.

Periodic sweep (scripted + judgment, roughly per milestone or after any multi-PR visual batch) - run the drift greps below, screenshot the main surfaces (the capture harness in design_system_workflow.md), and compare against docs/design/returns/*/design-final.html canvases. The ux-designer agent is the natural runner for the judgment half. Output: design-drift issues (one per theme, not per instance) plus promotion candidates.

Per-PR design checklist

  • Colors come from semantic tokens (--color-*); no raw oklch()/hex outside _global.css. Alpha variants of a token use color-mix() with the token, not a re-derived literal.
  • Radii, spacing, shadows, transitions come from tokens; a value the scale lacks is a conversation about the scale, not a literal.
  • Breakpoints follow the mobile-first round-number convention (css_best_practices.md, #431).
  • Single-class selectors, BEM-ish names, no !important outside utility.css, no id selectors.
  • Type sizes in rem; system font stack; focus states via --focus-ring/--focus-offset.
  • Every string in six locales; index pages use list rows; beta features use the beta indicator pattern (#388).
  • New repeated pattern? Flag it as a promotion candidate in the PR description.

Drift greps

# raw colors outside the token files
grep -rnE "oklch\(|#[0-9a-fA-F]{3,6}([^0-9a-fA-F]|$)" app/assets/stylesheets/ | grep -v "_global\|reset" | grep -v "var(--"
# unscaled radii / px type / importants / id selectors
grep -rn "border-radius: [0-9]" app/assets/stylesheets/*.css | grep -v "var(--radius"
grep -rn "font-size: [0-9]*px" app/assets/stylesheets/*.css
grep -rn "important" app/assets/stylesheets/*.css | grep -v utility.css
grep -rn "^ *#[a-z]" app/assets/stylesheets/*.css
# fractional/legacy breakpoints (should only be documented exceptions)
grep -rn "max-width: [0-9]*\.98px" app/assets/stylesheets/*.css
# legacy light-only palette tokens (pre --lch-* system; break dark mode)
grep -rnE "var\(--color-(primary|grey|text|white|black|blue|teal|orange|green|amber|red)" app/assets/stylesheets/*.css | grep -v _global
# focus suppression without a visible replacement (check each hit's context)
grep -rn -C3 "outline: none" app/assets/stylesheets/*.css
# cascade layers (banned) and dark-mode blocks outside the token file
grep -rn "@layer" app/assets/stylesheets/
grep -rl "prefers-color-scheme" app/assets/stylesheets/*.css | grep -v _global
# inline styles and inline scripts in views (dynamic values like widths are fine)
grep -rn 'style="' app/views/ --include="*.erb"
grep -rl "<script" app/views/ --include="*.erb"

First sweep results (2026-07-22)

Recorded here as the baseline; filed as issues rather than fixed inline:

  1. Token gaps the drift reveals - oklch(1 0 0) (white-on-accent) is hand-rolled in 4 files where text sits on accent fills: the palette needs an --color-on-accent semantic token. The radius scale stops at --radius-sm: 0.25rem + --radius-pill, so 0.3125/0.5rem and 8/10px literals proliferate (documents.css alone has 20): the scale needs --radius-md/--radius-lg, then a sweep. Alpha accents like oklch(var(--lch-accent) / 0.7) should become color-mix on the semantic token. One hardcoded gold in documents.css:1476.
  2. Pre-redesign stylesheets - article.css, article-header.css, base.css, modal.css, grammar_notes.css still carry px type sizes, and article-header.css styles the old audio player through id selectors. These predate the design system and want a modernization pass through the design loop when their surfaces are next touched.
  3. Healthy: !important is confined to utility.css (by design) plus 4 strays; transitions all use the token but one; breakpoints were swept to the round-number mobile-first convention in #431.

Second sweep results (2026-07-23)

Widened the lens beyond the first sweep’s color/radius/px greps to the remaining conventions (dark-mode single-sourcing, focus states, cascade, spacing, view layer); the new greps above came from this pass. Findings are listed on #457 as checkboxes; the baseline numbers:

  1. Legacy light-only palette is the largest drift by far - _global.css still defines the pre---lch-* palette (--color-primary, ten --color-grey-* steps, --color-text-*, --color-white, named colors), raw literals with no dark-mode redefinition. 244 usages across 36 files, and new files keep adopting them (profile-header.css, profile.css, profile-evaluate.css, coach.css) because the tokens still exist and look official.
  2. Focus suppression - 20 outline: none declarations across 10 files; several leave no visible focus indicator at all (button.css .btn-option and the close-button block). --focus-ring adoption: 10 files.
  3. Spacing scale barely adopted - 45 var(--space-*) usages vs 334 padding: literals. Whether to sweep literals onto the scale or accept raw rem values in components is an open scale conversation, not mechanical drift. (This sweep also fixed css_best_practices.md, which documented a --space-1..8 scale that never existed.)
  4. On-accent white regrew after the first-sweep baseline - four new oklch(1 0 0) instances in the profile train (profile-shell.css x2, profile-tabs.css, profile-evaluate.css) on top of #482’s four; drift compounds until --color-on-accent lands.
  5. Healthy: no @layer anywhere; the dark-mode block lives only in _global.css and redefines only --lch-* primitives plus the shadow tokens; index pages use list rows (shared/_list_grid + list-row.css); native.css’s four !importants are deliberate utility toggles, same class as utility.css; first-sweep stray-!important and id-selector counts unchanged (#483 still accurate).

Promotion candidates from the recent PR batch

The #454-#456 train shipped several patterns now used by 2+ surfaces and worth naming as components if they recur again: the workspace check-grid (deck-workspace-check), the pill radio group (deck workspace card styles, canvas 1e source switcher), and the quiet row-affordance link (Add example, workspace rows). No action yet; noted so the next sweep can count instead of rediscover.


This site uses Just the Docs, a documentation theme for Jekyll.