Overview
Brand tokens require different forms for each consumer. This skill encodes and converts the **same brand truth (color, typography, spacing, radius, shadow, motion)** across 3 layers. L1 (DTCG) is the SSOT, L2/L3 are derivatives, and consistency between layers is guaranteed through reverse extraction and roundtrip checks.
This SKILL.md includes everything: the 3-layer schema, bidirectional conversion rules, roundtrip checks, and usage examples. Color and font values are expressed as brand-agnostic placeholders (
#RRGGBB,<BrandFont>), and actual brand values are injected from the conversion input (DTCG source).
Trigger keywords
DTCG token conversion, 3-layer token mapping, CSS custom properties generation, shadcn semantic roles, Tailwind v4 @theme, token roundtrip, design tokens transformer
3-layer model
| Layer | Form | Consumer | Representative files (handoff.zip) |
|---|---|---|---|
| L1 — DTCG SSOT | W3C Design Tokens ($value/$type/$description), hierarchical groups |
Brand source of truth | assets/round3/02-tokens.json |
| L2 — Primitive CSS variables | --color-* · --neutral-50..950 · --tracking-* + self-host @font-face |
Single-file HTML render / card | colors_and_type.css |
| L3 — semantic/shadcn | --base-background · --base-primary · --base-ring · --base-sidebar-* · --base-chart-1..5 + Tailwind v4 @theme + .dark switching |
Production code (shadcn system) | assets/round3/globals.css |
L1 — DTCG SSOT schema
- W3C format: each token holds
$value/$type/$descriptionhas - Hierarchical groups:
color.{brand,neutral,semantic,dark,gradient}·typography.{family,weight,size,lineHeight,letterSpacing}·spacing·radius·shadow·motion.{duration,easing}·container·logo $descriptionrecords provenance and meaning → brand source of truth
Minimum token set per DTCG group
| Group | Minimum tokens | Notes |
|---|---|---|
color.brand |
primary, ink, background (+ primary-hover/active, surface) | Primary color, body text color, and background — 3 types are the required minimum |
color.neutral |
50·100·200·300·400·500·600·700·800·900·950 | 11-step luminance scale |
color.semantic |
success, warning, danger, info | 4 state colors |
color.dark |
background, surface, primary, foreground, border | Minimum dark override set |
color.gradient |
signature (+ soft/dark derivatives) | Signature asset |
typography.family |
sans (+ latin/mono/serif) | Body font required |
typography.weight |
regular(400), bold(700) (+ medium/semibold/black) | fontWeight |
typography.size |
xs…6xl + display | rem/clamp |
typography.lineHeight |
tight, normal (+ snug/relaxed) | number |
typography.letterSpacing |
display, heading, body, caption | Korean letter-spacing encoding |
spacing |
0…32 (4px base step) | dimension |
radius |
none, sm, md, lg, full (+ xl/pill) | dimension |
shadow |
sm, md, lg (+ xs/xl/signature) | shadow |
motion.duration |
fast, normal (+ instant/slow/page) | duration |
motion.easing |
default (+ bounce/smooth) | cubicBezier |
container·logo |
(Optional) breakpoints and asset paths | logo는 asset type |
$type allowed values
$type |
Value form | Standard |
|---|---|---|
color |
hex · rgb() · oklch() |
W3C |
dimension |
rem · px · em · clamp() |
W3C |
fontFamily |
Font stack string | W3C |
fontWeight |
100–900 integer | W3C |
number |
Unitless number (line-height, etc.) | W3C |
duration |
150ms · 0.4s |
W3C |
cubicBezier |
cubic-bezier(…) |
W3C |
shadow |
box-shadow string/object | W3C(composite) |
gradient |
linear-gradient(…) string |
W3C (composite) — bundles use the string shorthand form |
asset |
File path | Bundle extension (non-standard) — assets such as logos |
Example JSON (brand-agnostic — placeholders)
{
"$schema": "https://design-tokens.github.io/community-group/format/",
"$description": "<브랜드> Design Tokens — <출처 가이드라인> 흡수",
"$version": "1.0.0",
"$updatedAt": "YYYY-MM-DD",
"color": {
"brand": {
"primary": { "$value": "#RRGGBB", "$type": "color",
"$description": "<출처> 명시 — 브랜드 주색. 타이틀/CTA/아이콘" },
"ink": { "$value": "#RRGGBB", "$type": "color",
"$description": "본문 텍스트 — #000 대체" }
},
"neutral": { "500": { "$value": "#RRGGBB", "$type": "color" } }
},
"typography": {
"letterSpacing": {
"display-tight": { "$value": "-0.075em", "$type": "dimension",
"$description": "히어로 강조" },
"body": { "$value": "-0.025em", "$type": "dimension" }
}
}
}
$descriptionis the place that stamps **provenance and meaning** — by recording in the token which guideline/system the value came from, it makes L1 the source of truth.
L2 — Primitive CSS variable schema
--color-primary/--color-ink/--color-bg/--color-surface--neutral-50…--neutral-950(luminance scale)--tracking-display-tightLetter-spacing variables such as (Korean letter-spacing rule encoding)- self-host
@font-face(brand font) + CDN fallback font
CSS variable naming convention
| Category | Prefix token | 예 |
|---|---|---|
| Color role | --color-<역할> |
--color-primary·--color-ink·--color-bg·--color-surface |
| Luminance scale | --neutral-<50..950> |
--neutral-500 |
| Foreground | --fg-<1..3>·--fg-on-primary |
Body/secondary/placeholder |
| Border | --border-<1|2|strong>·--border-focus-ring |
|
| Gradient | --gradient-<이름> (+-soft/-dark) |
--gradient-signature |
| Font/weight | --font-<sans|latin|mono>·--fw-<regular..black> |
|
| Letter-spacing | --tracking-<역할> |
--tracking-body |
| Size/line-height | --text-<xs..6xl|display>·--lh-<tight..relaxed> |
|
| Spacing/radius/shadow | --space-<0..32>·--radius-<none..full>·--shadow-<xs..xl|signature> |
|
| Motion/container | --duration-<…>·--easing-<…>·--container-<sm..2xl> |
- Dark mode: L2 only in the
[data-theme="dark"]scope redefines values only (variable names identical). - Aliases (
bg←background,fw←weight,tracking←letterSpacing,space←spacing,lh←lineHeight) may only use those registered in the table — no arbitrary abbreviations.
@font-face block template (self-host)
/* 굵기 1개당 @font-face 1개 반복 */
@font-face {
font-family: "<BrandFont>";
font-weight: 400; /* 각 굵기별로 반복 (100~900) */
font-style: normal;
font-display: swap; /* FOIT 방지 */
src: url("./fonts/<BrandFont>-Regular.otf") format("opentype");
}
/* 보조 라틴/모노 폰트는 CDN @import */
@import url("https://fonts.googleapis.com/css2?family=<Latin>:wght@400;600;700&display=swap");
Letter-spacing variable mapping table (Korean typography encoding)
| Letter-spacing variable | 값 | Application | Naming (−N) |
|---|---|---|---|
--tracking-display-tight |
-0.075em | Hero large emphasis | −75 |
--tracking-display |
-0.05em | Main title | −50 |
--tracking-heading |
-0.05em | H1–H4 | −50 |
--tracking-body |
-0.025em | Body | −25 |
--tracking-body-tight |
-0.05em | Dense body | −50 |
--tracking-caption |
0 | Caption/label | 0 |
Pattern (values adjusted per brand): the larger the text, the stronger the negative letter-spacing — Korean large typography looks overly spaced between characters, so it is tightened, while captions and small text are left at 0. −25/−50/−75 The naming is an abbreviation of -0.025/-0.05/-0.075em.
L3 — semantic/shadcn role schema
--base-*Semantic roles:background/foreground/card/popover/surface/muted/primary/secondary/accent/destructive/border/input/ring/border-strong/chart-1..5/sidebar-*- Tailwind v4
@theme+@layer base/utilities .darkDark mode only via class → CSS variable switching ([HARD] dark:Direct use of Tailwind utilities prohibited)
--base-* → shadcn component role mapping table
--base-* 롤(쌍) |
shadcn consuming component | Derivation source (L2) |
|---|---|---|
background/foreground |
body/page root | --color-bg/--color-ink |
card/card-foreground |
Card·Dialog | surface/ink |
popover/popover-foreground |
Popover·Dropdown·Tooltip | surface/ink |
primary/primary-foreground |
Button(default)·Tabs active | brand.primary/white |
secondary/secondary-foreground |
Button(secondary)·Badge | neutral-100/ink |
muted/muted-foreground |
Skeleton/disabled/secondary text | neutral-100/neutral-600 |
accent/accent-foreground |
hover background/Calendar | accent/white |
destructive/destructive-foreground |
Button(destructive)·Alert | semantic.danger/white |
border·input·ring |
Global border/Input border/focus ring | neutral-200·neutral-200·primary |
chart-1..5 |
Recharts/data visualization | primary/warning/success/info/danger |
sidebar-* |
Sidebar-only role set | surface/ink/primary/muted mirror |
@theme block template (Tailwind v4)
@import 'tailwindcss';
@theme {
--font-sans: "<BrandFont>", system-ui, sans-serif;
--font-mono: "<Mono>", ui-monospace, monospace;
--color-primary-50: #RRGGBB; /* … 950까지 11단계 */
--color-primary-600: #RRGGBB; /* 라이트 주색 */
--color-primary-500: #RRGGBB; /* 다크 주색 */
--color-success: #RRGGBB; --color-warning: #RRGGBB;
--color-danger: #RRGGBB; --color-info: #RRGGBB;
--radius-sm: 0.5rem; --radius-lg: 1rem; --radius-full: 9999px;
--duration-fast: 150ms; --ease-out: cubic-bezier(0.4, 0, 0.2, 1);
}
:root { /* --base-* 라이트 롤 바인딩 */ }
.dark { /* --base-* 다크 오버라이드 */ }
.dark override rules
- Dark mode is only
.darkclass → CSS variable redefinition.dark:Direct use of Tailwind utilities prohibited ([HARD]). primary: brightness increased (ensuring contrast against dark backgrounds) — a bright value corresponding to the light hover tone.border/input: opaque gray → translucent whitergba(255,255,255,0.06~0.16).chart-1..5: overall brightness increased.gradient: replaced with a brighter stop.- Only override targets are redeclared; the rest inherit from
:root.
Conversion procedure (skeleton)
Forward — L1 → L2 → L3
- L1 load·validation — DTCG JSON parsing,
$typeand other validity checks, group completeness check - L1 → L2 derivation — flatten brand/neutral/semantic tokens into
--color-*/--neutral-*/--tracking-*CSS variables,@font-face·Generate CDN font block- Rule: map the group category as the prefix token, and the last 1–2 segments as the suffix.
color.brand.<x>→--color-<x>(brand removed) ·color.brand.background→--color-bg(alias)color.neutral.<n>→--neutral-<n>·color.semantic.<x>→--color-<x>·color.gradient.<x>→--gradient-<x>typography.family.<x>→--font-<x>·weight.<x>→--fw-<x>·size.<x>→--text-<x>·lineHeight.<x>→--lh-<x>·letterSpacing.<x>→--tracking-<x>spacing.<n>→--space-<n>·radius.<x>→--radius-<x>·shadow.<x>→--shadow-<x>·motion.duration.<x>→--duration-<x>·motion.easing.<x>→--easing-<x>- Aliases (bg·fw·tracking·space·lh) are only those registered in the table — no inference.
- L2 → L3 derivation — bind primitive variables to
--base-*semantic roles,@themeregistration,.darkgenerate override set- brand.primary →
primary·ring·sidebar-primary·sidebar-ring·chart-1 - brand.ink →
foreground·card-foreground·popover-foreground· brand.background →background· brand.surface →card·popover·surface·sidebar - neutral.100 →
muted·secondary·sidebar-accent· neutral.200 →border·input· neutral.600 →muted-foreground - semantic: success→
success+chart-3· warning→warning+accent+chart-2· info→info+chart-4· danger→destructive+error+chart-5 - chart derivation: chart-1=primary · 2=warning|accent · 3=success · 4=info · 5=danger (reuse status colors as data series to ensure palette consistency)
- sidebar derivation: mirror-copy surface/ink/primary/muted → so that the sidebar uses the same palette as the body but is kept as separate roles to allow independent toggling
- brand.primary →
- FROZEN rule verification — encode the prohibited values declared by the source (e.g.,
#000000prohibited → ink replacement), single-signature gradients, letter-spacing rules, etc. as comments at each layer- Mechanism: encode the prohibition rules declared by the source system in each layer's native comment syntax and inject them into the output. The converter does not create prohibited values on its own — if there is no declaration in the source, there is no injection.
- Injection location: L1 = token
$description+ file top-level$description/ L2 = CSS header comment (All tokens FROZEN per <출처>) + fallback value inline comment (/* #000 대체 */,/* 변경 금지 */) / L3 = header comment +[HARD]inline ([HARD] 다크모드는 .dark 클래스로만). - Verification: only cross-check whether the prohibited values declared by the source (e.g.,
#000000) have leaked into the output. - worked example (a list declared by a specific brand — not a universal law):
#000prohibited → ink replacement · no creating arbitrary gradients · comply with letter-spacing rules ·dark:utility prohibited. Other brands come with different prohibition lists.
Reverse — L3/L2 → L1
- Identify source layer — determine whether the input is CSS (L2) or globals.css (L3)
- Token extraction — from CSS variables and
@themedeclarations, reverse-extract values and meanings - DTCG reconstruction — reassemble the extracted values into hierarchical groups,
$descriptionis derived on a best-effort basis (state explicitly when the original source is unknown)- Reverse mapping (invert the forward table but resolve aliases):
--color-primary→color.brand.primary·--color-bg→color.brand.background·--neutral-<n>→color.neutral.<n>·--color-success류→color.semantic.<x>·--gradient-<x>→color.gradient.<x> --fw-<x>→typography.weight.<x>·--text-<x>→size.<x>·--tracking-<x>→letterSpacing.<x>·--space-<n>→spacing.<n>·--duration-<x>→motion.duration.<x>--base-<role>(L3) is an L2→L3 binding and does not correspond directly to an L1 group → restore the source token by back-referencing the binding table (forward 3 steps).$descriptionderivation policy: ① promote inline comments adjacent to the declaration → ② extract the source (guideline/system name) from the file header and use it as a prefix → ③ if it is a standard token, use the category standard description → ④ if all fail, do not invent, state"출처 미상"explicitly.
- Reverse mapping (invert the forward table but resolve aliases):
Round-trip integrity check
- After L1 → L2 → L3 → (reverse) → L1' round trip, compare L1 ≟ L1'
- Color: equality after hex normalization, typography: preserve family/weight/size, letter-spacing: no value loss
- When a mismatch is found, produce a diff report at the layer/token level Normalization rules (applied identically to both values before comparison)
| Item | Normalization | 예 |
|---|---|---|
| hex case | unify to lowercase | #FFF→#fff |
| 3-digit shorthand | expand to 6-digit | #fff→#ffffff |
rgb()/rgba() |
integer channel/alpha normalization, convert to hex if needed | rgb(255,255,255)→#ffffff |
oklch() |
compare after color space conversion (specify tolerance if lossless is impossible) | oklch(…)↔hex |
| dimension | unify units (rem preferred), em preserve letter-spacing form |
4px↔0.25rem |
| font stack | compare preserving order after normalizing whitespace and quotes | |
| gradient | normalize angle/stop/color (colors per the hex rules above) |
diff report format
## 라운드트립 diff (L1 vs L1')
| 왕복 | 토큰 경로 | 원본 | 왕복 후 | 판정 |
|---|---|---|---|---|
| L1→L2→L3→L1' | color.brand.primary | #RRGGBB | #RRGGBB | PASS |
| L1→L2→L3→L1' | typography.letterSpacing.body | -0.025em | -0.025em | PASS |
| L1→L2→L3→L1' | color.gradient.signature | linear-gradient(…) | (스톱 순서 상이) | FAIL |
정합: N PASS / M 불일치
output format
## 토큰 변환 완료 (L{src} → L{dst})
### 변환 요약
- 입력 계층: L{n} ([파일])
- 출력 계층: L{m}
- 변환 토큰: 색 N · 타이포 N · spacing N · radius N · shadow N · motion N
### 생성 파일
- [출력 경로]
### 라운드트립 검사
- 왕복 정합: PASS / N개 불일치
- (불일치 시) 계층·토큰 단위 diff
### FROZEN 규칙 검증
- [위반 없음 / 위반 목록]
usage examples
Example 1 — DTCG → shadcn forward direction (L1 → L3)
입력: 02-tokens.json (L1 DTCG)
동작:
1) color.brand.primary(#RRGGBB) → --color-primary → --base-primary/--base-ring/--base-chart-1
2) @theme에 --color-primary-50..950 등록, :root에 --base-* 롤 바인딩
3) .dark 오버라이드 세트 생성(primary 명도 상향, border 반투명 화이트)
출력: globals.css (@import tailwindcss + @theme + :root + .dark + @layer)
요약: 색 N · 자간 6 · radius 7 · shadow 6 / FROZEN 위반 0
Example 2 — globals.css → DTCG reverse extraction (L3 → L1)
입력: globals.css (L3)
동작:
1) @theme·:root·.dark 변수 선언 스캔
2) --base-* 롤은 바인딩표 역참조로 원천 토큰 복원, --color-*/--neutral-*는 직접 역매핑
3) 인접 주석에서 $description 유도, 불명은 "출처 미상"
출력: 02-tokens.json' (재구성 DTCG)
요약: 복원 토큰 N · $description 유도 N / 출처 미상 M
Example 3 — round-trip check (L1 → L2 → L3 → L1')
입력: 02-tokens.json (L1)
동작: 정규화(hex 소문자·shorthand 확장·rgb→hex) 후 L1 ≟ L1'
출력: diff 리포트
결과: 12/13 PASS, 1 불일치(gradient 스톱 순서) → 계층·토큰 단위 표기
Caveats
Do
- Always treat L1 (DTCG) as the SSOT — L2/L3 are derivatives; when editing manually, update L1 first and then re-derive
- Dark mode only via
.darkclass CSS variable switching — Tailwinddark:utility direct use prohibited - Preserve letter-spacing values losslessly across all layers — the core of Korean typography quality
Don't
- Do not modify only L2/L3 and leave L1 unattended — it will be overwritten on the next re-derivation
#000000Do not let FROZEN prohibited values such as these pass into conversion output- When reverse-extracting, do not arbitrarily invent unclear
$description— if it cannot be derived, state "source unknown"
related skills
| Skill | when to use |
|---|---|
moai-designer:design-handoff-reader |
Prerequisite: extract 3-tier token sources from the bundle |
moai-designer:design-system-prep |
Prerequisite: synthesize assets → DESIGN.md (token source) |
moai-designer:design-system-library |
Auxiliary: use 75 brand tokens as conversion input |
moai-designer:design-sync-upload |
Follow-up: upload converted tokens to Claude Design |