Reference · Proposal · Option D · 2026-04-22

The v4 catalogue. Brand modes move from Primitives onto Semantic. Primitives flattens into a brand-prefixed palette library with no modes. Theme, Regulatory, and Component keep the shape they have in v3.1. This document is structural — it enumerates the changes, not every single token. Full token enumeration follows the brand-collapse audit.

5
collections
~990
flat primitives
9–20
semantic brand modes
2
theme modes
7
jurisdiction modes

How to read v4

Each section belongs to one collection. Status badges tell you what changed from v3.1:

new — new in v4 (or structurally new: brand modes on Semantic, flat naming on Primitives).

changed — token kept, but naming or mode placement is different from v3.1.

unchanged — carried forward from v3.1 byte-for-byte.

Reading the alias notation

Throughout the document, means "aliases to." Example:

Semantic.interactive/primary/bg (mode: snabbare)Primitives.color/snabbare/primary-600
  = #1e8f3e

v3.1 → v4 · the structural diff

Two collections change. Three stay identical.

Collectionv3.1 (shipped)v4 (proposed)Status
1 · Primitives 20 brand modes, ~110 tokens each unmoded, flat names, ~990 tokens changed
2 · Theme 2 modes (light/dark), ~28 tokens same unchanged
3 · Semantic no modes, ~340 tokens 9–20 brand modes, ~340 tokens per mode brand modes
4 · Regulatory 7 jurisdiction modes, ~22 tokens same unchanged
5 · Component no modes, ~35 alias packs same unchanged

The invariant v4 establishes

Each mode axis lives in exactly one collection. Brand on Semantic. Theme on Theme. Jurisdiction on Regulatory. There is no cross-collection name-matching; all aliases point at explicit targets.

1 · Primitives changed

Primitives is a flat, unmoded palette library. Every brand's ramps, chrome, typography and layout values live as fully-qualified names. Nothing here aliases; everything is raw hex or raw value. This is the only collection marketing can edit.

Naming shape

# Colour ramps — fully-qualified per brand
color/lyllo/primary-{50,100,200,...,950}
color/lyllo/secondary-{50..950}
color/lyllo/tertiary-{50..950}
color/snabbare/primary-{50..950}
color/comeon/primary-{50..950}
# ... same shape for each brand

# Shared neutrals — not brand-specific
color/neutral-{0,50,100,...,950,1000}

# Shared status — not brand-specific
color/status/success-{50..950}
color/status/warning-{50..950}
color/status/error-{50..950}
color/status/info-{50..950}

# Chrome — per-brand where brands need a specific chrome
color/lyllo/header-chrome     # = #ffffff
color/snabbare/header-chrome  # = #0b5ed7  (isolated from brand primary)
color/comeon/header-chrome    # = brand-primary tone
# ... per brand

Brand ramps (one per brand)

