# Path-to-9 execution · PHASE A — motion vocabulary (Phase-0) + AA/affordance floor (Pillar 4) ### Contract: docs/ux-refactor/path-to-9-2026-07-07.md (v3, 5/5 panel-endorsed). Same honesty ### rules as waves P–X: surgical, both themes, testids/aria survive, reduced-motion gated. Two sequential stages (stage 2 depends on stage 1's exports). Per-lane gate: related vitest green + eslint 0 errors on touched files. NO benchmarks/**, NO packages/hive-mind-*. ## STAGE 1 (parallel lanes) ### Lane M — motion tokens + spring family + spec page (Phase-0 items 1,2,3,5,7) NEW files only + index.css additions. Files: `apps/web/src/lib/motion/tokens.ts` (new), `apps/web/src/lib/motion/tokens.test.ts` (new), `apps/web/src/pages/MotionSpec.tsx` (new, DEV-only route), route wiring (find the router config; gate the route on `import.meta.env.DEV`), `apps/web/src/index.css` (append motion custom props ONLY). 1. **The exported vocabulary** (`lib/motion/tokens.ts`) — THE single motion source: ```ts export const SPRING = { micro: { type: 'spring', stiffness: 550, damping: 35 }, // chips, presses, ≤150ms feel standard: { type: 'spring', stiffness: 380, damping: 30 }, // hovers, panel/route fades expressive: { type: 'spring', stiffness: 260, damping: 24 }, // hero morphs, settle gesture } as const; export const DUR = { fast: 0.15, base: 0.2, slow: 0.32, settle: 0.4 } as const; // seconds export const EASE_OUT = [0.22, 1, 0.36, 1] as const; // cubic-bezier for non-spring CSS export const STAGGER = { list: 0.04, brief: 0.08 } as const; // Wave W's 40ms / briefing 80ms /** Signature-moment frequency taxonomy (Phase-0.7): FULL settle only on rare * accrual milestones; MICRO for high-frequency actions; per-session cooldown. */ export const SIGNATURE = { full: { moments: ['memory-saved-first-of-session', 'install-success', 'agent-spawned'], durS: DUR.settle, perSessionCooldownMs: 60_000 }, micro: { moments: ['send-arm', 'selection'], durS: DUR.fast }, } as const; /** Reduced-motion mapping per tier (Phase-0.3) — documented AND consumed. */ export const REDUCED = { routeTransition: 'crossfade-only', hover: 'color-shadow-only-no-transform', settle: 'instant-state-color-pulse', streamingCaret: 'static', countUp: 'instant-set', ambient: 'off', } as const; ``` Tune values to FEEL right (the numbers above are starting points — validate on the spec page); all three springs must share one physical character (same "material"). 2. **CSS custom props** appended to index.css `:root` (both themes inherit — motion is theme-invariant): `--mo-fast: 150ms; --mo-base: 200ms; --mo-slow: 320ms; --mo-settle: 400ms; --mo-ease: cubic-bezier(0.22,1,0.36,1);` Nothing else in this file. 3. **Motion-spec page** (`/motion-spec`, DEV-only): sections demonstrating each spring variant (click-to-replay), hover tier on a sample card, entrance stagger, exit choreography (AnimatePresence in/out samples), the reduced-motion mapping table, and an INVENTORY table of already-shipped motion (see Lane R's list) with its migration status. This page is the arc's reviewable single source of motion truth. 4. Tests: token invariants (all springs share type, DUR ascending, SIGNATURE moments disjoint, REDUCED covers every tier key). ### Lane T — text-tertiary + focus-ring token tiers + offender migration (Pillar 4 items 1 + 3) Files: `apps/web/src/index.css` (token definitions ONLY — coordinate with Lane M: Lane M appends motion props, Lane T appends color tokens; both are pure additions in different blocks, no shared lines), `os/StatusBar.tsx`, `overlays/onboarding/WelcomeStep.tsx`, `overlays/LoginBriefing.tsx` ("Don't show again"), plus the specific offender usages listed below. Tests updated where classes are asserted. 1. **`--text-tertiary`**: a tier that is ≥4.5:1 on EVERY surface it appears over (--bg, --bg-2, --surface, --surface-2) in BOTH themes. Compute (WCAG relative luminance) and document the math in a comment next to the token. Dark: needs ≥ #a3987f-level; light: ≤ #6e6552-level. Verify per pair; show ratios. 2. **`--focus-ring` + `--line-affordance`** (Pillar 4.3, WCAG 1.4.11): non-text tier ≥3:1 against adjacent effective background BOTH themes — the light-theme honey ring is the known risk (honey-on-ivory); compute and pick the light value accordingly (e.g. a darker honey/ochre for light). Document ratios in-comment. 3. **Migrate the recurring offender families** onto --text-tertiary (verify each is still a REAL failure first — several were fixed in Waves V/X; do not churn compliant code): top utility bar (StatusBar plan/search/date cluster), onboarding subtitle + privacy footnote ("Skip setup" was fixed — verify), "Don't show again", unselected onboarding chips (raise rest contrast), card timestamps if any remain sub-AA. Compute before/after ratios in the lane report. ### Lane G — the CI gates (Pillar 4 item 2 + verification plumbing) NEW files: `scripts/ux-gates/contrast-tokens.mjs`, `scripts/ux-gates/text-color-guard.mjs`, `scripts/ux-gates/contrast-runtime.mjs`, `scripts/ux-gates/README.md`; `package.json` (root): three npm scripts (`ux:contrast`, `ux:color-guard`, `ux:contrast-runtime`). Do NOT touch .github/workflows (wiring into CI is a follow-up once scripts are proven). 1. **contrast-tokens.mjs**: parse index.css (+ waggle-theme.css if it defines text tokens), compute WCAG ratios for every (text-token × surface-token) pair in both themes, assert the documented floors (--text/--text-2/--text-muted/--text-tertiary ≥4.5:1 on their allowed surfaces; --focus-ring/--line-affordance ≥3:1). Exit 1 on failure with a table. Must run green against Lane T's final tokens. 2. **text-color-guard.mjs** (the generation-vector ban): scan apps/web/src (app surfaces; exclude tests, the tokens file, MotionSpec) for NEW non-token text colors: `text-[#`, raw hex in inline `style` color/background of text elements, `text-hive-*` palette classes, and opacity-modified text tokens BELOW the safe floor (`text-[var(--text-dim)]/40` style patterns; allow ≥/60 with a warning list). Baseline file (`scripts/ux-gates/color-guard-baseline.json`) freezes today's grandfathered instances; the gate fails only on NEW instances — a ratchet, not a big-bang. 3. **contrast-runtime.mjs** (composition-aware, Playwright): against the dev server, for each judged surface (/home, /workspaces, /memory, /agents, /marketplace, /settings, chat), walk visible text nodes, compute EFFECTIVE fg/bg (composite opacity up the tree; sample wallpaper/gradient via screenshot pixel at element center when bg is an image), report all <4.5:1 (text) and <3:1 (focus indicators — tab to 10 interactive elements per surface and measure the ring). Output a JSON + human table; exit 1 on NEW failures vs a baseline. Seed localStorage onboarding like the capture kit; run both themes. ## STAGE 2 (parallel lanes; imports from Lane M's tokens — stage 1 merged first) ### Lane R1 — retrofit: cards + entrances (Phase-0.6) Files: `apps/AllWorkspacesApp.tsx`, `apps/agents/SuggestedAgentCards.tsx`, `apps/extend/ExtensionCard.tsx`, their tests. Migrate every duration/easing to the vocabulary: hover transitions → `--mo-fast/--mo-base` + `--mo-ease` (or SPRING.standard where framer-motion), entrance stagger → STAGGER.list + DUR.slow, lift/bloom timings → tokens. Acceptance: `grep -E "duration-(75|100|150|200|300|500)|duration: '?0\.[0-9]|ease-(linear|in-out|out)\b"` on these files returns ONLY token-backed or justified-in-comment instances; zero raw magic numbers. ### Lane R2 — retrofit: memory + briefing + boot + theme (Phase-0.6) Files: `apps/memory/MemoryTrustManage.tsx`, `apps/memory/MemoryCenterTab.tsx`, `os/overlays/LoginBriefing.tsx`, `os/BootScreen.tsx`, `providers/ThemeProvider.tsx` (+ index.css `.theme-transition` duration → var), tests. Same acceptance as R1. The 600ms count-up, 360ms crossfade, briefing staggers, boot phase timings all move onto DUR/STAGGER/custom props (keep the VALUES if they feel right — tokenize, don't retune; note any value you deliberately change). ### Lane R3 — retrofit: chat + status chrome + keyframe sweep (Phase-0.6) Files: `apps/ChatApp.tsx`, `os/StatusBar.tsx` (transition durations only — Lane T owns its colors), `apps/WorkspaceDesktopApp.tsx`, index.css @keyframes durations where referenced with magic numbers in components, tests. Same acceptance. Also produce THE INVENTORY: a table (append to the motion-spec page's inventory section — coordinate: R3 owns the final inventory content) of every motion in the judged surfaces: name, file, tier, duration token, reduced-motion behavior. ## VERIFY STAGE (after stage 2; parallel) - **Reviewer V1 (motion)**: adversarially verify Phase-0 acceptance — grep the judged surfaces for non-token durations/easings (list any survivor with justification status), confirm the spec page renders every tier, confirm reduced-motion mappings are implemented not just documented (inspect the code paths). - **Reviewer V2 (floor)**: run `node scripts/ux-gates/contrast-tokens.mjs` and `node scripts/ux-gates/text-color-guard.mjs`; start dev servers if needed and run `contrast-runtime.mjs` on at least /home + /settings both themes; report the tables. Verify Lane T's in-comment math independently (recompute 3 spot pairs). ## Orchestrator after verify Full tsc (web), full apps/web vitest, eslint on all touched files, browser smoke (motion-spec page renders; one hover; theme flip), commit per stage or as one Phase-A commit. Then Phase B (Pillar 2) spec. ## Deferred in Phase A (explicitly) Competitive teardown side-by-sides (needs founder's Claude/ChatGPT sessions — flagged, not skipped silently); .github/workflows wiring (after scripts prove stable); the waggle-settle prototype (Pillar 1.3 — its own commissioned mini-arc later).