Skip to content

Material 3 Theming — milestone-level narrative archive

Verbatim preservation of the M001 and M002 milestone-level planning docs. The ../material-3-theming-roadmap.md distills these into a synthesized narrative; this file is the raw source.

Note on lifecycle: temporary planning archive for completed work. Once the outstanding follow-ups in the roadmap are tracked elsewhere, this archive can be retired.

Source: .gsd/milestones/M001/*.md + .gsd/milestones/M002/*.md at commit fc1bdf410.


M001: Design System Consolidation — Context

Gathered: 2026-03-13 Status: Ready for planning

Project Description

SyRF's Angular frontend has accumulated design debt across 205 components: a mix of Angular Material M2 theming, legacy Bootstrap CSS shims, hardcoded hex/rgba colors, inline styles, and 10 component theme mixin files using deprecated M2 APIs. This milestone replaces all of it with a unified M3-based theme using SCSS design tokens, enforced by Stylelint in CI, validated by Playwright layout integrity tests and axe-core accessibility audits, and documented with Storybook + Chromatic visual regression.

Why This Milestone

The current styling approach is fragile and inconsistent. Two styling systems (Material + Bootstrap), 62 files with hardcoded colors, and no enforcement mean every PR risks introducing more drift. Angular Material's M3 API is the current standard; M2 APIs will eventually be removed. Consolidating now prevents compounding debt and establishes guardrails that keep the system clean going forward.

User-Visible Outcome

When this milestone is complete, the user can:

  • Use the SyRF app with a modern M3 Material Design appearance themed with SyRF brand colors
  • Toggle dark mode (aspirational — M3 defaults, no hand-tuned palette)
  • Browse a Storybook component catalog showing all shared/core components in their themed states

Entry point / environment

  • Entry point: ng serve / npm start (Angular dev server)
  • Environment: local dev / browser
  • Live dependencies involved: none (styling changes only; no API/backend changes)

Completion Class

  • Contract complete means: Stylelint passes with zero violations; layout integrity Playwright tests pass on all key routes; axe-core audit passes WCAG 2.1 AA on key routes; zero Bootstrap classes in templates; zero hardcoded hex/rgba in SCSS
  • Integration complete means: Storybook builds and serves the component catalog; Chromatic captures baselines and runs visual regression in CI
  • Operational complete means: none (no runtime behavior changes)

Final Integrated Acceptance

To call this milestone complete, we must prove:

  • The full app renders correctly with M3 theme on all key routes — no layout breakage (overflow, clipping, hidden elements)
  • npm run lint:styles passes with zero violations and is CI-blocking
  • Storybook serves and Chromatic baseline is captured
  • axe-core reports zero WCAG 2.1 AA violations on key routes
  • Dark mode toggle switches the theme without layout breakage

Risks and Unknowns

  • M3 component dimensions differ from M2 (generally larger/more spacious) — may cause layout overflow in dense UIs. Mitigated by M3 density settings (-1 to -5) and layout integrity Playwright tests
  • 10 component theme mixins use mat.m2-get-color-from-palette() — migration path to M3 CSS custom properties requires per-mixin analysis
  • color input on Material components behaves differently in M3 (CSS class added but no built-in styles) — templates with color="primary" need review
  • Bootstrap shim removal across 22 templates is a large surface change — risk of subtle layout shifts
  • Storybook + Chromatic cost management — TurboSnap and selective story coverage needed

Existing Codebase / Prior Art

  • src/global-styles/syrf-theme.scss — current M2 theme definition with SyRF palette and .global-dark-theme class
  • src/global-styles/legacy-bootstrap/ — 3 SCSS shim files (buttons, panel, progress-bars)
  • src/global-styles/styles.scss — global style entry point
  • .planning/design-system/2026-02-08-design-system-master-plan.md — original 14-PR design system plan (reference, not binding)
  • .planning/design-system/2026-02-08-pr0-baseline-audit.md — audit script specification
  • .planning/design-system/2026-02-08-pr1-dead-code-icon-cleanup.md — dead code and icon cleanup specification

See .gsd/DECISIONS.md for all architectural and pattern decisions — it is an append-only register; read it during planning, append to it during execution.

Relevant Requirements

  • R001–R018 — all active requirements are owned by this milestone's slices
  • See .gsd/REQUIREMENTS.md for full details and traceability

Scope

In Scope

  • Dead code and unused component removal
  • Bootstrap class and SCSS shim removal
  • M2 → M3 global theme migration with SyRF brand palette
  • Component theme mixin migration (10 files)
  • Hardcoded hex, rgba(), and inline style elimination
  • SCSS design token system
  • Stylelint CI-blocking enforcement
  • Aspirational M3 dark mode
  • Design guidelines documentation
  • Storybook 10 component catalog (shared/core components)
  • Chromatic visual regression CI
  • Layout integrity Playwright tests
  • axe-core WCAG 2.1 AA accessibility audit
  • Visual regression baselines (Playwright screenshots)

Out of Scope / Non-Goals

  • OnPush change detection migration (D001)
  • Pixel-for-pixel visual parity with M2 theme (D002)
  • Custom design mockups or Figma integration (X001)
  • Backend/API changes
  • New feature development

Technical Constraints

  • Angular 21, Angular Material M2→M3
  • Must use ng generate @angular/material:m3-theme schematic for palette generation
  • Stylelint config must allow transparent, inherit, currentColor, 0, none as non-token values
  • Storybook must use official Angular Material recipe for font loading and theme styles
  • TurboSnap required for Chromatic cost management

Integration Points

  • CI pipeline — Stylelint must be added as a blocking step
  • CI pipeline — Chromatic must be added as a PR check
  • CI pipeline — axe-core and layout integrity tests must run as part of e2e suite
  • angular.json — global styles configuration will change (new theme file, remove legacy-bootstrap)

Open Questions

  • Which routes constitute "key routes" for layout integrity testing? — Current thinking: project index, stage review, screening, annotation form, project admin, study table (covers the most complex layouts)
  • How many Storybook stories are appropriate for initial coverage? — Current thinking: 20-30 stories covering shared/core components

M001: Design System Consolidation — Research

Conducted: 2026-03-13

Codebase Audit

Signal Count Notes
Components (total) 205
SCSS files in app 206
M2 theming API refs 103 mat.m2-* function calls
Files with hardcoded hex colors 62 In src/app/ SCSS files
Files with hardcoded rgba() 24 In src/app/ SCSS files
Templates with Bootstrap classes 22 btn-, panel-body, panel-heading, progress-bar
Legacy Bootstrap SCSS shims 3 legacy-bootstrap/{buttons,panel,progress-bars}.scss
Templates with inline style= 33 Some may be legitimate dynamic bindings
Component theme mixin files 10 Use mat.m2-get-color-from-palette()
OnPush components 25/205 (12%) Deferred — not in scope
Font Awesome refs 0 Already cleaned
Dark theme support CSS class toggle .global-dark-theme in syrf-theme.scss, no UI control
Global style entry points 3 styles.scss, syrf-theme.scss, syrf-load-logo.scss

M2 → M3 Migration Research

Key findings

  1. No automated migration path. Angular Material does not provide an automated M2→M3 migrator because design decisions are subjective. Manual migration is required.

  2. Palette generation. The ng generate @angular/material:m3-theme schematic generates M3 HCT-based tonal palettes from a single seed color. Run with --primaryColor=#203457 to get SyRF-branded M3 palettes.

  3. M3 emits CSS custom properties. mat.theme() emits --mat-sys-* custom properties, which aligns with the design token enforcement strategy. This is the foundation for Stylelint enforcement.

  4. Component visual changes. M3 components are generally larger/more spacious than M2. Density settings accept integers from 0 to -5 (each step reduces sizes by 4px). This gives control over compactness.

  5. color input behavior change. In M3 themes, the color input still adds a CSS class but there are no built-in styles targeting these classes. Templates using color="primary" or color="accent" need review.

  6. Dual-support path. Components can support both M2 and M3 themes simultaneously using mat.get-theme-version() to branch. This enables incremental migration — one feature route at a time.

  7. Dark mode simplification. M3 supports color-scheme: light dark with the light-dark() CSS function, making dark mode significantly simpler than M2's manual palette overrides.

Migration strategy (research-informed)

  • Generate M3 palettes from #203457 using the official schematic
  • Migrate the global theme first, then component mixins
  • Use density adjustment (-1 or -2) if components feel too large
  • Don't chase visual parity — verify layout integrity instead (overflow, clipping, hidden elements)
  • Convert one feature route at a time if needed, using get-theme-version() for dual support

Stylelint Enforcement Research

Key findings

  1. stylelint-declaration-strict-value enforces variables, functions, or custom CSS values for specified properties. It can require $token or var(--*) for color, font-size, spacing, etc.

  2. Built-in rules complement the plugin: color-named: never, color-no-hex, function-disallowed-list, declaration-property-unit-allowed-list.

  3. Carbon Design System precedent. IBM's Carbon uses a Stylelint plugin enforcing themes, colors, layout, type, and motion. Validates the approach at scale.

  4. Configuration comments (/* stylelint-disable */) are an intended escape hatch. Use reportDescriptionlessDisables: true to require justification.

