sc1m/design
FoundationsTokens

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:

src/app/globals.css
@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.
Value
Root token
Resolves
Current
--sc1m-orange
accent, accent-hover, ring
--sc1m-paper
bg, surface-raised, accent-fg

Neutral ramp

Nine steps. Surfaces, text and lines all come from here.
Value
Root token
Resolves
Current
--n-0
Deepest recess
--n-50
surface
--n-100
surface-raised in dark
--n-200
line
--n-300
line-strong
--n-400
Muted marks
--n-500
fg-subtle
--n-700
fg-muted
--n-900
fg

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.
Token
Role
Utility
bg
Page background
bg-bg resolves to
surface
Recessed surfaces
bg-surface resolves to
surface-raised
Cards, popups
bg-surface-raised resolves to

Text

Shown as type — the only thing they are used on.
Sample
Token
Role
Utility
Aa quick fox
fg
Primary text
text-fg
Aa quick fox
fg-muted
Secondary text
text-fg-muted
Aa quick fox
fg-subtle
Hints, placeholders
text-fg-subtle

Lines

Shown as hairlines, at the weight they actually paint.
Sample
Token
Role
Utility
line
Borders, dividers
border-line
line-strong
Emphasised borders
border-line-strong

Action

Everything interactive resolves to the brand hue.
Token
Role
Utility
accent
Primary actions
bg-accent resolves to
accent-hover
Action hover
bg-accent-hover resolves to
accent-fg
Text on accent
text-accent-fg resolves to
highlight
The add action. Nothing else.
bg-highlight resolves to
highlight-fg
Glyph on highlight
text-highlight-fg resolves to
ring
Focus indicator
outline-ring resolves to

Status

Outcome, not decoration. Five roles, four of them hues.
Token
Role
Utility
success
Positive outcomes
bg-success resolves to
warning
Caution, deprecation
bg-warning resolves to
danger
Errors, destructive actions
bg-danger resolves to
danger-fg
Text on danger
text-danger-fg resolves to
info
AI authorship signal
bg-info resolves to

Two of these carry a rule rather than just a role:

  • highlight is the add action. Nothing else.
  • info is 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.

Sample
Used for
Size
Weight
Page titles
Page titles
text-2xl
semibold
Section headings
Section headings
text-base
semibold
Labels and controls
Labels, controls
text-sm
medium
Body copy
Body copy
text-sm
regular
Captions
Captions, helper text
text-xs
regular

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.

Sample
Token
Used for
Current
brand-radius-sm
Chips, code, small marks
brand-radius-md
Inputs, tiles, small cards
brand-radius-lg
Cards, banners, panels
brand-radius-xl
Sheets, detached surfaces

Elevation

Sample
Token
Used for
Utility
shadow-raised
Cards, detached accordion items
shadow-raised
shadow-overlay
Popups, menus, dialogs
shadow-overlay

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.

src/styles/contract.ts
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;
Warning:

Adding a token touches four files

The contract is deliberately duplicated — in globals.css's @theme inline block, in contract.ts, in every brand file, and in the tokens item of registry.json (in the exact JSON shape the shadcn CLI can merge). A new token goes in all four. scripts/check-tokens-parity.mjs fails the build when they drift, so you will not get this wrong quietly.