Case study · Community Food Co-op · Jun–Aug 2026

One system in both directions

The co-op’s website runs on a WordPress theme I designed. In summer 2026 I replaced its drifted CSS with one token contract: written once in theme.json, emitted as CSS variables, mirrored as Figma variables, and checked for drift in both directions before any design work starts.

Contract tokens

94

One source of truth: theme.json.

theme.json v1.1

Figma variables

60

The other 34 are documented as deliberately not mirrored.

figma-parity.md · 2026-08-21

Unexplained deltas

0

Both directions. All four recorded parity runs ended in PASS.

figma-parity-check.py · 2026-08-21

Raw font sizes left

3

In the front-end CSS, down from 66 before the cleanup.

2026-06-24 → 2026-09-10
Figma cover page titled CFC Design System, with a green stamp reading Frozen v1.0, 2026-07-17, and five rule cards.
The Figma library’s cover: the freeze stamp, the naming rule, and what is deliberately not a variable.

A decade of CSS had drifted out of any system

The theme carried what its own audit called “a decade of drifted, hardcoded, duplicated CSS”. The front end had 66 raw font-size declarations, z-index values as high as 9999999, and nine competing page widths with no shared token.

One bug sat underneath it all. WordPress core silently overrides four font-size slugs unless a theme opts out, so body text had been rendering at 20 px instead of 16–17, and a retune did nothing for four of the six sizes.

WordPress’s own variable names became the contract

I decided against the usual second vocabulary, a --brand-green layer on top of WordPress. WordPress already turns theme.json into variables like --wp--preset--color--primary, so those emitted names are the contract, with “no parallel --cfc-* layer”.

The editor’s colour picker, the front-end CSS, the Figma file and the bakery plugin now speak the same strings. The cost is written down too: camelCase keys come out kebab-case, and one slug keeps its capital letter because renaming it would mean migrating saved content.

The naming rule: one token, three spellings Three columns joined by arrows. theme.json key settings.color.palette slug primary is emitted by WordPress as --wp--preset--color--primary and mirrored in Figma as color/primary, whose WEB code syntax is var(--wp--preset--color--primary). custom.radius.sm becomes --wp--custom--radius--sm and radius/sm. custom.color.errorBg comes out kebab-case as --wp--custom--color--error-bg and semantic/error-bg. A dashed line along the bottom shows the parity checker joining Figma and CSS on the WEB code syntax, in both directions. 01 · THEME.JSON The source, written once 02 · CSS VARIABLE Emitted by WordPress, never typed 03 · FIGMA VARIABLE Its WEB code syntax is the CSS var settings.color.palette slug "primary" #284734 · Pacific Moss PRESET --wp--preset--color--primary color/primary 1 Primitives WEBvar(--wp--preset--color--primary) settings.custom.radius "sm": "8px" CUSTOM --wp--custom--radius--sm radius/sm 1 Primitives WEBvar(--wp--custom--radius--sm) settings.custom.color "errorBg": "#F7E1DA" CUSTOM --wp--custom--color--error-bg camelCase comes out kebab-case semantic/error-bg 2 Semantic WEBvar(--wp--custom--color--error-bg) the parity check joins the two on this string, both ways The naming rule: one token, three spellings Three cards, one per token. In each, the theme.json key flows down to the CSS variable WordPress emits and then to the Figma variable, whose WEB code syntax is that CSS variable: color primary, radius sm, and errorBg, which comes out kebab-case as error-bg. The parity checker joins Figma and CSS on that string, in both directions. THEME.JSONsettings.color.palette slug "primary"#284734 CSSemitted by WordPress --wp--preset--color--primary FIGMA1 Primitives color/primary WEBvar(--wp--preset--color--primary) THEME.JSONsettings.custom.radius "sm": "8px" CSSemitted by WordPress --wp--custom--radius--sm FIGMA1 Primitives radius/sm WEBvar(--wp--custom--radius--sm) THEME.JSONsettings.custom.color "errorBg": "#F7E1DA" CSSemitted, kebab-case --wp--custom--color--error-bg FIGMA2 Semantic semantic/error-bg WEBvar(--wp--custom--color--error-bg) CSS ⇄ Figma: the parity check joins the two on the WEB code syntax, in both directions.
The Figma name is derivable from the CSS name and back, so the checker joins them without a lookup table.

Every variable’s description + WEB code syntax carries the exact emitted var — Figma→CSS resolves with zero lookup tables.

guides/design-system/DESIGN-SYSTEM.md §4

94 tokens in six families, and a quarter of them describe one button

The contract holds 94 tokens: 10 brand colours, 6 spacing steps, 6 font sizes, 2 families and 70 custom keys. Spacing is a fixed 8 px ladder with a deliberate gap at 12. The palette skips its sixth ordinal on purpose, because filling it would shift the mapping.

Figure 1 · Contract tokens by family

The button contract is a quarter of the system

The 94 tokens in theme.json v1.1, by family. Colour is drawn as two rows: brand, and semantic + neutral.

24 of 94 tokens are the button’s recipe: radius, border, padding and three minimum heights (44, 48 and 56 px), each pointing at a system token.
Source: theme.json at v1.1, via design-system-metrics.json, computed 2026-11-07 Data

Figma modes hold the two ends of every fluid size

Fluid type has no Figma equivalent, so the system mirrors “the endpoints, not the curve”. The Type collection has two modes: Mobile is each clamp() minimum and Desktop its maximum, so body text reads 16 px in one and 17 in the other, and the largest size 36 and 75.