Enforcement strategy

  • Base config: stylelint-config-standard-scss
  • Token enforcement: stylelint-declaration-strict-value for color, background-color, border-color, font-size, spacing properties
  • Allowed non-token values: transparent, inherit, currentColor, 0, none
  • CI integration: npm run lint:styles as a blocking step

Layout Integrity & UI/UX Evaluation Research

Key findings

  1. Playwright page.evaluate() custom checks can detect zero-sized interactive elements, elements clipped by overflow: hidden parents, elements positioned off-screen, and z-index collisions. Purpose-built for layout-dependent assertions.

  2. @axe-core/playwright provides automated WCAG 2.1 AA accessibility auditing within Playwright tests. Covers color contrast, ARIA roles, keyboard navigability, and focus management — the closest automated proxy for "good UI/UX."

  3. Playwright toHaveScreenshot() provides visual regression testing by comparing screenshots against baselines. Captures full-page or element screenshots and diffs them pixel-by-pixel.

Evaluation strategy (three layers)

  1. Layout integrity (Playwright): Custom checks on key routes for overflow, clipping, zero-size elements, off-screen positioning, z-index collisions
  2. Accessibility (axe-core): WCAG 2.1 AA audit on key routes, integrated into e2e suite
  3. Visual regression (Playwright screenshots): Baseline captured pre-migration (S01), compared during migration (S02+)

