← Changelog

Twenty-nine green assertions and a stylesheet the browser threw away

A human said the warning callout wasn't there. It was — the markup was perfect and had been all along. What was missing was every CSS rule meant to style it, silently discarded by the browser because we wrote Svelte syntax in a plain stylesheet. Here is the bug, the second one hiding behind it, and the two gates that make neither of them possible again.

Why Care?

The operator opened flave and said: there is no warning callout, no table component, and a note doesn’t work either.

Every one of those was true on screen. None of them was true in the code. The renderer had been emitting <aside class="callout callout-warning"> wrapping a proper <table> since the first commit, with a passing test proving it.

The gap between those two sentences is the whole story, and it is the argument for the human rung in one paragraph.

What’s New?

  • Callout is a real component — four tones over an open type vocabulary, a marker bar so the distinction never rests on colour alone, and scoped styles that actually reach the browser.
  • Table is a real component — column alignment threaded from the mdast node, and a scroll container so a wide table inside a callout stops shoving the document sideways.
  • Four --color-tone-* tokens across all three modes, documented in DESIGN.md.
  • Two new gates in pnpm prove that make today’s two bug classes unshippable.

The bug

apps/editor/src/styles/app.css is a plain stylesheet. It contained rules like:

.rendered :global(.callout) { border-left: 3px solid …; }

:global(...) is a Svelte compiler construct. Inside a component’s <style> block it means “don’t scope this.” In a plain .css file it is not valid CSS, so the browser parses the selector, fails, and discards the entire rule.

Seven rules were written that way. Callouts, tables, pre blocks, and CodeMirror’s height all shipped in the bundle and did nothing:

$ grep -o ':global([^)]*)' dist/assets/*.css
.rendered :global(.callout){border-left:3px solid …}   ← shipped, dead

No server-rendered test can see this. The suite asserts markup and never evaluates CSS, which is exactly why 29 assertions stayed green while the surface was blank.

The second bug, hiding behind the first

Fixing the selectors surfaced something worse. An audit of every token the editor referenced:

✘ --color-surface          ✘ --font-sans
✘ --color-surface-raised   ✘ --font-display
✘ --color-text-secondary   ✘ --font-mono
✘ --color-text-tertiary

Seven of eleven did not exist. The real names are --color-bg-raised, --color-text-soft, --color-text-dim, and — the one that would have taken longest to spot by eye — --font__display, with a double underscore.

Every reference had a hardcoded fallback, so nothing looked broken in dark mode. It just quietly ignored the theme contract, rendered in the browser’s default font, and would have collapsed in light mode. A fallback does not make a missing token safe; it makes it invisible.

Two gates, so neither happens twice

scripts/check-styles.mjs, now rung 0 of pnpm prove:

  1. :global() in a plain .css file → fail, with the reason.
  2. var(--token) that theme.css does not define → fail, fallback or not.
── rung 0: styles resolve ──
✓ styles: 2 css file(s) clean, 236 token reference(s) all resolve

It found 17 problems on its first run. It also caught two things while being written: a comment describing the :global rule tripped the rule (comments are now stripped before scanning), and --lv, which the splash injects inline at runtime and is legitimately not a theme token.

This is the same move as check-frontmatter.mjs last commit. A convention an agent has to remember degrades exactly when it matters; a convention the proof script enforces does not.

Why callouts got their own colour axis

The palette had no semantic state colour — only the accent, the clearance ramp, and the misregistration pair. Amber was sitting right there, and > [!warning] wants amber.

theme.css says not to, in a comment written before any of this:

The clearance ramp — cool to hot as exposure increases. SEMANTIC ONLY. If a surface uses one of these, it is making a claim about who may see something. Never reach for amber to add warmth.

That rule is load-bearing. Clearance means who may see this; a public document can carry a warning and a private one can carry none. Two meanings on one token would make the clearance scan — §11.1, the core promise of the whole product — unverifiable, because an amber rule could mean “LP-visible” or “be careful” and nothing could tell them apart.

So --color-tone-* draws from the same tier-1 raws under its own names. The palette does not grow; the axes stay separate.

Four tones, not one per type, because lfm’s callout vocabulary is open — it accepts any [A-Za-z0-9_-]+. > [!spaceship-status] is valid input. Unknown types resolve to neutral and keep their raw type on the element, so nothing is ever dropped; only its colour is defaulted. A closed switch would have silently swallowed exactly the extensibility flave exists to protect.

A near-miss worth naming

The tone tokens went into dark and light mode, and the theme has three — dark, light, and vibrant. Vibrant would have rendered tone-less callouts, and no test would have noticed, because tests don’t switch modes. Caught by counting blocks rather than trusting the two that were visible.

theme.css also now exists in two copies — splash/ and apps/editor/ — which will drift. Both are synced and byte-identical today. That is a real follow-up, not a solved problem.

What’s Next

Still unproven, unchanged: there is no codified browser drive. Today is the second time in one day that a human eye caught something the suite could not, which is a strong argument for writing the click-path rather than continuing to rely on it.

The specific iteration items behind the original “needs iteration” are still the operator’s to name. This entry closes the two they did.