Figma modes hold the two ends of every fluid size Font size in pixels against viewport width for the six fluid sizes. Each size is flat at its clamp minimum on narrow screens, rises in a straight line, and is flat at its clamp maximum on wide screens. The Mobile mode stores the minimum and the Desktop mode the maximum; the rising part in between exists only in CSS. The largest size goes from 36 to 75 pixels; body text from 16 to 17. 0 20 40 60 80 px MOBILE clamp() min DESKTOP clamp() max IN BETWEEN CSS only, not in Figma 36 75 xxx-large narrow wide viewport width →
Type variableMobileDesktop
font-size/small1415
font-size/medium1617
font-size/large2024
font-size/x-large2432
font-size/xx-large3044
font-size/xxx-large3675
The six fluid sizes in px. Each mode holds one end of the clamp(); the dashed line between them is the part Figma doesn’t draw.

A checker fails when Figma and code disagree

Before any design session, a 221-line Python script compares a dump of the Figma variables with theme.json. Every token must exist in Figma or be listed as not mirrored, with a reason. Every Figma variable must map back to a real token, values and alias targets must match, and the five shadow styles must exist.

Any other result prints the delta and exits 1. The only passing line is this one:

guides/design-system/figma-parity-check.pyexit 0
PARITY PASS — zero unexplained deltas (both directions).

The allowlist doubles as documentation: 34 tokens are deliberately not variables, such as the z-index ladder (“Figma has no z axis”) and transitions (“no time axis”). All four recorded runs ended in PASS; the last, on 21 August, first caught radius/pill drifting from 999 to 99.

An agent can’t hardcode a value without it surfacing

A project skill tells every Claude session to name each token by slug before writing CSS, and to end with a proof table for every raw value in its diff. A lint script with six rule families runs as a PostToolUse hook after each CSS edit, and exits 2 to put the violation, with file and line, back in the conversation.

I tested it cold on four models, with bare asks like “try a new orange, something vivid like a tangerine” and “hardcode 13px”. The skill triggered unprompted, contrast math ruled out white on orange, and a planted #ff6600 made the hook fire.

The hook is the backstop, not the process.

.claude/skills/cfc-design-system/SKILL.md, Enforcement

Every change goes through the front door, with a version number

  1. A missing token stops the work.

    Nobody hardcodes “for now”; the session proposes a name, a value and a reason.

  2. theme.json changes first.

    Then the token is mirrored to Figma in the same session and gets a dated changelog entry.

  3. Adding is a minor version; renaming is major.

    A rename also needs a migration note, and the bakery plugin’s copy of the contract updated in the same change.

  4. A reusable colour mix becomes a named token.

    A mix hidden inside one component is a consistency leak.

Grow the system through the front door, never silently.

José, 2026-07-22 · guides/design-system/token-system.md §7.2

The contract froze on 17 July, and the bakery plugin was waiting for it

The cleanup ran from 25 June to 21 August; 359 commits landed in that window. On 17 July an overnight run froze v1.0 at commit 1a43808, rebuilt the Figma library to match, and shipped the checker, the skill and the hook. The next morning I ratified 13 of its 14 provisional calls and reversed one.

Figure 2 · Theme commits per day

94 commits on the day the contract froze

Commits per day on the theme repo, March to September 2026. The design-system window, 25 June to 21 August, is shaded; v1.0 is marked on 17 July.

The bakery plugin’s token map could only be written against names that wouldn’t move, so its token work waited for this date.
Source: git log --all on joseocfc/cfc-theme, computed 2026-11-07 Data
Front-end CSS at the commit before the cleanup and on main after it.
Front-end CSS24 June10 September
var(--wp--preset--…) references4461,270
var(--wp--custom--…) references0518
Raw font-size declarations663
Raw hex lines2614plus 25 tagged exceptions
Raw z-index of 100 or more126all in documented contexts

Changes were verified by diff, not by eye: 59 font-size repoints with 0 regressions, and a 64-agent visual diff of 32 surfaces at two widths that also found 0.

The Figma library is the same system, drawn

The library has three collections and 60 variables, each with its code syntax set to the exact CSS variable. The Button is one component set of 7 variants, 3 sizes and 5 states, 105 in all plus a Loading switch, with its placement rules written on the canvas.

Contrast is computed, not eyeballed. White on the brand green measured 4.48:1, which “fails AA by 0.02”, so the Tertiary fill was darkened from #228848 to #1E7A40.

Figma colour section: ten brand swatches with names and hex values, a dashed empty swatch, neutrals, and status colours with contrast ratios.
Ten brand colours with their slugs and usage, and a dashed swatch where the skipped ordinal would be.
Figma typography section with paired mobile and desktop type specimens and a heading role map.
Type specimens in both modes, with the role map from hero to h6.
A large grid of button components in seven styles, three sizes and five states, with placement rules beside it. Full set
The Button set: 7 variants × 3 sizes × 5 states, every fill, radius and padding bound to a variable.
A tall three-column Figma board titled Token Board, with token chips under Theme, Bakery-only and Proposed. Full board
The Token Board: every token in exactly one column, owned by the theme, the bakery plugin, or still proposed.
A Figma changelog frame with five dated entries for versions 1.0 and 1.1.
The changelog: date, version, what, why and who, newest first.

It’s live on communityfood.coop

On 7 November the production site’s markup carried the theme, the Header V2 design, the button variants and 68 custom properties from the contract.

Community Food Co-op home page at desktop width: centred logo header, a photo hero and rounded content cards on an almond background. Full capture
The co-op’s home page at 1440 px, 7 November 2026.
The co-op home page on a phone, with the header, hero and stacked cards.
The same page at 390 px.

The file still has loose ends, and they’re worth showing