tangled
alpha
login
or
join now
malpercio.dev
/
atbb
5
fork
atom
WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node
typescript
hono
htmx
atproto
5
fork
atom
overview
issues
pulls
pipelines
docs: ATB-52 implementation plan
malpercio.dev
1 week ago
745e5ec5
efd7307b
+579
1 changed file
expand all
collapse all
unified
split
docs
plans
2026-03-02-atb-52-css-token-extraction.md
+579
docs/plans/2026-03-02-atb-52-css-token-extraction.md
···
1
1
+
# ATB-52: CSS Token Extraction Implementation Plan
2
2
+
3
3
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
4
+
5
5
+
**Goal:** Extract all remaining hardcoded CSS values in `theme.css` into the design token system and ship `neobrutal-light.json` + `neobrutal-dark.json` preset files.
6
6
+
7
7
+
**Architecture:** `theme.css` already uses `var(--token)` for ~95% of values. Two sections (moderation UI, structure UI) were added separately and never aligned. The preset is currently a TypeScript named export; we convert it to JSON (default import) which is already supported by `resolveJsonModule: true` in `tsconfig.base.json`. `tokensToCss()` and the `BaseLayout` injection are unchanged.
8
8
+
9
9
+
**Tech Stack:** CSS custom properties, TypeScript JSON imports, Vitest
10
10
+
11
11
+
**Design doc:** `docs/plans/2026-03-02-css-token-extraction-design.md`
12
12
+
13
13
+
---
14
14
+
15
15
+
## Running tests
16
16
+
17
17
+
From the repo root (requires devenv PATH):
18
18
+
19
19
+
```bash
20
20
+
PATH=/path/to/repo/.devenv/profile/bin:/bin:/usr/bin:$PATH pnpm --filter @atbb/web test
21
21
+
```
22
22
+
23
23
+
Or from inside `apps/web/`:
24
24
+
25
25
+
```bash
26
26
+
PATH=/path/to/repo/.devenv/profile/bin:/bin:/usr/bin:$PATH pnpm exec vitest run
27
27
+
```
28
28
+
29
29
+
If you're in a worktree, the `.devenv` symlink is absent — use the absolute path to the main repo's `.devenv/profile/bin`.
30
30
+
31
31
+
---
32
32
+
33
33
+
## Task 1: Write failing preset completeness tests
34
34
+
35
35
+
**Files:**
36
36
+
- Create: `apps/web/src/styles/presets/__tests__/presets.test.ts`
37
37
+
38
38
+
**Step 1: Create the test file**
39
39
+
40
40
+
```typescript
41
41
+
// apps/web/src/styles/presets/__tests__/presets.test.ts
42
42
+
import { describe, it, expect } from "vitest";
43
43
+
import neobrutalLight from "../neobrutal-light.json";
44
44
+
import neobrutalDark from "../neobrutal-dark.json";
45
45
+
import { tokensToCss } from "../../../lib/theme.js";
46
46
+
47
47
+
const REQUIRED_TOKENS = [
48
48
+
// Colors
49
49
+
"color-bg", "color-surface", "color-text", "color-text-muted",
50
50
+
"color-primary", "color-primary-hover", "color-secondary",
51
51
+
"color-border", "color-shadow", "color-success", "color-warning",
52
52
+
"color-danger", "color-code-bg", "color-code-text",
53
53
+
// Typography
54
54
+
"font-body", "font-heading", "font-mono",
55
55
+
"font-size-base", "font-size-sm", "font-size-xs",
56
56
+
"font-size-lg", "font-size-xl", "font-size-2xl",
57
57
+
"font-weight-normal", "font-weight-bold",
58
58
+
"line-height-body", "line-height-heading",
59
59
+
// Spacing & layout
60
60
+
"space-xs", "space-sm", "space-md", "space-lg", "space-xl",
61
61
+
"radius", "border-width", "shadow-offset", "content-width",
62
62
+
// Components
63
63
+
"button-radius", "button-shadow", "card-radius", "card-shadow",
64
64
+
"btn-press-hover", "btn-press-active",
65
65
+
"input-radius", "input-border", "nav-height",
66
66
+
];
67
67
+
68
68
+
describe("neobrutal-light preset", () => {
69
69
+
it("contains all required tokens including font-size-xs", () => {
70
70
+
for (const token of REQUIRED_TOKENS) {
71
71
+
expect(neobrutalLight, `missing token: ${token}`).toHaveProperty(token);
72
72
+
}
73
73
+
});
74
74
+
75
75
+
it("produces valid CSS via tokensToCss", () => {
76
76
+
const css = tokensToCss(neobrutalLight as Record<string, string>);
77
77
+
expect(css).toContain("--color-bg:");
78
78
+
expect(css).toContain("--font-size-xs:");
79
79
+
expect(css).toContain("--nav-height:");
80
80
+
});
81
81
+
});
82
82
+
83
83
+
describe("neobrutal-dark preset", () => {
84
84
+
it("contains all required tokens including font-size-xs", () => {
85
85
+
for (const token of REQUIRED_TOKENS) {
86
86
+
expect(neobrutalDark, `missing token: ${token}`).toHaveProperty(token);
87
87
+
}
88
88
+
});
89
89
+
90
90
+
it("has a darker background than the light preset", () => {
91
91
+
expect((neobrutalDark as Record<string, string>)["color-bg"]).not.toBe(
92
92
+
(neobrutalLight as Record<string, string>)["color-bg"]
93
93
+
);
94
94
+
});
95
95
+
96
96
+
it("produces valid CSS via tokensToCss", () => {
97
97
+
const css = tokensToCss(neobrutalDark as Record<string, string>);
98
98
+
expect(css).toContain("--color-bg:");
99
99
+
expect(css).toContain("--font-size-xs:");
100
100
+
});
101
101
+
});
102
102
+
```
103
103
+
104
104
+
**Step 2: Run the test to verify it fails**
105
105
+
106
106
+
Expected: compile error — `neobrutal-light.json` and `neobrutal-dark.json` don't exist yet.
107
107
+
108
108
+
```bash
109
109
+
pnpm exec vitest run src/styles/presets/__tests__/presets.test.ts
110
110
+
```
111
111
+
112
112
+
---
113
113
+
114
114
+
## Task 2: Create `neobrutal-light.json`
115
115
+
116
116
+
**Files:**
117
117
+
- Create: `apps/web/src/styles/presets/neobrutal-light.json`
118
118
+
119
119
+
**Step 1: Create the file**
120
120
+
121
121
+
```json
122
122
+
{
123
123
+
"color-bg": "#f5f0e8",
124
124
+
"color-surface": "#ffffff",
125
125
+
"color-text": "#1a1a1a",
126
126
+
"color-text-muted": "#555555",
127
127
+
"color-primary": "#ff5c00",
128
128
+
"color-primary-hover": "#e04f00",
129
129
+
"color-secondary": "#3a86ff",
130
130
+
"color-border": "#1a1a1a",
131
131
+
"color-shadow": "#1a1a1a",
132
132
+
"color-success": "#2ec44a",
133
133
+
"color-warning": "#ffbe0b",
134
134
+
"color-danger": "#ff006e",
135
135
+
"color-code-bg": "#1a1a1a",
136
136
+
"color-code-text": "#f5f0e8",
137
137
+
"font-body": "'Space Grotesk', system-ui, sans-serif",
138
138
+
"font-heading": "'Space Grotesk', system-ui, sans-serif",
139
139
+
"font-mono": "'JetBrains Mono', ui-monospace, monospace",
140
140
+
"font-size-base": "16px",
141
141
+
"font-size-sm": "14px",
142
142
+
"font-size-xs": "12px",
143
143
+
"font-size-lg": "20px",
144
144
+
"font-size-xl": "28px",
145
145
+
"font-size-2xl": "36px",
146
146
+
"font-weight-normal": "400",
147
147
+
"font-weight-bold": "700",
148
148
+
"line-height-body": "1.6",
149
149
+
"line-height-heading": "1.2",
150
150
+
"space-xs": "4px",
151
151
+
"space-sm": "8px",
152
152
+
"space-md": "16px",
153
153
+
"space-lg": "24px",
154
154
+
"space-xl": "40px",
155
155
+
"radius": "0px",
156
156
+
"border-width": "2px",
157
157
+
"shadow-offset": "2px",
158
158
+
"content-width": "100%",
159
159
+
"button-radius": "0px",
160
160
+
"button-shadow": "2px 2px 0 var(--color-shadow)",
161
161
+
"card-radius": "0px",
162
162
+
"card-shadow": "4px 4px 0 var(--color-shadow)",
163
163
+
"btn-press-hover": "1px",
164
164
+
"btn-press-active": "2px",
165
165
+
"input-radius": "0px",
166
166
+
"input-border": "2px solid var(--color-border)",
167
167
+
"nav-height": "64px"
168
168
+
}
169
169
+
```
170
170
+
171
171
+
---
172
172
+
173
173
+
## Task 3: Create `neobrutal-dark.json`
174
174
+
175
175
+
**Files:**
176
176
+
- Create: `apps/web/src/styles/presets/neobrutal-dark.json`
177
177
+
178
178
+
**Step 1: Create the file**
179
179
+
180
180
+
Same structure as light; only color tokens differ:
181
181
+
182
182
+
```json
183
183
+
{
184
184
+
"color-bg": "#1a1a1a",
185
185
+
"color-surface": "#2d2d2d",
186
186
+
"color-text": "#f5f0e8",
187
187
+
"color-text-muted": "#a0a0a0",
188
188
+
"color-primary": "#ff5c00",
189
189
+
"color-primary-hover": "#ff7a2a",
190
190
+
"color-secondary": "#3a86ff",
191
191
+
"color-border": "#f5f0e8",
192
192
+
"color-shadow": "#000000",
193
193
+
"color-success": "#2ec44a",
194
194
+
"color-warning": "#ffbe0b",
195
195
+
"color-danger": "#ff006e",
196
196
+
"color-code-bg": "#111111",
197
197
+
"color-code-text": "#f5f0e8",
198
198
+
"font-body": "'Space Grotesk', system-ui, sans-serif",
199
199
+
"font-heading": "'Space Grotesk', system-ui, sans-serif",
200
200
+
"font-mono": "'JetBrains Mono', ui-monospace, monospace",
201
201
+
"font-size-base": "16px",
202
202
+
"font-size-sm": "14px",
203
203
+
"font-size-xs": "12px",
204
204
+
"font-size-lg": "20px",
205
205
+
"font-size-xl": "28px",
206
206
+
"font-size-2xl": "36px",
207
207
+
"font-weight-normal": "400",
208
208
+
"font-weight-bold": "700",
209
209
+
"line-height-body": "1.6",
210
210
+
"line-height-heading": "1.2",
211
211
+
"space-xs": "4px",
212
212
+
"space-sm": "8px",
213
213
+
"space-md": "16px",
214
214
+
"space-lg": "24px",
215
215
+
"space-xl": "40px",
216
216
+
"radius": "0px",
217
217
+
"border-width": "2px",
218
218
+
"shadow-offset": "2px",
219
219
+
"content-width": "100%",
220
220
+
"button-radius": "0px",
221
221
+
"button-shadow": "2px 2px 0 var(--color-shadow)",
222
222
+
"card-radius": "0px",
223
223
+
"card-shadow": "4px 4px 0 var(--color-shadow)",
224
224
+
"btn-press-hover": "1px",
225
225
+
"btn-press-active": "2px",
226
226
+
"input-radius": "0px",
227
227
+
"input-border": "2px solid var(--color-border)",
228
228
+
"nav-height": "64px"
229
229
+
}
230
230
+
```
231
231
+
232
232
+
**Step 2: Run the preset tests — expect them to pass now**
233
233
+
234
234
+
```bash
235
235
+
pnpm exec vitest run src/styles/presets/__tests__/presets.test.ts
236
236
+
```
237
237
+
238
238
+
Expected: all 6 tests PASS.
239
239
+
240
240
+
---
241
241
+
242
242
+
## Task 4: Update `base.tsx` to import from JSON
243
243
+
244
244
+
**Files:**
245
245
+
- Modify: `apps/web/src/layouts/base.tsx:1-6`
246
246
+
247
247
+
**Step 1: Replace the import and usage**
248
248
+
249
249
+
Old lines 1–6:
250
250
+
```typescript
251
251
+
import type { FC, PropsWithChildren } from "hono/jsx";
252
252
+
import { tokensToCss } from "../lib/theme.js";
253
253
+
import { neobrutalLight } from "../styles/presets/neobrutal-light.js";
254
254
+
import type { WebSession } from "../lib/session.js";
255
255
+
256
256
+
const ROOT_CSS = `:root { ${tokensToCss(neobrutalLight)} }`;
257
257
+
```
258
258
+
259
259
+
New lines 1–6:
260
260
+
```typescript
261
261
+
import type { FC, PropsWithChildren } from "hono/jsx";
262
262
+
import { tokensToCss } from "../lib/theme.js";
263
263
+
import neobrutalLight from "../styles/presets/neobrutal-light.json";
264
264
+
import type { WebSession } from "../lib/session.js";
265
265
+
266
266
+
const ROOT_CSS = `:root { ${tokensToCss(neobrutalLight as Record<string, string>)} }`;
267
267
+
```
268
268
+
269
269
+
Note: TypeScript infers JSON imports as their literal type (e.g., `{ "color-bg": string; ... }`), which is not directly assignable to `Record<string, string>` without a cast. The `as Record<string, string>` cast is safe here — all values in the JSON are strings.
270
270
+
271
271
+
**Step 2: Run the base layout tests**
272
272
+
273
273
+
```bash
274
274
+
pnpm exec vitest run src/layouts/__tests__/base.test.tsx
275
275
+
```
276
276
+
277
277
+
Expected: all tests PASS (especially "injects neobrutal tokens as :root CSS custom properties" which checks `--color-bg:` and `--color-primary:` appear in the HTML).
278
278
+
279
279
+
---
280
280
+
281
281
+
## Task 5: Delete `neobrutal-light.ts`
282
282
+
283
283
+
**Files:**
284
284
+
- Delete: `apps/web/src/styles/presets/neobrutal-light.ts`
285
285
+
286
286
+
**Step 1: Delete the file**
287
287
+
288
288
+
```bash
289
289
+
rm apps/web/src/styles/presets/neobrutal-light.ts
290
290
+
```
291
291
+
292
292
+
**Step 2: Run the full test suite to verify nothing else imported it**
293
293
+
294
294
+
```bash
295
295
+
pnpm exec vitest run
296
296
+
```
297
297
+
298
298
+
Expected: all tests PASS. If any test fails with "cannot find module neobrutal-light.js", search for other imports of this file and update them.
299
299
+
300
300
+
**Step 3: Commit**
301
301
+
302
302
+
```bash
303
303
+
git add apps/web/src/styles/presets/
304
304
+
git add apps/web/src/layouts/base.tsx
305
305
+
git commit -m "feat(web): convert presets to JSON, add neobrutal-dark, add font-size-xs token (ATB-52)"
306
306
+
```
307
307
+
308
308
+
---
309
309
+
310
310
+
## Task 6: Fix moderation UI hardcoded values in `theme.css`
311
311
+
312
312
+
**Files:**
313
313
+
- Modify: `apps/web/public/static/css/theme.css` (lines ~751–821)
314
314
+
315
315
+
The existing base layout tests act as the regression harness for CSS — they verify the layout renders and tokens are injected. The visual regression (forum looks identical) requires a manual browser check after this task.
316
316
+
317
317
+
**Step 1: Replace the entire moderation UI section**
318
318
+
319
319
+
Find this block (from `/* ─── Moderation UI ──` to just before `/* ═══ RESPONSIVE BREAKPOINTS`):
320
320
+
321
321
+
```css
322
322
+
/* ─── Moderation UI ──────────────────────────────────────────────────────── */
323
323
+
324
324
+
.post-card__mod-actions {
325
325
+
display: flex;
326
326
+
gap: var(--space-2);
327
327
+
margin-top: var(--space-2);
328
328
+
padding-top: var(--space-2);
329
329
+
border-top: 1px solid var(--color-border);
330
330
+
}
331
331
+
332
332
+
.mod-btn {
333
333
+
font-size: 0.75rem;
334
334
+
padding: 0.25rem 0.6rem;
335
335
+
border: 2px solid currentColor;
336
336
+
border-radius: 0;
337
337
+
cursor: pointer;
338
338
+
background: transparent;
339
339
+
font-family: inherit;
340
340
+
font-weight: 700;
341
341
+
text-transform: uppercase;
342
342
+
letter-spacing: 0.05em;
343
343
+
}
344
344
+
345
345
+
.mod-btn--hide,
346
346
+
.mod-btn--lock {
347
347
+
color: var(--color-danger, #d00);
348
348
+
}
349
349
+
350
350
+
.mod-btn--hide:hover,
351
351
+
.mod-btn--lock:hover {
352
352
+
background: var(--color-danger, #d00);
353
353
+
color: #fff;
354
354
+
}
355
355
+
356
356
+
.mod-btn--unhide,
357
357
+
.mod-btn--unlock,
358
358
+
.mod-btn--ban {
359
359
+
color: var(--color-text-muted, #666);
360
360
+
}
361
361
+
362
362
+
.mod-btn--unhide:hover,
363
363
+
.mod-btn--unlock:hover,
364
364
+
.mod-btn--ban:hover {
365
365
+
background: var(--color-text-muted, #666);
366
366
+
color: #fff;
367
367
+
}
368
368
+
369
369
+
.topic-mod-controls {
370
370
+
margin-bottom: var(--space-4);
371
371
+
}
372
372
+
373
373
+
.mod-dialog {
374
374
+
border: 3px solid var(--color-border);
375
375
+
border-radius: 0;
376
376
+
padding: var(--space-6);
377
377
+
max-width: 480px;
378
378
+
width: 90vw;
379
379
+
box-shadow: 6px 6px 0 var(--color-shadow);
380
380
+
background: var(--color-bg);
381
381
+
}
382
382
+
383
383
+
.mod-dialog::backdrop {
384
384
+
background: rgba(0, 0, 0, 0.5);
385
385
+
}
386
386
+
387
387
+
.mod-dialog__title {
388
388
+
margin-top: 0;
389
389
+
margin-bottom: var(--space-4);
390
390
+
font-size: 1.25rem;
391
391
+
}
392
392
+
```
393
393
+
394
394
+
Replace with:
395
395
+
396
396
+
```css
397
397
+
/* ─── Moderation UI ──────────────────────────────────────────────────────── */
398
398
+
399
399
+
.post-card__mod-actions {
400
400
+
display: flex;
401
401
+
gap: var(--space-sm);
402
402
+
margin-top: var(--space-sm);
403
403
+
padding-top: var(--space-sm);
404
404
+
border-top: var(--border-width) solid var(--color-border);
405
405
+
}
406
406
+
407
407
+
.mod-btn {
408
408
+
font-size: var(--font-size-xs);
409
409
+
padding: var(--space-xs) var(--space-sm);
410
410
+
border: var(--border-width) solid currentColor;
411
411
+
border-radius: var(--radius);
412
412
+
cursor: pointer;
413
413
+
background: transparent;
414
414
+
font-family: inherit;
415
415
+
font-weight: var(--font-weight-bold);
416
416
+
text-transform: uppercase;
417
417
+
letter-spacing: 0.05em;
418
418
+
}
419
419
+
420
420
+
.mod-btn--hide,
421
421
+
.mod-btn--lock {
422
422
+
color: var(--color-danger);
423
423
+
}
424
424
+
425
425
+
.mod-btn--hide:hover,
426
426
+
.mod-btn--lock:hover {
427
427
+
background: var(--color-danger);
428
428
+
color: var(--color-surface);
429
429
+
}
430
430
+
431
431
+
.mod-btn--unhide,
432
432
+
.mod-btn--unlock,
433
433
+
.mod-btn--ban {
434
434
+
color: var(--color-text-muted);
435
435
+
}
436
436
+
437
437
+
.mod-btn--unhide:hover,
438
438
+
.mod-btn--unlock:hover,
439
439
+
.mod-btn--ban:hover {
440
440
+
background: var(--color-text-muted);
441
441
+
color: var(--color-surface);
442
442
+
}
443
443
+
444
444
+
.topic-mod-controls {
445
445
+
margin-bottom: var(--space-md);
446
446
+
}
447
447
+
448
448
+
.mod-dialog {
449
449
+
border: var(--border-width) solid var(--color-border);
450
450
+
border-radius: var(--radius);
451
451
+
padding: var(--space-lg);
452
452
+
max-width: 480px;
453
453
+
width: 90vw;
454
454
+
box-shadow: var(--card-shadow);
455
455
+
background: var(--color-bg);
456
456
+
}
457
457
+
458
458
+
.mod-dialog::backdrop {
459
459
+
background: rgba(0, 0, 0, 0.5);
460
460
+
}
461
461
+
462
462
+
.mod-dialog__title {
463
463
+
margin-top: 0;
464
464
+
margin-bottom: var(--space-md);
465
465
+
font-size: var(--font-size-lg);
466
466
+
}
467
467
+
```
468
468
+
469
469
+
**Step 2: Run tests**
470
470
+
471
471
+
```bash
472
472
+
pnpm exec vitest run
473
473
+
```
474
474
+
475
475
+
Expected: all tests PASS (CSS changes don't affect unit tests; the layout tests still pass).
476
476
+
477
477
+
---
478
478
+
479
479
+
## Task 7: Fix structure UI hardcoded fallback values in `theme.css`
480
480
+
481
481
+
**Files:**
482
482
+
- Modify: `apps/web/public/static/css/theme.css` (lines ~1003–1154)
483
483
+
484
484
+
These are fallback values in `var(--token, fallback)` form — the fallback is a hardcoded value. Remove the fallbacks.
485
485
+
486
486
+
**Step 1: Apply the following replacements** (use your editor's find-and-replace):
487
487
+
488
488
+
| Find | Replace |
489
489
+
|------|---------|
490
490
+
| `var(--space-6, 1.5rem)` | `var(--space-lg)` |
491
491
+
| `var(--radius, 0.5rem)` | `var(--radius)` |
492
492
+
| `var(--radius, 0.375rem)` | `var(--radius)` |
493
493
+
| `var(--font-size-xl, 2rem)` | `var(--font-size-xl)` |
494
494
+
495
495
+
There are multiple occurrences of `var(--radius, ...)` in the structure UI — replace all of them.
496
496
+
497
497
+
**Step 2: Run tests**
498
498
+
499
499
+
```bash
500
500
+
pnpm exec vitest run
501
501
+
```
502
502
+
503
503
+
Expected: all tests PASS.
504
504
+
505
505
+
**Step 3: Commit**
506
506
+
507
507
+
```bash
508
508
+
git add apps/web/public/static/css/theme.css
509
509
+
git commit -m "fix(web): replace hardcoded CSS values with design tokens in mod and structure UI (ATB-52)"
510
510
+
```
511
511
+
512
512
+
---
513
513
+
514
514
+
## Task 8: Verify no hardcoded values remain
515
515
+
516
516
+
**Step 1: Search for remaining hardcoded values**
517
517
+
518
518
+
Check for any remaining hardcoded color hex values:
519
519
+
```bash
520
520
+
grep -n '#[0-9a-fA-F]\{3,6\}' apps/web/public/static/css/theme.css
521
521
+
```
522
522
+
523
523
+
Expected: no matches (the `rgba(0, 0, 0, 0.5)` backdrop uses `rgba` not hex — this is acceptable since it's a structural opacity value that isn't part of the token schema).
524
524
+
525
525
+
Check for remaining hardcoded pixel sizes outside responsive breakpoints:
526
526
+
```bash
527
527
+
grep -n '[0-9]\+px' apps/web/public/static/css/theme.css
528
528
+
```
529
529
+
530
530
+
Review the output — pixel values inside `@media (min-width: ...)` breakpoint declarations are expected. Any pixel values in rule bodies are a problem.
531
531
+
532
532
+
Check for non-standard spacing token names:
533
533
+
```bash
534
534
+
grep -n 'var(--space-[0-9]' apps/web/public/static/css/theme.css
535
535
+
```
536
536
+
537
537
+
Expected: no matches.
538
538
+
539
539
+
**Step 2: Manual visual check**
540
540
+
541
541
+
Start the dev server:
542
542
+
```bash
543
543
+
pnpm --filter @atbb/web dev
544
544
+
```
545
545
+
546
546
+
Visit each view and confirm visual appearance is unchanged:
547
547
+
- Homepage (category/board list)
548
548
+
- Board page (topic list)
549
549
+
- Topic page (post thread)
550
550
+
- New topic compose form
551
551
+
- Login form
552
552
+
- Admin panel index
553
553
+
- Admin members page
554
554
+
- Admin structure management page
555
555
+
- Any mod action dialogs
556
556
+
557
557
+
---
558
558
+
559
559
+
## Task 9: Run full test suite and final commit
560
560
+
561
561
+
**Step 1: Run all tests**
562
562
+
563
563
+
```bash
564
564
+
pnpm --filter @atbb/web test
565
565
+
```
566
566
+
567
567
+
Expected: all tests PASS.
568
568
+
569
569
+
**Step 2: Update Linear**
570
570
+
571
571
+
Mark ATB-52 as Done and add a comment summarizing:
572
572
+
- `theme.css` now zero hardcoded values
573
573
+
- `neobrutal-light.json` and `neobrutal-dark.json` ship as preset files
574
574
+
- `--font-size-xs: 12px` added to the token schema
575
575
+
- `tokensToCss()` unchanged, fully tested
576
576
+
577
577
+
**Step 3: Update the plan doc status**
578
578
+
579
579
+
In `docs/plans/2026-03-02-atb-52-css-token-extraction.md`, update the header status or move to `docs/plans/complete/` when the PR is merged.