ATB-52: CSS Token Extraction — Design#
Status: Approved, ready for implementation Linear: ATB-52 Date: 2026-03-02
Context#
The web UI uses a neobrutal aesthetic with a CSS custom property token system. Most of theme.css already references var(--token) exclusively. Two sections were added separately (moderation UI, structure management UI) and were never aligned with the token schema. This design covers the full extraction.
tokensToCss() and neobrutal-light.ts already exist. The work is: fix the remaining hardcoded values, add one missing token, convert presets to JSON, and add the dark preset.
Audit: Hardcoded Values Remaining#
Moderation UI (theme.css lines 751–821)#
| Hardcoded value | Replace with |
|---|---|
var(--space-2) |
var(--space-sm) (8px = 0.5rem) |
var(--space-4) |
var(--space-md) (16px = 1rem) |
var(--space-6) |
var(--space-lg) (24px = 1.5rem) |
1px solid var(--color-border) |
var(--border-width) solid var(--color-border) |
2px solid currentColor |
var(--border-width) solid currentColor |
border-radius: 0 |
var(--radius) |
font-weight: 700 |
var(--font-weight-bold) |
0.25rem 0.6rem (mod-btn padding) |
var(--space-xs) var(--space-sm) |
0.75rem (mod-btn font-size) |
var(--font-size-xs) ← new token |
1.25rem (dialog title font-size) |
var(--font-size-lg) (20px = 1.25rem) |
6px 6px 0 var(--color-shadow) |
var(--card-shadow) |
color: #fff (hover text) |
var(--color-surface) |
var(--color-danger, #d00) |
var(--color-danger) (remove fallback) |
var(--color-text-muted, #666) |
var(--color-text-muted) (remove fallback) |
3px solid var(--color-border) |
var(--border-width) solid var(--color-border) |
Structure UI (theme.css lines 1003–1154)#
| Hardcoded value | Replace with |
|---|---|
var(--space-6, 1.5rem) |
var(--space-lg) (remove fallback) |
var(--radius, 0.5rem) |
var(--radius) (remove fallback) |
var(--radius, 0.375rem) |
var(--radius) (remove fallback) |
var(--font-size-xl, 2rem) |
var(--font-size-xl) (remove fallback) |
Token Schema Addition#
One new token added to complete the type scale:
| Token | neobrutal-light | neobrutal-dark | Description |
|---|---|---|---|
font-size-xs |
12px |
12px |
Extra-small text (mod buttons, badges) |
Preset Files#
Format#
Convert from TypeScript to JSON. resolveJsonModule: true is already set in tsconfig.base.json — no config changes needed. Import in base.tsx changes to:
import neobrutalLight from "../styles/presets/neobrutal-light.json" assert { type: "json" };
Or, since moduleResolution: bundler is set, the assert clause may not be required — verify during implementation.
neobrutal-light.json (converted from existing TS, adds font-size-xs)#
{
"color-bg": "#f5f0e8",
"color-surface": "#ffffff",
"color-text": "#1a1a1a",
"color-text-muted": "#555555",
"color-primary": "#ff5c00",
"color-primary-hover": "#e04f00",
"color-secondary": "#3a86ff",
"color-border": "#1a1a1a",
"color-shadow": "#1a1a1a",
"color-success": "#2ec44a",
"color-warning": "#ffbe0b",
"color-danger": "#ff006e",
"color-code-bg": "#1a1a1a",
"color-code-text": "#f5f0e8",
"font-body": "'Space Grotesk', system-ui, sans-serif",
"font-heading": "'Space Grotesk', system-ui, sans-serif",
"font-mono": "'JetBrains Mono', ui-monospace, monospace",
"font-size-base": "16px",
"font-size-sm": "14px",
"font-size-xs": "12px",
"font-size-lg": "20px",
"font-size-xl": "28px",
"font-size-2xl": "36px",
"font-weight-normal": "400",
"font-weight-bold": "700",
"line-height-body": "1.6",
"line-height-heading": "1.2",
"space-xs": "4px",
"space-sm": "8px",
"space-md": "16px",
"space-lg": "24px",
"space-xl": "40px",
"radius": "0px",
"border-width": "2px",
"shadow-offset": "2px",
"content-width": "100%",
"button-radius": "0px",
"button-shadow": "2px 2px 0 var(--color-shadow)",
"card-radius": "0px",
"card-shadow": "4px 4px 0 var(--color-shadow)",
"btn-press-hover": "1px",
"btn-press-active": "2px",
"input-radius": "0px",
"input-border": "2px solid var(--color-border)",
"nav-height": "64px"
}
neobrutal-dark.json (new)#
Same structural/typography/spacing tokens. Color tokens that differ:
| Token | Value |
|---|---|
color-bg |
#1a1a1a |
color-surface |
#2d2d2d |
color-text |
#f5f0e8 |
color-text-muted |
#a0a0a0 |
color-primary-hover |
#ff7a2a (lightened for dark bg) |
color-border |
#f5f0e8 (inverted from light) |
color-shadow |
#000000 |
color-code-bg |
#111111 |
All other tokens (primary, secondary, success, warning, danger, all typography, all spacing, all component) are identical to neobrutal-light.
File Changes#
| File | Action |
|---|---|
public/static/css/theme.css |
Fix ~15 hardcoded values in mod UI + structure UI |
src/styles/presets/neobrutal-light.ts |
Delete |
src/styles/presets/neobrutal-light.json |
Create (converted from TS, adds font-size-xs) |
src/styles/presets/neobrutal-dark.json |
Create (dark color tokens, same structure) |
src/layouts/base.tsx |
Update import to JSON |
src/lib/theme.ts |
No changes |
src/lib/__tests__/theme.test.ts |
No changes |
Acceptance Criteria (from ATB-52)#
-
theme.csscontains zero hardcoded color values, font stacks, spacing values, or font sizes — all usevar(--token) - No fallback values in
var()calls (fallbacks are hardcoded values in disguise) -
tokensToCss()utility exists and is tested (already satisfied) -
neobrutal-light.jsonandneobrutal-dark.jsonship with complete token sets -
--font-size-xsadded to both presets -
base.tsximports from JSON and the forum renders identically to before - All existing views render correctly after the refactor