This morning I re-requested every URL in munchable.app/sitemap.xml and grepped the HTML for one string. 516 pages. 515 of them open their content with <main class="legal">. The only page that does not is the homepage.

Three of those 515 are legal pages: the privacy notice, the terms, and the licences page. The other 512 are 461 ingredient answer pages, 38 recipes, 8 condition guides, and a few index and about pages. Open a recipe and the privacy notice in two tabs and view source on both: same class, same column, same typography.

How a class ends up with a wrong name

The first long-form pages on the site were the privacy notice and the terms of service. Both wanted the same thing, which is the oldest layout on the web: one centred column of prose, narrow enough to read, with headings that do not shout. So that got written, and it got called .legal, because at the time the set of pages that needed it was exactly the legal pages.

The shell is two rules and nine lines:

.legal { padding: 72px 0 88px; } .legal-inner { max-width: 720px; margin: 0 auto; } 

Then the content pages arrived: condition guides, then a few hundred generated answer pages, then recipes, then a reference page. Every one of them is a column of prose with headings. At that point there are two options. Copy the column rules under a new name per page type and accept that the five copies will drift. Or put class="legal" on a page that has nothing to do with law.

We did the second thing, 11 route files now say className="legal", and I do not regret it. What it cost was one incident.

23 rules, and 12 of them had to be rewritten

Our stylesheet is one hand-written file: 3,145 lines, 444 rules. 23 of those rules mention .legal.

The typography rules were originally written the way you would expect, against the page wrapper:

/* What these used to be */ .legal h1 { ... } .legal h2 { ... } .legal p { ... } .legal a { ... } 

Then the guide pages grew a closing call to action. It is a shared component: a warm card with a cookie mark, a heading, one sentence, and two store buttons. It already existed because the homepage ends with it, and a guide page needs to make the same point, so the component got reused rather than rebuilt.

That card sits inside <main class="legal"> and outside .legal-inner. The moment it rendered on a guide page, two things broke, for two different reasons.

The heading went flush left. The card's own rule is:

.closing h2 { font-size: clamp(34px, 4.6vw, 54px); max-width: 16ch; margin: 0 auto; } 

.closing h2 and .legal h2 have identical specificity, one class and one element each. When specificity ties, source order decides, and the .legal block sits about 130 lines further down the file. So .legal h2's margin: 40px 0 0 replaced margin: 0 auto, and a 16ch block that is meant to be centred sat against the left edge of a centred card.

The store buttons turned the wrong colour. That one was not a tie:

.store-btn { color: var(--ink); } /* 0,1,0 */ .legal a { color: var(--caramel-ink); font-weight: 700; } /* 0,1,1 */ 

One class versus one class and one element. The descendant selector wins on specificity regardless of order, so every store button inside a guide page came out caramel and bold instead of ink.

The component was not wrong. It rendered perfectly on the homepage, which is the one page in the site with no .legal on it, which is also why this was invisible until the day a guide page shipped.

The fix was to narrow 12 selectors from .legal to .legal-inner, and to leave the reason in the file so the next person adding a rule here does not undo it:

Scoped to the reading column, not to `.legal`, on purpose. These are typography rules for a column of prose, and the guide pages now end with the shared app CTA, which sits inside `.legal` but outside `.legal-inner`. 

A page wrapper selector is a contract with components nobody has written yet

That is the part worth taking away from a dull little CSS bug. .legal p is not a rule about the paragraphs on your legal pages. It is a promise about every paragraph that will ever be placed anywhere on that page, including paragraphs inside components that do not exist yet, written by somebody who will never read this file.

Scoping to the column makes a narrower promise that is actually true: everything inside the reading column is prose and is styled by the page, everything outside it is a component and is responsible for its own appearance.

The 11 rules still hanging off .legal are the safe kind, and the difference is instructive. They are .legal .cond-lede, .legal .cond-card h2, .legal .cond-q, .legal .sib h3, .legal .closing. Every one of them names a class that only exists on the page type it was written for. The dangerous selector is the one that names a bare element, because elements are what other people's components are made of.

Is a wrong name worth renaming?

.prose would be the honest name. So would .reading-column. The rename is mechanical: 11 route files, 23 rules, no visual change at all.

I have not done it, and the reason is not laziness, it is that a rename with no behavioural change is a diff nobody can review. You cannot test it, you can only read it, and the failure mode is a page you forgot that silently loses its column. The stylesheet comment carries the truth instead, which is the cheaper half of the fix.

But the cost of the wrong name is real and I would rather state it than pretend it is free: the next person who needs a column of prose will not guess that the class they want is called legal. They will write a second column, under a better name, and the site will have two. Names are documentation, and this one documents the first two pages we happened to write rather than the 515 that use it.

If you want to see the shell doing four different jobs, compare the privacy notice it was named for with a condition guide, a reference list and a recipe. Then scroll to the bottom of any of them and look at the store buttons. They are ink, not caramel, and that is the whole of what twelve rewritten selectors bought.