Munchable's marketing site is a Next.js app with 426 URLs and one stylesheet. Not one stylesheet per component: one file, app/globals.css, 3,133 lines, imported once in the root layout.
$ find apps/web -name '*.module.css' | wc -l 0 $ grep -iE 'tailwind|styled|emotion|vanilla-extract' apps/web/package.json $ ls apps/web | grep -iE 'postcss|tailwind' Three empty results. The runtime dependencies are Next, React, Supabase, Stripe, Drizzle, Resend, OpenAI and the two workspace packages. There is no CSS tooling in the project at all, because globals.css is plain CSS that the browser could parse as typed: no nesting, no preprocessor, two !important in the whole file.
I did not set out to defend that. I set out to find out whether it had quietly become a problem, which meant counting. The counting is the interesting part.
Where the lines went
The file is divided by section comments, and they make it easy to measure:
| Section | Lines |
|---|---|
| Hero demo: the app scanning a real looking product | 676 |
| Feature-row phones | 540 |
| Support page | 190 |
| App store buttons | 173 |
| Legal + status pages | 146 |
| Editorial pages: conditions, ingredients, answers | 148 |
| Pricing | 135 |
| Everything else | ~1,125 |
The top two entries are the same thing twice. They draw phones. The landing page shows the app running, five times, and none of those are screenshots: they are divs, spans and inline SVG, animated in CSS. I wrote about the decision to have zero images on that page separately; what I had never done is measure the bill.
1,216 lines, 38.8% of the stylesheet, for decoration on one page. The two prefixes those sections use, .sd- and .pm-, account for 78 of the 221 distinct class names in the file. A third of the stylesheet, and a third of its vocabulary, is scenery.
Now the other direction. 420 of the site's 426 URLs are the condition guides, the answer pages and the recipes. Every one of them renders into the same shell:
$ grep -rl 'legal-inner' apps/web/app --include='*.tsx' privacy licenses answers [question] terms about recipes recipes/[slug] conditions conditions/[slug] .legal is a single readable column with type rules on it, 146 lines, and the editorial section that extends it for the guides is another 148. 294 lines carry 420 pages. Four lines of CSS per decorative phone-pixel, and seven-tenths of a line per page of actual content.
I want to be clear that this is not a complaint. Those 1,216 lines are the thing a visitor looks at for the first four seconds, and the 294 are a column of text that is supposed to get out of the way. The ratio is upside down relative to page count and right side up relative to attention. But I did not know the number before I counted it, and "where is my CSS" turned out to be a much better question than "how much CSS do I have".
What makes one global file survivable
The failure mode of a single global stylesheet is not length. It is two people writing .card for different cards. Four things keep that from happening here, and only one of them is a technique.
Prefix per section. .sd- is the scan demo, .pm- is the phone mock, .cond- is the condition guides, .nf- is the 404. The prefix is the module boundary, hand-maintained, visible in the markup. It is what CSS modules would give you automatically, bought with discipline instead of a build step.
Flat selectors. The deepest selector in the file is six parts, and the overwhelming majority are one or two. Nothing is written to out-specify anything else, which is why there are two !important rather than a hundred.
The section comments are the table of contents, and they say why rather than what:
/* ── Editorial pages: conditions, ingredients, food ────────────────────────── These share the .legal shell (a single readable column) and add the few extras the guides need. */ A token block at the top, 28 custom properties. Colours, three radii, two shadows, the max width, three font stacks. Every rule below pulls from them, so a brand change is a change to the top of one file.
That last one is a deliberate duplicate. The app's palette lives in @munchable/design-tokens, a workspace package the app, the web app and the video renderer all import. This file keeps its own copy, and why the copy is the right call is a decision I stand behind: a CSS custom property and a TypeScript constant are different artefacts, and generating one from the other buys consistency the design does not actually want. The marketing site is allowed to have a warmer section background than any app screen.
The markup side of the ledger
className attributes across app/ and components/ : 558 .tsx files in those directories : 60 inline style objects : 2 Nine class attributes per file, and essentially no inline styles. That second number is the one I would watch. Inline styles are where a global stylesheet goes to die: the moment it becomes easier to write style={{ marginTop: 12 }} than to find out whether a utility exists, the file stops being the source of truth and starts being a historical record. Two is a number you can keep at two by looking at it occasionally.
When I would stop doing this
Honestly: when a second person joins, or when the site stops being mostly one page plus three templates.
The entire reason this works is that the site has a small number of page shapes. One landing page with a lot of bespoke art, one editorial shell used by 420 URLs, one form page, one admin area with its own file. A global stylesheet scales with the number of distinct page shapes, not with the number of pages, and those are wildly different numbers here.
What I would not do is adopt a framework to solve a problem I measured and did not find. The counting took ten minutes and answered the question better than any amount of opinion about utility classes would have.
See it working
- munchable.app is the 1,216 lines. The phones are live DOM: zoom in, select the text on one of them, watch the scan animation run its four phases.
- munchable.app/conditions/low-fodmap-ibs is the 294 lines, and so is is onion low FODMAP, and so are the other 418.
- munchable.app/recipes/gentle-rice-congee is the same shell again with a recipe in it.
Reader's exercise, since the numbers above came out of the repository: open DevTools on the landing page and on any answer page and compare what is actually in the cascade for each. The gap is the post.