What it solves

A site that was built with a design system and still drifted — because a design system nobody enforces is a folder of comments.

The build had system.css with tokens at the top and a rule written into the file: a component may not invent a colour, a size, a gap or a radius. Three weeks later, measured:

where                     radius typed by hand
contact form fields       8px
dashboard cards           12px
a small tag               6px
a checkbox                4px
Button.svelte             8px  (the proposal's button was 13px)

Seven files, four different numbers, none of them a token. The client noticed before anyone else did — the button on the site did not match the button in the proposal he had already sent, which reads to a client not as a rounding error but as two different companies.

Worse, three pages each held their own copy of the same field styles. Each copy had already drifted a few pixels from the others, so "fix the form" meant finding three places and getting all three right.

The fix is three rules, and the third is the one that makes the other two survive contact with a busy week:

  1. A page may not invent a value. No colour, size, gap or radius typed at the call site. If it is not a token, it gets a name in the system first, then gets used.
  2. Two things needing the same look is a third component, not a second copy. FormPanel.svelte exists because three pages had drifted copies of the same fields.
  3. Every rule gets a gate that fails the build. Not a lint warning anybody can scroll past — a non-zero exit that stops the deploy.

The code

Wire the gates in as the first line of the deploy script, before the build, so a failure stops the ship rather than filing a warning nobody reads:

bash bin/check.sh || exit 1

The eight gates

Every one of these exists because the thing it catches actually happened on this build. None are hypothetical and none are style opinions.

Gate Catches
buttons come from Button.svelte class="btn btn--solid" on a page, matching nothing
nothing outside the tokens types a value a colour, type size, spacing or radius typed at a call site
selects are reset a <select> painting itself as a system control
fields come from FormPanel a page starting its own copy of the field styles
the reset appears once the CSS reset pasted into the sheet twice
the schema has every column the flow writes CREATE TABLE IF NOT EXISTS silently keeping an old shape
every queued job has a handler a job enqueued with nothing to run it
every internal link lands on a route a link to a page that does not exist

Each gate found something the first time it ran. The link gate found six 404s — every service card on the site pointed at /book/<slug>, which was never a route. Six dead ends in the middle of the client's own path, live, for weeks.

The trick that makes one component own every field

Svelte scopes styles to the component that declares them, which normally means a wrapper cannot style the markup its children pass in. :global inside a fenced class solves it — the styles reach the fields, but nothing escapes onto anything else:

.panel :global(input),
.panel :global(select),
.panel :global(textarea) { ... }

That is what lets four pages share one set of field styles while the markup stays where it belongs, on the page.

Notes / gotchas

A gate you have not watched fail is not a gate. Break each rule on purpose, confirm the gate goes red and the script exits non-zero, then put it back. The first version of the link gate here only matched strings written directly after href= — so the moment the link moved into a $derived it went silent, on the exact link it had been written to catch. It passed clean and proved nothing. Ten seconds of deliberately breaking it found that.

Some rules cannot be grepped, and grep-shaped gates give false comfort. The schema, job and link gates run as real scripts against real structures. gates/schema.js builds the schema into a throwaway database and checks it against every INSERT the code actually performs — because CREATE TABLE IF NOT EXISTS is silent when the table already exists with the wrong shape, which is how an old table survived a rewrite and 500'd every form submission.

Adapt the file paths. gates/schema.js and gates/jobs.js read src/lib/server/flow.js and db.js by name. Point them at wherever the equivalent lives; the technique is the point, not the paths.

One place genuinely cannot read the tokens. A password gate rendered in hooks.server.js is served before the app's CSS loads, so its values are copied by hand. That is fine — but say so in a comment, or the next person changes a token and cannot work out why one screen did not move.

Name a token for its job, not its size. --radius-control, --radius-card, --radius-button survive a redesign; --radius-8 does not. The one exception worth making is matching an artifact the client has already seen — here the button's 13px, weight 650 and 15px/26px padding came straight off the proposal document, deliberately, and --pad-button carries a comment saying so.

A section scale is not a control scale, and pretending otherwise is how call sites start inventing numbers. The page scale here is 6/12/20/32/52/80 — right for laying out sections, useless for a control that needs 14px of padding to sit at 52px tall. Twenty-eight spacing values had been typed at call sites for exactly this reason. The fix is not to loosen the rule, it is a second set of tokens named for the job: --pad-field, --pad-button, --pad-chip, --pad-card, --pad-tag, --gap-label, --gap-tight.

Check declarations, not lines. gates/tokens.js parses each property: value pair, because a rule that reads a token for one value and hardcodes the next — border-radius: var(--radius-card); padding: 16px 18px — passes any grep that looks for a line without var(--.