Each audited brand gets a complete set of ramps under color/<brand>/*. The ramp step count (9 steps: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950) is preserved from v3.1 — no ramp steps are trimmed.

lyllo
dual palette: pink + orange
color/lyllo/primary-{50..950}
color/lyllo/secondary-{50..950}
color/lyllo/tertiary-{50..950}
snabbare
green brand · blue header
color/snabbare/primary-{50..950}
color/snabbare/header-chrome
comeon
neon green · header = page
color/comeon/primary-{50..950}
mobilebet
dark green · no splits
color/mobilebet/primary-{50..950}
mobilespin
red + black icons · yellow CTA
color/mobilespin/primary-{50..950}
color/mobilespin/cta-yellow
getlucky
orange + pink · landing tag
color/getlucky/primary-{50..950}
color/getlucky/tag-blue
reviant-a
red + brown icon split · nav r=12
color/reviant-a/primary-{50..950}
reviant-b
red + purple · pill r=0
color/reviant-b/primary-{50..950}
888.nl
yellow primary · black header
color/888nl/primary-{50..950}
color/888nl/header-chrome

Brand-collapse candidates

Before rename, the brand-collapse audit identifies brands that may be ≥95% Lyllo-identical with only accent overrides. Candidates: reviant-a, reviant-b, 888.nl. If any collapses, its Primitives shrink to only the 3–8 accent tokens that differ — the rest inherits via Semantic's sub-brand mode.

Shared tokens (not per-brand)

Tokens that don't vary across brands live under flat, non-brand-prefixed names:

color/neutral-{0,50,100,200,300,400,500,600,700,800,900,950,1000}
color/status/success-{50..950}
color/status/warning-{50..950}
color/status/error-{50..950}
color/status/info-{50..950}
color/always/white    # = #ffffff
color/always/black    # = #000000

Chrome (per-brand where needed)

Chrome tokens that were color/chrome/* in v3.1 (added in the 2026-04-21 Theme rewire) keep their role but become brand-prefixed where they vary:

color/lyllo/header-chrome
color/snabbare/header-chrome
color/comeon/header-chrome
# ... per brand

# Universal chrome stays under shared namespace
color/neutral/scrim
color/neutral/divider

Typography, radius, layout

Typography families, radius scale, and layout tokens remain per-brand where they differ (e.g. typography/lyllo/family-display) and shared otherwise (radius/scale-4, space/4). Identical shape to v3.1, just with the brand prefix flattened into the name.

2 · Theme unchanged

Theme holds the ~28 neutral chrome tokens that flip between light and dark. Carries forward from v3.1 byte-for-byte. Two modes, same token names, same bindings — except aliases now point at the flat Primitives names (e.g. color/neutral-950) instead of moded ones.

Surface

Tokenmode: lightmode: dark
surface/page→ color/neutral-50→ color/neutral-950
surface/raised→ color/neutral-0→ color/neutral-900
surface/elevated→ color/neutral-0→ color/neutral-800
surface/sunken→ color/neutral-100→ color/neutral-950
surface/inverse→ color/neutral-950→ color/neutral-50
surface/skeleton→ color/neutral-100→ color/neutral-800
surface/skeleton-shimmer→ color/neutral-200→ color/neutral-700
surface/tooltip→ color/neutral-900→ color/neutral-100
surface/code→ color/neutral-50→ color/neutral-900

Text & border

Tokenmode: lightmode: dark
text/primary→ color/neutral-950→ color/neutral-50
text/secondary→ color/neutral-700→ color/neutral-200
text/muted→ color/neutral-500→ color/neutral-400
text/disabled→ color/neutral-300→ color/neutral-600
text/inverse→ color/neutral-50→ color/neutral-950
text/placeholder→ color/neutral-400→ color/neutral-500
border/default→ color/neutral-200→ color/neutral-800
border/subtle→ color/neutral-100→ color/neutral-900
border/strong→ color/neutral-400→ color/neutral-600
overlay/backdrop-opacity0.40.6

Note on Theme's relationship to brand

Theme is brand-agnostic. It flips chrome between light and dark using neutrals only. Brand colouring happens in Semantic (per brand mode), which aliases Theme for chrome bits and Primitives for brand-coloured bits. Theme never needs to know the current brand.

3 · Semantic brand modes

The biggest structural change in v4. Semantic gains 9–20 brand modes. Each mode contains the same ~340 role tokens, with aliases that point to the correct brand-prefixed Primitives. ~290 tokens are brand-identical — they have the same alias in every mode. ~50 are brand-divergent — different aliases per mode.

The mode table at a glance

Below, each row is a Semantic token. Columns show its alias under three representative brand modes (lyllo · snabbare · comeon). Italic rows are brand-identical. Coloured rows are brand-divergent.

Interactive · diverges per brand

Semantic tokenmode: lyllomode: snabbaremode: comeon
interactive/primary/bg → color/lyllo/primary-600 → color/snabbare/primary-600 → color/comeon/primary-600
interactive/primary/bg-hover → color/lyllo/primary-700 → color/snabbare/primary-700 → color/comeon/primary-700
interactive/primary/bg-pressed → color/lyllo/primary-800 → color/snabbare/primary-800 → color/comeon/primary-800
interactive/primary/fg → color/always/white → color/always/white → color/always/black
interactive/secondary/bg → color/lyllo/secondary-500 → Theme.surface/raised → Theme.surface/raised
interactive/accent/bg → color/lyllo/tertiary-500 → color/snabbare/primary-500 → color/comeon/primary-400

Text · mostly brand-identical

Semantic tokenmode: lyllomode: snabbaremode: comeon
text/primary → Theme.text/primary → Theme.text/primary → Theme.text/primary
text/secondary → Theme.text/secondary → Theme.text/secondary → Theme.text/secondary
text/brand → color/lyllo/primary-600 → color/snabbare/primary-600 → color/comeon/primary-600
text/on-brand → color/always/white → color/always/white → color/always/black
text/link → color/lyllo/primary-700 → color/snabbare/primary-700 → color/comeon/primary-700

Icon · partially diverges

Semantic tokenmode: lyllomode: snabbaremode: comeon
icon/default → Theme.text/primary → Theme.text/primary → Theme.text/primary
icon/action → color/lyllo/primary-600 → color/snabbare/primary-600 → color/comeon/primary-600
icon/chevron → color/neutral-400 → color/neutral-400 → color/neutral-400
icon/big → color/lyllo/secondary-500 → color/snabbare/primary-500 → color/comeon/primary-500
icon/descriptive → color/lyllo/tertiary-400 → color/snabbare/primary-400 → color/comeon/primary-400
icon/nav-selected → color/lyllo/primary-600 → color/snabbare/primary-600 → color/comeon/primary-600

Pill & badge · diverges

Semantic tokenmode: lyllomode: snabbaremode: comeon
pill/active/bg → color/lyllo/primary-600 → color/snabbare/primary-600 → color/comeon/primary-600
pill/inactive/bg → Theme.surface/raised → Theme.surface/raised → Theme.surface/raised
badge/accent/bg → color/lyllo/tertiary-500 → color/snabbare/primary-500 → color/comeon/primary-500
badge/live/bg → color/status/error-500 → color/status/error-500 → color/status/error-500

Radius · structural (mostly brand-identical)

Semantic tokenmode: lyllomode: snabbaremode: reviant-a
radius/interactive → radius/scale-8 → radius/scale-8 → radius/scale-12
radius/pill → radius/scale-full → radius/scale-full → radius/scale-0
radius/card → radius/scale-12 → radius/scale-12 → radius/scale-12
radius/modal → radius/scale-16 → radius/scale-16 → radius/scale-16
radius/badge → radius/scale-4 → radius/scale-4 → radius/scale-4

Rough split: what's brand-identical vs. brand-divergent

Across all ~340 Semantic tokens:

  • ~290 brand-identical — same alias across every mode (chrome, neutrals, status, typography, most radii). Copy-paste per mode.
  • ~50 brand-divergent — different alias per mode (interactive, accent, brand-coloured icons, pill active, badge accent, text-on-brand).

Sub-brands (from brand-collapse audit) override only ~3–8 of the 50 divergent tokens; the rest inherits from the parent brand.

4 · Regulatory unchanged

Jurisdiction-specific compliance tokens. 7 modes: default, SE, DK, MT, ON, NL, PL. ~22 tokens covering responsible gaming (rg/*), bonus terms (bonus/*), and age gate (age-gate/*). Carries forward from v3.1 byte-for-byte.

Regulatory sits beside Semantic as a narrow override layer — it aliases Primitives directly for a small set of compliance-related tokens that must vary per jurisdiction regardless of brand or theme. Devs consume these only in compliance contexts (rg-limit-display, bonus-terms-footer, age-gate-cover).

Why Regulatory stays separate: the jurisdiction axis is independent of brand and theme. SE rules apply regardless of which brand the user is on. Moving it onto Semantic would force a 7× multiplier on an already 9-mode collection for zero UX gain.

Token families (unchanged from v3.1)

  • rg/limit-display/* — deposit/loss/session limit displays
  • rg/self-exclusion/* — exclusion banners and flows
  • rg/reality-check/* — reality-check modal surfaces
  • bonus/terms/* — wagering T&Cs typography and emphasis
  • bonus/disclaimer/* — fine-print colours per jurisdiction
  • age-gate/cover/* — age-gate splash colours

5 · Component unchanged

Locked alias packs for specific UI elements that can't be expressed by a generic Semantic role alone. ~35 tokens across cta/*, header/*, nav/*, menu-row/*, view-all/*. Marketing cannot edit. Inherits brand variation through Semantic without participating in modes.

Component's one rule: aliases only Semantic. Never reaches around to Primitives directly. Never touches Theme directly. Because Semantic resolves brand per mode, Component inherits brand-correct values automatically.

Three worked examples

Example 1 · CTA primary

Component.cta/primary/bgSemantic.interactive/primary/bg   (brand-dependent)

# When Semantic.mode = snabbare:
  → Primitives.color/snabbare/primary-600 = #1e8f3e

# When Semantic.mode = lyllo:
  → Primitives.color/lyllo/primary-600 = #ff6699

Example 2 · Snabbare's isolated header

Component.header/bgSemantic.chrome/header/bg   (brand-dependent)

# When Semantic.mode = snabbare:
  → Primitives.color/snabbare/header-chrome = #0b5ed7

# When Semantic.mode = lyllo:
  → Primitives.color/lyllo/header-chrome = #ffffff

Example 3 · Menu-row icon split

Component.menu-row/action-iconSemantic.icon/action              (brand-dependent)

Component.menu-row/chevronSemantic.icon/chevron             (brand-identical, neutral)

Summary of Component's unchanged packs

  • cta/primary/*, cta/form/*, cta/wallet/*, cta/landing/* — CTA variants with different radii per context
  • header/* — isolated header chrome
  • menu-row/* — action/chevron split
  • nav/selected-* — navigation selected state
  • view-all/* — label/icon split

Migration · v3.1 → v4 token map

First 20 high-traffic tokens and their v4 equivalents. The full map covers ~340 Semantic tokens × 9 brand modes.

v3.1 token (current)v4 equivalentChange
color/primary-600 [mode=snabbare] (Primitives)color/snabbare/primary-600 (Primitives, flat)renamed, unmoded
color/primary-600 [mode=lyllo]color/lyllo/primary-600renamed, unmoded
color/primary-600 [mode=comeon]color/comeon/primary-600renamed, unmoded
color/chrome/header-snabbare (Primitives)color/snabbare/header-chromename normalised, brand prefix moved to front
color/neutral-50 (Primitives)color/neutral-50unchanged (not brand-specific)
interactive/primary/bg (Semantic, no modes)interactive/primary/bg (Semantic, 9–20 brand modes)same name, now moded
text/primary (Semantic, no modes)text/primary (Semantic, mode=*, all aliases → Theme.text/primary)brand-identical across modes
icon/action (Semantic)icon/action (Semantic, per-brand)brand-divergent, now moded
icon/chevron (Semantic)icon/chevron (Semantic, brand-identical)same alias in every mode
pill/active/bg (Semantic)pill/active/bg (Semantic, per-brand)brand-divergent, now moded
radius/pill (Semantic)radius/pill (Semantic, per-brand)brand-divergent for reviant-b (r=0)
surface/page (Theme)surface/page (Theme)unchanged
border/default (Theme)border/default (Theme)unchanged
rg/limit-display/bg (Regulatory)rg/limit-display/bg (Regulatory)unchanged
cta/primary/bg (Component)cta/primary/bg (Component)unchanged bind; brand resolved via Semantic mode
header/bg (Component)header/bg (Component)unchanged
menu-row/action-icon (Component)menu-row/action-icon (Component)unchanged
nav/selected-bg (Component)nav/selected-bg (Component)unchanged
view-all/label (Component)view-all/label (Component)unchanged
cta/form/radius (Component)cta/form/radius (Component)unchanged

Migration phases

  1. Brand-collapse audit — read-only against brand-audit.html. Flag sub-brands.
  2. Flatten Primitives — start with Snabbare (99.5% hex parity). Rename moded → flat. Verify byte-for-byte.
  3. Add Semantic brand modeslyllo + snabbare first. Bind ~340 aliases per mode.
  4. Style Dictionary pipeline — emit dist/tokens/<brand>-<theme>.css.
  5. Swap prototype — Global Search Casino off hand-written theme.css to generated bundles at localhost:5180.
  6. Scale & cut-over — all 9 active brands × 2 themes = 18 bundles. Archive v3.1 Figma as read-only reference.
↑ top