Storybook + Chromatic Research

Key findings

  1. Official Angular Material Storybook recipe exists, covering font loading, theme styles, and story setup.

  2. Chromatic TurboSnap reduces snapshot usage by 60-90% by only capturing stories affected by code changes. Essential for cost management.

  3. Story determinism requires mocked API data, fixed dates, disabled animations, and consistent test fixtures.

Strategy

  • Start with shared/core components (~20-30 stories)
  • Enable TurboSnap from day one
  • Use composite snapshot stories where appropriate to reduce snapshot count

Risk Assessment (Post-Research)

Area Pre-Research Risk Post-Research Risk Key Mitigation
Baseline + cleanup high medium Well-defined scope, existing plans, FA already done
M3 theme core high medium Official schematic for palettes; layout integrity tests instead of visual parity; density settings for sizing
Hardcoded value sweep medium medium Mechanical but large (62+ files); Stylelint tooling well-proven
Stylelint enforcement medium low stylelint-declaration-strict-value is battle-tested
Design guidelines + dark mode low low M3 dark mode simpler than M2; guidelines are docs work
Storybook + Chromatic medium medium Official recipe works; TurboSnap for cost management
Layout integrity testing low Playwright is purpose-built for this; axe-core well-established


id: M001 status: complete started: 2026-03-13 completed: 2026-03-14 slices_completed: 6 slices_total: 6


M001: Design System Consolidation — Complete

Replaced SyRF's fragmented M2 + Bootstrap + hardcoded value styling with a unified M3 design system — token-enforced, layout-verified, accessibility-audited.

