01 / 22
20-Brand White Label · Casino & Sportsbook Platform

Design Token
Architecture v2

A 5-collection, multi-axis Figma Variable system for brand, theme, jurisdiction, and iGaming-specific surfaces — built on a 4-point scale linked to 16px body.

20 brands light + dark SE · DK · MT · ON · NL · PL Figma Enterprise native Variables 4-point · rem SemVer · quarterly

→ Token Reference · all tokens in tables

Confidential · Design System Documentation · 2026-04-17

02 What changed from v1

v1 was sound — but single-axis.

The v1 doc treated "brand" as the only dimension of variation. The real shape is four axes, and v2 splits them so the matrix stays tractable at scale.

v1 · Assumed

One axis: Brand

18 modes × 410 tokens → ~7,380 values. Theme hidden in "neutral-50 or neutral-950" strings. No regulatory story. No scaling discipline. No density story.

v2 · Reality

Four axes + 4-point scale

Brand (20) × Theme (2) × Jurisdiction (7) + density embedded in layout primitives. 4-point scale linked to 1rem = 16px. ~550 tokens, ~2,305 distinct values.

Collections

3 → 5

Brands

18 → 20

Mode axes

1 → 3

Scale base

— → 4px
03 The core insight

The matrix is multi-dimensional.

A single surface/page token needs to resolve differently based on four independent questions:

20
Brand
which operator is this?
2
Theme
light or dark?
7
Jurisdiction
which regulator applies?
Viewport
which breakpoint?

Why splitting helps

Fused into one axis, you'd manage 20 × 2 × 7 = 280 modes on every token. Split across collections, each axis multiplies only the tokens that actually vary along it. Theme touches ~25 tokens, Regulatory ~15, Brand ~90. The math stays manageable.

04 Decisions locked · 2026-04-17

Locked answers.

Axis / ScopeDecision
Brand count20 live brands   v1's 18 was stale
Themeseparate axis every brand supports light + dark
Regulatory7 modes default · SE (Spelinspektionen) · DK (Spillemyndigheden) · MT (MGA) · ON (iGO/AGCO) · NL (Kansspelautoriteit) · PL (MF)
Scale base4px · 1rem = 16px all spacing, radius, type, layout are multiples of 4 · rem-linked
Densitybrand-embedded in layout primitives + responsive. No user-toggle.
RTLLTR only no logical-direction tokens
ToolingFigma Enterprise + native Variables chained collections, no plugins
Aggregator chromefull chrome tokens top bar, bottom bar, loading, error, live-casino controls
VIP / LoyaltyPrimitives + Semantic tier colors brand-moded; badge semantics shared
VersioningSemVer · quarterly minor deprecations one release ahead
Ownershipsingle DS lead bus-factor risk → mitigate with TOKENS.md
05 5-collection architecture

Five collections, chained by alias.

Each collection owns one mode axis. Frames select modes independently per collection. Figma resolves aliases across collections at render time.

5 · Component global · no modes · <10 tokens
4 · Regulatory 7 modes · default · SE · DK · MT · ON · NL · PL
3 · Semantic global · ~340 intent tokens
↓   ↘
2 · Theme 2 modes · light · dark
1 · Primitives 20 brand modes · 4-point scale

The golden rule (unchanged from v1)

Each layer references only the layer directly below. Semantic → Theme or Primitives. Regulatory → Semantic. Component → Semantic. Never skip a layer. Never alias upward.

06 How Figma-native resolves all this

One frame, three mode selectors.

Each collection carries one mode axis. A frame picks a mode per collection independently — Figma resolves cross-collection aliases at render.

# Frame-level mode selection (Figma right panel):
  Primitives  →  Brand: ComeOn!    Theme  →  Dark    Regulatory  →  SE

# Alias chain for one resolved value:
  surface/page
     theme/surface/page          (Theme · Dark)
       neutral-950               (Primitives · ComeOn!)
        = #0B0A1A

How the three axes combine

  • Primitives picks the brand → brand-moded raws (color, layout, radius)
  • Theme picks light/dark → flips surfaces, text, borders
  • Regulatory picks jurisdiction → overrides ~15 compliance tokens
  • Semantic & Component are modeless — they just route aliases

Axes are orthogonal: 20 × 2 × 7 = 280 render states, but each collection only stores its own mode set.

