Tokens
The semantic token contract — the fixed set of role names every brand must resolve.
The contract is a fixed list of role names. A brand assigns values to them; components consume them through Tailwind utilities and never see the values. Add a name and every brand must answer for it — which is why the list is short and grows reluctantly.
The two namespaces
Start here, because it decides where everything else lives. Brand files assign
bare names; globals.css maps them into Tailwind's namespace:
@theme inline {
--color-accent: var(--accent);
--color-fg-muted: var(--fg-muted);
/* … */
}inline is load-bearing. It makes the generated CSS reference var(--accent)
at the use site, so utilities re-resolve when .dark or [data-brand] flips.
Remove it and dark mode stops working.
Root palette
Tier one: the raw values a brand assigns from. Only a brand file reads these — the semantic roles in the next section point at them, and dark mode swaps the values under the same names.
The Current column reads each token back out of the page, so it follows the theme and brand switchers rather than describing one fixed state.
Hue
Brand-owned, and named per brand. Switch brands to see the other set.Neutral ramp
Nine steps. Surfaces, text and lines all come from here.Only the ramp's names are shared across brands. The hue names are not — sc1m
declares --sc1m-orange, Luntian declares a --sage-* ramp and two money hues — so
nothing outside a brand file may reference them. That is the whole reason the
semantic tier exists.
Colour
Tier two, and the only tier a component may touch: nineteen role names in five families, each resolved from the palette above. Every swatch is painted with its own utility class, so these tables are also a live test — switch theme or brand and they follow.
Surfaces
Three depths, page to popup. Nothing else stacks.Text
Shown as type — the only thing they are used on.Lines
Shown as hairlines, at the weight they actually paint.Action
Everything interactive resolves to the brand hue.Status
Outcome, not decoration. Five roles, four of them hues.Two of these carry a rule rather than just a role:
highlightis the add action. Nothing else.infois AI authorship. It is the hue the AI surfaces own — see AI authorship.
Typography
Type is brand-driven through --brand-font-sans and --brand-font-mono. The
fallback chain ends in a real generic family, so the chain still resolves
outside Next.js — Storybook has no next/font variables.
Radius
Radius is brand-owned: sc1m is sharp, Luntian is softer. The sample column repaints with the brand, and Current reports what it resolved to.
Elevation
Brands may tint elevation through the optional --brand-shadow-raised and
--brand-shadow-overlay slots; skip them and the structural defaults apply.
The contract, as data
src/styles/contract.ts lists the same names as a TypeScript array, and the
token parity test iterates it to prove that every registered brand resolves
every token in both themes.
export const COLOR_TOKENS = ["bg", "surface", "surface-raised", /* … */] as const;
export const SHAPE_TOKENS = ["brand-radius-sm", /* … */ "brand-font-mono"] as const;
export const BRANDS = [
{ key: "sc1m", attr: null }, // default brand owns :root
{ key: "luntian", attr: "luntian" },
] as const;