← Dev Log

The Housekeeping Post

A three-phase CSS cleanup across twenty-five files. The codebase got bigger. The site got easier to change. Those two things are not a contradiction — and it took doing the work to stop confusing one for the other.

This site has no framework. No build step. No bundler. Every page is a hand-written HTML file that loads one shared stylesheet and one shared nav script. That's a feature — it keeps the deploy story to "drop files into a folder" and keeps the page-weight small. But it's also how you end up, seven weeks in, with the same .post-hero declaration copy-pasted into thirteen blog post files, each one a tiny snowflake that drifted half a pixel away from its neighbors.

So we did a housekeeping pass. Three phases, twenty-five files. What follows is honest about the numbers, because the most interesting thing about this kind of work is the gap between what it feels like you're doing and what the diff actually says.

A screenshot of the site.css design tokens section showing primitive and semantic color tokens, container widths, and typography tokens
site.css — the consolidated design tokens section

Phase 1 — Tokens

The first phase was the easy one to feel good about. The old site.css was a 443-line flat list of rules that used hardcoded hex colors and pixel values everywhere — #0D253D appeared thirty-seven times across the stylesheet and the HTML files. The new site.css starts with a :root token block that names those values once and then derives everything else from them.

The architecture, in one sentence

Primitive tokens (--navy-900: #0D253D) feed semantic tokens (--color-bg-hero: var(--navy-900)) which feed component rules (.page-hero { background: var(--color-bg-hero) }), and the site's accent and border tints are generated from the primitives using color-mix() in oklch instead of being hand-picked.

The reason that matters: if I ever decide the navy is too cold and want to shift the whole site three degrees warmer, I change one line. Before this, I would have changed thirty-seven.

One false start worth mentioning: I wrapped the whole stylesheet in CSS cascade layers — @layer base, components, utilities — because that's what the modern playbook says to do. It broke the site. Many of the inline <style> blocks on individual pages include an un-layered * { padding: 0 } reset, and un-layered rules always beat layered ones no matter the specificity. I reverted to traditional cascade and left a comment in site.css explaining why. Modern tools are better when they match the rest of the environment. When they don't, they're worse than whatever they replaced.

Phase 2 — Blog posts

This is where the real consolidation happened. Every blog post up to this one carried its own inline copy of the blog post skeleton — the navy hero, the Cinzel title font sizing, the prose column widths, the signature block, the previous-post nav. Thirteen posts × ~32 lines each = 426 lines of CSS that was 95% identical across files.

The shared rules moved into site.css. What stayed inline in each post is only what's actually different from post to post — the table styling in the rules-audit post, the dark-navy milestone callout in the win post, the orchard-themed callout in the early-game post, the error tables in the Kennel post. Post-by-post the inline CSS went from about 35 lines to about 5.

Phase 2.5 — The layout bug nobody filed a ticket for

While migrating blog posts, a layout inconsistency surfaced that had been in the site the whole time. Posts 01 through 07 used a 1100px-wide navy hero background with a 760px-wide prose column inside it for the body text — but the hero itself had no such inner column. So the title and dek stretched the full 1100px while the body text wrapped at 760px. The hero was visibly wider than the page.

Posts 08 through 13 used 780px everywhere — internally consistent, but different from the older posts, so the Dev Log looked like it was designed by two people who didn't meet.

The fix was to pick one layout and apply it everywhere. Went with the wider one — 1100px navy hero, 760px prose column inside it used on both the hero text and the body text. Every post now has the same alignment on the left margin from the back link all the way down to the signature line. This wasn't on any todo list until the cleanup surfaced it.

Phase 3 — Section hubs

Same approach applied to the section hub pages (rules, reference, print, design, blog, changelog) and a handful of reference docs (design_ideas, design_deviations, inventory, room_glossary, rollout_plan, tracking). Four different hero class names that were doing the same job — .section-hero, .cl-hero, .blog-hero, .gloss-hero — collapsed into a single .page-hero class defined once in site.css.

Smaller wins this time. The section hub files went from about 20 lines of inline CSS each down to 3. The reference docs had more unique styling (filterable tables, custom controls, badges) so they only shed a handful of lines each. But the four-way class-name collision was resolved, which means the next time I add a new hub page I don't have to pick which legacy class to copy.

Phase 4 — Not doing Phase 4

There was a planned Phase 4: the big "paper document" files — the full rules manual, the first-run tutorial, how-to-win, campaign_sheet_v2, the production guide, the shopping list. These use a different layout — body { padding: 40px 20px } to create a margin around a .page element with a cream-colored border, styled to look like a printed document.

Looked at them honestly. Each one is mostly unique — different tables, different callouts, different content flow. The shared surface between them is maybe 30% — the outer .page frame and the doc-header styling. Migrating them would save another 100–150 lines but carry real risk of per-file regressions, and those files don't change often enough for the maintenance win to matter.

Called it. Stopped. Documented the decision in the changelog so future-me doesn't relitigate it.

The numbers, honestly

This is the part that's easy to get wrong when you're telling someone about a cleanup. The natural instinct is to say "removed four hundred lines of code." Which is true, if you only count one side of the ledger.

PhaseFilesInline CSS beforeInline CSS afterChange
Phase 2 (blog posts)1342664−362
Phase 3 (hubs + docs)12426353−73
Combined25852417−435

435 lines of inline CSS removed from 25 files. That's the number you'd quote at standup. But site.css itself grew from 443 lines to 1003 lines — a 560-line increase — because every rule that left those 25 files had to land somewhere, and the new token architecture added about 125 lines of infrastructure that didn't exist before.

Total CSS across all touched filesBeforeAfterChange
Inline in HTML (25 files) + site.css1,2951,420+125

Net, we added 125 lines of CSS to the codebase. The cleanup made the site bigger.

Why the cleanup was worth it anyway

Because lines of code is a proxy, and a bad one. What actually matters is: how many places do you have to change to change one thing?

Before the cleanup, changing the navy color meant editing 37+ places across the HTML and CSS. Now it means editing one line in :root. Changing the blog post hero padding meant editing 13 files. Now it's one rule in site.css. Changing the container width, the typography scale, the border-gold tint, the hover transition timing — all of them used to be per-file edits. All of them are now one edit.

That's the real win. Not the number of lines in the files, but the number of edits required per change. Every token and shared component is a point where future work collapses from "touch N files" to "touch 1 file." The 125-line "cost" in the diff buys back an unbounded number of future edits.

The lesson, in one line

A cleanup isn't measured in lines removed. It's measured in how many places you have to visit the next time something needs to change. The codebase got bigger by 125 lines. The surface area of future changes got smaller by an order of magnitude. Those are different numbers.

What changed visually

Nothing. That was the goal. The entire three-phase pass landed without any intentional visual change except for the Phase 2.5 hero-alignment fix, which was a bug. Every hub, every doc, every blog post renders the same pixels it did before.

Good housekeeping is boring by design.

— QQ · April 21, 2026
← Seven Tiles, One Sharpie