# 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.