Day-to-day in Figma

  • One library file owns all 5 collections; product files consume it
  • Mode selectors — one per collection — live in the right panel
  • Page defaults cascade; frames override for QA / comps
  • New brand = new mode in Primitives, no schema change
  • Scope + describe every variable to prevent misuse
07 Collection 1 · Primitives · part 1/2

Primitives — brand-moded values

20 brand modes. Raw values only — never used directly in components.

Color

  • color/brand/primary-{100..900}
  • color/brand/secondary-{100..900}
  • color/brand/tertiary-{100..900}
  • color/brand/gradient-start · end
  • color/neutral-{50..950}
  • color/status/success · warning · error · info
  • color/always/white · black · transparent
  • color/special/pitch-green · jackpot-gold · live-pulse
  • color/vip/bronze · silver · gold · platinum · diamond new

Typography · Shape · Layout (all 4-point)

  • typography/family/base · heading · display · mono
  • radius/{none, sm, md, lg, xl, 2xl, full} · 4-point
  • layout/header-height · sidebar-width · input-height new
  • layout/tile-min-width · launcher-top-bar-height new
  • layout/launcher-bottom-bar-height new
  • gradient/promo-hero · jackpot · hero-fade new

Why layout/* moves here

v1 had nav/sidebar/width: 240 or 280 as a literal string in Semantic. That's not a semantic decision — it's a raw brand measurement. Promoting it to a brand-moded primitive lets Figma track it properly and lets layout/* align with the 4-point scale.

08 Collection 1 · Primitives · part 2/2

Primitives — global scales

Same value across all 20 modes. All dimensions are multiples of 4px, exported as rem. Kept in Primitives for simplicity.

Typography scale

size (xs..4xl) · weight · line-height · letter-spacing · transform

Spacing · 4-point

spacing/1..20  (4px..80px)
all multiples of 4

opacity new

opacity/0 · 10 · 20 · 40 · 60 · 80 · 95

breakpoint new

xs · sm · md · lg · xl · 2xl

icon/size new

xs (16) · sm (20) · md (24) · lg (32) · xl (40)

z-index promoted

from Component → now a primitive (stacking is foundational)

Motion · Shadow

duration · easing · shadow/xs..overlay

transition new

compound: duration + easing pairs

Why globals here?

Spacing/opacity/etc. don't vary by brand. Keeping them in Primitives avoids a sixth collection.

Kills one v1 smell

v1 hardcoded inline: surface/overlay: neutral-800 at 96% opacity. With an opacity/* primitive, the Semantic alias becomes a clean reference — no more magic numbers leaking into the intent layer.

09 Scaling system · part 1/2

1rem = 16px. Everything's a multiple of 4.

One base unit links spacing, radius, typography, icons, and layout. Every dimension in the system is either a multiple of 4 or a documented exception.

Spacing

spacing/140.25rem
spacing/280.5rem
spacing/3120.75rem
spacing/4161rem
spacing/5201.25rem
spacing/6241.5rem
spacing/8322rem
spacing/12483rem
spacing/16644rem
spacing/20805rem

Radius

radius/none00
radius/sm40.25rem
radius/md80.5rem
radius/lg120.75rem
radius/xl161rem
radius/2xl241.5rem
radius/full9999

v1's radius/xs: 2px is dropped — sub-4 breaks the scale. Min curve is now 4px.

Typography

size/xs120.75rem
size/sm140.875rem*
size/md161rem
size/lg201.25rem
size/xl241.5rem
size/2xl322rem
size/3xl402.5rem
size/4xl483rem

* size/sm: 14px is a sanctioned half-step — 12 and 16 don't cover body-secondary well. All others strict 4-point.

One rule, consistently applied

A designer should be able to hit the right value without a reference card: start from 16, go up or down in multiples of 4. If a value isn't on the scale, it doesn't get shipped — or it becomes a sanctioned exception documented in the variable description.

10 Scaling system · part 2/2

Figma speaks px. CSS needs rem.

Figma Variables support Number, Color, String, Boolean — no CSS units. The solution isn't to fight it; it's to accept that Figma stores px and the export pipeline translates to rem.

Figma Variable spacing/4 = 16 Number (raw px)
Transform rem = px / 16 Script / CSS calc / plugin
CSS output --spacing-4: 1rem; What components consume

Three ways to bridge the gap

1 · Tiny export script recommended

~20-line Node script hits the Figma Variables REST API, divides dimension values by 16, writes a CSS/JSON token file. Run manually on release or in CI. Figma stays clean (only numbers). CSS is semantic (only rem). Script is ~1 hour of work.

2 · CSS calc() wrapper

Export raw JSON. Your CSS template uses calc(16 / 16 * 1rem) — the browser does the math at parse time. Zero pipeline logic, slightly redundant runtime. Good if you don't want any build step.

3 · Dual-token convention

Every dimension variable gets a String sibling: spacing/4 = 16 + spacing/4/css = "1rem". Lives entirely in Figma — no pipeline. Drift-prone: two sources of truth per token. Only use if you cannot ship a script.

Naming convention

  • Abstract names (xs, sm, md, lg) or positional (1, 2, 3)
  • Never embed units: not spacing/16px or size/rem-1
  • Standard description on every dim variable: "px in Figma · exported as rem (value ÷ 16)"

What not to tokenize

  • Line-height: ship as unitless (1.5), not px or rem
  • Letter-spacing: ship as em at consumption time, not rem
  • radius/full: ship as 9999px, not rem (pill shape must be absolute)
11 Collection 2 · Theme

Theme — minimal, 2 modes

Only the tokens that flip direction based on light/dark. References brand-moded Primitives for actual neutral values. Everything v1 wrote as "neutral-X or neutral-Y" lives here.

Theme · Surface

theme/surface/page
  light   neutral-50
  dark    neutral-950

theme/surface/raised
  light   neutral-100
  dark    neutral-900

theme/surface/elevated · sunken ·
  inverse · skeleton · skeleton-shimmer ·
  tooltip · code

Theme · Text & Border

theme/text/primary
  light   neutral-900
  dark    white

theme/text/secondary · muted · disabled ·
  inverse · caption · heading-color

theme/border/default · subtle · strong ·
  inverse · divide

theme/overlay/backdrop-opacity
  (if varies by theme)
~25
tokens
2
modes
~50
distinct values
12 Collection 3 · Semantic

Semantic — the intent layer

Global (no modes). References Theme and Primitives. This is where components pull 99% of what they need. v1 sections 02–17 mostly land here.

Surfaces

surface · text · border · divide

Interactive

primary · secondary · tertiary · destructive · viewall

Status

success · warning · error · info · toast

Navigation

header · sidebar · tab · bottom · breadcrumb

Forms

input · select · checkbox · radio · toggle · slider

Overlays

modal · drawer · tooltip · popover · sheet

iGaming · Sportsbook

odds · live · betslip · result

iGaming · Casino

casino · promo · rg · payment

+ new groups v2

see next slide

Naming refinement

brand/primary | secondary | tertiary is fragile when brands disagree on what "tertiary" means. Consider role aliases in Semantic: brand/action, brand/accent, brand/support → pointing to the primary/secondary/tertiary primitives. Intent-named consumption, ramp-named definition.

13 Semantic · new in v2

Filling the gaps v1 missed.

Every group here was either entirely absent in v1 or under-specified. Most are iGaming-critical surfaces players spend real time on.

VIP / Loyalty

vip/tier/{bronze, silver, gold, platinum, diamond}/{bg, text, border, icon}

Launcher chrome

launcher/{top-bar, bottom-bar, close-icon, balance-text, deposit-shortcut, fullscreen-toggle, favorite-icon, session-timer, loading-bg, error-state, live-casino/mute-icon, live-casino/chat-icon}

Sportsbook · Bet Builder

sportsbook/bet-builder/{leg-default, leg-selected, correlation-warning}

Sportsbook · Accumulator

sportsbook/accumulator/{row, boosted-bg, boosted-text, multiplier-badge}

Casino · Live Dealer

casino/live-dealer/{studio-chrome, seat-count, min-max-chips, dealer-name, countdown}

Casino · Jackpot tiers

casino/jackpot/{mini, minor, major, grand}/{bg, text, ticker}

Platform

notification · chat · geo-block · receipt · tournament

Fixes

gaming/result/cashout-full + cashout-partial · feedback/toast/info-bg · warning-bg

Geo-block is not cosmetic

An iGaming product served to a jurisdiction it's not licensed in is a legal incident. The geo-block page must be immediately recognizable and per-brand compliant. Tokenize it.

14 Collection 4 · Regulatory

Regulatory — the override layer.

7 modes. Narrow scope: only tokens regulators actually constrain. Sits above Semantic in the alias chain so regulator requirements win over brand preference.

ModeRegulatorKey constraints
defaultfallbackStandard brand-driven values; used when no jurisdiction override applies
SESpelinspektionenSpelpaus self-exclusion prominence · deposit-limit bar · strict bonus-language rules · 18+
DKSpillemyndighedenROFUS self-exclusion · Spillemyndigheden seal · mandatory RG info · 18+
MTMGAMGA seal placement · RG logo visibility · responsible-play reminder
ONiGO / AGCOConnexOntario resource · strict ad restrictions · no celebrity endorsements · 19+
NLKansspelautoriteit (KSA)Cruks self-exclusion · deposit-limit mandatory · 24+ for ads · KOA compliance
PLMinisterstwo FinansówState-regulated scope · Gambling Act warnings · 18+ · restricted game types

Tokens that live here

reg/rg/warning/{bg, text, border, icon, min-size-hint}
reg/rg/self-excl/{bg, text, cta-bg, program-name}   → Spelpaus | ROFUS | Cruks | ConnexOntario | ...
reg/rg/limit/{bar-track, bar-fill, bar-fill-near, bar-fill-at}
reg/bonus-disclaimer/{text, min-contrast-hint, min-size-hint}
reg/logo/{color, hover, min-size-hint}            → GamCare · Spelpaus · Stodlinjen · Cruks · ...
reg/age-gate/{min-age, text}                      → 18+ / 19+ (ON) / 24+ (NL ads)

Naming rule

Every regulator-gated token in Semantic must alias to its reg/* counterpart, not directly to Primitives. This ensures a jurisdiction switch actually takes effect. Engineers should never see status/warning-600 inside gaming/rg/warning/* — they should see reg/rg/warning/bg.

15 Collection 5 · Component

Component — last resort.

Under 10 tokens. Global. Only things that genuinely cannot live in Semantic or Primitives.

What stays

  • Brand-specific component backdrop opacity exceptions (if any survive the cleanup)
  • True single-component layout constraints not derivable from layout/* primitives
  • Nothing else, ideally

What moved out

  • z-indexes → promoted to Primitives
  • odds-button min-width/height → layout/* in Primitives
  • card border-radius overrides → radius/* (brand-moded)

The 10-token rule

If this collection grows beyond 10 tokens, the Semantic layer is under-specified. Audit before adding. Every addition here is a new exception for engineers to remember; exceptions don't scale across 20 brands.

16 Token count math

Why splitting saves the values.

CollectionTokensModesValues in play
1 · Primitives~16020 brand (of which ~90 brand-moded)~1,800 + globals
2 · Theme~252 light/dark~50
3 · Semantic~3401 global~340
4 · Regulatory~157 jurisdictions~105
5 · Component~81 global~8
Total v2~550~2,305
~11,000
naive v2 (fused axis · 20 × 2 × 7 × 40)
~2,305
v2 with split axes

Only brand-moded tokens multiply by 20. Theme multiplies by 2. Regulatory by 7. Semantic not at all.

17 Alias chain rules

Four hard rules. No exceptions.

Rule 1 · Direction

Aliases flow downward only. Regulatory → Semantic → Theme/Primitives. Never upward, never sideways.

Rule 2 · No skipping

Components reference Semantic. Semantic references Theme or Primitives. Components may never reference Primitives directly — that's the whole reason Semantic exists.

Rule 3 · Regulatory first

For regulator-gated Semantic tokens, the alias must pass through Regulatory. gaming/rg/warning/bg → reg/rg/warning/bg, not directly to status/warning-100.

Rule 4 · No raw values in Semantic

No hex codes, no opacity percentages, no pixel values in the Semantic layer. Everything resolves to a Primitive or Theme alias. If you can't express it that way, the Primitive is missing.

Enforcement

Figma doesn't enforce these rules for you. A designer can create a Semantic token that points directly to #FDCB6E. Policy + review catches that. A short TOKENS.md in the design repo, plus a quarterly audit, is cheap insurance against drift.

18 The consumption contract

What engineering is allowed to import.

Token hygiene on the code side mirrors the alias chain. Components consume Semantic. Only platform code touches Primitives directly.

ConsumerMay readMay NOT read
UI components
(Button, Card, Odds, etc.)
semantic/* primitive/* · theme/* · reg/*
Feature flows
(Betslip, Deposit, KYC)
semantic/* primitive/* · theme/* · reg/*
Platform / layout shell
(routing, chrome, error pages)
semantic/* · component/* · primitive/layout/* · primitive/z-index/* · primitive/breakpoint/* primitive/color/*
Design System internals everything

Enforce via linting

A simple ESLint/Stylelint rule can block primitive/color/* imports outside the DS package. The contract becomes self-enforcing without code review burden.

19 Versioning · SemVer · quarterly

Predictable for engineering. Humane for design.

What counts as what

MAJORRename or remove a token. Change alias chain shape. Add a new mode axis.
MINORAdd new tokens. Add values to existing modes. Add a new brand mode.
PATCHCorrect a value. Fix a typo. Update a description.

Release cadence

  • Minor: quarterly. Batched adds and improvements.
  • Patch: any time. Fixes ship immediately.
  • Major: rare, telegraphed two quarters ahead.
  • Deprecation: mark in docs + description one minor release before removal.
# Example changelog entry
## 2.3.0 — 2026-07-15 (minor)

### Added
- casino/jackpot/tier/{mini, minor, major, grand} — splits the
  single jackpot-gold primitive into four tiers per brand demand.
- sportsbook/bet-builder/correlation-warning

### Deprecated (removal in 3.0)
- casino/jackpot/text → use casino/jackpot/tier/major/text

### Fixed
- NL mode: reg/age-gate/min-age corrected from 18 → 24 for ad surfaces
20 Implementation roadmap

Nine steps, in this order.

01

Correct the reference doc

Update v1 PDF → v2: brand count 18 → 20, redraw architecture with 5 collections, document the 4-point scale. This file is your source.

02

Pick the prototype brand

Use one of the 2 transparent-header outliers. It will stress-test the system on day one, not day ninety.

03

Build one brand end-to-end

All 5 collections. Both themes. SE jurisdiction (ComeOn heartland). Don't scale to 20 until this one is clean.

04

Write the px-to-rem transform

~20-line Node script reading the Figma Variables REST API, outputting CSS tokens with rem values. Validate output matches designer expectations.

05

Add missing primitive groups

opacity · breakpoint · icon/size · layout · gradient · transition · z-index · VIP tier colors. All on the 4-point scale.

06

Add missing Semantic categories

VIP · launcher · bet-builder · live-dealer · jackpot tiers · tournament · notification · chat · geo-block · receipt · cashout split · toast info/warning.

07

Draft the consumption contract

Write the rules from slide 18 as a TOKENS.md. Share with engineering before any Figma Variables are declared "final."

08

Wire jurisdictions in order

SE first (core market, strictest day-to-day). Then NL (tight compliance). Then DK · MT · ON · PL. Each uses the same Regulatory pattern — only values differ.

09

Scale to 20 brands

Once the prototype + transform + contract are all clean: the remaining 19 brands are repetition, not design.

21 Risks & mitigations

Known soft spots — design around them early.

RiskMitigation
Bus factor 1 on single DS lead Write TOKENS.md now. Record a Loom walkthrough of the alias chain. Identify a backup.
Figma mode drift — a brand's Primitives diverge from the ramp spec Quarterly audit script: export all Variables as JSON, diff brand ramps against spec, flag stops outside tolerance.
Scale discipline slips — a designer ships a 14px gap instead of spacing/4 Transform script rejects non-multiple-of-4 values in dimension variables. Build-time error, not a lint warning.
px / rem transform breaks — root font-size overridden somewhere Lock html { font-size: 100%; } in reset CSS. Document as non-negotiable. Test accessibility zoom at 200%.
Regulator changes mid-release — new KSA rule in Q3 PATCH releases ship any time. Regulatory is a narrow collection; a 3-token patch is fast to land.
Engineering bypasses Semantic — imports a Primitive directly Linting rule (slide 18). Code review. Make the contract a CI check, not a convention.
Figma native multi-mode UX — designers forget to set all three mode selectors Document the three-selector checklist on every frame template. Provide starter file per brand with modes pre-configured.
The bottom line

Five collections.
One prototype.
Then scale.

Don't try to build for 20 brands on day one. Build it right for one, on a 4-point scale, with a clean px-to-rem bridge. The architecture will carry the rest.

5
collections
20
brands
~550
tokens
~2.3K
values
4px
scale base
Decisions locked 2026-04-17 Ready for prototype brand

→ Open the Token Reference

Complete token tables · every variable to create in Figma

Plan spec  ·  ~/.claude/plans/users-zlatko-lazarov-desktop-tokens-des-peppy-parnas.md