# SESSION-0019.1 — App.css + index.css token sweep Parent: SESSION-0019.0-TRANSCRIPT-… (UX-polish wave, rfc-app v0.21.0, roadmap item #31) ## Goal Sweep `frontend/src/App.css` (2434 lines) and `frontend/src/index.css` onto the design-token system established in `frontend/src/styles/tokens.css`: hardcoded hex → nearest semantic/primitive token, font-size literals → `--text-*` scale, border-radius literals → `--radius-*` scale, safe spacing where it maps cleanly, a coherent transition vocabulary + a consistent `:focus-visible` ring, and base rules pointed at semantic tokens. Hard constraint: never rename/remove a selector (parallel siblings reference these class names); values + additive rules only. ## Pre-state - Worktree: `/Users/benstull/git/rfc-app-v0.21.0-appcss` - Branch `feature/v0.21.0-appcss-sweep`, based on `5be2c48` (token-module foundation). - index.css used literal `#1a1a1a` / `#fafaf8` / inline font stack. - App.css had ~98 distinct hex (66× `#fff`, 51× `#1a1a1a`, plus a long tail of near-duplicate grays: #888/#999/#777/#666/#555 etc.), 16 distinct font-size px values, 12 distinct radius px values. ## Turn-by-turn arc 1. Read tokens.css (the foundation) + both target files in full (App.css needed two reads — 2434 lines). Confirmed branch/base. 2. Extracted the full distinct-hex inventory via grep+uniq across both files to build the mapping deterministically rather than guessing. 3. Round-1 script (`/tmp/sweep.py`): regex word-boundary, case-insensitive, longest-key-first replacement of all hex that map cleanly to a ramp step or status/accent token. 615 replacements. - One dict-key collision worth noting: `#ddd` appears as both a gray-300 candidate and an on-dark-muted candidate. Resolved to `--c-gray-300` (the consolidation target); the header user-name `#ddd`-on-dark shifts imperceptibly to #d1d5db. 4. Re-grepped remainder; 35 distinct hex left — all genuinely off-token (true blues, violet tints/shades, functional state dots, deep diff-contrast shades, warm oranges with no token). 5. Round-2 script (`/tmp/sweep2.py`): conservative — only the 7 hex that are unambiguously the same ROLE as an existing token and visually near it (light-red error bgs/borders → danger-bg/-border, warm soft bgs → warning-bg-soft). 15 replacements. Left everything else. 6. index.css base `:root` → `var(--font-sans)`, `var(--color-text)`, `var(--color-bg)`. 7. Round-3 script (`/tmp/sweep3.py`): font-size px → `--text-*` (280), border-radius px → `--radius-*` (128), `rgba(255,255,255,.15/.25)` → `--color-on-dark-soft/-hover` (5), the three 48px header-band values → `--header-height` (3). Note: script rewrite invalidated an Edit I had queued for `.app-header`; re-read + re-applied. 8. Promoted `.app-header` to semantic `--color-header-bg` / `--color-text-inverse`. 9. Appended (not interleaved) the interaction-polish layer: a transition vocab on hover-reactive selectors + one `:focus-visible` ring using `--color-focus-ring`. Additive only — no resting-state change, reduced-motion handled by the token module's media query. 10. Build: first attempt failed on `VITE_APP_NAME is required` (env gate, not CSS). Re-ran with `VITE_APP_NAME=OHM` → built in 249ms, only the expected ~500kB chunk-size warning. CSS is sound. 11. Committed; wrote this transcript. ## Hex → token MAPPING TABLE Grays (→ neutral ramp): | literal(s) | token | note | |---|---|---| | #ffffff, #fff | --c-white | | | #fafafa, #fafaf8, #fafaf9, #fafbfc, #f9f9f9, #f9fafb | --c-gray-50 | | | #f3f4f6, #f5f5f5, #f6f6f4, #f7f7f5, #f7f6f0 | --c-gray-100 | | | #f0f0ee, #fcfcfb | --c-gray-150 | warm canvas | | #e5e5e5, #e5e7eb, #e5e5e0, #e7e5e4, #eee | --c-gray-200 | | | #d1d5db, #d4d4d4, #ddd, #ccc | --c-gray-300 | **#ccc→#d1d5db slightly lighter; #ddd→#d1d5db imperceptible** | | #9ca3af, #aaa, #b0b0b0, #94a3b8 | --c-gray-400 | #94a3b8 is slightly cooler→neutral, minor | | #6b7280, #888, #999, #777, #666, #555 | --c-gray-500 | **#555→#6b7280 and #999→#6b7280 are the most noticeable gray shifts (the desired consolidation)** | | #4b5563, #444, #475569 | --c-gray-600 | **#444→#4b5563 noticeable; #475569→#4b5563 slightly warmer** | | #374151, #333 | --c-gray-700 | **#333→#374151 slightly cooler, noticeable** | | #1f2937, #222 | --c-gray-800 | **#222→#1f2937 noticeable** | | #111 | --c-gray-900 | | | #1a1a1a | --c-ink (header→--color-header-bg, base text→--color-text) | | Accent / status (→ family tokens): | literal | token | |---|---| | #5b5bd6 | --c-accent | | #4338ca | --c-accent-strong | | #7c3aed | --c-violet | | #166534 | --c-success-fg | | #dcfce7 | --c-success-bg | | #991b1b | --c-danger-fg | | #b91c1c | --c-danger-fg-strong | | #fef2f2, #fff5f5 | --c-danger-bg | | #fecaca, #fca5a5, #fcc | --c-danger-border | | #fee2e2, #fee | --c-danger-bg (light red error bg; **slightly pinker→#fef2f2**) | | #92400e | --c-warning-fg | | #b45309 | --c-warning-accent | | #fef3c7 | --c-warning-bg | | #fffbeb, #fff7ed, #fff8e0 | --c-warning-bg-soft (**#fff7ed warm→#fffbeb, minor**) | Font-size (→ --text-* scale; round-to-nearest flagged): 10→2xs · 11→xs · 12→sm · 13→base · 14→md · **15→lg(16)** · 16→lg · **17→xl(18)** · 18→xl · **19→xl(18)** · 22→2xl · **24→2xl(22)** · **26→3xl(28)** · 28→3xl. 9px left as literal (below the 10px scale floor — the .beta-chip micro-tag). Border-radius (→ --radius-* scale): 2→xs · **3→sm(4)** · 4→sm · **5→md(6)** · 6→md · 8→lg · **10→xl(12)** · 12→xl · **14→xl(12)** · **20→pill** (model-pill, already pill-shaped at its height) · 99→pill · 999→pill. `50%` (circle dots) left untouched. On-dark + layout: rgba(255,255,255,0.15)→--color-on-dark-soft; rgba(255,255,255,0.25)→--color-on-dark-hover; 48px header band (×3)→--header-height. rgba(255,255,255,0.08) (header link hover) LEFT — no token. Left as literals (no clean token, intentional): blues #3b82f6 #2563eb #1e40af #dbeafe #bfdbfe #eff6ff #eef2ff #c7d2fe; violet tints/shades #c4b5fd #ede9fe #ddd6fe #faf5ff #4c1d95 #3737a0 #3730a3; status dots / functional #ef4444 #dc2626 #fbbf24 #f59e0b #d97706 #10b981; deep diff-contrast #14532d #7f1d1d #78350f #bbf7d0 #f0fdf4; warm/misc #9a3412 #c2410c #4a3f00 #cb6a6a #cfc8a8 #e6dca0 #fde68a #fdba74 #fed7aa #f1f5f9. Spacing (--space-*) left untouched this pass — prioritized color/type/radius per the brief; no spacing literal was both clearly mappable and zero-layout-risk enough to be worth it. ## Cut state - Branch: `feature/v0.21.0-appcss-sweep` - Final commit: `cbf02d5507f91cc5bf16a68b3f8a8b6b52990849` - Files changed: `frontend/src/App.css`, `frontend/src/index.css` (only). - Replacements: ~630 color, 280 font-size, 128 radius, 5 on-dark, 3 header-height. Build green (`VITE_APP_NAME=OHM npm run build`, 249ms, expected chunk-size warning only). ## What the driver needs to know - No selector renamed/removed — safe for parallel sibling subagents. - Gray consolidation deliberately shifts a handful of mid-grays by a visually-noticeable amount (#555/#999→gray-500, #444→gray-600, #333→gray-700, #222→gray-800). This IS the intended polish; operator should eyeball muted-text and secondary-button text before deploy. - 15px reading-body → 16px (lg): markdown/entry/landing body text gets 1px larger. Intentional rounding; check it reads well. - The appended polish layer adds a global `:focus-visible` ring and transitions to broad element selectors (button/a/input/…). Low risk (focus-visible never fires on pointer), but it is the one place that touches elements beyond the named classes. ## §19.2 candidates - The on-dark translucent-white pattern (`--color-on-dark-soft/-hover`) and the focus-ring/transition vocabulary are deployment-agnostic and would belong in a future `flotilla-core`-adjacent shared UI layer if rfc-app's frontend ever extracts one — but that's an rfc-app concern, not flotilla. No flotilla-side extraction implied by this work.