Design Token
Architecture v3.2
The final architecture for ComeOn's white-label platform. Six collections. Two locked invariants. Nine brand modes. Light + Dark theme. Brand DNA in three ramps plus a tertiary accent. Implemented in Figma file fysBbW10LRJUNgisaBKXJo on 2026-04-29.
→ Token Reference v3.2 → Component Audit → Brand Audit (evidence)
v3.2 supersedes v3.1 and v4. The v4 exploration is preserved in the Archive; it was dropped on 2026-04-29.
v4 was an architecture; v3.2 is a discipline.
v4 proposed flattening Primitives into a brand-prefixed library and moving the brand mode axis onto Semantic. After three iteration sessions the team concluded the migration cost outweighed the structural win — and that the real problem was not the shape of the collections but the discipline applied inside them. v3.2 keeps the v3.1 collection model and locks two rules that make it stay clean.
Why v4 was dropped
- Flattening 20 brand modes to ~990 flat tokens lost the single-edit-point per ramp (marketing now has to touch 9 places to retint a ramp).
- Moving brand mode to Semantic broke the orthogonality between brand and theme — the
.darktoggle stopped being a single-axis flip. - Library subscribers (Lyllo Casino, Brand Tokens, etc.) would each need their own v4 adoption; without that, Semantic would alias into stale Primitives.
- The migration generated a measurable amount of dormant
_legacy/*binding cruft that took two cleanup sessions to retire.
What v3.2 locks down
- Hex invariant — raw hex lives in exactly two homes: Global (system + black/white/alpha) and Modes brand DNA (primary, secondary, neutral, tertiary).
- Strict 4-point scale —
Global/scale/*contains only[0, 4, 8, 12, 16, 20, 24, 28, 32, 40, 48, 56, 64, 72, 80, 96, 112, 128, 144, 160]. No off-grid stops. - Tertiary ramp in brand DNA for accent colours that don't fit primary/secondary (Lyllo orange, Comeon lime).
- Semantic spacing + icon-size exposed for component consumption.
- Theme is alias-only — every Light + Dark resolution chains through Modes DNA.
The shape of v3.2.
Each axis lives in exactly one collection. Aliases flow downward only: Global is the floor, Component Packs is the ceiling.
Total: ~382 active tokens across ~2,180 distinct values when unrolled across all modes. Down from v3.1's ~525 because the chrome layer collapsed when Modes absorbed it.
How a token resolves at the component.
Components bind Component Packs or Semantic. Each layer aliases the layer below. Frame mode = brand × theme. The chain ends at Global or Modes DNA.
# Frame mode: brand=Lyllo, theme=Light Component Packs/cta/primary/background → Semantic/color/brand/primary → Modes/color/brand/primary # resolves per brand mode → Modes/color/primary/500 # Lyllo: #F02884 · raw hex (DNA) Component Packs/cta/primary/text → Theme/color/text/on-brand → Modes/color/text/on-brand # Light: aliases Global/color/white → Global/color/white # #FFFFFF · raw hex Component Packs/cta/radius → Semantic/radius/sm → Modes/radius/cta → Global/scale/4 # 4px (rounded from legacy 3) Component Packs/cta/padding/x → Semantic/spacing/md → Global/scale/16 # 16px
Read this as: component → role → brand-or-theme → DNA-or-Global. Switching Theme=Dark at the frame swaps only the Theme leg; switching Modes=Snabarre swaps the Modes leg. Global is invariant across everything.
The floor: brand-invariant raw values.
116 variables + 5 elevation Effect Styles. Single Default mode. Holds black, white, system colours, the 4-point scale, typography sizes, breakpoints, strokes — anything that does not change per brand or theme.
| Group | Sizes | Notes |
|---|---|---|
| color/black, /white | 2 | Pure raw hex |
| color/black-alpha/{0..90} | 10 | Overlay alphas |
| color/white-alpha/{0..90} | 10 | Overlay alphas |
| color/system/{success, warning, error, info}/50-900 | 40 | Brand-invariant feedback |
| scale/{0,4,8,…,160} | 20 | Strict 4-point only |
| typography/size/* + line-height/* | ~16 | Aliases scale |
| typography/letter-spacing/{tight,normal,wide} | 3 | Raw FLOAT |
| stroke/{hairline,default,thick,strong} | 4 | Aliases scale |
| breakpoint/{mobile,tablet,desktop,desktop-wide} | 4 | FLOAT |
| elevation/0..4 | 5 | Effect Styles |
Why Global is small
Anything that could vary per brand goes into Modes instead. Anything that must stay the same across every brand and every theme — like color/white or scale/16 — lives here. The size of Global is the rate at which we add new universal foundations, which should be near zero in steady state.
Hex appears here, deliberately
Global is one of two homes for raw hex (the other is Modes brand DNA). Editing Global/color/system/error/500 updates every error surface across all 9 brands and both themes — that single-edit-point property is the reason Global exists.
Brand DNA + brand-tinted chrome.
140 variables across 9 brand modes. Brand DNA holds the only raw hex outside Global. Everything else in Modes is an alias to brand DNA or Global.
Brand DNA · the raw-hex layer
color/primary/{50..900}— 10 stopscolor/secondary/{50..900}— 10 stopscolor/neutral/{50..900}— 10 stops, brand-tintedcolor/tertiary/{50..900}— 10 stops, accent
40 raw hex tokens × 9 brand modes = 360 raw hex values. The only place outside Global where hex is allowed.
Why tertiary was added
Lyllo's accent is orange (#FD6A00) but its primary is pink and its secondary is also pink-leaning. There was no clean home for a third accent without forcing it into a non-DNA token. Tertiary unblocks brands that need a third hue — Comeon lime (#C3FF00), future palettes — and stays at zero cost for brands that don't (other 7 brands mirror primary as a placeholder).
Alias chrome inside Modes
Every other Modes token aliases DNA — radii, brand chrome (header, banner, landing, menu-row, nav, surface, text, icon, border), typography family/weight, flag/meta strings.
Modes/color/header/surface-primary → Modes/color/neutral/600 # Lyllo: #5b5c60 (brand-tinted grey) Modes/radius/cta → Global/scale/4 # 4px — rounded from legacy 3 Modes/radius/pill → Global/scale/160 # 160px — true pill
Light and Dark, 100% alias.
31 variables, 2 modes (Light, Dark). Every token in Theme is an alias — never raw hex. The Light alias chains through Modes to brand DNA; the Dark alias does the same with the dark-side stop.
theme/surface/page
Light → Modes/color/surface/page
→ Modes/color/neutral/100 # brand-tinted off-white
Dark → Modes/color/surface/page
→ Modes/color/neutral/900 # brand-tinted near-black
theme/text/primary
Light → Modes/color/text/primary
→ Modes/color/neutral/900 # near-black text on light
Dark → Modes/color/text/primary
→ Modes/color/neutral/100 # off-white text on dark
theme/nav/shadow
Light → Global/color/black-alpha/10
Dark → Global/color/black-alpha/0 # no shadow needed on dark
Why theme tokens never hold hex
If theme/surface/page ever held a raw hex, that hex would be wrong for at least 8 of 9 brands. The alias forces the resolution to flow back into the brand-tinted neutrals, so Lyllo's "page" is its own off-white/near-black, not a generic grey.
Theme is the cheap toggle
A single frame mode flip — Theme = Dark — switches every Theme alias's resolution to its Dark target. Brand modes stay where they are. Component bindings don't change. Add a per-brand dark stop in Modes once, every Theme token picks it up.
Role tokens for designers and devs.
53 variables, single mode, 100% alias. Spacing, icon-size, and 36 colour roles. Components bind Semantic when they need a role-named token without component-specific naming.
| Token | Aliases |
|---|---|
| spacing/xxs | Global/scale/4 |
| spacing/xs | Global/scale/8 |
| spacing/sm | Global/scale/12 |
| spacing/md | Global/scale/16 |
| spacing/lg | Global/scale/24 |
| spacing/xl | Global/scale/32 |
| spacing/2xl | Global/scale/48 |
| spacing/3xl | Global/scale/64 |
| Token | Aliases |
|---|---|
| icon-size/sm | Global/scale/16 |
| icon-size/md | Global/scale/20 |
| icon-size/lg | Global/scale/24 |
| icon-size/xl | Global/scale/32 |
Plus 36 colour aliases mapping common roles (text, surface, border, icon, status, brand) to Modes or Theme tokens.
Last-mile alias packs for component masters.
38 variables. Component-specific aliases that sit one layer above Semantic — used when a component master needs a token name that does not generalise (e.g. menu-row/label/on-icon).
Component Packs/menu-row/icon
→ Semantic/color/icon/content
→ Theme/color/icon/content
→ Modes/color/icon/content
→ Modes/color/neutral/700
Component Packs/cta/primary/background
→ Semantic/color/brand/primary
→ Modes/color/brand/primary
→ Modes/color/primary/500
Component Packs/header/active-selected
→ Modes/color/header/active-selected
→ Modes/color/primary/500 # NOT neutral — must contrast surface-primary
Header rule
header/active-selected must never alias the same Modes/neutral stop as header/surface-primary for a brand — that produces an invisible nav active state on coloured headers. Pin active-selected to a brand stop (primary/500) instead. This rule is locked in the component audit.
Raw hex lives in two homes. Everywhere else is an alias.
This is the discipline rule. It is enforced by a single Plugin API audit that any session can run.
Raw hex allowed in
Global/color/black,/whiteGlobal/color/black-alpha/{0..90}Global/color/white-alpha/{0..90}Global/color/system/{success,warning,error,info}/{50..900}Modes/color/primary/{50..900}Modes/color/secondary/{50..900}Modes/color/neutral/{50..900}Modes/color/tertiary/{50..900}
Raw hex forbidden in
- Modes (outside primary/secondary/neutral/tertiary) —
brand/*,surface/*,text/*,icon/*,border/*,banner/*,landing/*,menu-row/*,nav/*,header/*,chrome/* - Theme — every var in both modes
- Semantic — every var
- Component Packs — every var
- Regulatory — every var
Audit: getLocalVariablesAsync() filtered to COLOR; assert non-DNA / non-Global vars resolve to VARIABLE_ALIAS in every mode.
Strict 4-point scale. No exceptions.
Global/scale/* contains only 0 or multiples of 4. Twenty stops. Off-grid legacy values round to the closest stop — type rounds up, radii round to closest.
Canonical scale (20 stops): 0, 4, 8, 12, 16, 20, 24, 28, 32, 40, 48, 56, 64, 72, 80, 96, 112, 128, 144, 160
| Legacy | Rounded to | Why |
|---|---|---|
| 3 (radii) | scale/4 | Closest stop |
| 6 (radii) | scale/4 or scale/8 | Closest stop · team to choose |
| 100 (radii) | scale/96 | Closest stop |
| 11 (type) | scale/12 | Round up |
| 15 (type) | scale/16 | Round up |
| 17 (type) | scale/20 | Round up |
| 19 (type) | scale/20 | Round up |
| 22 (type) | scale/24 | Round up |
| 36 (line-height) | scale/40 | Round up |
| 50 / 60 (display) | scale/48 / 64 | Closest stop |
The exception is letter-spacing
typography/letter-spacing/{tight, normal, wide} are raw FLOAT (fractional values like ±0.02). They live in their own micro-scale and don't pretend to fit scale/*.
Why the rule is strict
Once you allow off-grid stops "just for this radius", every legacy value becomes a candidate. The scale erodes into a list of historical accidents. A 1–4px visual drift on a few radii is a one-time cost; carrying off-grid stops forever is a recurring cost on every designer.
Same DNA, nine personalities.
Each brand mode in Modes overrides the four DNA ramps. Everything else in Modes (chrome aliases, radii, typography family, meta strings) re-resolves automatically when the frame's brand mode flips.
| Brand | primary/500 | secondary/500 | tertiary/500 | Notes |
|---|---|---|---|---|
| Lyllo | #F02884 pink | pink-leaning | #FD6A00 orange | Visible · Tertiary unblocks orange accent |
| Snabarre | green | blue | placeholder | Visible · Header lives in blue |
| Mobilespin | red | red-deep | placeholder | Visible · Icons stay charcoal |
| Comeon | green | green-deep | #C3FF00 lime | Modes-only · Tertiary lime accent |
| Mobilebet | green | green-deep | placeholder | Modes-only · No frame yet |
| Get Lucky | orange | pink-magenta | placeholder | Modes-only · Dual-palette |
| Reviant | red | warm | placeholder | Modes-only |
| 888 Casino | yellow | black | placeholder | Modes-only · Yellow CTAs |
| Reviant 2 | red-pink | blue-purple | placeholder | Modes-only · Sharp-edge variant |
Three brands have visible frames in fysBbW10LRJUNgisaBKXJo; the remaining six exist as Modes entries ready for future visual coverage.
v3.1 → v3.2 in one table.
Renames preserve binding IDs in Figma — components pick up new names without per-instance edits.
| v3.1 token / family | v3.2 location | Change |
|---|---|---|
| color/brand/primary-N | Modes/color/primary/N | Renamed · group flat |
| color/brand/secondary-N | Modes/color/secondary/N | Renamed |
| color/brand/tertiary-N | Modes/color/tertiary/N | Promoted from optional |
| color/neutral-N | Modes/color/neutral/N | Brand-tinted, moved to Modes |
| color/status/* | Global/color/system/* | Renamed · Global, brand-invariant |
| color/always/{black,white} | Global/color/{black,white} | Flat in Global |
| spacing/N | Global/scale/N + Semantic/spacing/{xxs..3xl} | Two-layer · scale + named |
| radius/{base-3,input,box,card,pill,navigation,large} | Modes/radius/* → Global/scale/* | All aliased to scale |
| icon/sizes | Semantic/icon-size/{sm,md,lg,xl} | Promoted to Semantic |
| elevation tokens | Global/elevation/0..4 (Effect Style) | Effect Styles · 5 levels |
| launcher/* (deferred) | — | Stays deferred |
| platform/* (deferred) | — | Stays deferred |
Who can edit what.
Marketing edits brand DNA only. Compliance edits Regulatory. The DS lead owns the rest. This boundary is what makes the architecture survive contact with day-to-day work.
| Collection | Marketing | Compliance | DS lead | Engineering |
|---|---|---|---|---|
| Global | — | — | ✓ writes | read-only via export |
| Modes · brand DNA | ✓ writes (only here) | — | review | read-only |
| Modes · alias chrome | — | — | ✓ writes | read-only |
| Theme | — | — | ✓ writes | read-only |
| Semantic | — | — | ✓ writes | read-only |
| Component Packs | — | — | ✓ writes | read-only |
| Regulatory | — | ✓ writes | review | read-only |
The single-edit-point property of brand DNA is what enables marketing self-service: edit one ramp stop, the whole chain updates. Component bindings never need touching at the marketing level.
v3.2 ships in the variable layer. Components are next.
The variables landed on 2026-04-29. Component masters on the Components page still bind earlier library names — see the component audit for the full gap analysis and remediation plan.
Done
- 6 collections rebuilt in
fysBbW10LRJUNgisaBKXJo - 9 brand modes seeded with DNA
- Tertiary ramp added across 9 brands
- Strict 4-point scale enforced (off-grid stops removed)
- Theme alias-only audit passes
- Hex invariant audit passes
- Live HTML reference (this set)
Outstanding
- Component-master rewire — components still bind
Main/*,Generic/*,Lobby Header/*,Whiteframe/* - Focus tokens — none defined yet (WCAG 2.4.7 / 2.4.11 / 2.4.13)
- Notification text-on-color contrast — confirmed AA failures (success, warning, info)
- Touch target sizes < 24×24 in Toggle thumb, Notification ×, Modal ×, Micro Button
- "OLD" components retiring (Payment Method OLD, duplicate Sub Tabs)
- Per-brand dark-mode neutrals beyond the placeholder set
Six collections.
Two invariants.
Edit brand DNA. Everything else flows. Audit at any time with one Plugin API call. The architecture is locked — the work that remains is component adoption.