File name
Commit message
Commit date
File name
Commit message
Commit date
@import "tailwindcss";
/* 색상 토큰 출처: edupay-app lib/ui/core/tokens/app_colors.dart
(Figma: FOX PAY 1차 제작시안, node 91:10205). 다크 값은 edupay-app에 대응
다크 시안이 없어 "임의 도출 — 정식 다크 시안 확정 시 교체".
메커니즘: `color-scheme` + `light-dark()`(Chrome 123+/Safari 17.5+/Firefox 120+
지원, 미지원 브라우저는 값이 무효화되므로 사내 어드민 한정 트레이드오프로 채택
— design 계획 §4.1/§5.3, 2026-07-28 게이트 승인). 각 시맨틱 토큰이
`light-dark(라이트값, 다크값)`으로 라이트/다크 값을 한 선언에 함께 보유해
드리프트를 구조적으로 차단한다. 수동 전환은 아래 `:root[data-theme]`가
`color-scheme`만 덮어써 처리하므로 값 자체는 이 블록 한 곳에만 존재한다
(Next.js 16 번들 문서 preventing-flash-before-hydration.md §Themes 패턴). */
:root {
color-scheme: light dark;
/* Brand */
--primary: light-dark(#502ef5, #877aec);
--primary-container: light-dark(#6d5ce7, #502ef5);
--on-primary: light-dark(#ffffff, #ffffff);
--on-primary-container: light-dark(#e2e0fb, #e2e0fb);
--secondary: light-dark(#877aec, #9b92ff);
/* Surface / background (원 토큰: surface / gray0) */
--background: light-dark(#ffffff, #14161b);
--surface: light-dark(#ffffff, #1e2128);
--surface-muted: light-dark(#f5f7fa, #2c3038);
/* Foreground / text (원 토큰: onSurface 계열) */
--foreground: light-dark(#2c3038, #f5f7fa);
--foreground-muted: light-dark(#565d6b, #bbc4d8);
--foreground-subtle: light-dark(#6b7486, #8790a3);
/* Border (원 토큰: outline 계열) */
--border: light-dark(#dae0ed, #424752);
--border-subtle: light-dark(#e1e6f1, #343943);
/* Status */
--danger: light-dark(#ec2e2f, #ff5c5d);
--success: light-dark(#21b770, #3acb93);
--warning: light-dark(#ffb020, #ffc24d);
/* Overlay */
--scrim: light-dark(rgb(0 0 0 / 0.6), rgb(0 0 0 / 0.7));
}
/* 수동 테마 선택 — <html data-theme="light"|"dark">가 `color-scheme`만 덮어써
위 light-dark() 값이 해당 분기로 고정된다(값 자체는 여기서 재정의하지 않음).
data-theme 속성이 없거나 "system"이면 :root의 `color-scheme: light dark`가
유지되어 OS `prefers-color-scheme`를 그대로 따른다(feature 전달 사양). */
:root[data-theme="light"] {
color-scheme: light;
}
:root[data-theme="dark"] {
color-scheme: dark;
}
@theme inline {
--color-primary: var(--primary);
--color-primary-container: var(--primary-container);
--color-on-primary: var(--on-primary);
--color-on-primary-container: var(--on-primary-container);
--color-secondary: var(--secondary);
--color-background: var(--background);
--color-surface: var(--surface);
--color-surface-muted: var(--surface-muted);
--color-foreground: var(--foreground);
--color-foreground-muted: var(--foreground-muted);
--color-foreground-subtle: var(--foreground-subtle);
--color-border: var(--border);
--color-border-subtle: var(--border-subtle);
--color-danger: var(--danger);
--color-success: var(--success);
--color-warning: var(--warning);
--color-scrim: var(--scrim);
/* WantedSans 연결 전에도 안전한 폴백(feature 레인이 app/layout.tsx에서
next/font/local로 --font-wanted-sans를 정의). */
--font-sans: var(--font-wanted-sans, system-ui), system-ui,
"Apple SD Gothic Neo", sans-serif;
--font-mono: var(--font-geist-mono);
/* 타이포 토큰 출처: edupay-app lib/ui/core/tokens/app_typography.dart
(Figma: FOX PAY 1차 제작시안, node 112:11439). voucher/numeric 등 앱 전용
스타일은 어드민 무관으로 미이식. */
--text-display-lg: 3rem;
--text-display-lg--line-height: 1.5;
--text-display-lg--font-weight: 700;
--text-display-md: 2.25rem;
--text-display-md--line-height: 1.5;
--text-display-md--letter-spacing: -0.72px;
--text-display-md--font-weight: 700;
--text-display-sm: 2rem;
--text-display-sm--line-height: 1.5;
--text-display-sm--letter-spacing: -0.64px;
--text-display-sm--font-weight: 700;
--text-headline-lg: 1.5rem;
--text-headline-lg--line-height: 1.5;
--text-headline-lg--letter-spacing: -0.72px;
--text-headline-lg--font-weight: 700;
--text-headline-md: 1.25rem;
--text-headline-md--line-height: 1.5;
--text-headline-md--letter-spacing: -0.6px;
--text-headline-md--font-weight: 700;
--text-headline-sm: 1.125rem;
--text-headline-sm--line-height: 1.3;
--text-headline-sm--letter-spacing: -0.54px;
--text-headline-sm--font-weight: 700;
--text-title-lg: 1.125rem;
--text-title-lg--line-height: 1.5;
--text-title-lg--font-weight: 700;
--text-title-md: 1rem;
--text-title-md--line-height: 1.5;
--text-title-md--font-weight: 700;
--text-title-sm: 0.875rem;
--text-title-sm--line-height: 1.5;
--text-title-sm--letter-spacing: -0.42px;
--text-title-sm--font-weight: 700;
--text-body-lg: 1rem;
--text-body-lg--line-height: 1.5;
--text-body-lg--letter-spacing: -0.48px;
--text-body-lg--font-weight: 400;
--text-body-md: 0.875rem;
--text-body-md--line-height: 1.5;
--text-body-md--font-weight: 400;
--text-body-sm: 0.75rem;
--text-body-sm--line-height: 1.5;
--text-body-sm--font-weight: 400;
--text-label-lg: 1rem;
--text-label-lg--line-height: 1.4;
--text-label-lg--letter-spacing: -0.48px;
--text-label-lg--font-weight: 700;
--text-label-md: 0.875rem;
--text-label-md--letter-spacing: -0.28px;
--text-label-md--font-weight: 400;
--text-label-sm: 0.75rem;
--text-label-sm--font-weight: 400;
/* radius 토큰 출처: edupay-app lib/ui/core/tokens/app_radius.dart.
Tailwind 기본 스케일과 다른 값으로 의도적 재정의(앱 일체화, §8-4 승인).
pill(999)은 기본 rounded-full로 충족, card(50)은 앱 전용 미이식. */
--radius-xs: 0.25rem;
--radius-sm: 0.5rem;
--radius-md: 0.75rem;
--radius-lg: 1rem;
--radius-xl: 1.25rem;
--radius-2xl: 1.5rem;
/* 그림자 토큰 출처: edupay-app lib/ui/core/tokens/app_shadows.dart —
어드민 유효분만 이식(voucher inner·stacked 등 미이식). */
--shadow-card: 0 5.16px 5.16px rgb(93 100 119 / 0.12);
--shadow-list-card: 0 8px 16px rgb(93 100 119 / 0.12);
--shadow-dialog: 0 8px 16px rgb(0 0 0 / 0.1);
--shadow-popup: 2.16px 6.32px 6px rgb(177 178 180 / 0.24);
--shadow-toast: 0 10px 12px rgb(0 0 0 / 0.2);
--shadow-subtle: 0 2px 4px rgb(0 0 0 / 0.08);
/* 애니메이션 토큰(신규 네임스페이스 — §4.2 승인). 오버레이·다이얼로그·토스트·
스플래시 전환에 사용. --ease-*는 Tailwind 기본값 사용, duration은 값에
내장(트랜지션 duration 표준은 bare 유틸 duration-200 사용). */
--animate-fade-in: fade-in 200ms var(--ease-out);
--animate-fade-out: fade-out 200ms var(--ease-in);
--animate-dialog-in: dialog-in 200ms var(--ease-out);
--animate-toast-in: toast-in 300ms var(--ease-out);
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* 스플래시 등 풀스크린 오버레이 퇴장용 — fade-in의 역방향 키프레임. */
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes dialog-in {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes toast-in {
from {
opacity: 0;
transform: translateY(0.5rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 레이아웃 전환(View Transitions API, feature 레인 A안) — feature가 전환 루트
요소에 .layout-transition 클래스를 부여하면 view-transition-class로 매핑되어
아래 ::view-transition-old/new 규칙이 부드러운 크로스페이드를 적용한다.
(CSS View Transitions Level 2, view-transition-class 프로퍼티 사용) */
.layout-transition {
view-transition-class: layout-transition;
}
::view-transition-old(.layout-transition) {
animation: var(--animate-fade-out);
}
::view-transition-new(.layout-transition) {
animation: var(--animate-fade-in);
}
@media (prefers-reduced-motion: reduce) {
::view-transition-group(.layout-transition),
::view-transition-old(.layout-transition),
::view-transition-new(.layout-transition) {
animation: none;
}
}
/* 간격 토큰 매핑 (변경 없음) — edupay-app lib/ui/core/tokens/app_spacing.dart의
4/8 grid(4·8·12·16·20·24·32·40)는 Tailwind 기본 --spacing: 0.25rem 스케일과
정확히 일치한다: xs→p-1, sm→p-2, md→p-3, lg→p-4, xl→p-5, xxl→p-6,
xxxl→p-8, xxxxl→p-10. */
/* 테마 수동 전환 — feature 소비처(예: 헤더 테마 토글)를 위한 CSS 구동 변형자.
design 계획 §9 게이트 교차 검증에서 feature 안(아이콘 3개를 모두 DOM에 렌더하고
CSS만으로 하나를 표시)을 채택했다 — 서버/클라 출력이 항상 동일해 hydration
불일치 표면적이 0이다. `theme-light`/`theme-dark`는 명시적 선택에만 매칭되고,
`theme-system`은 data-theme 부재 **및** 명시적 data-theme="system" 값 둘 다
커버해야 하므로(둘은 동일하게 동작 — feature 전달 사양) 부정 선택자 두 개로
"light도 dark도 아님"을 표현한다(속성 부재 시에도 :not()은 매칭된다). */
@custom-variant theme-system (&:where(html:not([data-theme="light"]):not([data-theme="dark"]) *));
@custom-variant theme-light (&:where(html[data-theme="light"] *));
@custom-variant theme-dark (&:where(html[data-theme="dark"] *));
/* 테마 전환 색상 트랜지션 — feature가 토글 직전 <html>에 [data-theme-transition]을
부여하고 ~200ms 후 제거하는 시간 한정 opt-in이다(상시 트랜지션이 아니므로 다른
hover/트랜지션 유틸과 경쟁하지 않는다). prefers-reduced-motion: no-preference로
게이팅해 감소 모션 환경에서는 아무 트랜지션도 걸리지 않는다(전환 없이 즉시 반영).
!important는 유틸리티의 transition 계열 클래스(transition-none 포함)가 이 200ms
동안만 존재하는 규칙을 덮어쓰지 않도록 한다. box-shadow는 제외 — --shadow 토큰은
테마별로 값이 바뀌지 않아 전환할 대상이 없다. */
@media (prefers-reduced-motion: no-preference) {
html[data-theme-transition],
html[data-theme-transition] *,
html[data-theme-transition] *::before,
html[data-theme-transition] *::after {
transition:
background-color 200ms var(--ease-out),
border-color 200ms var(--ease-out),
color 200ms var(--ease-out),
fill 200ms var(--ease-out),
stroke 200ms var(--ease-out),
outline-color 200ms var(--ease-out) !important;
}
}
body {
background: var(--background);
color: var(--foreground);
}