Final State

  • M3 theme live with HCT palettes from SyRF primary (#203457)
  • Zero M2 API references in any SCSS file
  • Zero Bootstrap class usage
  • Zero hardcoded hex colors in component SCSS (64 files migrated)
  • Near-zero rgba() in component SCSS (1 legitimate Sass fn remains)
  • Stylelint enforced — color-no-hex + color-named rules, zero violations
  • 45 Playwright tests passing: 30 layout integrity (light + dark), 5 visual regression, 10 a11y
  • Dark mode toggle in navigation with localStorage persistence
  • Design guidelines published at docs/design/design-system.md
  • ~60 design tokens covering colors, borders, surfaces, text, spacing, typography, elevation
  • Production build succeeds

Slices

  1. S01 — Baseline audit, dead code cleanup, visual baselines (19 dead components removed)
  2. S02 — M3 theme core with HCT palettes, all 10 component mixins migrated, layout integrity tests
  3. S03 — Hardcoded value sweep: 64 SCSS files → tokens, rgba() → tokens, inline style colors → CSS classes
  4. S04 — Stylelint enforcement: color-no-hex + color-named rules, 96 named colors replaced
  5. S05 — Design guidelines doc, dark mode toggle with ThemeService, dark mode layout verification
  6. S06 — axe-core a11y audit (WCAG 2.1 AA), Storybook 10 with all stories rendering, Playwright-based visual regression (replaces Chromatic)

Known Limitations

  • Storybook M3 system colors: CSS custom properties from the M3 theme don't apply in Storybook's isolated preview context (expected — Storybook doesn't load the full app theme).
  • Layout/a11y tests cover public routes only: Authenticated routes need Auth0 mock infrastructure.
  • image-alt violations: Pre-existing — decorative images lack alt text (excluded from a11y tests).
  • Remaining inline style= attributes: 27 templates have layout-only inline styles (not color-related).

Requirements Status

Requirement Status
R001 — Audit script ✅ Validated
R002 — Dead code removal ✅ Validated
R003 — M3 global theme ✅ Validated
R004 — SyRF brand on M3 ✅ Validated
R005 — Component mixin migration ✅ Validated
R006 — Hex color elimination ✅ Validated
R007 — rgba() elimination ✅ Validated
R009 — Stylelint enforcement ✅ Validated
R010 — Design guidelines ✅ Validated
R011 — Dark mode toggle ✅ Validated
R012 — axe-core a11y audit ✅ Validated
R013 — Storybook catalog ✅ Validated
R014 — Visual regression CI ✅ Playwright-based (replaces Chromatic)
R015 — Layout integrity ✅ Validated
R016 — Layout test suite ✅ Validated
R018 — Visual regression baselines ✅ Validated

Key Decisions Made

  1. M3 theme uses mat.define-theme() + mat.theme() split
  2. Component mixins use var(--mat-sys-*) directly (not mat.get-theme-color())
  3. M2 warn → M3 error, accent → tertiary semantic mapping
  4. Token granularity: specific tokens per use case rather than forced semantic reuse
  5. Formatting rules disabled in Stylelint (out of scope)
  6. axe-core filters to serious/critical only; minor violations logged but don't fail

M002: M3 System Variable Integration

Problem

M001 migrated the Angular Material API from M2 to M3 and replaced hardcoded colors with SCSS design tokens. But the design tokens (_design-tokens.scss) are a custom intermediate layer — they don't integrate with the M3 system variables (--mat-sys-*) that Angular Material emits. This causes:

  1. Toolbar white-on-white: M3 ignores color="primary" on mat-toolbar, so the toolbar renders with the default surface (white) background while nav text is hardcoded to white
  2. Button style loss: mat-raised-button with color="accent" renders differently in M3 — color-variants-backwards-compatibility doesn't cover all M2 behaviors
  3. Color inconsistency: 461 references to custom tokens.$color-* across 125 SCSS files coexist with 83 references to --mat-sys-*, creating two competing color systems
  4. No body/surface defaults: Missing body { background: var(--mat-sys-surface); color: var(--mat-sys-on-surface) } means text/background don't participate in light/dark theming
  5. Dark mode broken: The dark theme class re-emits the M3 theme, but components using custom tokens don't respond to the cascade

Strategy

Replace the custom design tokens with M3 system variables (--mat-sys-*) wherever a direct M3 equivalent exists. Keep domain-specific tokens (warning, info, success, annotation colors) as custom CSS properties that layer on top of the M3 foundation.

M3 Color Role Mapping

Custom Token M3 System Variable Context
$color-white (on primary bg) var(--mat-sys-on-primary) Text/icons on primary backgrounds
$color-white (literal) #fff / keep token Not semantic — just white
$color-black var(--mat-sys-on-surface) Text on surfaces
$color-surface var(--mat-sys-surface) Backgrounds
$color-surface-variant var(--mat-sys-surface-variant) Card/panel backgrounds
$color-text-primary var(--mat-sys-on-surface) Primary body text
$color-text-secondary var(--mat-sys-on-surface-variant) Secondary/subdued text
$color-text-muted* var(--mat-sys-on-surface-variant) Muted/hint text
$color-text-hint var(--mat-sys-on-surface-variant) Hint text
$color-gray var(--mat-sys-on-surface-variant) General gray text
$color-lightgray var(--mat-sys-outline-variant) Borders/dividers
$color-error* var(--mat-sys-error) / var(--mat-sys-error-container) Error states
$color-primary-500 var(--mat-sys-primary) Brand primary
$color-primary-tint var(--mat-sys-primary-container) Light primary fills
$color-background var(--mat-sys-surface) Page backgrounds

Keep as Custom Tokens (no M3 equivalent)

  • $color-warning* — M3 has no warning role
  • $color-info* — custom semantic
  • $color-success* / $color-annotation-* — domain-specific
  • $color-dnd-* — drag-and-drop states
  • $color-shadow-* — elevation/shadow values
  • Spacing, typography, z-index, breakpoint tokens — all keep

Scale

  • 461 custom color token references across ~125 SCSS files
  • 148 color="primary|accent|warn" HTML attributes (most work via backwards-compat, some don't)
  • 10 component theme mixins already on M3 (no change needed)

M002 Research: M3 System Variable Integration

Problem Statement

M001 migrated Angular Material from M2 to M3 API (mat.define-theme() / mat.theme()), replaced hardcoded hex colors with SCSS design tokens (_design-tokens.scss), and removed Bootstrap. But the custom token layer doesn't integrate with M3's native system variables (--mat-sys-*). Result: the app looks visually broken.

Root Causes Identified

1. Toolbar white-on-white (CRITICAL)

  • mat-toolbar has color="primary" in the HTML template
  • M3 ignores color="primary" on mat-toolbar — confirmed via Angular Material docs: "This API is supported in M2 themes only, it has no effect in M3 themes"
  • Toolbar renders with default --mat-sys-surface (white) background
  • Nav text is hardcoded to tokens.$color-white → white text on white background = invisible
  • Fix: Use mat.toolbar-overrides() mixin per https://material.angular.dev/components/toolbar/styling

2. Missing body surface defaults

  • No body { background: var(--mat-sys-surface); color: var(--mat-sys-on-surface) }
  • Angular Material M3 docs explicitly recommend this: https://material.angular.dev/guide/theming-your-components

3. Button style loss

  • SIGN IN / CREATE YOUR ACCOUNT buttons have color="accent" + mat-raised-button
  • color-variants-backwards-compatibility partially works but .action-button class conflicts
  • Fix: Remove color="accent", use M3 component tokens (--mdc-filled-button-container-color)

4. Dual color system

  • 461 refs to custom tokens.$color-* across ~125 SCSS files
  • 83 refs to --mat-sys-* in theme mixins
  • Two competing systems = inconsistent colors, dark mode doesn't work on custom tokens

5. Feature icons/text wrong color

  • home.component.theme.scss uses var(--mat-sys-primary-container) for .feature color
  • primary-container is a light pastel — should be var(--mat-sys-primary) for the dark brand color

Scale of Changes

Custom color token refs (excluding _design-tokens.scss):  461
M3 system variable refs:                                   83
SCSS files changed vs main:                               125
color="primary|accent|warn" in HTML templates:            148
M2 API calls remaining:                                     0
Component theme mixins (already on M3):                    10

Token Mapping Table

Mappable to M3 System Variables (~280 refs)

Custom Token M3 Variable Usage Context
$color-white (on primary bg) var(--mat-sys-on-primary) Text/icons on toolbar/primary surfaces
$color-white (literal) keep as #fff or token Background color, not semantic
$color-black var(--mat-sys-on-surface) Text on light surfaces
$color-surface var(--mat-sys-surface) Background surfaces
$color-surface-variant var(--mat-sys-surface-variant) Card/panel backgrounds
$color-text-primary var(--mat-sys-on-surface) Primary body text
$color-text-secondary var(--mat-sys-on-surface-variant) Secondary/subdued text
$color-text-muted* var(--mat-sys-on-surface-variant) Muted text
$color-text-hint var(--mat-sys-on-surface-variant) Placeholder/hint text
$color-text-on-primary var(--mat-sys-on-primary) Text on primary backgrounds
$color-gray var(--mat-sys-on-surface-variant) General gray text
$color-lightgray var(--mat-sys-outline-variant) Borders, dividers
$color-error / $color-error-dark var(--mat-sys-error) Error states
$color-error-light var(--mat-sys-error-container) Error backgrounds
$color-primary-500 var(--mat-sys-primary) Brand primary
$color-primary-tint var(--mat-sys-primary-container) Light primary fills
$color-background var(--mat-sys-surface) Page backgrounds
$color-divider-light var(--mat-sys-outline-variant) Divider lines

Keep as Custom Tokens (~180 refs, no M3 equivalent)

  • $color-warning* — M3 has no warning color role
  • $color-info* — custom semantic color
  • $color-success* / $color-annotation-* — domain-specific
  • $color-dnd-* — drag-and-drop UI states
  • $color-red — semantic danger color
  • $color-shadow-* — shadow/elevation custom values
  • All spacing, typography, z-index, breakpoint tokens — keep as-is

Key M3 Guidance (from Angular Material docs)

Toolbar (per https://material.angular.dev/components/toolbar/styling)

:root {
  @include mat.toolbar-overrides((
    container-background-color: var(--mat-sys-primary),
    container-text-color: var(--mat-sys-on-primary),
  ));
}

Body defaults (per https://material.angular.dev/guide/theming-your-components)

body {
  background: var(--mat-sys-surface);
  color: var(--mat-sys-on-surface);
}

Custom component theming (per https://material.angular.dev/guide/theming-your-components)

:host {
  background: var(--mat-sys-primary-container);
  color: var(--mat-sys-on-primary-container);
  border: 1px solid var(--mat-sys-outline-variant);
  font: var(--mat-sys-body-large);
}

Available M3 system variables

  • Color: --mat-sys-primary, --mat-sys-on-primary, --mat-sys-primary-container, --mat-sys-on-primary-container, --mat-sys-secondary, --mat-sys-tertiary, --mat-sys-error, --mat-sys-on-error, --mat-sys-error-container, --mat-sys-surface, --mat-sys-on-surface, --mat-sys-on-surface-variant, --mat-sys-surface-variant, --mat-sys-surface-container-*, --mat-sys-outline, --mat-sys-outline-variant, --mat-sys-inverse-surface, --mat-sys-inverse-on-surface, --mat-sys-scrim, --mat-sys-shadow
  • Typography: --mat-sys-body-{sm,md,lg}, --mat-sys-display-{sm,md,lg}, --mat-sys-headline-{sm,md,lg}, --mat-sys-label-{sm,md,lg}, --mat-sys-title-{sm,md,lg}
  • Elevation: --mat-sys-level{0-5}
  • Shape: --mat-sys-corner-{none,extra-small,small,medium,large,extra-large,full}

Stashed Work

git stash list has "WIP: ad-hoc M3 system variable migration" — contains directionally correct but unplanned changes to: - syrf-theme.scss — toolbar overrides, body defaults, system-classes (correct approach) - nav.component.scss — white→on-primary replacements (correct approach) - home.component.* — button/feature fixes (correct approach) - styles.scss — container/form color swaps (correct approach)

These can be git stash pop'd as a starting point for T01-T03 after proper planning.

Context-Sensitive Cases (need human judgment in migration script)

  1. $color-white — means "on-primary text" on toolbar, but "literal white background" elsewhere
  2. $color-black — means "on-surface text" in body, but "literal black" for borders
  3. $color-lightgray — sometimes border (outline-variant), sometimes background
  4. Any color used with !important — needs individual review
  5. Colors inside ::ng-deep — may need different scoping

M002: M3 System Variable Integration — Roadmap

Slices

  • S01: Foundation — M3 Global Defaults & Token Bridge risk:low depends:[] Set up the M3 foundation layer: toolbar overrides, body surface defaults, system utility classes, and a token bridge that re-exports custom tokens as CSS custom properties so they work with dark mode. Write a migration script that automates the bulk token→system-variable replacement. This slice fixes the structural issues (white toolbar, missing body colors) and builds the tooling for S02.

  • S02: Automated Bulk Migration of Component Styles risk:medium depends:[S01] Run the migration script across all ~125 changed SCSS files. Manually review and fix edge cases where context matters (e.g., $color-white meaning "on-primary text" vs "literal white background"). Fix remaining color="accent" button issues. Visual verification of all public pages.

  • S03: Verification & Cleanup risk:low depends:[S02] Run full build, lint, and test suite. Update Storybook baselines. Run Playwright layout integrity tests. Remove unused tokens from _design-tokens.scss. Update documentation. Final visual walkthrough.


M002 Status Report: M3 System Variable Integration

Date: 2026-03-14 Branch: claude/standardize-syrf-theme-ZUBP7 (PR #2322) Status: M002 complete (3/3 slices shipped)


What Changed

The Problem

After M001 migrated Angular Material from M2 to M3 API, the app had a dual color system: M3's --mat-sys-* CSS custom properties coexisted with 461 custom SCSS token references (tokens.$color-*). The result was a visually broken app — white toolbar on white background, invisible nav links, unstyled buttons, wrong feature icon colors.

What We Did (3 Slices)

S01 — Foundation (3 commits) - Fixed the toolbar white-on-white: mat.toolbar-overrides() with primary bg/on-primary text - Added body surface/text defaults (background: var(--mat-sys-surface)) - Fixed nav component: hardcoded white → var(--mat-sys-on-primary) - Fixed home buttons: mat-raised-buttonmat-flat-button (M3 filled button) - Built domain token bridge: exported warning/info/success/annotation colors as --syrf-color-* CSS custom properties with dark-mode overrides - Built migration script (scripts/migrate-tokens-to-m3.sh) with dry-run support

S02 — Bulk Migration (1 commit) - Applied 200 automated token→M3 replacements across 67 SCSS files - Fixed critical regex bug: \b word boundary matched partial tokens (e.g., $color-surface inside $color-surface-tint-subtle). Replaced with (?![\w-]) PCRE negative lookahead.

S03 — Cleanup (1 commit) - Fixed 11 stylelint color-no-hex violations - Added --syrf-color-error-light to domain bridge (eliminates hex fallback in var()) - Removed 25 dead color tokens from _design-tokens.scss - Added migration comments documenting which M3 vars replaced each token group

By the Numbers

Metric Before M002 After M002
M3 system variable refs 83 285 (+243%)
Custom SCSS token refs 461 283 (-39%)
Domain bridge refs 0 12
Tokens defined in _design-tokens.scss ~248 223 (-25 dead removed)
Stylelint errors 11 (new) 0
ng build status

Files Changed (M002 only)

  • 5 commits, ~85 files touched
  • Key files: syrf-theme.scss, _design-tokens.scss, styles.scss, nav.component.scss, home.component.*, 67 component SCSS files, scripts/migrate-tokens-to-m3.sh

Current State

What Works

  • Toolbar: Dark navy background with visible white nav links (via mat.toolbar-overrides())
  • Body: White surface background, dark text (via M3 surface/on-surface)
  • CTA buttons: Filled dark blue with white text (mat-flat-button + M3 tokens)
  • Feature icons/text: Correct M3 primary and on-surface-variant colors
  • Domain colors: Warning, info, success, annotation colors available as --syrf-color-* CSS custom properties that respond to dark mode
  • Build: Clean (ng build exits 0, warnings only)
  • Lint: Clean (stylelint 0 errors)

What Remains (Not in M002 Scope)

1. Remaining Custom Token Refs (283 refs)

These are domain-specific colors that have no M3 equivalent — they're intentionally kept:

Category Refs Examples
Warning/status ~40 $color-warning, $color-warning-light-bg
Info ~33 $color-info, $color-info-secondary
Success ~18 $color-success, $color-success-dark
White/Black ~28 $color-white (literal), $color-black
Annotation ~23 $color-annotation-primary, etc.
Surface tints ~8 $color-surface-tint-light (opacity-based)
PDF/DnD/misc ~50 $color-pdf-highlight-*, $color-dnd-*
Borders ~20 $color-border-light, $color-border-muted
Error sub-variants ~10 $color-error-border, $color-error-text
Other domain ~53 $color-red, $color-purple, env indicators

2. HTML color= Attributes (147 refs)

M3 ignores color="primary", color="accent", color="warn" on many components (toolbar, buttons, etc.). The color-variants-backwards-compatibility mixin provides partial support, but these should eventually be removed or replaced with M3 component token overrides.

3. Infrastructure Tokens

Z-index, spacing, typography, transitions, breakpoints are defined in _design-tokens.scss but many are aspirational (not yet referenced). These are foundation tokens for future use — not dead code.


Recommendations

Short Term (Before PR Merge)

  1. Review the PR diff carefully — 85 files is a lot of surface area. The migration script changes are mechanical but worth a scan.
  2. Test authenticated pages — we could only verify public pages (homepage, about). Auth-gated pages (projects, stages, screening) use many of the migrated tokens and should be spot-checked.
  3. Check dark mode toggle — the domain bridge has dark overrides, but we couldn't test dark mode visually since the toggle requires app state.

Medium Term (Future Milestones)

  1. Migrate $color-white/$color-black refs (28 total) — these are context-dependent and couldn't be automated. "White" on a toolbar means on-primary, but "white" as a background means literal #fff or surface. Each needs manual judgment.
  2. Migrate remaining border tokens to M3$color-border-light, $color-border-muted, etc. could map to var(--mat-sys-outline-variant) but need per-usage review.
  3. Remove color="primary|accent|warn" from HTML (147 refs) — these are M2 API that M3 ignores on many components. Use component token overrides instead.
  4. Migrate surface-tint tokens$color-surface-tint-* (opacity-based rgba values) don't map directly to M3 surface-container roles. Need design review.

Long Term

  1. Consider migrating domain colors to the --syrf-color-* bridge — currently warning/info/success components use SCSS tokens directly. Moving to var(--syrf-color-*) would give them dark mode support.
  2. Remove color-variants-backwards-compatibility — once all color= HTML attrs are removed, this shim is dead weight.
  3. Audit _design-tokens.scss — 223 tokens is still large. Many infrastructure tokens (z-index, transitions, spacing aliases) aren't referenced yet. Decide: keep as aspirational design system, or trim to what's actually used.

Key Decisions Made (D14–D24)

# Decision Rationale
D17 Replace mappable tokens with --mat-sys-* M3 vars handle light/dark automatically
D18 mat.toolbar-overrides() for toolbar color="primary" has no effect in M3
D20 mat-flat-button for filled CTAs mat-raised-button uses --mdc-protected-button-* (wrong token set)
D21 Re-apply toolbar overrides after mat.theme() mat.theme() re-emits toolbar tokens, undoing :root override
D22 --syrf-color-* namespace for domain bridge Avoids collision with --mat-sys-*
D23 (?![\w-]) regex boundary, not \b Hyphens cause false matches with \b
D24 Error-light in domain bridge Avoids hex fallback that triggers stylelint

Artifacts

  • Migration script: scripts/migrate-tokens-to-m3.sh (--dry-run, --report, --verbose)
  • GSD state: .gsd/milestones/M002/ (research, roadmap, 3 slice plans + summaries)
  • Decisions register: .gsd/DECISIONS.md (D14–D24)

PR #2322 Status Report: SyRF Theme Standardization

Date: 2026-03-14
Branch: claude/standardize-syrf-theme-ZUBP7
Worktree: .worktrees/pr2322.standardize-theme
Status: M001 + M002 complete. Build clean. Lint clean. Ready for review.


Executive Summary

This PR migrates SyRF's Angular Material theme system from M2 (legacy) to M3 (current), consolidates the design token system, and eliminates the dual-color-system that was causing visual breakage (white-on-white toolbar, invisible nav links, unstyled buttons).

The work was executed across two milestones totaling 9 slices, producing 30 commits that touch 351 files with a net reduction of 15,236 lines (19,077 insertions / 34,313 deletions).


What Changed

Milestone 1 — Design System Consolidation (M001, 6 slices)

Foundational cleanup that prepared the codebase for M3:

  • Baseline audit — catalogued all design token usage, dead code, and Bootstrap remnants
  • Dead code removal — stripped Font Awesome CDN, Bootstrap assets, dead custom components
  • Icon migration — replaced all Font Awesome <i> tags and fontSet/fontIcon attrs with Material Icons
  • M3 theme generation — used Angular's official schematic to generate HCT-based tonal palettes from SyRF brand color #203457
  • Full M2→M3 API migration — migrated mat.define-*-theme()mat.define-theme(), all 18 component @include mat.*-theme() mixins → single mat.theme(), and density configuration
  • Visual regression infrastructure — Playwright-based visual tests for public pages (replaced Chromatic)
  • Sentry & SonarCloud fixes — CSS custom property interpolation, duplicate declarations, coverage config

Milestone 2 — M3 System Variable Integration (M002, 3 slices)

Eliminated the dual color system created by M001's API migration:

  • Toolbar fixmat.toolbar-overrides() with primary bg / on-primary text (M3 ignores color="primary")
  • Body defaultsbackground: var(--mat-sys-surface), color: var(--mat-sys-on-surface)
  • Button fixmat-raised-buttonmat-flat-button (correct M3 filled button API)
  • Nav fix — hardcoded white → var(--mat-sys-on-primary)
  • Domain token bridge--syrf-color-* CSS custom properties for warning/info/success/annotation/error colors with dark-mode overrides
  • Bulk token migration — 200 automated tokens.$color-*var(--mat-sys-*) replacements across 67 SCSS files via custom migration script
  • Dead token removal — 25 unused color tokens removed from _design-tokens.scss
  • Lint cleanup — 11 color-no-hex violations fixed; --syrf-color-error-light added to domain bridge

Current Metrics

Metric Before (main) After (this PR) Change
M3 system variable refs (--mat-sys-*) 83 251 +202%
Custom SCSS token refs (tokens.$*) 461 271 -41%
Domain bridge refs (--syrf-color-*) 0 10 new
Token definitions in _design-tokens.scss ~248 223 -25 dead removed
Total SCSS files 193 193
Total components 180 180
ng build clean
Stylelint (new errors) 0 0 clean†
Migration script dry-run remaining 0 fully applied

† 4 pre-existing vendor-prefix errors in syrf-load-logo.scss (unchanged from main, not introduced by this PR).

Remaining Token Refs by Category (271 total)

These are intentionally kept — they have no M3 system equivalent:

Category Count Examples
White/Black (context-dependent) 28 $color-white (21), $color-black (7)
Warning/status 35 $color-warning, $color-warning-light
Info 32 $color-info, $color-info-dark, $color-info-secondary
Annotation 21 $color-annotation-primary, $color-annotation-success
Success 15 $color-success, $color-success-light
DnD (drag & drop) 13 $color-dnd-stripe, $color-dnd-border
PDF highlights 11 $color-pdf-highlight-light-1
Surface tints (opacity-based) 11 $color-surface-tint-light
Named colors / env 21 $color-red, $color-green, $color-deep-orange
Borders 9 $color-border-light
Shadows / disabled 10 $color-shadow-light, $color-disabled-bg
Error sub-variants 2 $color-error-border
Infrastructure (non-color) 13 $z-index-nav, $spacing-8, $shadow-nav, $radius-xs

HTML color= Attributes (147 refs, unchanged)

color="primary", color="accent", color="warn" on Angular Material components. M3 ignores these on most components but provides a backwards-compatibility shim (color-variants-backwards-compatibility mixin) that preserves partial functionality. These are functional but deprecated.


Build & Tooling Health

Check Status
ng build ✅ passes (39 warnings — all pre-existing: ESM compat, budget, selector)
ng serve ✅ running on localhost:4200
stylelint ✅ 0 new errors (4 pre-existing vendor-prefix in unchanged file)
Migration script (--dry-run) ✅ 0 remaining replacements
Working tree ✅ clean
Unpushed commits 1 (.gitignore update for .bg-shell/)

Key Architectural Decisions (D1–D24)

Most impactful decisions made during this work:

# Decision Impact
D2 M3 defaults + SyRF brand #203457 primary Defines entire color palette
D5 Visual parity with M2 is not a goal Unblocked modernization
D13 mat.define-theme() + mat.theme() split Core M3 API pattern
D14 CSS custom properties directly, not mat.get-theme-color() Simpler, cascade-friendly
D17 Replace mappable tokens with --mat-sys-* Eliminated dual color system
D18 mat.toolbar-overrides() for toolbar Only M3-supported approach
D20 mat-flat-button for filled CTAs Correct M3 filled button API
D22 --syrf-color-* namespace for domain bridge Clean separation from Material
D23 (?![\w-]) regex boundary in migration script Prevents partial-match bugs

Full register: .gsd/DECISIONS.md


What's Not Yet Tested

This work was verified against public pages only (home, about, mission, protocols, FAQ, contact). The following require authenticated access to verify:

  • Project list / project overview pages
  • Screening workflow (stages, decisions, reconciliation)
  • Data export pages
  • Admin / impersonation
  • Annotation tool (uses $color-annotation-* tokens heavily)
  • PDF viewer (uses $color-pdf-* tokens)
  • Dark mode toggle (requires app state / user preference)

The migrated tokens are used across all of these — the automated replacements are correct syntactically (ng build passes), but visual verification in context hasn't been done.


Recommendations

Before Merge

  1. Visual spot-check of auth-gated pages — log in and scan project overview, screening, annotation, and data export pages. The token migrations are mechanical but touching 67 SCSS files warrants eyes on the result.
  2. Test dark mode — the domain bridge has dark-mode overrides, but this couldn't be verified without auth state.
  3. Push the last commit — 1 unpushed commit (.gitignore for .bg-shell/).

Future Work (Not in This PR)

High value, low risk: 4. Migrate $color-white/$color-black (28 refs) — context-dependent, needs per-usage judgment. "White" on a toolbar = on-primary, "white" as background = literal #fff or surface. 5. Migrate border tokens ($color-border-light, $color-border-muted) → var(--mat-sys-outline-variant) — 9 refs, straightforward but needs visual review.

Medium value, medium risk: 6. Remove color="primary|accent|warn" HTML attrs (147 refs) — replace with M3 component token overrides. This is the path to removing the backwards-compatibility shim. 7. Migrate surface-tint tokens (11 refs) — opacity-based rgba values that don't map directly to M3 surface-container roles. Needs design review.

Low urgency: 8. Move domain colors to --syrf-color-* bridge — currently warning/info/success use SCSS tokens directly. Bridge vars would give them dark mode support. 9. Remove color-variants-backwards-compatibility mixin — once all color= HTML attrs are gone. 10. Audit _design-tokens.scss — 223 definitions, 13 non-color infrastructure tokens in use. Many are aspirational. Decide: keep as design system foundation or trim to what's referenced.


Artifacts

Artifact Path
Migration script scripts/migrate-tokens-to-m3.sh (--dry-run, --report, --verbose)
GSD milestone state .gsd/milestones/M001/, .gsd/milestones/M002/
Decision register .gsd/DECISIONS.md (D1–D24)
This report .gsd/milestones/M002/PR2322-STATUS-REPORT.md