임동욱 임동욱 08-11
Merge branch 'claude/design-system-setup-46d8aa' into hub
Figma 토큰 531개 이식 + JSON→SCSS 변환기 + 카탈로그 재작성.
빈 껍데기였던 @fox/styles가 실제 토큰으로 채워져 빌드가 복구된다.

Co-Authored-By: Claude Opus 5 
@0caa9e1f783b6eabf5638481b1375b22a5a6e81d
 
@fox/styles/_functions.scss (added)
+++ @fox/styles/_functions.scss
@@ -0,0 +1,103 @@
+// 토큰 접근 함수 — 디자인 시스템 규율을 **컴파일러가 강제**하는 지점.
+//
+// 화면 코드는 `color: #256EF4`나 `padding: 13px`를 쓸 수 없고, 반드시
+// `color: fox.color(font-neutral-strong)` / `padding: fox.padding(6)`로 토큰을
+// 지목해야 한다. 존재하지 않는 이름을 쓰면 `@error`로 **빌드가 실패**하고 사용
+// 가능한 토큰 목록이 함께 출력된다.
+//
+// 반환값은 CSS 커스텀 프로퍼티 참조(`var(--fox-*)`)다. 값이 아니라 참조를 돌려주므로
+// (1) 라이트/다크와 pc/모바일이 런타임에 전환되고 (2) 특정 영역만 토큰을 덮어쓰는
+// 스코프 오버라이드가 가능하며 (3) devtools에서 어떤 토큰인지 그대로 보인다.
+
+@use "sass:map";
+@use "tokens";
+
+/// 토큰 이름을 검증하고 커스텀 프로퍼티 참조를 반환한다.
+@function _token($group, $key, $registry, $var-name) {
+  @if not map.has-key($registry, $key) {
+    @error "[@fox] 알 수 없는 #{$group} 토큰: `#{$key}` — 사용 가능한 값: #{map.keys($registry)}";
+  }
+
+  @return var(--fox-#{$var-name});
+}
+
+
+@function color($name) {
+  @return _token("color", $name, tokens.$color, "color-#{$name}");
+}
+
+@function primitive($name) {
+  @return _token("primitive", $name, tokens.$primitive, "primitive-#{$name}");
+}
+
+@function theme($name) {
+  @return _token("theme", $name, tokens.$theme, "theme-#{$name}");
+}
+
+@function number($name) {
+  @return _token("number", $name, tokens.$number, "number-#{$name}");
+}
+
+@function font-family($name) {
+  @return _token("font-family", "family-#{$name}", tokens.$font, "font-family-#{$name}");
+}
+
+@function font-weight($name) {
+  @return _token("font-weight", "weight-#{$name}", tokens.$font, "font-weight-#{$name}");
+}
+
+@function border($name) {
+  @return _token("border", "border-#{$name}", tokens.$size, "border-#{$name}");
+}
+
+@function padding($name) {
+  @return _token("padding", "padding-#{$name}", tokens.$size, "padding-#{$name}");
+}
+
+@function gap($name) {
+  @return _token("gap", "gap-#{$name}", tokens.$size, "gap-#{$name}");
+}
+
+@function radius($name) {
+  @return _token("radius", "radius-#{$name}", tokens.$size, "radius-#{$name}");
+}
+
+@function icon($name) {
+  @return _token("icon", "icon-#{$name}", tokens.$size, "icon-#{$name}");
+}
+
+@function shadow($name) {
+  @return _token("shadow", "shadow-#{$name}", tokens.$size, "shadow-#{$name}");
+}
+
+@function backdrop($name) {
+  @return _token("backdrop", "backdrop-#{$name}", tokens.$size, "backdrop-#{$name}");
+}
+
+@function font-size($name) {
+  @return _token("font-size", "font-size-#{$name}", tokens.$responsive, "font-size-#{$name}");
+}
+
+@function form($name) {
+  @return _token("form", "form-#{$name}", tokens.$responsive, "form-#{$name}");
+}
+
+@function grid($name) {
+  @return _token("grid", "grid-#{$name}", tokens.$responsive, "grid-#{$name}");
+}
+
+@function card($name) {
+  @return _token("card", "card-#{$name}", tokens.$responsive, "card-#{$name}");
+}
+
+@function modal($name) {
+  @return _token("modal", "modal-#{$name}", tokens.$responsive, "modal-#{$name}");
+}
+
+@function spacing($name) {
+  @return _token("spacing", "spacing-#{$name}", tokens.$responsive, "spacing-#{$name}");
+}
+
+@function section($name) {
+  @return _token("section", "section-#{$name}", tokens.$responsive, "section-#{$name}");
+}
 
@fox/styles/_mixins.scss (added)
+++ @fox/styles/_mixins.scss
@@ -0,0 +1,20 @@
+// 믹스인 — 여러 선언이 항상 함께 가야 하는 패턴을 묶는다.
+
+@use "tokens";
+
+/// PC 이상(>= 768px)에서 적용. 모바일 우선이므로 기본 스타일은 밖에 쓴다.
+///
+/// ⚠️ 미디어 쿼리 조건부는 `var()`를 해석하지 못하므로 브레이크포인트만은 CSS 변수가
+/// 아닌 컴파일타임 값이다. 그래서 이 믹스인 경유가 강제된다.
+@mixin pc {
+  @media (min-width: tokens.$breakpoint-pc) {
+    @content;
+  }
+}
+
+/// PC 미만에서만 적용. `pc`와 경계가 겹치지 않도록 0.02px을 뺀다.
+@mixin mobile {
+  @media (max-width: tokens.$breakpoint-pc - 0.02px) {
+    @content;
+  }
+}
 
@fox/styles/_root.scss (added)
+++ @fox/styles/_root.scss
@@ -0,0 +1,71 @@
+// 토큰 CSS 출력 — `tokens/`의 SCSS map을 `--fox-*` 커스텀 프로퍼티로 선언한다.
+//
+// **앱 전체에서 딱 한 번만 로드되어야 한다** (`index.scss` 경유).
+// 컴포넌트의 `.module.scss`에서는 절대 `@use` 하지 않는다.
+//
+// map을 순회해 생성하므로 토큰을 추가할 때 이 파일은 손대지 않는다.
+
+@use "sass:map";
+@use "tokens";
+
+:root {
+  // 기본은 OS 설정 추종. 아래 `[data-theme]` 규칙이 이 값만 덮어써 수동 선택을
+  // 처리하므로, 색상 값 자체는 `tokens/_color.scss` 한 곳에만 존재한다.
+  color-scheme: light dark;
+
+  @each $name, $value in tokens.$primitive {
+    --fox-primitive-#{$name}: #{$value};
+  }
+
+  @each $name, $value in tokens.$font {
+    --fox-font-#{$name}: #{$value};
+  }
+
+  @each $name, $value in tokens.$number {
+    --fox-number-#{$name}: #{$value};
+  }
+
+  @each $name, $value in tokens.$theme {
+    --fox-theme-#{$name}: #{$value};
+  }
+
+  // size / color / responsive 는 이름에 그룹이 이미 들어 있어 접두사가 `--fox-` 뿐이다.
+  @each $name, $value in tokens.$size {
+    --fox-#{$name}: #{$value};
+  }
+
+  @each $name, $value in tokens.$color {
+    --fox-color-#{$name}: #{$value};
+  }
+
+  // 모바일 우선 기본값.
+  @each $name, $value in tokens.$responsive {
+    --fox-#{$name}: #{$value};
+  }
+}
+
+// PC 모드 — 반응형 토큰만 덮어쓴다. 색상은 뷰포트와 무관하므로 여기 오지 않는다.
+@media (min-width: tokens.$breakpoint-pc) {
+  :root {
+    @each $name, $value in tokens.$responsive-pc {
+      --fox-#{$name}: #{$value};
+    }
+  }
+}
+
+// 수동 테마 선택 — `color-scheme`만 고정하면 `light-dark()`가 해당 분기로 확정된다.
+// 속성이 없거나 `"system"`이면 위 `:root`의 `light dark`가 유지되어 OS를 따른다.
+:root[data-theme="light"] {
+  color-scheme: light;
+}
+
+:root[data-theme="dark"] {
+  color-scheme: dark;
+}
+
+// ⚠️ 1rem = 10px 규약의 근거. 브라우저 기본 글자 크기(16px)의 62.5%가 10px다.
+// 모든 토큰의 rem 값이 이 선언을 전제로 계산돼 있다.
+// 미디어 쿼리 안의 rem은 이 값의 영향을 받지 않는다(항상 브라우저 기본 크기 기준).
+html {
+  font-size: 62.5%;
+}
 
@fox/styles/abstracts.scss (added)
+++ @fox/styles/abstracts.scss
@@ -0,0 +1,17 @@
+// 저작(authoring) 진입점 — 컴포넌트의 `.module.scss`가 `@use`하는 파일.
+//
+//   @use "@fox/styles/abstracts" as fox;
+//
+//   .button {
+//     padding: fox.padding(5) fox.padding(7);
+//     background: fox.color(button-primary-surface);
+//     border-radius: fox.radius(3);
+//     font-size: fox.font-size(label-md);
+//   }
+//
+// ⚠️ **이 파일은 CSS를 한 줄도 출력하지 않는다.** 함수·믹스인·map 정의만 있다.
+// 실제 CSS 출력(토큰 선언)은 앱이 딱 한 번 import하는 `index.scss`가 전담한다.
+
+@forward "tokens";
+@forward "functions";
+@forward "mixins";
@fox/styles/index.scss
--- @fox/styles/index.scss
+++ @fox/styles/index.scss
@@ -1,0 +1,10 @@
+// 글로벌 스타일 진입점 — **호스트 앱이 딱 한 번 import한다.**
+//
+//   // app/globals.scss
+//   @use "@fox/styles";
+//
+// 여기서만 CSS가 출력된다. 컴포넌트의 `.module.scss`는 이 파일이 아니라
+// `abstracts.scss`를 `@use`해야 한다 — 그쪽은 CSS 출력이 0이라 몇 번을 참조해도
+// 중복이 생기지 않는다.
+
+@use "root";
 
@fox/styles/tokens/_color.scss (added)
+++ @fox/styles/tokens/_color.scss
@@ -0,0 +1,283 @@
+// ⚠️ 이 파일은 Figma Design Tokens export에서 **자동 생성**되었습니다.
+// 원본: light.tokens.json / dark.tokens.json
+// 손으로 고치지 말고 원본 JSON을 다시 export한 뒤 변환기를 재실행하세요.
+
+// 시맨틱 색상 — 라이트/다크 값을 `light-dark()` 한 줄에 함께 보유한다.
+// 전환은 `_root.scss`의 `color-scheme`이 담당하므로 다크 전용 블록이 없고,
+// 따라서 한쪽만 고쳐서 생기는 테마 불일치가 구조적으로 불가능하다.
+//
+// 값이 `var(--fox-primitive-*)` / `var(--fox-theme-*)`인 항목은 Figma의 alias를
+// 그대로 옮긴 것이다 — 바닥층을 바꾸면 여기까지 전파된다.
+$color: (
+  background-default: light-dark(var(--fox-primitive-neutral-0), var(--fox-primitive-neutral-100)),
+  background-neutral: light-dark(var(--fox-primitive-neutral-5), var(--fox-primitive-neutral-95)),
+  background-neutral-strong: light-dark(var(--fox-primitive-neutral-10), var(--fox-primitive-neutral-90)),
+  background-primary: light-dark(var(--fox-theme-primary-5), var(--fox-theme-primary-95)),
+  background-secondary: light-dark(var(--fox-theme-secondary-5), var(--fox-theme-secondary-95)),
+  background-accent: light-dark(var(--fox-theme-accent-5), var(--fox-theme-accent-95)),
+  background-dim: light-dark(var(--fox-primitive-alpha-black50), var(--fox-primitive-alpha-black75)),
+  background-dim-inverse: light-dark(var(--fox-primitive-alpha-white50), var(--fox-primitive-alpha-black75)),
+  surface-neutral-default: light-dark(var(--fox-primitive-neutral-0), var(--fox-primitive-neutral-95)),
+  surface-neutral-default-strong: light-dark(var(--fox-primitive-neutral-0), var(--fox-primitive-neutral-90)),
+  surface-neutral-default-static: light-dark(var(--fox-primitive-neutral-0), var(--fox-primitive-neutral-0)),
+  surface-neutral-default-inverse: light-dark(var(--fox-primitive-neutral-90), var(--fox-primitive-neutral-10)),
+  surface-neutral-default-inverse-static: light-dark(var(--fox-primitive-neutral-90), var(--fox-primitive-neutral-90)),
+  surface-neutral-gray: light-dark(var(--fox-primitive-neutral-5), var(--fox-primitive-neutral-90)),
+  surface-neutral-gray-strong: light-dark(var(--fox-primitive-neutral-10), var(--fox-primitive-neutral-80)),
+  surface-neutral-gray-stronger: light-dark(var(--fox-primitive-neutral-20), var(--fox-primitive-neutral-70)),
+  surface-neutral-gray-inverse: light-dark(var(--fox-primitive-neutral-80), var(--fox-primitive-neutral-20)),
+  surface-neutral-gray-strong-inverse: light-dark(var(--fox-primitive-neutral-70), var(--fox-primitive-neutral-30)),
+  surface-neutral-gray-stronger-inverse: light-dark(var(--fox-primitive-neutral-60), var(--fox-primitive-neutral-40)),
+  surface-neutral-disabled: light-dark(var(--fox-primitive-neutral-20), var(--fox-primitive-neutral-70)),
+  surface-theme-primary: light-dark(var(--fox-theme-primary-5), var(--fox-theme-primary-95)),
+  surface-theme-primary-inverse: light-dark(var(--fox-theme-primary-60), var(--fox-theme-primary-40)),
+  surface-theme-secondary: light-dark(var(--fox-theme-secondary-5), var(--fox-theme-secondary-95)),
+  surface-theme-secondary-inverse: light-dark(var(--fox-theme-secondary-60), var(--fox-theme-secondary-40)),
+  surface-theme-accent: light-dark(var(--fox-theme-accent-5), var(--fox-theme-accent-95)),
+  surface-theme-accent-inverse: light-dark(var(--fox-theme-accent-60), var(--fox-theme-accent-40)),
+  surface-system-danger: light-dark(var(--fox-primitive-danger-5), var(--fox-primitive-danger-95)),
+  surface-system-danger-inverse: light-dark(var(--fox-primitive-danger-60), var(--fox-primitive-danger-40)),
+  surface-system-warning: light-dark(var(--fox-primitive-warning-5), var(--fox-primitive-warning-95)),
+  surface-system-warning-inverse: light-dark(var(--fox-primitive-warning-60), var(--fox-primitive-warning-40)),
+  surface-system-success: light-dark(var(--fox-primitive-success-5), var(--fox-primitive-success-95)),
+  surface-system-success-inverse: light-dark(var(--fox-primitive-success-60), var(--fox-primitive-success-40)),
+  surface-system-information: light-dark(var(--fox-primitive-information-5), var(--fox-primitive-information-95)),
+  surface-system-information-inverse: light-dark(var(--fox-primitive-information-60), var(--fox-primitive-information-40)),
+  border-neutral-default: light-dark(var(--fox-primitive-neutral-30), var(--fox-primitive-neutral-70)),
+  border-neutral-subtle: light-dark(var(--fox-primitive-neutral-20), var(--fox-primitive-neutral-80)),
+  border-neutral-subtler: light-dark(var(--fox-primitive-neutral-10), var(--fox-primitive-neutral-90)),
+  border-neutral-strong: light-dark(var(--fox-primitive-neutral-90), var(--fox-primitive-neutral-0)),
+  border-neutral-disabled: light-dark(var(--fox-primitive-neutral-30), var(--fox-primitive-neutral-70)),
+  border-neutral-overlay: light-dark(var(--fox-primitive-alpha-black10), var(--fox-primitive-alpha-white25)),
+  border-theme-primary: light-dark(var(--fox-theme-primary-50), var(--fox-theme-primary-50)),
+  border-theme-primary-inverse: light-dark(var(--fox-theme-primary-10), var(--fox-theme-primary-90)),
+  border-theme-secondary: light-dark(var(--fox-theme-secondary-60), var(--fox-theme-secondary-40)),
+  border-theme-secondary-inverse: light-dark(var(--fox-theme-secondary-10), var(--fox-theme-secondary-90)),
+  border-theme-accent: light-dark(var(--fox-theme-accent-50), var(--fox-theme-accent-50)),
+  border-theme-accent-inverse: light-dark(var(--fox-theme-accent-10), var(--fox-theme-accent-90)),
+  border-system-danger: light-dark(var(--fox-primitive-danger-50), var(--fox-primitive-danger-50)),
+  border-system-danger-inverse: light-dark(var(--fox-primitive-danger-10), var(--fox-primitive-danger-90)),
+  border-system-warning: light-dark(var(--fox-primitive-warning-50), var(--fox-primitive-warning-50)),
+  border-system-warning-inverse: light-dark(var(--fox-primitive-warning-10), var(--fox-primitive-warning-90)),
+  border-system-success: light-dark(var(--fox-primitive-success-50), var(--fox-primitive-success-50)),
+  border-system-success-inverse: light-dark(var(--fox-primitive-success-10), var(--fox-primitive-success-90)),
+  border-system-information: light-dark(var(--fox-primitive-information-50), var(--fox-primitive-information-50)),
+  border-system-information-inverse: light-dark(var(--fox-primitive-information-10), var(--fox-primitive-information-90)),
+  font-neutral-strong: light-dark(var(--fox-primitive-neutral-90), var(--fox-primitive-neutral-5)),
+  font-neutral-default: light-dark(var(--fox-primitive-neutral-70), var(--fox-primitive-neutral-20)),
+  font-neutral-subtle: light-dark(var(--fox-primitive-neutral-50), var(--fox-primitive-neutral-30)),
+  font-neutral-subtler: light-dark(var(--fox-primitive-neutral-40), var(--fox-primitive-neutral-40)),
+  font-neutral-strong-inverse: light-dark(var(--fox-primitive-alpha-white100), var(--fox-primitive-neutral-95)),
+  font-neutral-default-inverse: light-dark(var(--fox-primitive-alpha-white75), var(--fox-primitive-neutral-90)),
+  font-neutral-subtle-inverse: light-dark(var(--fox-primitive-alpha-white50), var(--fox-primitive-neutral-80)),
+  font-neutral-subtler-inverse: light-dark(var(--fox-primitive-alpha-white25), var(--fox-primitive-neutral-70)),
+  font-neutral-static: light-dark(var(--fox-primitive-neutral-90), var(--fox-primitive-neutral-90)),
+  font-neutral-static-inverse: light-dark(var(--fox-primitive-neutral-0), var(--fox-primitive-neutral-0)),
+  font-neutral-disabled: light-dark(var(--fox-primitive-neutral-40), var(--fox-primitive-neutral-60)),
+  font-neutral-disabled-strong: light-dark(var(--fox-primitive-neutral-50), var(--fox-primitive-neutral-50)),
+  font-theme-primary: light-dark(var(--fox-theme-primary-60), var(--fox-theme-primary-20)),
+  font-theme-primary-inverse: light-dark(var(--fox-theme-primary-10), var(--fox-theme-primary-70)),
+  font-theme-secondary: light-dark(var(--fox-theme-secondary-60), var(--fox-theme-secondary-20)),
+  font-theme-secondary-inverse: light-dark(var(--fox-theme-secondary-10), var(--fox-theme-secondary-70)),
+  font-theme-accent: light-dark(var(--fox-theme-accent-60), var(--fox-theme-accent-20)),
+  font-theme-accent-inverse: light-dark(var(--fox-theme-accent-10), var(--fox-theme-accent-70)),
+  font-system-danger: light-dark(var(--fox-primitive-danger-60), var(--fox-primitive-danger-20)),
+  font-system-danger-inverse: light-dark(var(--fox-primitive-danger-10), var(--fox-primitive-danger-70)),
+  font-system-warning: light-dark(var(--fox-primitive-warning-60), var(--fox-primitive-warning-20)),
+  font-system-warning-inverse: light-dark(var(--fox-primitive-warning-10), var(--fox-primitive-warning-70)),
+  font-system-success: light-dark(var(--fox-primitive-success-60), var(--fox-primitive-success-20)),
+  font-system-success-inverse: light-dark(var(--fox-primitive-success-10), var(--fox-primitive-success-70)),
+  font-system-information: light-dark(var(--fox-primitive-information-60), var(--fox-primitive-information-20)),
+  font-system-information-inverse: light-dark(var(--fox-primitive-information-10), var(--fox-primitive-information-70)),
+  icon-neutral-strong: light-dark(var(--fox-primitive-neutral-90), var(--fox-primitive-neutral-5)),
+  icon-neutral-default: light-dark(var(--fox-primitive-neutral-70), var(--fox-primitive-neutral-20)),
+  icon-neutral-subtle: light-dark(var(--fox-primitive-neutral-50), var(--fox-primitive-neutral-30)),
+  icon-neutral-subtler: light-dark(var(--fox-primitive-neutral-40), var(--fox-primitive-neutral-40)),
+  icon-neutral-fill: light-dark(var(--fox-primitive-alpha-black25), var(--fox-primitive-alpha-white25)),
+  icon-neutral-strong-inverse: light-dark(var(--fox-primitive-alpha-white100), var(--fox-primitive-neutral-95)),
+  icon-neutral-default-inverse: light-dark(var(--fox-primitive-alpha-white75), var(--fox-primitive-neutral-90)),
+  icon-neutral-subtle-inverse: light-dark(var(--fox-primitive-alpha-white50), var(--fox-primitive-neutral-80)),
+  icon-neutral-subtler-inverse: light-dark(var(--fox-primitive-alpha-white25), var(--fox-primitive-neutral-70)),
+  icon-neutral-fill-inverse: light-dark(var(--fox-primitive-alpha-white25), var(--fox-primitive-neutral-70)),
+  icon-neutral-static: light-dark(var(--fox-primitive-neutral-90), var(--fox-primitive-neutral-90)),
+  icon-neutral-static-inverse: light-dark(var(--fox-primitive-neutral-0), var(--fox-primitive-neutral-0)),
+  icon-neutral-disabled: light-dark(var(--fox-primitive-neutral-40), var(--fox-primitive-neutral-60)),
+  icon-neutral-disabled-strong: light-dark(var(--fox-primitive-neutral-50), var(--fox-primitive-neutral-50)),
+  icon-theme-primary: light-dark(var(--fox-theme-primary-50), var(--fox-theme-primary-20)),
+  icon-theme-primary-strong: light-dark(var(--fox-theme-primary-60), var(--fox-theme-primary-30)),
+  icon-theme-primary-static: light-dark(var(--fox-theme-primary-50), var(--fox-theme-primary-50)),
+  icon-theme-primary-inverse: light-dark(var(--fox-theme-primary-10), var(--fox-theme-primary-70)),
+  icon-theme-secondary: light-dark(var(--fox-theme-secondary-60), var(--fox-theme-secondary-30)),
+  icon-theme-secondary-strong: light-dark(var(--fox-theme-secondary-70), var(--fox-theme-secondary-40)),
+  icon-theme-secondary-static: light-dark(var(--fox-theme-secondary-50), var(--fox-theme-secondary-50)),
+  icon-theme-secondary-inverse: light-dark(var(--fox-theme-secondary-10), var(--fox-theme-secondary-70)),
+  icon-theme-accent: light-dark(var(--fox-theme-accent-50), var(--fox-theme-accent-20)),
+  icon-theme-accent-strong: light-dark(var(--fox-theme-accent-60), var(--fox-theme-accent-30)),
+  icon-theme-accent-static: light-dark(var(--fox-theme-accent-50), var(--fox-theme-accent-50)),
+  icon-theme-accent-inverse: light-dark(var(--fox-theme-accent-10), var(--fox-theme-accent-70)),
+  icon-system-danger: light-dark(var(--fox-primitive-danger-50), var(--fox-primitive-danger-20)),
+  icon-system-danger-strong: light-dark(var(--fox-primitive-danger-60), var(--fox-primitive-danger-30)),
+  icon-system-danger-inverse: light-dark(var(--fox-primitive-danger-10), var(--fox-primitive-danger-70)),
+  icon-system-warning: light-dark(var(--fox-primitive-warning-50), var(--fox-primitive-warning-20)),
+  icon-system-warning-strong: light-dark(var(--fox-primitive-warning-60), var(--fox-primitive-warning-30)),
+  icon-system-warning-inverse: light-dark(var(--fox-primitive-warning-10), var(--fox-primitive-warning-70)),
+  icon-system-success: light-dark(var(--fox-primitive-success-50), var(--fox-primitive-success-20)),
+  icon-system-success-strong: light-dark(var(--fox-primitive-success-60), var(--fox-primitive-success-30)),
+  icon-system-success-inverse: light-dark(var(--fox-primitive-success-10), var(--fox-primitive-success-70)),
+  icon-system-information: light-dark(var(--fox-primitive-information-50), var(--fox-primitive-information-20)),
+  icon-system-information-strong: light-dark(var(--fox-primitive-information-60), var(--fox-primitive-information-30)),
+  icon-system-information-inverse: light-dark(var(--fox-primitive-information-10), var(--fox-primitive-information-70)),
+  element-neutral-strong: light-dark(var(--fox-primitive-neutral-60), var(--fox-primitive-neutral-40)),
+  element-neutral-default: light-dark(var(--fox-primitive-neutral-50), var(--fox-primitive-neutral-50)),
+  element-neutral-subtle: light-dark(var(--fox-primitive-neutral-10), var(--fox-primitive-neutral-90)),
+  element-neutral-subtler: light-dark(var(--fox-primitive-neutral-5), var(--fox-primitive-neutral-95)),
+  element-neutral-inverse: light-dark(var(--fox-primitive-neutral-0), var(--fox-primitive-neutral-95)),
+  element-neutral-inverse-static: light-dark(var(--fox-primitive-neutral-0), var(--fox-primitive-neutral-0)),
+  element-neutral-alpha: light-dark(rgb(0 0 0 / 0.25), rgb(0 0 0 / 0.5)),
+  element-neutral-alpha-inverse: light-dark(rgb(255 255 255 / 0.5), rgb(255 255 255 / 0.25)),
+  element-neutral-disabled: light-dark(var(--fox-primitive-neutral-20), var(--fox-primitive-neutral-80)),
+  element-neutral-disabled-strong: light-dark(var(--fox-primitive-neutral-40), var(--fox-primitive-neutral-60)),
+  element-theme-primary: light-dark(var(--fox-theme-primary-50), var(--fox-theme-primary-50)),
+  element-theme-primary-strong: light-dark(var(--fox-theme-primary-60), var(--fox-theme-primary-40)),
+  element-theme-primary-subtle: light-dark(var(--fox-theme-primary-10), var(--fox-theme-primary-90)),
+  element-theme-primary-subtler: light-dark(var(--fox-theme-primary-5), var(--fox-theme-primary-95)),
+  element-theme-secondary: light-dark(var(--fox-theme-secondary-70), var(--fox-theme-secondary-60)),
+  element-theme-secondary-strong: light-dark(var(--fox-theme-secondary-70), var(--fox-theme-secondary-60)),
+  element-theme-secondary-subtle: light-dark(var(--fox-theme-secondary-10), var(--fox-theme-secondary-90)),
+  element-theme-secondary-subtler: light-dark(var(--fox-theme-secondary-5), var(--fox-theme-secondary-95)),
+  element-theme-accent: light-dark(var(--fox-theme-accent-50), var(--fox-theme-accent-50)),
+  element-theme-accent-strong: light-dark(var(--fox-theme-accent-60), var(--fox-theme-accent-40)),
+  element-theme-accent-subtle: light-dark(var(--fox-theme-accent-10), var(--fox-theme-accent-90)),
+  element-theme-accent-subtler: light-dark(var(--fox-theme-accent-5), var(--fox-theme-accent-95)),
+  element-system-danger: light-dark(var(--fox-primitive-danger-50), var(--fox-primitive-danger-50)),
+  element-system-danger-strong: light-dark(var(--fox-primitive-danger-60), var(--fox-primitive-danger-40)),
+  element-system-danger-inverse: light-dark(var(--fox-primitive-danger-5), var(--fox-primitive-danger-95)),
+  element-system-warning: light-dark(var(--fox-primitive-warning-30), var(--fox-primitive-warning-30)),
+  element-system-warning-strong: light-dark(var(--fox-primitive-warning-50), var(--fox-primitive-warning-20)),
+  element-system-warning-inverse: light-dark(var(--fox-primitive-warning-5), var(--fox-primitive-warning-95)),
+  element-system-success: light-dark(var(--fox-primitive-success-50), var(--fox-primitive-success-50)),
+  element-system-success-strong: light-dark(var(--fox-primitive-success-60), var(--fox-primitive-success-40)),
+  element-system-success-inverse: light-dark(var(--fox-primitive-success-5), var(--fox-primitive-success-95)),
+  element-system-information: light-dark(var(--fox-primitive-information-50), var(--fox-primitive-information-50)),
+  element-system-information-strong: light-dark(var(--fox-primitive-information-60), var(--fox-primitive-information-40)),
+  element-system-information-inverse: light-dark(var(--fox-primitive-information-5), var(--fox-primitive-information-95)),
+  alpha-default100: light-dark(var(--fox-primitive-alpha-white100), var(--fox-primitive-alpha-black100)),
+  alpha-default75: light-dark(var(--fox-primitive-alpha-white75), var(--fox-primitive-alpha-black75)),
+  alpha-default50: light-dark(var(--fox-primitive-alpha-white50), var(--fox-primitive-alpha-black50)),
+  alpha-default25: light-dark(var(--fox-primitive-alpha-white25), var(--fox-primitive-alpha-black25)),
+  alpha-default10: light-dark(var(--fox-primitive-alpha-white10), var(--fox-primitive-alpha-black10)),
+  alpha-default0: light-dark(var(--fox-primitive-alpha-white0), var(--fox-primitive-alpha-black0)),
+  alpha-default-inverse100: light-dark(var(--fox-primitive-alpha-black100), var(--fox-primitive-alpha-white100)),
+  alpha-default-inverse75: light-dark(var(--fox-primitive-alpha-black75), var(--fox-primitive-alpha-white75)),
+  alpha-default-inverse50: light-dark(var(--fox-primitive-alpha-black50), var(--fox-primitive-alpha-white50)),
+  alpha-default-inverse25: light-dark(var(--fox-primitive-alpha-black25), var(--fox-primitive-alpha-white25)),
+  alpha-default-inverse10: light-dark(var(--fox-primitive-alpha-black10), var(--fox-primitive-alpha-white10)),
+  alpha-default-inverse0: light-dark(var(--fox-primitive-alpha-black0), var(--fox-primitive-alpha-white0)),
+  action-default: light-dark(var(--fox-primitive-neutral-0), var(--fox-primitive-neutral-100)),
+  action-default-hover: light-dark(var(--fox-primitive-neutral-5), var(--fox-primitive-neutral-95)),
+  action-default-pressed: light-dark(var(--fox-primitive-neutral-10), var(--fox-primitive-neutral-90)),
+  action-default-selected: light-dark(var(--fox-primitive-neutral-10), var(--fox-primitive-neutral-90)),
+  action-default-active: light-dark(var(--fox-primitive-neutral-70), var(--fox-primitive-neutral-60)),
+  action-neutral: light-dark(var(--fox-primitive-neutral-5), var(--fox-primitive-neutral-95)),
+  action-neutral-hover: light-dark(var(--fox-primitive-neutral-10), var(--fox-primitive-neutral-90)),
+  action-neutral-pressed: light-dark(var(--fox-primitive-neutral-20), var(--fox-primitive-neutral-80)),
+  action-neutral-selected: light-dark(var(--fox-primitive-neutral-20), var(--fox-primitive-neutral-80)),
+  action-neutral-active: light-dark(var(--fox-primitive-neutral-70), var(--fox-primitive-neutral-60)),
+  action-primary: light-dark(var(--fox-primitive-alpha-white0), var(--fox-primitive-alpha-black0)),
+  action-primary-hover: light-dark(var(--fox-theme-primary-5), var(--fox-theme-primary-95)),
+  action-primary-pressed: light-dark(var(--fox-theme-primary-10), var(--fox-theme-primary-90)),
+  action-primary-selected: light-dark(var(--fox-theme-primary-5), var(--fox-theme-primary-95)),
+  action-primary-active: light-dark(var(--fox-theme-primary-50), var(--fox-theme-primary-50)),
+  action-secondary: light-dark(var(--fox-primitive-alpha-white0), var(--fox-primitive-alpha-black0)),
+  action-secondary-hover: light-dark(var(--fox-theme-secondary-5), var(--fox-theme-secondary-95)),
+  action-secondary-pressed: light-dark(var(--fox-theme-secondary-10), var(--fox-theme-secondary-90)),
+  action-secondary-selected: light-dark(var(--fox-theme-secondary-5), var(--fox-theme-secondary-90)),
+  action-secondary-active: light-dark(var(--fox-theme-secondary-70), var(--fox-theme-secondary-60)),
+  button-primary-surface: light-dark(var(--fox-theme-primary-50), var(--fox-theme-primary-50)),
+  button-primary-surface-hover: light-dark(var(--fox-theme-primary-60), var(--fox-theme-primary-60)),
+  button-primary-surface-pressed: light-dark(var(--fox-theme-primary-70), var(--fox-theme-primary-70)),
+  button-primary-border: light-dark(var(--fox-primitive-alpha-black0), var(--fox-primitive-alpha-black0)),
+  button-primary-font: light-dark(var(--fox-primitive-alpha-white100), var(--fox-primitive-alpha-white100)),
+  button-secondary-surface: light-dark(var(--fox-theme-primary-5), var(--fox-theme-primary-95)),
+  button-secondary-surface-hover: light-dark(var(--fox-theme-primary-10), var(--fox-theme-primary-90)),
+  button-secondary-surface-pressed: light-dark(var(--fox-theme-primary-20), var(--fox-theme-primary-80)),
+  button-secondary-border: light-dark(var(--fox-theme-primary-30), var(--fox-theme-primary-70)),
+  button-secondary-font: light-dark(var(--fox-theme-primary-60), var(--fox-theme-primary-30)),
+  button-default-surface: light-dark(var(--fox-primitive-alpha-white0), var(--fox-primitive-alpha-black0)),
+  button-default-surface-hover: light-dark(var(--fox-primitive-neutral-5), var(--fox-primitive-neutral-95)),
+  button-default-surface-pressed: light-dark(var(--fox-primitive-neutral-10), var(--fox-primitive-neutral-80)),
+  button-default-border: light-dark(var(--fox-primitive-neutral-30), var(--fox-primitive-neutral-70)),
+  button-default-font: light-dark(var(--fox-primitive-neutral-70), var(--fox-primitive-neutral-20)),
+  button-text-surface: light-dark(var(--fox-primitive-alpha-white0), var(--fox-primitive-alpha-black0)),
+  button-text-surface-hover: light-dark(var(--fox-theme-secondary-5), var(--fox-theme-secondary-95)),
+  button-text-surface-pressed: light-dark(var(--fox-theme-secondary-10), var(--fox-theme-secondary-90)),
+  button-text-visited: light-dark(#5917B8, #C49FF9),
+  button-disabled-surface: light-dark(var(--fox-primitive-neutral-20), var(--fox-primitive-neutral-80)),
+  button-disabled-border: light-dark(var(--fox-primitive-neutral-30), var(--fox-primitive-neutral-70)),
+  button-disabled-font: light-dark(var(--fox-primitive-neutral-50), var(--fox-primitive-neutral-50)),
+  input-surface: light-dark(var(--fox-primitive-neutral-0), var(--fox-primitive-neutral-90)),
+  input-surface-disabled: light-dark(var(--fox-primitive-neutral-20), var(--fox-primitive-neutral-95)),
+  input-border: light-dark(var(--fox-primitive-neutral-60), var(--fox-primitive-neutral-40)),
+  input-border-active: light-dark(var(--fox-theme-primary-50), var(--fox-theme-primary-50)),
+  input-border-error: light-dark(var(--fox-primitive-danger-50), var(--fox-primitive-danger-50)),
+  input-border-success: light-dark(var(--fox-primitive-success-50), var(--fox-primitive-success-50)),
+  input-border-disabled: light-dark(var(--fox-primitive-neutral-30), var(--fox-primitive-neutral-70)),
+  chart-default-bar: light-dark(var(--fox-theme-primary-20), var(--fox-theme-primary-70)),
+  chart-default-surface: light-dark(var(--fox-theme-primary-10), var(--fox-theme-primary-70)),
+  chart-default-line: light-dark(var(--fox-theme-primary-50), var(--fox-theme-primary-70)),
+  chart-default-disabled: light-dark(var(--fox-primitive-neutral-20), var(--fox-primitive-neutral-70)),
+  chart-level-1: light-dark(var(--fox-theme-primary-90), var(--fox-theme-primary-10)),
+  chart-level-2: light-dark(var(--fox-theme-primary-70), var(--fox-theme-primary-20)),
+  chart-level-3: light-dark(var(--fox-theme-primary-50), var(--fox-theme-primary-40)),
+  chart-level-4: light-dark(var(--fox-theme-primary-40), var(--fox-theme-primary-50)),
+  chart-level-5: light-dark(var(--fox-theme-primary-20), var(--fox-theme-primary-70)),
+  chart-level-6: light-dark(var(--fox-theme-primary-10), var(--fox-theme-primary-90)),
+  chart-level-7: light-dark(var(--fox-primitive-neutral-60), var(--fox-primitive-neutral-10)),
+  chart-level-8: light-dark(var(--fox-primitive-neutral-40), var(--fox-primitive-neutral-20)),
+  chart-level-9: light-dark(var(--fox-primitive-neutral-20), var(--fox-primitive-neutral-40)),
+  chart-level-10: light-dark(var(--fox-primitive-neutral-10), var(--fox-primitive-neutral-60)),
+  chart-type-1-bar: light-dark(var(--fox-theme-primary-20), var(--fox-theme-primary-50)),
+  chart-type-1-line: light-dark(var(--fox-theme-primary-40), var(--fox-theme-primary-30)),
+  chart-type-1-surface: light-dark(var(--fox-theme-primary-5), var(--fox-theme-primary-95)),
+  chart-type-2-bar: light-dark(var(--fox-theme-accent-20), var(--fox-theme-accent-50)),
+  chart-type-2-line: light-dark(var(--fox-theme-accent-30), var(--fox-theme-accent-30)),
+  chart-type-2-surface: light-dark(var(--fox-theme-accent-5), var(--fox-theme-accent-95)),
+  chart-type-3-bar: light-dark(var(--fox-theme-secondary-20), var(--fox-theme-secondary-50)),
+  chart-type-3-line: light-dark(var(--fox-theme-secondary-30), var(--fox-theme-secondary-30)),
+  chart-type-3-surface: light-dark(var(--fox-theme-secondary-5), var(--fox-theme-secondary-95)),
+  chart-type-4-bar: light-dark(#FFDF94, #DEB451),
+  chart-type-4-line: light-dark(#FBC35A, #FBC35A),
+  chart-type-4-surface: light-dark(#FFF7E9, #2F2D28),
+  chart-type-5-bar: light-dark(#FF9F85, #D96645),
+  chart-type-5-line: light-dark(#FF7C57, #FF7C57),
+  chart-type-5-surface: light-dark(#FFECE7, #3F3330),
+  chart-alt-level-1: light-dark(#002D31, #D4F4F7),
+  chart-alt-level-2: light-dark(#005A62, #9EE7EE),
+  chart-alt-level-3: light-dark(#00828E, #1EBCCB),
+  chart-alt-level-4: light-dark(#1EBCCB, #00828E),
+  chart-alt-level-5: light-dark(#9EE7EE, #005A62),
+  chart-alt-level-6: light-dark(#D4F4F7, #002D31),
+  chart-alt-level-7: light-dark(var(--fox-primitive-neutral-60), var(--fox-primitive-neutral-10)),
+  chart-alt-level-8: light-dark(var(--fox-primitive-neutral-40), var(--fox-primitive-neutral-20)),
+  chart-alt-level-9: light-dark(var(--fox-primitive-neutral-20), var(--fox-primitive-neutral-40)),
+  chart-alt-level-10: light-dark(var(--fox-primitive-neutral-10), var(--fox-primitive-neutral-60)),
+  chart-alt-default-bar: light-dark(#9EE7EE, #005A62),
+  chart-alt-default-surface: light-dark(#D4F4F7, #005A62),
+  chart-alt-default-line: light-dark(#00828E, #005A62),
+  chart-alt-default-disabled: light-dark(var(--fox-primitive-neutral-20), var(--fox-primitive-neutral-70)),
+  chart-alt-type-1-bar: light-dark(#9EE7EE, #00828E),
+  chart-alt-type-1-line: light-dark(#1EBCCB, #57D2DE),
+  chart-alt-type-1-surface: light-dark(#E9FAFC, #001A1C),
+  chart-alt-type-2-bar: light-dark(#CDE566, #5E8100),
+  chart-alt-type-2-line: light-dark(#A7CC00, #A7CC00),
+  chart-alt-type-2-surface: light-dark(#F2F9D9, #121900),
+  chart-alt-type-3-bar: light-dark(#97CBED, #00577D),
+  chart-alt-type-3-line: light-dark(#52A6D3, #52A6D3),
+  chart-alt-type-3-surface: light-dark(#E8F4FC, #00080C),
+  chart-alt-type-4-bar: light-dark(#FFDF94, #DEB451),
+  chart-alt-type-4-line: light-dark(#FBC35A, #FBC35A),
+  chart-alt-type-4-surface: light-dark(#FFF7E9, #2F2D28),
+  chart-alt-type-5-bar: light-dark(#FF9F85, #D96645),
+  chart-alt-type-5-line: light-dark(#FF7C57, #FF7C57),
+  chart-alt-type-5-surface: light-dark(#FFECE7, #3F3330)
+);
 
@fox/styles/tokens/_index.scss (added)
+++ @fox/styles/tokens/_index.scss
@@ -0,0 +1,10 @@
+// 토큰 map 모음 — **CSS를 한 줄도 출력하지 않는다.**
+// 값 선언(이 폴더)과 CSS 출력(`../_root.scss`)을 분리해 두었기 때문에, 컴포넌트의
+// `.module.scss`가 몇 개든 `abstracts`를 `@use`해도 `:root` 블록이 중복 출력되지 않는다
+// (각 CSS Module은 별도 컴파일 단위라 CSS를 내보내는 모듈을 공유하면 파일마다 복제된다).
+
+@forward "primitive";
+@forward "theme";
+@forward "size";
+@forward "color";
+@forward "responsive";
 
@fox/styles/tokens/_primitive.scss (added)
+++ @fox/styles/tokens/_primitive.scss
@@ -0,0 +1,112 @@
+// ⚠️ 이 파일은 Figma Design Tokens export에서 **자동 생성**되었습니다.
+// 원본: primitive.tokens.json
+// 손으로 고치지 말고 원본 JSON을 다시 export한 뒤 변환기를 재실행하세요.
+
+// 원시 팔레트 — 화면에서 직접 쓰지 않는다. 시맨틱 토큰(`$color`)이 참조하는 바닥층이다.
+$primitive: (
+  neutral-0: #FFFFFF,
+  neutral-5: #F4F5F6,
+  neutral-10: #E6E8EA,
+  neutral-20: #CDD1D5,
+  neutral-30: #B1B8BE,
+  neutral-40: #8A949E,
+  neutral-50: #6D7882,
+  neutral-60: #58616A,
+  neutral-70: #464C53,
+  neutral-80: #33363D,
+  neutral-90: #1E2124,
+  neutral-95: #131416,
+  neutral-100: #000000,
+  danger-5: #FDEFEC,
+  danger-10: #FCDFD9,
+  danger-20: #F7AFA1,
+  danger-30: #F48771,
+  danger-40: #F05F42,
+  danger-50: #DE3412,
+  danger-60: #BD2C0F,
+  danger-70: #8A240F,
+  danger-80: #5C180A,
+  danger-90: #390D05,
+  danger-95: #260903,
+  warning-5: #FFF3DB,
+  warning-10: #FFE0A3,
+  warning-20: #FFC95C,
+  warning-30: #FFB114,
+  warning-40: #C78500,
+  warning-50: #9E6A00,
+  warning-60: #8A5C00,
+  warning-70: #614100,
+  warning-80: #422C00,
+  warning-90: #2E1F00,
+  warning-95: #241800,
+  success-5: #EAF6EC,
+  success-10: #D8EEDD,
+  success-20: #A9DAB4,
+  success-30: #7EC88E,
+  success-40: #3FA654,
+  success-50: #228738,
+  success-60: #267337,
+  success-70: #285D33,
+  success-80: #1F4727,
+  success-90: #122B18,
+  success-95: #0E2012,
+  information-5: #E7F4FE,
+  information-10: #D3EBFD,
+  information-20: #9ED2FA,
+  information-30: #5FB5F7,
+  information-40: #2098F3,
+  information-50: #0B78CB,
+  information-60: #096AB3,
+  information-70: #085691,
+  information-80: #053961,
+  information-90: #03253F,
+  information-95: #021A2C,
+  alpha-black100: #000000,
+  alpha-black75: rgb(0 0 0 / 0.75),
+  alpha-black50: rgb(0 0 0 / 0.5),
+  alpha-black25: rgb(0 0 0 / 0.25),
+  alpha-black10: rgb(0 0 0 / 0.1),
+  alpha-black0: rgb(0 0 0 / 0),
+  alpha-white100: #FFFFFF,
+  alpha-white75: rgb(255 255 255 / 0.75),
+  alpha-white50: rgb(255 255 255 / 0.5),
+  alpha-white25: rgb(255 255 255 / 0.25),
+  alpha-white10: rgb(255 255 255 / 0.1),
+  alpha-white0: rgb(255 255 255 / 0)
+);
+
+// 서체 — family는 문자열, weight는 무단위 수치(길이가 아니므로 rem 변환 대상이 아니다).
+$font: (
+  family-title: Pretendard,
+  family-body: Pretendard,
+  weight-regular: 400,
+  weight-medium: 600,
+  weight-bold: 800
+);
+
+// 원시 수치 스케일 — size 컬렉션이 참조하는 값들. 1rem = 10px 기준으로 변환됨.
+$number: (
+  0: 0,
+  1: 0.1rem,
+  2: 0.2rem,
+  3: 0.4rem,
+  4: 0.6rem,
+  5: 0.8rem,
+  6: 1rem,
+  7: 1.2rem,
+  8: 1.6rem,
+  9: 2rem,
+  10: 2.4rem,
+  11: 2.8rem,
+  12: 3.2rem,
+  13: 3.6rem,
+  14: 4rem,
+  15: 4.4rem,
+  16: 4.8rem,
+  17: 5.6rem,
+  18: 6.4rem,
+  19: 7.2rem,
+  20: 8rem,
+  21: 9.6rem,
+  max: 100rem
+);
 
@fox/styles/tokens/_responsive.scss (added)
+++ @fox/styles/tokens/_responsive.scss
@@ -0,0 +1,163 @@
+// ⚠️ 이 파일은 Figma Design Tokens export에서 **자동 생성**되었습니다.
+// 원본: mobile.tokens.json / pc.tokens.json
+// 손으로 고치지 말고 원본 JSON을 다시 export한 뒤 변환기를 재실행하세요.
+
+// 뷰포트에 따라 달라지는 크기 토큰. 키는 두 모드가 완전히 동일하다.
+// 모바일 우선 — `$responsive`가 기본값이고 `$responsive-pc`가 브레이크포인트 이상에서 덮는다.
+$breakpoint-pc: 768px;
+
+$responsive: (
+  font-size-display-lg: 4.4rem,
+  font-size-display-md: 3.2rem,
+  font-size-display-sm: 2.8rem,
+  font-size-heading-xlg: 2.8rem,
+  font-size-heading-lg: 2.4rem,
+  font-size-heading-md: 2rem,
+  font-size-heading-sm: 1.7rem,
+  font-size-heading-xsm: 1.5rem,
+  font-size-heading-2xsm: 1.3rem,
+  font-size-body-lg: 1.5rem,
+  font-size-body-md: 1.3rem,
+  font-size-body-sm: 1.2rem,
+  font-size-body-xsm: 1rem,
+  font-size-label-lg: 1.5rem,
+  font-size-label-md: 1.3rem,
+  font-size-label-sm: 1.2rem,
+  font-size-label-xsm: 1rem,
+  form-radius-xsm: 0.4rem,
+  form-radius-sm: 0.4rem,
+  form-radius-md: 0.8rem,
+  form-radius-lg: 1.2rem,
+  form-padding-lg: 2rem,
+  form-padding-md: 1.2rem,
+  form-padding-sm: 0.8rem,
+  form-padding-xsm: 0.4rem,
+  form-icon-lg: 2rem,
+  form-icon-md: 1.6rem,
+  form-icon-sm: 1.2rem,
+  form-icon-xsm: 1.2rem,
+  form-height-xsm: 2rem,
+  form-height-sm: 2.8rem,
+  form-height-md: 4rem,
+  form-height-lg: 4.8rem,
+  grid-wrap-full: 38rem,
+  grid-wrap-default: 34.8rem,
+  grid-wrap-sm: 32rem,
+  grid-wrap-xsm: 28rem,
+  grid-margin-full: 0,
+  grid-margin-default: 1.6rem,
+  grid-margin-sm: 3rem,
+  grid-margin-xsm: 5rem,
+  grid-gutter-sm: 0.8rem,
+  grid-gutter-md: 1.6rem,
+  grid-gutter-lg: 2rem,
+  grid-gutter-xlg: 2rem,
+  grid-gutter-2xlg: 4rem,
+  card-radius-xlg: 1.6rem,
+  card-radius-lg: 1.6rem,
+  card-radius-md: 1.2rem,
+  card-radius-sm: 0.8rem,
+  card-padding-xlg: 2.4rem,
+  card-padding-lg: 2rem,
+  card-padding-md: 1.6rem,
+  card-padding-sm: 1.2rem,
+  modal-width-full: 38rem,
+  modal-width-lg: 34.8rem,
+  modal-width-md: 34.8rem,
+  modal-width-sm: 34.8rem,
+  modal-padding-default: 2.4rem,
+  modal-btn-close: 5.6rem,
+  spacing-top-2xlg: 8rem,
+  spacing-top-xlg: 5.6rem,
+  spacing-top-lg: 3.2rem,
+  spacing-top-md: 2rem,
+  spacing-top-sm: 1.2rem,
+  spacing-top-none: 0,
+  spacing-bottom-xlg: 4.8rem,
+  spacing-bottom-lg: 2.8rem,
+  spacing-bottom-md: 2rem,
+  spacing-bottom-sm: 1.2rem,
+  spacing-bottom-none: 0,
+  section-spacing-lg: 8rem,
+  section-spacing-md: 4rem,
+  section-spacing-sm: 2.4rem,
+  section-spacing-xsm: 1.6rem
+);
+
+$responsive-pc: (
+  font-size-display-lg: 4rem,
+  font-size-display-md: 3.2rem,
+  font-size-display-sm: 2.6rem,
+  font-size-heading-xlg: 3.2rem,
+  font-size-heading-lg: 2.6rem,
+  font-size-heading-md: 2rem,
+  font-size-heading-sm: 1.7rem,
+  font-size-heading-xsm: 1.5rem,
+  font-size-heading-2xsm: 1.3rem,
+  font-size-body-lg: 1.7rem,
+  font-size-body-md: 1.5rem,
+  font-size-body-sm: 1.3rem,
+  font-size-body-xsm: 1.1rem,
+  font-size-label-lg: 1.7rem,
+  font-size-label-md: 1.5rem,
+  font-size-label-sm: 1.3rem,
+  font-size-label-xsm: 1.1rem,
+  form-radius-xsm: 0.4rem,
+  form-radius-sm: 0.4rem,
+  form-radius-md: 0.8rem,
+  form-radius-lg: 1.2rem,
+  form-padding-lg: 2rem,
+  form-padding-md: 1.2rem,
+  form-padding-sm: 0.8rem,
+  form-padding-xsm: 0.8rem,
+  form-icon-lg: 2.4rem,
+  form-icon-md: 2rem,
+  form-icon-sm: 1.6rem,
+  form-icon-xsm: 1.2rem,
+  form-height-xsm: 2.4rem,
+  form-height-sm: 2.8rem,
+  form-height-md: 4rem,
+  form-height-lg: 5.6rem,
+  grid-wrap-full: 192rem,
+  grid-wrap-default: 158.4rem,
+  grid-wrap-sm: 108rem,
+  grid-wrap-xsm: 80rem,
+  grid-margin-full: 0,
+  grid-margin-default: 4.8rem,
+  grid-margin-sm: 42rem,
+  grid-margin-xsm: 56rem,
+  grid-gutter-sm: 0.8rem,
+  grid-gutter-md: 1.6rem,
+  grid-gutter-lg: 2.4rem,
+  grid-gutter-xlg: 4rem,
+  grid-gutter-2xlg: 6.4rem,
+  card-radius-xlg: 2.4rem,
+  card-radius-lg: 2.4rem,
+  card-radius-md: 1.6rem,
+  card-radius-sm: 1.2rem,
+  card-padding-xlg: 4rem,
+  card-padding-lg: 3.2rem,
+  card-padding-md: 2.4rem,
+  card-padding-sm: 1.6rem,
+  modal-width-full: 192rem,
+  modal-width-lg: 76rem,
+  modal-width-md: 56rem,
+  modal-width-sm: 40rem,
+  modal-padding-default: 4rem,
+  modal-btn-close: 6.4rem,
+  spacing-top-2xlg: 8rem,
+  spacing-top-xlg: 6.4rem,
+  spacing-top-lg: 4rem,
+  spacing-top-md: 2.4rem,
+  spacing-top-sm: 1.6rem,
+  spacing-top-none: 0,
+  spacing-bottom-xlg: 3.6rem,
+  spacing-bottom-lg: 2.8rem,
+  spacing-bottom-md: 2rem,
+  spacing-bottom-sm: 1.2rem,
+  spacing-bottom-none: 0,
+  section-spacing-lg: 12rem,
+  section-spacing-md: 8rem,
+  section-spacing-sm: 4rem,
+  section-spacing-xsm: 2.4rem
+);
 
@fox/styles/tokens/_size.scss (added)
+++ @fox/styles/tokens/_size.scss
@@ -0,0 +1,62 @@
+// ⚠️ 이 파일은 Figma Design Tokens export에서 **자동 생성**되었습니다.
+// 원본: size.tokens.json
+// 손으로 고치지 말고 원본 JSON을 다시 export한 뒤 변환기를 재실행하세요.
+
+// 모드와 무관한 크기 스케일. 값은 1rem = 10px 기준 rem.
+$size: (
+  border-0: 0,
+  border-1: 0.1rem,
+  border-2: 0.2rem,
+  border-3: 0.4rem,
+  padding-1: 0.2rem,
+  padding-2: 0.4rem,
+  padding-3: 0.8rem,
+  padding-4: 1rem,
+  padding-5: 1.2rem,
+  padding-6: 1.6rem,
+  padding-7: 2rem,
+  padding-8: 2.4rem,
+  padding-9: 3.2rem,
+  padding-10: 4rem,
+  padding-11: 6.4rem,
+  gap-1: 0.2rem,
+  gap-2: 0.4rem,
+  gap-3: 0.8rem,
+  gap-4: 1.2rem,
+  gap-5: 1.6rem,
+  gap-6: 2rem,
+  gap-7: 2.4rem,
+  gap-8: 3.2rem,
+  gap-9: 4rem,
+  gap-10: 4.8rem,
+  gap-11: 6.4rem,
+  gap-12: 8rem,
+  radius-1: 0.2rem,
+  radius-2: 0.4rem,
+  radius-3: 0.8rem,
+  radius-4: 1.2rem,
+  radius-5: 1.6rem,
+  radius-6: 2.4rem,
+  radius-7: 4rem,
+  radius-max: 100rem,
+  icon-1: 0.8rem,
+  icon-2: 1.2rem,
+  icon-3: 1.6rem,
+  icon-4: 2rem,
+  icon-5: 2.4rem,
+  icon-6: 3.2rem,
+  icon-7: 4rem,
+  shadow-location1: 0.1rem,
+  shadow-location2: 0.2rem,
+  shadow-location3: 0.4rem,
+  shadow-location4: 0.8rem,
+  shadow-location5: 1.6rem,
+  shadow-blur1: 0.2rem,
+  shadow-blur2: 0.4rem,
+  shadow-blur3: 0.8rem,
+  shadow-blur4: 1.6rem,
+  shadow-blur5: 3.2rem,
+  backdrop-blur1: 1rem,
+  backdrop-blur2: 2.4rem,
+  backdrop-blur3: 4rem
+);
 
@fox/styles/tokens/_theme.scss (added)
+++ @fox/styles/tokens/_theme.scss
@@ -0,0 +1,40 @@
+// ⚠️ 이 파일은 Figma Design Tokens export에서 **자동 생성**되었습니다.
+// 원본: theme.tokens.json
+// 손으로 고치지 말고 원본 JSON을 다시 export한 뒤 변환기를 재실행하세요.
+
+// 브랜드 램프 — primary / secondary / accent.
+$theme: (
+  primary-5: #ECF2FE,
+  primary-10: #D8E5FD,
+  primary-20: #B1CEFB,
+  primary-30: #86AFF9,
+  primary-40: #4C87F6,
+  primary-50: #256EF4,
+  primary-60: #0B50D0,
+  primary-70: #083891,
+  primary-80: #052561,
+  primary-90: #03163A,
+  primary-95: #020F27,
+  secondary-5: #EEF2F7,
+  secondary-10: #D6E0EB,
+  secondary-20: #BACBDE,
+  secondary-30: #90B0D5,
+  secondary-40: #6B96C7,
+  secondary-50: #346FB2,
+  secondary-60: #1C589C,
+  secondary-70: #063A74,
+  secondary-80: #052B57,
+  secondary-90: #031F3F,
+  secondary-95: #02162C,
+  accent-5: #FBEFF0,
+  accent-10: #F5D6D9,
+  accent-20: #EBADB2,
+  accent-30: #E0858C,
+  accent-40: #D65C66,
+  accent-50: #D63D4A,
+  accent-60: #AB2B36,
+  accent-70: #7A1F26,
+  accent-80: #521419,
+  accent-90: #310C0F,
+  accent-95: #21080A
+);
 
@fox/tools/build-tokens.py (added)
+++ @fox/tools/build-tokens.py
@@ -0,0 +1,486 @@
+"""Figma Design Tokens(JSON) → @fox/styles SCSS 토큰 변환기.
+
+입력: $FOX_TOKENS_SRC/*.tokens.json (기본 ~/Downloads/mode · 7개, Figma 컬렉션 5개/모드 7개)
+출력: @fox/styles/tokens/*.scss + _root.scss + _functions.scss + _mixins.scss
+
+실행: python3 @fox/tools/build-tokens.py
+
+규칙
+- 길이 값은 1rem = 10px 기준으로 value/10 → rem. font.weight만 무단위.
+- alpha < 1 색상은 rgb(R G B / A)로 출력(hex는 알파를 담지 못한다).
+- Figma의 alias(참조)는 CSS에서도 var() 참조로 보존한다 — 원본 계층이 유지되어
+  primitive를 바꾸면 semantic까지 전파된다. 단, 참조 대상이 제공된 파일에 없거나
+  값이 일치하지 않으면 리터럴로 떨어뜨린다(무결성 우선).
+"""
+
+import json
+import os
+from collections import OrderedDict
+
+SRC = os.environ.get("FOX_TOKENS_SRC", os.path.expanduser("~/Downloads/mode"))
+OUT = os.environ.get("FOX_OUT", os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "styles"))
+
+FILES = {
+    "primitive": "primitive.tokens.json",
+    "theme": "theme.tokens.json",
+    "size": "size.tokens.json",
+    "light": "light.tokens.json",
+    "dark": "dark.tokens.json",
+    "pc": "pc.tokens.json",
+    "mobile": "mobile.tokens.json",
+}
+
+
+def leaves(node, path=""):
+    if isinstance(node, dict):
+        if "$type" in node:
+            yield path, node
+        else:
+            for k, v in node.items():
+                if k == "$extensions":
+                    continue
+                yield from leaves(v, f"{path}.{k}" if path else k)
+
+
+raw = {}
+for key, fname in FILES.items():
+    doc = json.load(open(os.path.join(SRC, fname), encoding="utf-8"))
+    raw[key] = OrderedDict(leaves(doc))
+
+
+def resolve_refs(table):
+    """같은 파일 안의 {a.b.c} 참조를 실제 값으로 치환한다."""
+    for path, node in table.items():
+        val = node["$value"]
+        seen = set()
+        while isinstance(val, str) and val.startswith("{") and val.endswith("}"):
+            target = val[1:-1]
+            if target in seen or target not in table:
+                raise SystemExit(f"참조 해결 실패: {path} -> {val}")
+            seen.add(target)
+            val = table[target]["$value"]
+        node["$value"] = val
+
+
+for table in raw.values():
+    resolve_refs(table)
+
+
+def css_color(value):
+    comps = value["components"]
+    alpha = value.get("alpha", 1)
+    if alpha >= 1:
+        return value["hex"].upper()
+    r, g, b = (round(c * 255) for c in comps)
+    a = round(alpha, 4)
+    return f"rgb({r} {g} {b} / {a})"
+
+
+def css_len(value):
+    """px 수치를 rem으로. 1rem = 10px."""
+    if value == 0:
+        return "0"
+    rem = value / 10
+    text = f"{rem:.10f}".rstrip("0").rstrip(".")
+    return f"{text}rem"
+
+
+# ── 변수명 매핑 ───────────────────────────────────────────────────────────────
+def name_primitive(path):
+    p = path.split(".")
+    if p[0] == "color":
+        return f"primitive-{p[1]}-{p[2]}"
+    if p[0] == "font":
+        return f"font-{p[1]}-{p[2]}"
+    if p[0] == "number":
+        return f"number-{p[1]}"
+    raise SystemExit(f"primitive 이름 매핑 없음: {path}")
+
+
+def name_theme(path):
+    p = path.split(".")
+    assert p[0] == "color"
+    return f"theme-{p[1]}-{p[2]}"
+
+
+def name_size(path):
+    p = path.split(".")
+    if p[0] == "effect":
+        return f"{p[1]}-{p[2]}"          # effect.shadow.blur3 → shadow-blur3
+    return "-".join(p)                    # padding.6 → padding-6
+
+
+CHART_ALT_GROUP = {}
+for k in [str(i) for i in range(1, 11)]:
+    CHART_ALT_GROUP[k] = f"chart-alt-level-{k}"
+for k in ("bar", "surface", "line", "disabled"):
+    CHART_ALT_GROUP[k] = f"chart-alt-default-{k}"
+for i in range(1, 6):
+    for s in ("bar", "line", "surface"):
+        CHART_ALT_GROUP[f"{i}-{s}"] = f"chart-alt-type-{i}-{s}"
+
+
+def name_semantic(path):
+    if path in CHART_ALT_GROUP:
+        return f"color-{CHART_ALT_GROUP[path]}"
+    p = path.split(".")
+    assert p[0] == "color", path
+    return "color-" + "-".join(p[1:])
+
+
+def name_responsive(path):
+    p = path.split(".")
+    if p[0] == "font" and p[1] == "size":
+        return "font-size-" + "-".join(p[2:])    # font.size.body.md → font-size-body-md
+    return "-".join(p)                            # form.height.md → form-height-md
+
+
+# ── alias 역참조 테이블 (Figma 변수명 → 우리 CSS 변수명) ──────────────────────
+alias_lookup = {}          # (컬렉션명, "color/neutral/0") → (varname, 원본값)
+for coll, namer in (("primitive", name_primitive), ("theme", name_theme), ("size", name_size)):
+    for path, node in raw[coll].items():
+        alias_lookup[(coll, path.replace(".", "/"))] = (namer(path), node["$value"])
+
+
+def same_value(a, b):
+    if isinstance(a, dict) and isinstance(b, dict):
+        return css_color(a) == css_color(b)
+    return a == b
+
+
+stats = {"var": 0, "literal": 0, "no-alias": 0}
+
+
+def color_expr(node):
+    """alias가 검증되면 var() 참조, 아니면 리터럴 색상."""
+    ext = node.get("$extensions", {})
+    alias = ext.get("com.figma.aliasData")
+    if alias:
+        key = (alias["targetVariableSetName"], alias["targetVariableName"])
+        hit = alias_lookup.get(key)
+        if hit and same_value(hit[1], node["$value"]):
+            stats["var"] += 1
+            return f"var(--fox-{hit[0]})"
+        stats["literal"] += 1
+    else:
+        stats["no-alias"] += 1
+    return css_color(node["$value"])
+
+
+# ── 맵 생성 ──────────────────────────────────────────────────────────────────
+def build(entries):
+    return OrderedDict(entries)
+
+
+primitive_map, font_map, number_map = OrderedDict(), OrderedDict(), OrderedDict()
+for path, node in raw["primitive"].items():
+    name = name_primitive(path)
+    if name.startswith("primitive-"):
+        primitive_map[name[len("primitive-"):]] = css_color(node["$value"])
+    elif name.startswith("font-"):
+        v = node["$value"]
+        font_map[name[len("font-"):]] = v if isinstance(v, (str, int)) else css_color(v)
+    else:
+        number_map[name[len("number-"):]] = css_len(node["$value"])
+
+theme_map = OrderedDict(
+    (name_theme(p)[len("theme-"):], css_color(n["$value"])) for p, n in raw["theme"].items()
+)
+
+size_map = OrderedDict((name_size(p), css_len(n["$value"])) for p, n in raw["size"].items())
+
+# 시맨틱 색상: light/dark 를 light-dark() 한 줄로.
+# 소속이 불분명해 chart-alt로 묶은 29개는 뒤로 보낸다 — 원본 JSON에서는 이들이 루트에
+# 먼저 오지만, 순서에 의미가 없고 카탈로그 첫 화면이 주요 색상이어야 읽기 쉽다.
+color_map = OrderedDict()
+ordered_light = sorted(raw["light"].items(), key=lambda kv: kv[0] in CHART_ALT_GROUP)
+for path, lnode in ordered_light:
+    dnode = raw["dark"][path]
+    key = name_semantic(path)[len("color-"):]
+    color_map[key] = f"light-dark({color_expr(lnode)}, {color_expr(dnode)})"
+
+# 반응형: 모바일 기본 + pc 오버라이드
+resp_mobile = OrderedDict(
+    (name_responsive(p), css_len(n["$value"])) for p, n in raw["mobile"].items()
+)
+resp_pc = OrderedDict((name_responsive(p), css_len(n["$value"])) for p, n in raw["pc"].items())
+assert list(resp_mobile) == list(resp_pc)
+
+BREAKPOINT_PC = "768px"
+
+
+# ── SCSS 출력 ────────────────────────────────────────────────────────────────
+def scss_map(name, mapping, indent="  "):
+    lines = [f"${name}: ("]
+    items = list(mapping.items())
+    for i, (k, v) in enumerate(items):
+        comma = "," if i < len(items) - 1 else ""
+        key = f'"{k}"' if not k.replace("-", "").replace("_", "").isalnum() else k
+        lines.append(f"{indent}{key}: {v}{comma}")
+    lines.append(");")
+    return "\n".join(lines)
+
+
+HEADER = """// ⚠️ 이 파일은 Figma Design Tokens export에서 **자동 생성**되었습니다.
+// 원본: {src}
+// 손으로 고치지 말고 원본 JSON을 다시 export한 뒤 변환기를 재실행하세요.
+"""
+
+
+def write(rel, body):
+    path = os.path.join(OUT, rel)
+    os.makedirs(os.path.dirname(path), exist_ok=True)
+    open(path, "w", encoding="utf-8").write(body)
+    return rel
+
+
+written = []
+
+written.append(write("tokens/_primitive.scss", HEADER.format(src="primitive.tokens.json") + f"""
+// 원시 팔레트 — 화면에서 직접 쓰지 않는다. 시맨틱 토큰(`$color`)이 참조하는 바닥층이다.
+{scss_map("primitive", primitive_map)}
+
+// 서체 — family는 문자열, weight는 무단위 수치(길이가 아니므로 rem 변환 대상이 아니다).
+{scss_map("font", font_map)}
+
+// 원시 수치 스케일 — size 컬렉션이 참조하는 값들. 1rem = 10px 기준으로 변환됨.
+{scss_map("number", number_map)}
+"""))
+
+written.append(write("tokens/_theme.scss", HEADER.format(src="theme.tokens.json") + f"""
+// 브랜드 램프 — primary / secondary / accent.
+{scss_map("theme", theme_map)}
+"""))
+
+written.append(write("tokens/_size.scss", HEADER.format(src="size.tokens.json") + f"""
+// 모드와 무관한 크기 스케일. 값은 1rem = 10px 기준 rem.
+{scss_map("size", size_map)}
+"""))
+
+written.append(write("tokens/_color.scss", HEADER.format(src="light.tokens.json / dark.tokens.json") + f"""
+// 시맨틱 색상 — 라이트/다크 값을 `light-dark()` 한 줄에 함께 보유한다.
+// 전환은 `_root.scss`의 `color-scheme`이 담당하므로 다크 전용 블록이 없고,
+// 따라서 한쪽만 고쳐서 생기는 테마 불일치가 구조적으로 불가능하다.
+//
+// 값이 `var(--fox-primitive-*)` / `var(--fox-theme-*)`인 항목은 Figma의 alias를
+// 그대로 옮긴 것이다 — 바닥층을 바꾸면 여기까지 전파된다.
+{scss_map("color", color_map)}
+"""))
+
+written.append(write("tokens/_responsive.scss", HEADER.format(src="mobile.tokens.json / pc.tokens.json") + f"""
+// 뷰포트에 따라 달라지는 크기 토큰. 키는 두 모드가 완전히 동일하다.
+// 모바일 우선 — `$responsive`가 기본값이고 `$responsive-pc`가 브레이크포인트 이상에서 덮는다.
+$breakpoint-pc: {BREAKPOINT_PC};
+
+{scss_map("responsive", resp_mobile)}
+
+{scss_map("responsive-pc", resp_pc)}
+"""))
+
+written.append(write("tokens/_index.scss", """// 토큰 map 모음 — **CSS를 한 줄도 출력하지 않는다.**
+// 값 선언(이 폴더)과 CSS 출력(`../_root.scss`)을 분리해 두었기 때문에, 컴포넌트의
+// `.module.scss`가 몇 개든 `abstracts`를 `@use`해도 `:root` 블록이 중복 출력되지 않는다
+// (각 CSS Module은 별도 컴파일 단위라 CSS를 내보내는 모듈을 공유하면 파일마다 복제된다).
+
+@forward "primitive";
+@forward "theme";
+@forward "size";
+@forward "color";
+@forward "responsive";
+"""))
+
+# _root.scss
+root_lines = []
+root_lines.append("""// 토큰 CSS 출력 — `tokens/`의 SCSS map을 `--fox-*` 커스텀 프로퍼티로 선언한다.
+//
+// **앱 전체에서 딱 한 번만 로드되어야 한다** (`index.scss` 경유).
+// 컴포넌트의 `.module.scss`에서는 절대 `@use` 하지 않는다.
+//
+// map을 순회해 생성하므로 토큰을 추가할 때 이 파일은 손대지 않는다.
+
+@use "sass:map";
+@use "tokens";
+
+:root {
+  // 기본은 OS 설정 추종. 아래 `[data-theme]` 규칙이 이 값만 덮어써 수동 선택을
+  // 처리하므로, 색상 값 자체는 `tokens/_color.scss` 한 곳에만 존재한다.
+  color-scheme: light dark;
+""")
+for var_prefix, map_name in (
+    ("primitive", "primitive"),
+    ("font", "font"),
+    ("number", "number"),
+    ("theme", "theme"),
+):
+    root_lines.append(f"""  @each $name, $value in tokens.${map_name} {{
+    --fox-{var_prefix}-#{{$name}}: #{{$value}};
+  }}
+""")
+root_lines.append("""  // size / color / responsive 는 이름에 그룹이 이미 들어 있어 접두사가 `--fox-` 뿐이다.
+  @each $name, $value in tokens.$size {
+    --fox-#{$name}: #{$value};
+  }
+
+  @each $name, $value in tokens.$color {
+    --fox-color-#{$name}: #{$value};
+  }
+
+  // 모바일 우선 기본값.
+  @each $name, $value in tokens.$responsive {
+    --fox-#{$name}: #{$value};
+  }
+}
+
+// PC 모드 — 반응형 토큰만 덮어쓴다. 색상은 뷰포트와 무관하므로 여기 오지 않는다.
+@media (min-width: tokens.$breakpoint-pc) {
+  :root {
+    @each $name, $value in tokens.$responsive-pc {
+      --fox-#{$name}: #{$value};
+    }
+  }
+}
+
+// 수동 테마 선택 — `color-scheme`만 고정하면 `light-dark()`가 해당 분기로 확정된다.
+// 속성이 없거나 `"system"`이면 위 `:root`의 `light dark`가 유지되어 OS를 따른다.
+:root[data-theme="light"] {
+  color-scheme: light;
+}
+
+:root[data-theme="dark"] {
+  color-scheme: dark;
+}
+
+// ⚠️ 1rem = 10px 규약의 근거. 브라우저 기본 글자 크기(16px)의 62.5%가 10px다.
+// 모든 토큰의 rem 값이 이 선언을 전제로 계산돼 있다.
+// 미디어 쿼리 안의 rem은 이 값의 영향을 받지 않는다(항상 브라우저 기본 크기 기준).
+html {
+  font-size: 62.5%;
+}
+""")
+written.append(write("_root.scss", "\n".join(root_lines)))
+
+# _functions.scss
+GROUPS = [
+    # (함수명, 대상 map, 키 접두사(없으면 None), CSS 변수 접두사)
+    ("color", "color", None, "color-"),
+    ("primitive", "primitive", None, "primitive-"),
+    ("theme", "theme", None, "theme-"),
+    ("number", "number", None, "number-"),
+    ("font-family", "font", "family", "font-"),
+    ("font-weight", "font", "weight", "font-"),
+    ("border", "size", "border", ""),
+    ("padding", "size", "padding", ""),
+    ("gap", "size", "gap", ""),
+    ("radius", "size", "radius", ""),
+    ("icon", "size", "icon", ""),
+    ("shadow", "size", "shadow", ""),
+    ("backdrop", "size", "backdrop", ""),
+    ("font-size", "responsive", "font-size", ""),
+    ("form", "responsive", "form", ""),
+    ("grid", "responsive", "grid", ""),
+    ("card", "responsive", "card", ""),
+    ("modal", "responsive", "modal", ""),
+    ("spacing", "responsive", "spacing", ""),
+    ("section", "responsive", "section", ""),
+]
+
+fn_lines = ["""// 토큰 접근 함수 — 디자인 시스템 규율을 **컴파일러가 강제**하는 지점.
+//
+// 화면 코드는 `color: #256EF4`나 `padding: 13px`를 쓸 수 없고, 반드시
+// `color: fox.color(font-neutral-strong)` / `padding: fox.padding(6)`로 토큰을
+// 지목해야 한다. 존재하지 않는 이름을 쓰면 `@error`로 **빌드가 실패**하고 사용
+// 가능한 토큰 목록이 함께 출력된다.
+//
+// 반환값은 CSS 커스텀 프로퍼티 참조(`var(--fox-*)`)다. 값이 아니라 참조를 돌려주므로
+// (1) 라이트/다크와 pc/모바일이 런타임에 전환되고 (2) 특정 영역만 토큰을 덮어쓰는
+// 스코프 오버라이드가 가능하며 (3) devtools에서 어떤 토큰인지 그대로 보인다.
+
+@use "sass:map";
+@use "tokens";
+
+/// 토큰 이름을 검증하고 커스텀 프로퍼티 참조를 반환한다.
+@function _token($group, $key, $registry, $var-name) {
+  @if not map.has-key($registry, $key) {
+    @error "[@fox] 알 수 없는 #{$group} 토큰: `#{$key}` — 사용 가능한 값: #{map.keys($registry)}";
+  }
+
+  @return var(--fox-#{$var-name});
+}
+"""]
+for fn, mapname, prefix, varprefix in GROUPS:
+    if prefix is None:
+        fn_lines.append(f"""
+@function {fn}($name) {{
+  @return _token("{fn}", $name, tokens.${mapname}, "{varprefix}#{{$name}}");
+}}""")
+    else:
+        fn_lines.append(f"""
+@function {fn}($name) {{
+  @return _token("{fn}", "{prefix}-#{{$name}}", tokens.${mapname}, "{varprefix}{prefix}-#{{$name}}");
+}}""")
+written.append(write("_functions.scss", "\n".join(fn_lines) + "\n"))
+
+written.append(write("_mixins.scss", f"""// 믹스인 — 여러 선언이 항상 함께 가야 하는 패턴을 묶는다.
+
+@use "tokens";
+
+/// PC 이상(>= {BREAKPOINT_PC})에서 적용. 모바일 우선이므로 기본 스타일은 밖에 쓴다.
+///
+/// ⚠️ 미디어 쿼리 조건부는 `var()`를 해석하지 못하므로 브레이크포인트만은 CSS 변수가
+/// 아닌 컴파일타임 값이다. 그래서 이 믹스인 경유가 강제된다.
+@mixin pc {{
+  @media (min-width: tokens.$breakpoint-pc) {{
+    @content;
+  }}
+}}
+
+/// PC 미만에서만 적용. `pc`와 경계가 겹치지 않도록 0.02px을 뺀다.
+@mixin mobile {{
+  @media (max-width: tokens.$breakpoint-pc - 0.02px) {{
+    @content;
+  }}
+}}
+"""))
+
+written.append(write("abstracts.scss", """// 저작(authoring) 진입점 — 컴포넌트의 `.module.scss`가 `@use`하는 파일.
+//
+//   @use "@fox/styles/abstracts" as fox;
+//
+//   .button {
+//     padding: fox.padding(5) fox.padding(7);
+//     background: fox.color(button-primary-surface);
+//     border-radius: fox.radius(3);
+//     font-size: fox.font-size(label-md);
+//   }
+//
+// ⚠️ **이 파일은 CSS를 한 줄도 출력하지 않는다.** 함수·믹스인·map 정의만 있다.
+// 실제 CSS 출력(토큰 선언)은 앱이 딱 한 번 import하는 `index.scss`가 전담한다.
+
+@forward "tokens";
+@forward "functions";
+@forward "mixins";
+"""))
+
+written.append(write("index.scss", """// 글로벌 스타일 진입점 — **호스트 앱이 딱 한 번 import한다.**
+//
+//   // app/globals.scss
+//   @use "@fox/styles";
+//
+// 여기서만 CSS가 출력된다. 컴포넌트의 `.module.scss`는 이 파일이 아니라
+// `abstracts.scss`를 `@use`해야 한다 — 그쪽은 CSS 출력이 0이라 몇 번을 참조해도
+// 중복이 생기지 않는다.
+
+@use "root";
+"""))
+
+print("생성된 파일:")
+for w in written:
+    print("  ", w)
+print()
+print(f"토큰 수 — primitive {len(primitive_map)} / font {len(font_map)} / number {len(number_map)}"
+      f" / theme {len(theme_map)} / size {len(size_map)} / color {len(color_map)}"
+      f" / responsive {len(resp_mobile)}")
+print(f"색상 표현 — var() 참조 {stats['var']} · 리터럴(참조 검증 실패) {stats['literal']}"
+      f" · alias 없음 {stats['no-alias']}")
+total = len(primitive_map) + len(font_map) + len(number_map) + len(theme_map) + len(size_map) + len(color_map) + len(resp_mobile)
+print(f"총 CSS 커스텀 프로퍼티: {total}")
app/design/_components/token-catalog.tsx
--- app/design/_components/token-catalog.tsx
+++ app/design/_components/token-catalog.tsx
@@ -2,9 +2,44 @@
 
 import type { CSSProperties } from "react";
 import { useTokenRegistry } from "../_hooks/use-token-registry";
-import { typoPresetNames, type TokenEntry } from "../_lib/read-tokens";
+import type { TokenEntry } from "../_lib/read-tokens";
 import { ThemeSwitch } from "./theme-switch";
 import styles from "../design.module.scss";
+
+/**
+ * 렌더 방식만 다를 뿐, 목록은 전부 CSSOM에서 읽는다 — 여기에 토큰 이름을 적지 않는다.
+ * `kind`는 그 그룹을 어떻게 보여줄지만 정한다: 색은 스와치, 길이는 막대, 나머지는 값만.
+ */
+const GROUPS: { key: string; label: string; kind: "swatch" | "bar" | "value"; note?: string }[] = [
+  {
+    key: "color",
+    label: "시맨틱 색상",
+    kind: "swatch",
+    note: "light/dark 값이 light-dark() 한 줄에 함께 정의돼 있습니다. 위 전환기로 두 테마를 대조해 보세요.",
+  },
+  { key: "theme", label: "브랜드 램프", kind: "swatch", note: "primary · secondary · accent" },
+  {
+    key: "primitive",
+    label: "원시 팔레트",
+    kind: "swatch",
+    note: "화면에서 직접 쓰지 않는 바닥층입니다. 시맨틱 색상이 이 값들을 참조합니다.",
+  },
+  { key: "font", label: "서체", kind: "value", note: "size 계열은 뷰포트에 따라 값이 바뀝니다." },
+  { key: "padding", label: "패딩", kind: "bar" },
+  { key: "gap", label: "간격", kind: "bar" },
+  { key: "radius", label: "모서리 반경", kind: "bar" },
+  { key: "icon", label: "아이콘 크기", kind: "bar" },
+  { key: "border", label: "테두리 두께", kind: "bar" },
+  { key: "number", label: "원시 수치", kind: "bar" },
+  { key: "shadow", label: "그림자 수치", kind: "value" },
+  { key: "backdrop", label: "배경 흐림", kind: "value" },
+  { key: "form", label: "폼", kind: "value" },
+  { key: "card", label: "카드", kind: "value" },
+  { key: "modal", label: "모달", kind: "value" },
+  { key: "grid", label: "그리드", kind: "value" },
+  { key: "spacing", label: "여백", kind: "value" },
+  { key: "section", label: "섹션", kind: "value" },
+];
 
 /**
  * ⚠️ 이 파일은 인라인 `style`을 쓰는 **유일하게 허용되는 예외**다. 카탈로그는 토큰
@@ -17,41 +52,59 @@
   return { [property]: `var(${token.property})` } as CSSProperties;
 }
 
-function typoRef(preset: string): CSSProperties {
-  return {
-    fontSize: `var(--fox-typo-${preset}-size)`,
-    fontWeight: `var(--fox-typo-${preset}-weight)`,
-    lineHeight: `var(--fox-typo-${preset}-line-height)`,
-    letterSpacing: `var(--fox-typo-${preset}-letter-spacing)`,
-  };
+function Swatches({ tokens, fn }: { tokens: TokenEntry[]; fn: string }) {
+  return (
+    <div className={styles.grid}>
+      {tokens.map((token) => (
+        <div key={token.property} className={styles.swatch}>
+          <div className={styles.swatchChip} style={ref("background", token)} />
+          <div className={styles.swatchMeta}>
+            <span className={styles.name}>
+              fox.{fn}({token.name})
+            </span>
+            <span className={styles.ref}>{token.value}</span>
+          </div>
+        </div>
+      ))}
+    </div>
+  );
 }
 
-function ValueTable({ rows }: { rows: { label: string; value: string }[] }) {
+function ValueTable({
+  tokens,
+  fn,
+  withBar,
+}: {
+  tokens: TokenEntry[];
+  fn: string;
+  withBar: boolean;
+}) {
   return (
     <table className={styles.table}>
       <thead>
         <tr>
           <th scope="col">토큰</th>
           <th scope="col">값</th>
+          {withBar ? <th scope="col">크기</th> : null}
         </tr>
       </thead>
       <tbody>
-        {rows.map((row) => (
-          <tr key={row.label}>
-            <td>{row.label}</td>
-            <td>{row.value}</td>
+        {tokens.map((token) => (
+          <tr key={token.property}>
+            <td>
+              fox.{fn}({token.name})
+            </td>
+            <td className={styles.tdValue}>{token.value}</td>
+            {withBar ? (
+              <td>
+                <span className={styles.bar} style={ref("inlineSize", token)} />
+              </td>
+            ) : null}
           </tr>
         ))}
       </tbody>
     </table>
   );
-}
-
-function rows(group: TokenEntry[] | undefined, fn: string) {
-  return (group ?? []).map((token) => ({
-    label: `fox.${fn}(${token.name})`,
-    value: token.value,
-  }));
 }
 
 export function TokenCatalog() {
@@ -67,11 +120,7 @@
     );
   }
 
-  const colors = registry.color ?? [];
-  const spaces = registry.space ?? [];
-  const radii = registry.radius ?? [];
-  const shadows = registry.shadow ?? [];
-  const presets = typoPresetNames(registry);
+  const total = Object.values(registry).reduce((sum, list) => sum + list.length, 0);
 
   return (
     <main className={styles.page}>
@@ -79,109 +128,33 @@
         <div>
           <h1 className={styles.title}>디자인 시스템 카탈로그</h1>
           <p className={styles.lede}>
-            @fox/styles/tokens 전량을 CSSOM에서 읽어 그립니다. 개발 환경에서만 열립니다.
+            @fox/styles/tokens 전량 {total}개를 CSSOM에서 읽어 그립니다. 개발 환경에서만
+            열립니다. 반응형 토큰은 현재 창 크기 기준 값입니다.
           </p>
         </div>
         <ThemeSwitch />
       </header>
 
-      <section className={styles.section}>
-        <h2 className={styles.sectionTitle}>색상 · {colors.length}</h2>
-        <p className={styles.sectionNote}>
-          라이트/다크 값이 light-dark() 한 줄에 함께 정의돼 있습니다. 위 전환기로 두 테마를
-          대조해 보세요.
-        </p>
-        <div className={styles.grid}>
-          {colors.map((token) => (
-            <div key={token.property} className={styles.swatch}>
-              <div className={styles.swatchChip} style={ref("background", token)} />
-              <div className={styles.swatchMeta}>
-                <span className={styles.name}>fox.color({token.name})</span>
-                <span className={styles.ref}>{token.value}</span>
-              </div>
-            </div>
-          ))}
-        </div>
-      </section>
+      {GROUPS.map(({ key, label, kind, note }) => {
+        const tokens = registry[key];
+        if (!tokens?.length) {
+          return null;
+        }
 
-      <section className={styles.section}>
-        <h2 className={styles.sectionTitle}>타이포 프리셋 · {presets.length}</h2>
-        <p className={styles.sectionNote}>
-          크기·굵기·행간·자간 4종이 한 묶음입니다. @include fox.typo(이름)으로 적용합니다.
-        </p>
-        {presets.map((preset) => (
-          <div key={preset} className={styles.typoRow}>
-            <span className={styles.typoName}>{preset}</span>
-            <span className={styles.typoSample} style={typoRef(preset)}>
-              다람쥐 헌 쳇바퀴에 타고파 · Handgloves 0123
-            </span>
-          </div>
-        ))}
-      </section>
-
-      <section className={styles.section}>
-        <h2 className={styles.sectionTitle}>간격 · {spaces.length}</h2>
-        <p className={styles.sectionNote}>
-          4px 배수 스케일. 1rem = 10px이므로 fox.space(4) = 1.6rem = 16px입니다.
-        </p>
-        {spaces.map((token) => (
-          <div key={token.property} className={styles.spaceRow}>
-            <span className={styles.typoName}>
-              space({token.name}) · {token.value}
-            </span>
-            <span className={styles.spaceBar} style={ref("inlineSize", token)} />
-          </div>
-        ))}
-      </section>
-
-      <section className={styles.section}>
-        <h2 className={styles.sectionTitle}>모서리 반경 · {radii.length}</h2>
-        <div className={styles.grid}>
-          {radii.map((token) => (
-            <div key={token.property} className={styles.tile} style={ref("borderRadius", token)}>
-              {token.name} · {token.value}
-            </div>
-          ))}
-        </div>
-      </section>
-
-      <section className={styles.section}>
-        <h2 className={styles.sectionTitle}>그림자 · {shadows.length}</h2>
-        <p className={styles.sectionNote}>크기가 아니라 쓰임(고도)으로 이름 붙였습니다.</p>
-        <div className={styles.grid}>
-          {shadows.map((token) => (
-            <div key={token.property} className={styles.shadowCell}>
-              <div className={styles.shadowTile} style={ref("boxShadow", token)}>
-                {token.name}
-              </div>
-            </div>
-          ))}
-        </div>
-      </section>
-
-      <section className={styles.section}>
-        <h2 className={styles.sectionTitle}>모션</h2>
-        <ValueTable
-          rows={[...rows(registry.duration, "duration"), ...rows(registry.easing, "easing")]}
-        />
-      </section>
-
-      <section className={styles.section}>
-        <h2 className={styles.sectionTitle}>서체 · 굵기 · z 층 · 브레이크포인트</h2>
-        <p className={styles.sectionNote}>
-          브레이크포인트는 미디어 쿼리가 var()를 해석하지 못하므로 실제 사용은 fox.media()
-          믹스인 경유입니다. 또한 미디어 쿼리 안의 rem은 html font-size의 영향을 받지 않아
-          48rem은 480px가 아니라 768px입니다.
-        </p>
-        <ValueTable
-          rows={[
-            ...rows(registry.font, "font"),
-            ...rows(registry.weight, "weight"),
-            ...rows(registry.z, "z"),
-            ...rows(registry.breakpoint, "media"),
-          ]}
-        />
-      </section>
+        return (
+          <section key={key} className={styles.section}>
+            <h2 className={styles.sectionTitle}>
+              {label} · {tokens.length}
+            </h2>
+            {note ? <p className={styles.sectionNote}>{note}</p> : null}
+            {kind === "swatch" ? (
+              <Swatches tokens={tokens} fn={key} />
+            ) : (
+              <ValueTable tokens={tokens} fn={key} withBar={kind === "bar"} />
+            )}
+          </section>
+        );
+      })}
     </main>
   );
 }
app/design/_lib/read-tokens.ts
--- app/design/_lib/read-tokens.ts
+++ app/design/_lib/read-tokens.ts
@@ -1,12 +1,13 @@
 export interface TokenEntry {
-  /** 토큰 이름 (`--fox-color-primary` → `primary`) */
+  /** 그룹을 뗀 토큰 이름 (`--fox-color-background-default` → `background-default`) */
   name: string;
   /**
-   * 현재 테마에서 실제로 해석된 값 (`#818cf8`).
+   * 현재 테마·뷰포트에서 실제로 해석된 값 (`#FFFFFF`, `1.6rem`).
    *
-   * 선언 원문이 아니라 계산값을 쓴다 — Lightning CSS가 `light-dark()`를
+   * 선언 원문이 아니라 계산값을 쓴다 — 색상은 Lightning CSS가 `light-dark()`를
    * `var(--lightningcss-light, …) var(--lightningcss-dark, …)`로 폴리필해 내보내므로
-   * 원문은 읽기 어렵고, 계산값은 지금 테마에서 눈에 보이는 색과 정확히 일치한다.
+   * 원문이 읽기 어렵고, 크기는 `alias`가 `var(--fox-…)` 참조라 원문만으로는 값을 알 수
+   * 없다. 계산값은 지금 화면에 실제로 적용되는 값과 정확히 일치한다.
    */
   value: string;
   /** 스타일에서 참조할 커스텀 프로퍼티 전체 이름 */
@@ -16,13 +17,12 @@
 export type TokenRegistry = Record<string, TokenEntry[]>;
 
 const PREFIX = "--fox-";
-const TYPO_FIELDS = ["-size", "-weight", "-line-height", "-letter-spacing"];
 
 /**
  * 문서의 모든 스타일시트를 훑어 `--fox-*` 커스텀 프로퍼티를 그룹별로 모은다.
  *
- * 토큰 목록을 카탈로그가 따로 들고 있지 않게 하려는 것이 목적이다 — `tokens/`에 값을
- * 추가하면 카탈로그에 자동으로 나타나고, 목록이 두 곳에 존재해 어긋날 일이 없다.
+ * 토큰 목록을 카탈로그가 따로 들고 있지 않게 하려는 것이 목적이다 — `@fox/styles/tokens/`에
+ * 값을 추가하면 카탈로그에 자동으로 나타나고, 목록이 두 곳에 존재해 어긋날 일이 없다.
  * CSSOM은 선언 순서를 보존하므로 SCSS map에 적은 순서가 그대로 유지된다.
  *
  * 브라우저에서만 동작한다(문서가 있어야 한다).
@@ -59,11 +59,8 @@
           continue;
         }
 
-        const group = rest.slice(0, separator);
-        const name = rest.slice(separator + 1);
-
-        (registry[group] ??= []).push({
-          name,
+        (registry[rest.slice(0, separator)] ??= []).push({
+          name: rest.slice(separator + 1),
           value:
             computed.getPropertyValue(property).trim() ||
             rule.style.getPropertyValue(property).trim(),
@@ -74,26 +71,4 @@
   }
 
   return registry;
-}
-
-/**
- * 타이포 프리셋은 이름 하나가 4개의 커스텀 프로퍼티로 펼쳐져 있으므로
- * (`--fox-typo-title-md-size` 등) 접미사를 떼어 프리셋 이름만 순서대로 복원한다.
- */
-export function typoPresetNames(registry: TokenRegistry): string[] {
-  const names: string[] = [];
-
-  for (const entry of registry.typo ?? []) {
-    const field = TYPO_FIELDS.find((suffix) => entry.name.endsWith(suffix));
-    if (!field) {
-      continue;
-    }
-
-    const preset = entry.name.slice(0, -field.length);
-    if (!names.includes(preset)) {
-      names.push(preset);
-    }
-  }
-
-  return names;
 }
app/design/design.module.scss
--- app/design/design.module.scss
+++ app/design/design.module.scss
@@ -4,208 +4,158 @@
 // SCSS 값을 JS로 내보내는 CSS Modules의 `:export`를 처음 썼으나, Next.js 16의 기본
 // 번들러인 Turbopack은 Lightning CSS로 CSS를 파싱해 `:export`(webpack css-loader 시절의
 // ICSS 확장)를 인식하지 못한다 — 빌드는 통과하지만 규칙이 통째로 버려져 값이 `undefined`가
-// 된다. `--fox-*` 커스텀 프로퍼티를 CSSOM에서 직접 열거하면 같은 "목록을 두 곳에 두지
-// 않는다"는 목적을 번들러 의존 없이 달성한다.
+// 된다. `--fox-*` 커스텀 프로퍼티를 CSSOM에서 직접 열거하면 같은 목적을 번들러 의존 없이
+// 달성한다.
 
 @use "@fox/styles/abstracts" as fox;
 
 .page {
   flex: 1;
-  padding: fox.space(8) fox.space(6);
-
-  @include fox.container(xl, 6);
+  padding: fox.spacing(top-lg) fox.grid(margin-default) fox.spacing(bottom-xlg);
 }
 
 .header {
   display: flex;
   flex-wrap: wrap;
-  gap: fox.space(4);
+  gap: fox.gap(5);
   align-items: baseline;
   justify-content: space-between;
-  padding-block-end: fox.space(6);
-  border-block-end: 1px solid fox.color(border);
+  padding-block-end: fox.padding(7);
+  border-block-end: fox.border(1) solid fox.color(border-neutral-default);
 }
 
 .title {
-  @include fox.typo(headline-lg);
+  font-size: fox.font-size(heading-lg);
+  font-weight: fox.font-weight(bold);
 }
 
 .lede {
-  margin-block-start: fox.space(2);
-  color: fox.color(foreground-muted);
-
-  @include fox.typo(body-md);
+  margin-block-start: fox.gap(2);
+  color: fox.color(font-neutral-subtle);
+  font-size: fox.font-size(body-md);
 }
 
 // ── 테마 전환 ───────────────────────────────────────────────────────────────
 .themeSwitch {
   display: flex;
-  gap: fox.space(1);
-  padding: fox.space(1);
-  border: 1px solid fox.color(border);
-  border-radius: fox.radius(full);
-  background: fox.color(surface-muted);
+  gap: fox.gap(1);
+  padding: fox.padding(1);
+  border: fox.border(1) solid fox.color(border-neutral-default);
+  border-radius: fox.radius(max);
+  background: fox.color(surface-neutral-gray);
 }
 
 .themeButton {
-  padding: fox.space(2) fox.space(4);
-  border-radius: fox.radius(full);
-  color: fox.color(foreground-muted);
-
-  @include fox.typo(label-md);
-  @include fox.focus-ring;
-  @include fox.transition((background-color, color));
+  padding: fox.padding(3) fox.padding(6);
+  border-radius: fox.radius(max);
+  color: fox.color(font-neutral-subtle);
+  font-size: fox.font-size(label-sm);
+  font-weight: fox.font-weight(medium);
 
   &[aria-pressed="true"] {
-    background: fox.color(primary);
-    color: fox.color(on-primary);
+    background: fox.color(button-primary-surface);
+    color: fox.color(button-primary-font);
+  }
+
+  &:focus-visible {
+    outline: fox.border(2) solid fox.color(border-theme-primary);
+    outline-offset: fox.border(2);
   }
 }
 
+// ── 섹션 ────────────────────────────────────────────────────────────────────
 .section {
-  padding-block: fox.space(8);
-  border-block-end: 1px solid fox.color(border-subtle);
+  padding-block: fox.section(spacing-xsm);
+  border-block-end: fox.border(1) solid fox.color(border-neutral-subtle);
 }
 
 .sectionTitle {
-  margin-block-end: fox.space(1);
-
-  @include fox.typo(title-lg);
+  margin-block-end: fox.gap(1);
+  font-size: fox.font-size(heading-sm);
+  font-weight: fox.font-weight(bold);
 }
 
 .sectionNote {
-  margin-block-end: fox.space(5);
-  color: fox.color(foreground-muted);
-
-  @include fox.typo(body-sm);
+  margin-block-end: fox.gap(5);
+  color: fox.color(font-neutral-subtle);
+  font-size: fox.font-size(body-sm);
 }
 
 .grid {
   display: grid;
-  grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
-  gap: fox.space(4);
+  grid-template-columns: repeat(auto-fill, minmax(fox.grid(wrap-xsm), 1fr));
+  gap: fox.gap(4);
+
+  @include fox.pc {
+    grid-template-columns: repeat(auto-fill, minmax(24rem, 1fr));
+  }
 }
 
-// ── 색상 ────────────────────────────────────────────────────────────────────
+// ── 색상 스와치 ─────────────────────────────────────────────────────────────
 .swatch {
   overflow: hidden;
-  border: 1px solid fox.color(border);
-  border-radius: fox.radius(md);
-  background: fox.color(surface);
+  border: fox.border(1) solid fox.color(border-neutral-subtle);
+  border-radius: fox.radius(3);
+  background: fox.color(surface-neutral-default);
 }
 
 .swatchChip {
-  block-size: 6.4rem;
+  block-size: fox.icon(7);
+  border-block-end: fox.border(1) solid fox.color(border-neutral-overlay);
 }
 
 .swatchMeta {
-  padding: fox.space(3);
+  padding: fox.padding(5);
 }
 
 .name {
   display: block;
-  color: fox.color(foreground);
+  color: fox.color(font-neutral-default);
+  font-size: fox.font-size(label-sm);
+  font-weight: fox.font-weight(medium);
   word-break: break-all;
-
-  @include fox.typo(label-lg);
 }
 
 .ref {
   display: block;
-  margin-block-start: fox.space(1);
-  color: fox.color(foreground-subtle);
-  font-family: fox.font(mono);
+  margin-block-start: fox.gap(1);
+  color: fox.color(font-neutral-subtler);
+  font-size: fox.font-size(body-xsm);
   word-break: break-all;
-
-  @include fox.typo(body-sm);
-}
-
-// ── 타이포 ──────────────────────────────────────────────────────────────────
-.typoRow {
-  display: flex;
-  flex-wrap: wrap;
-  gap: fox.space(2) fox.space(6);
-  align-items: baseline;
-  padding-block: fox.space(3);
-  border-block-end: 1px solid fox.color(border-subtle);
-}
-
-.typoName {
-  flex: 0 0 14rem;
-  color: fox.color(foreground-subtle);
-  font-family: fox.font(mono);
-
-  @include fox.typo(body-sm);
-}
-
-.typoSample {
-  flex: 1;
-  min-inline-size: 20rem;
-}
-
-// ── 간격 ────────────────────────────────────────────────────────────────────
-.spaceRow {
-  display: flex;
-  gap: fox.space(4);
-  align-items: center;
-  padding-block: fox.space(2);
-}
-
-.spaceBar {
-  block-size: 1.6rem;
-  border-radius: fox.radius(xs);
-  background: fox.color(primary);
-}
-
-// ── radius · shadow ─────────────────────────────────────────────────────────
-.tile {
-  display: flex;
-  block-size: 9.6rem;
-  align-items: center;
-  justify-content: center;
-  border: 1px solid fox.color(border);
-  background: fox.color(surface);
-  color: fox.color(foreground-muted);
-
-  @include fox.typo(label-md);
-}
-
-.shadowTile {
-  display: flex;
-  block-size: 9.6rem;
-  align-items: center;
-  justify-content: center;
-  border-radius: fox.radius(lg);
-  background: fox.color(surface);
-  color: fox.color(foreground-muted);
-
-  @include fox.typo(label-md);
-}
-
-.shadowCell {
-  padding: fox.space(4);
 }
 
 // ── 값 테이블 ───────────────────────────────────────────────────────────────
 .table {
   inline-size: 100%;
+  border-collapse: collapse;
   text-align: start;
 }
 
 .table th,
 .table td {
-  padding: fox.space(2) fox.space(3);
-  border-block-end: 1px solid fox.color(border-subtle);
+  padding: fox.padding(3) fox.padding(5);
+  border-block-end: fox.border(1) solid fox.color(border-neutral-subtle);
 }
 
 .table th {
-  color: fox.color(foreground-muted);
-
-  @include fox.typo(label-md);
+  color: fox.color(font-neutral-subtle);
+  font-size: fox.font-size(label-xsm);
+  font-weight: fox.font-weight(medium);
 }
 
 .table td {
-  font-family: fox.font(mono);
+  color: fox.color(font-neutral-default);
+  font-size: fox.font-size(body-sm);
+}
 
-  @include fox.typo(body-sm);
+.tdValue {
+  color: fox.color(font-neutral-subtle);
+}
+
+// 크기 토큰을 눈으로 비교하기 위한 막대. 폭은 인라인 style이 토큰을 참조해 넣는다.
+.bar {
+  display: block;
+  block-size: fox.icon(2);
+  border-radius: fox.radius(1);
+  background: fox.color(surface-theme-primary);
 }
Add a comment
List