Design TokenN
이 가이드가 쓰는 토큰 원본(CSS 변수 · Light/Dark)과 Radius · Motion · Accessibility 규칙입니다. --accent-* 만 제품 컬러로 바꾸면 나머지는 그대로 씁니다. 색의 의미와 사용 규칙은 Foundations › Colors 를 참고하세요.
Token Layers
--neutral-* --brand-* --accent-* --red-*… 값의 원천. 컴포넌트에서 직접 쓰지 않음 (Colors › Atomic)--bg-* --text-* --fill-* --line-* --border-* --accent --danger --sev-*… 컴포넌트는 이것만 참조 (Colors › Semantic)--accent-50~900을 생성(명도 전개) → 나머지 토큰은 공통CSS Variables
Light 기본값 + Dark 오버라이드(prefers-color-scheme 및 [data-theme="dark"])를 포함합니다.
:root{
/* Neutral — 라이트·다크 공통 중립 램프. 참고 시스템의 Cool Neutral 계열이다(라이트 의미 토큰이 알파를 얹는 베이스 #70737C · #37383C · #2E2F33 와
불투명 정지점 #171719 · #F7F7F8 · #989BA2 · #1B1C1E · #212225 가 모두 여기 있다). 종전의 slate --gray-* 는 제거했다 */
--neutral-0:#FFFFFF;--neutral-50:#F7F7F8;--neutral-100:#EAEBEC;--neutral-200:#C2C4C8;--neutral-300:#AEB0B6;--neutral-400:#989BA2;--neutral-500:#70737C;--neutral-600:#5A5C63;--neutral-700:#37383C;--neutral-800:#2E2F33;--neutral-850:#212225;--neutral-900:#1B1C1E;--neutral-950:#171719;--neutral-980:#0F0F10;
/* Brand: 지란지교시큐리티 */
--brand-50:#FFF4E8;--brand-100:#FFE4C7;--brand-200:#FFD1A3;--brand-300:#FFB870;--brand-400:#FF9C33;--brand-500:#FF7F00;--brand-600:#E06F00;--brand-700:#B85A00;--brand-800:#8A4300;--brand-900:#5C2D00;
/* Accent: 제품 메인 컬러 슬롯 (기본값은 예시. 제품별로 교체)
기본값 #1D4ED8 — 제품 색 요건은 '다크 기준 단계(500)에서 면 위 글자와 4.5 이상'이다.
한 단계 밝은 #2563EB 는 500 단계가 흰 글자 4.28 · 어두운 글자 4.18 로 양쪽 다 미달이었다 */
--accent-50:var(--prod-50,#EFF2FA);--accent-100:var(--prod-100,#D8DFF3);--accent-200:var(--prod-200,#B1BFE7);--accent-300:var(--prod-300,#849CE1);--accent-400:var(--prod-400,#5A7CDC);--accent-500:var(--prod-500,#3661DA);--accent-600:var(--prod-600,#1D4ED8);--accent-700:var(--prod-700,#173DA8);--accent-800:var(--prod-800,#112D7D);--accent-900:var(--prod-900,#0B1E52);
/* Feedback */
/* Hue 램프 — 참고 시스템 Atomic 값. 단계 번호는 우리 체계(50 밝음 → 950 어두움)를 유지하고 매핑은 우리 600 ← 그쪽 40 에 맞췄다:
95→50 · 90→100 · 80→200 · 70→300 · 60→400 · 50→500 · 40→600 · 30→700 · 20→800 · 10→900 · 950 은 900 에서 보간.
그쪽 50(우리 500)이 가족의 간판색이고 40(우리 600)이 텍스트 등급이라, 우리 라이트 기준 600 이 흰 글자와 4.5 를 넘는다. Pink 는 싣지 않았다 */
--red-50:#FEECEC;--red-100:#FED5D5;--red-200:#FFB5B5;--red-300:#FF8C8C;--red-400:#FF6363;--red-500:#FF4242;--red-600:#E52222;--red-700:#B00C0C;--red-800:#730303;--red-900:#3B0101;--red-950:#200101;
--orange-50:#FEF4E6;--orange-100:#FEE6C6;--orange-200:#FFD49C;--orange-300:#FFC06E;--orange-400:#FFA938;--orange-500:#FF9200;--orange-600:#D47800;--orange-700:#9C5800;--orange-800:#663A00;--orange-900:#361E00;--orange-950:#1E1000;
--green-50:#D9FFE6;--green-100:#ACFCC7;--green-200:#7DF5A5;--green-300:#49E57D;--green-400:#1ED45A;--green-500:#00BF40;--green-600:#009632;--green-700:#006E25;--green-800:#004517;--green-900:#00240C;--green-950:#001407;
--cyan-50:#DEFAFF;--cyan-100:#B5F4FF;--cyan-200:#8AEDFF;--cyan-300:#57DFF7;--cyan-400:#28D0ED;--cyan-500:#00BDDE;--cyan-600:#0098B2;--cyan-700:#006F82;--cyan-800:#004854;--cyan-900:#00252B;--cyan-950:#001418;
/* 아래 넷은 아직 소비자가 없는 예비 hue — 참고 시스템에 있어 함께 싣는다 */
--blue-50:#EAF2FE;--blue-100:#C9DEFE;--blue-200:#9EC5FF;--blue-300:#69A5FF;--blue-400:#3385FF;--blue-500:#0066FF;--blue-600:#0054D1;--blue-700:#003E9C;--blue-800:#002966;--blue-900:#001536;--blue-950:#000C1E;
--lime-50:#E6FFD4;--lime-100:#CCFCA9;--lime-200:#AEF779;--lime-300:#88F03E;--lime-400:#6BE016;--lime-500:#58CF04;--lime-600:#48AD00;--lime-700:#347D00;--lime-800:#225200;--lime-900:#112900;--lime-950:#091700;
--violet-50:#F0ECFE;--violet-100:#DBD3FE;--violet-200:#C0B0FF;--violet-300:#9E86FC;--violet-400:#7D5EF7;--violet-500:#6541F2;--violet-600:#4F29E5;--violet-700:#3A16C9;--violet-800:#23098F;--violet-900:#11024D;--violet-950:#09012A;
--purple-50:#F9EDFF;--purple-100:#F2D6FF;--purple-200:#E9BAFF;--purple-300:#DE96FF;--purple-400:#D478FF;--purple-500:#CB59FF;--purple-600:#AD36E3;--purple-700:#861CB8;--purple-800:#580A7D;--purple-900:#290247;--purple-950:#170127;
--static-white:#FFFFFF;--static-black:#000000;
/* Semantic: Light */
/* 라이트 의미 토큰 — 참고 시스템의 라이트 semantic 값. 그쪽은 계조를 알파로 만든다(베이스는 중립 램프) */
--bg-canvas:var(--neutral-0);--bg-surface:var(--neutral-0);--bg-subtle:var(--neutral-50);--bg-panel:var(--neutral-100);--bg-inverse:var(--neutral-900);
/* secondary 는 그쪽 값 그대로 88% 알파. tertiary 는 그쪽이 61%(#858688, 흰 바탕 3.64) 인데 AA 에 못 미친다. 중립 500 도 패널 위에서 3.97 이라 600 으로 둔다(캔버스 6.56 · 패널 5.5).
disabled 도 그쪽은 16% 알파(비활성 면 위 1.15) 라 라벨이 사라진다 — 중립 400 으로 2.33 */
--text-primary:var(--neutral-950);--text-strong:var(--static-black);--text-secondary:rgba(46,47,51,.88);--text-tertiary:var(--neutral-600);--text-disabled:var(--neutral-400);--text-inverse:var(--neutral-0);
--fill-normal:rgba(112,115,124,.08);--fill-strong:rgba(112,115,124,.16);--fill-alternative:rgba(112,115,124,.05);
--line-normal:rgba(112,115,124,.22);--line-neutral:rgba(112,115,124,.16);--line-alternative:rgba(112,115,124,.08);
--border-default:var(--neutral-100);--border-strong:var(--neutral-200);--border-subtle:var(--neutral-50);--border-focus:var(--accent-500);
--interaction-inactive:var(--neutral-400);--interaction-disable:var(--neutral-100);--dimmer:rgba(23,23,25,.52);
--accent:var(--accent-600);--accent-hover:var(--accent-700);--accent-pressed:var(--accent-800);--accent-subtle:var(--accent-50);--accent-on:var(--static-white);--accent-text:var(--accent-600);--accent-inverse:var(--accent-300);--accent-graphic:var(--accent-600);
--brand:var(--brand-500);--brand-subtle:var(--brand-50);
/* 상태색 — 기준(base)은 라이트 600 · 다크 500. hover·pressed 는 기준에서 '면 위 글자색 반대 방향'으로 한 단계·두 단계 간다.
-text 는 글자·아이콘 전용으로 한 단계 더 짙게(라이트 700 · 다크 400) — 600 으로는 success 3.77 · warning 3.19 · info 4.10 으로 AA 미달이다.
-on 은 면 위 글자. warning·success 는 600 위에서 흰 글자가 3.19 · 3.77 로 떠서 어두운 글자를 쓰고,
그래서 두 가족만 라이트에서도 상태가 밝은 쪽(500·400)으로 간다 — 짙은 쪽이면 어두운 글자가 3.56 · 3.26 으로 깨진다.
--info-on 은 두지 않는다 — cyan/600 은 흰 3.42 · 어두운 5.23 이지만 글자 올라가는 면으로 쓰이지 않아 두지 않는다., --info 는 점·아이콘·보더 전용이다 */
--danger:var(--red-600);--danger-hover:var(--red-700);--danger-pressed:var(--red-800);--danger-subtle:var(--red-50);--danger-on:var(--static-white);--danger-text:var(--red-700);
--warning:var(--orange-600);--warning-hover:var(--orange-500);--warning-pressed:var(--orange-400);--warning-subtle:var(--orange-50);--warning-on:var(--neutral-950);--warning-text:var(--orange-700);
--success:var(--green-600);--success-hover:var(--green-500);--success-pressed:var(--green-400);--success-subtle:var(--green-50);--success-on:var(--neutral-950);--success-text:var(--green-700);
--info:var(--cyan-600);--info-hover:var(--cyan-700);--info-pressed:var(--cyan-800);--info-subtle:var(--cyan-50);--info-text:var(--cyan-700);
/* 심각도 — 상태색과 같은 3층이다: 면(--sev-*) · 면 위 글자(-on) · 옅은 바탕 위 글자(-text).
면은 태그 solid · 바 · 점 · 밴드에, -text 는 기본형 태그와 KPI 숫자처럼 글자에 쓴다.
-on 이 가족마다 갈리는 이유 — orange/600 위의 흰 글자는 3.24, cyan/600 은 3.42, green/600 은 3.88 로 떠서 어두운 글자를 쓴다 */
--sev-critical:var(--red-700);--sev-high:var(--red-500);--sev-medium:var(--orange-500);--sev-low:var(--cyan-500);--sev-info:var(--neutral-500);
--sev-critical-on:var(--static-white);--sev-high-on:var(--neutral-950);--sev-medium-on:var(--neutral-950);--sev-low-on:var(--neutral-950);--sev-info-on:var(--static-white);
--sev-critical-text:var(--red-800);--sev-high-text:var(--red-700);--sev-medium-text:var(--orange-700);--sev-low-text:var(--cyan-700);--sev-info-text:var(--neutral-600);
--sev-critical-bg:var(--red-100);--sev-high-bg:var(--red-50);--sev-medium-bg:var(--orange-50);--sev-low-bg:var(--cyan-50);--sev-info-bg:var(--neutral-100);
/* Spacing / Radius / Elevation / Motion */
--space-1:4px;--space-2:8px;--space-3:12px;--space-4:16px;--space-5:20px;--space-6:24px;--space-8:32px;--space-10:40px;--space-12:48px;--space-16:64px;--space-20:80px;
--card-pad:24px;--card-gap:20px;--row-h:52px;
--radius-xs:2px;--radius-sm:4px;--radius-md:6px;--radius-lg:8px;--radius-xl:10px;--radius-full:999px;
--shadow-none:none;--shadow-xs:0 1px 2px 0 rgba(23,23,25,.04);--shadow-sm:0 1px 3px 0 rgba(0,0,0,.05);--shadow-md:0 4px 12px 0 rgba(23,23,25,.06),0 1px 3px 0 rgba(0,0,0,.05);--shadow-lg:0 8px 24px 0 rgba(23,23,25,.08),0 2px 6px 0 rgba(0,0,0,.05);--shadow-xl:0 12px 32px 0 rgba(23,23,25,.10),0 2px 6px 0 rgba(0,0,0,.05);
--shadow-spread-sm:0 0 8px 0 rgba(23,23,25,.08);--shadow-spread-md:0 0 24px 0 rgba(23,23,25,.12);
--shadow-1:var(--shadow-sm);--shadow-2:var(--shadow-md);--shadow-3:var(--shadow-xl);
--motion-fast:120ms ease-out;--motion-base:200ms ease-in-out;--motion-slow:320ms cubic-bezier(.2,.8,.2,1);
--font-sans:"Pretendard",-apple-system,"Apple SD Gothic Neo","Noto Sans KR",sans-serif;
--font-mono:ui-monospace,"SF Mono",Menlo,Consolas,"D2Coding",monospace;
/* 코드 패널(라이트·다크 모두 어두운 배경) */
--code-bg:var(--neutral-950);--code-chip:var(--neutral-800);--code-chip-border:var(--neutral-700);--code-chip-fg:var(--neutral-200);--code-head-fg:var(--neutral-300);
--header-h:64px;--sidebar-w:232px;--toc-w:200px;--shell-max:1400px;
color-scheme:light;
}
/* 제품 메인 컬러가 상태색과 색상환 15도 미만이면 app.js 가 --accent-50~900 자리에 회색 램프를 넣고 이 속성을 켠다.
램프 기본 매핑은 --accent → 600 이라 라이트에서는 너무 옅다 → 끝(900)으로 당기고, 진한 바탕이므로 hover·pressed 는 밝아지는 쪽으로.
subtle 도 한 단계(50 → 100) 올린다 — 회색 50 은 흰 캔버스에서 선택 행 배경으로 보이지 않는다.
다크 블록이 뒤에서 같은 특정도로 다시 덮으므로 이 규칙은 라이트에만 걸린다 — 순서를 바꾸면 다크에서 버튼이 캔버스에 묻힌다. */
:root[data-accent-neutral]{--accent:var(--accent-900);--accent-hover:var(--accent-800);--accent-pressed:var(--accent-700);--accent-subtle:var(--accent-100);--accent-text:var(--accent-900)}
/* 메인 컬러가 밝으면 그 위의 흰 글자가 뜬다(제품A #00AAB6 에서 2.83) — 글자를 어둡게 쓰고, 상태는 글자 반대쪽(밝은 쪽)으로 간다.
짙어지는 방향(700·800)으로 보내면 어두운 글자가 4.05 · 2.55 로 깨진다. 판정은 applyAccent 가 하고 data-accent-light 로 켠다.
--accent-text 도 함께 내린다 — 밝은 제품은 600 으로 글자를 쓰면 패널 위에서 2.58 이고, 800 이면 6.4 다(다크는 램프가 밝은 쪽이라 300 그대로).
상태색과 겹치는 제품은 무채색 램프라 늘 흰 글자여서, data-accent-neutral 과는 함께 켜지지 않는다 */
:root[data-accent-light]{--accent-on:var(--neutral-950);--accent-hover:var(--accent-500);--accent-pressed:var(--accent-400);--accent-text:var(--accent-800)} /* 라이트 기준 600 에서 나온 단계 — 다크(기준 500)는 아래 전용 블록이 다시 덮는다 */
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){
--bg-canvas:var(--neutral-900);--bg-surface:var(--neutral-850);--bg-subtle:var(--neutral-800);--bg-panel:var(--neutral-800);--bg-inverse:var(--neutral-50);
--text-primary:var(--neutral-50);--text-strong:var(--neutral-0);--text-secondary:var(--neutral-200);--text-tertiary:var(--neutral-300);--text-disabled:var(--neutral-400);--text-inverse:var(--neutral-950);
--fill-normal:rgba(112,115,124,.22);--fill-strong:rgba(112,115,124,.28);--fill-alternative:rgba(112,115,124,.12);
--line-normal:rgba(112,115,124,.32);--line-neutral:rgba(112,115,124,.28);--line-alternative:rgba(112,115,124,.22);
--border-default:var(--neutral-700);--border-strong:var(--neutral-600);--border-subtle:var(--neutral-800);--border-focus:var(--accent-400);
--interaction-inactive:var(--neutral-600);--interaction-disable:var(--neutral-800);--dimmer:rgba(23,23,25,.74);
/* 다크 accent — 기준만 한 단계 다르다(라이트 600 · 다크 500). 글자색(--accent-on)과 밝은 제품 판정은 라이트와 공유한다.
흰 글자 제품은 상태가 짙은 쪽(600·700), 밝은 제품은 밝은 쪽 — 그쪽은 아래 [data-accent-light] 다크 블록이 덮는다.
텍스트는 300(400 은 파랑 계열에서 패널 위 4.08 로 미달. 상태색 400 은 이미 밝아 통과한다),
선택 배경은 제품과 무관한 중립 700(제품 램프 900 은 캔버스와 1.05~1.16),
글자 없는 지표는 400(기준 500 이면 패널 위 2.47 로 3:1 미달), 반전 표면 위 Primary 는 800(--bg-inverse 가 다크에서는 밝다). 포커스 링도 400(위 줄) */
--accent:var(--accent-500);--accent-hover:var(--accent-600);--accent-pressed:var(--accent-700);
--accent-subtle:var(--neutral-700);--accent-text:var(--accent-300);--accent-inverse:var(--accent-800);--accent-graphic:var(--accent-400);
--brand-subtle:#3A2410;
/* 다크 상태색 — 기준 500. 다크에서는 네 가족 모두 면 위에 어두운 글자를 쓰므로 hover·pressed 가 밝은 쪽(400·300)이다.
짙은 쪽으로 보내면 red-600 위의 어두운 글자가 3.71 로 깨진다. -text 는 400(캔버스 6.16~10.22) */
--danger:var(--red-500);--danger-hover:var(--red-400);--danger-pressed:var(--red-300);--danger-subtle:rgba(255,66,66,.12);--danger-on:var(--neutral-950);--danger-text:var(--red-400);
--warning:var(--orange-500);--warning-hover:var(--orange-400);--warning-pressed:var(--orange-300);--warning-subtle:rgba(255,146,0,.12);--warning-on:var(--neutral-950);--warning-text:var(--orange-400);
--success:var(--green-500);--success-hover:var(--green-400);--success-pressed:var(--green-300);--success-subtle:rgba(0,191,64,.12);--success-on:var(--neutral-950);--success-text:var(--green-400);
--info:var(--cyan-500);--info-hover:var(--cyan-400);--info-pressed:var(--cyan-300);--info-subtle:rgba(0,189,222,.12);--info-text:var(--cyan-400);
/* 다크 심각도 — critical 과 high 가 둘 다 red-400 이라 대비 1.00 으로 같은 색이었다(5단계가 4단계로 읽혔다).
라이트의 관계를 뒤집어 유지한다 — 어느 테마에서든 critical 이 배경과 대비가 더 크다(다크 8.98 vs 4.53). 분리 1.98(라이트는 1.72).
다크 면은 다섯 다 밝아서 -on 은 전부 어두운 글자다. -text 는 면보다 한두 단계 밝다 — 태그가 패널 위에 놓이면 반투명 -bg 합성 바탕이 밝아져,
red-300 은 critical 바탕에서 5.54 로 되지만 high 바탕에서 red-400 은 4.06 으로 미달이다(캔버스 기준으로만 재면 5.18 로 통과해 보인다) */
--sev-critical:var(--red-300);--sev-high:var(--red-500);--sev-medium:var(--orange-400);--sev-low:var(--cyan-400);--sev-info:var(--neutral-300);
--sev-critical-on:var(--neutral-950);--sev-high-on:var(--neutral-950);--sev-medium-on:var(--neutral-950);--sev-low-on:var(--neutral-950);--sev-info-on:var(--neutral-950);
--sev-critical-text:var(--red-200);--sev-high-text:var(--red-300);--sev-medium-text:var(--orange-400);--sev-low-text:var(--cyan-400);--sev-info-text:var(--neutral-300);
--sev-critical-bg:rgba(255,99,99,.16);--sev-high-bg:rgba(255,66,66,.12);--sev-medium-bg:rgba(255,169,56,.12);--sev-low-bg:rgba(40,208,237,.12);--sev-info-bg:rgba(112,115,124,.16);
--code-bg:var(--neutral-950);--code-chip:var(--neutral-800);--code-chip-border:var(--neutral-700);--code-chip-fg:var(--neutral-200);--code-head-fg:var(--neutral-300);
--shadow-xs:0 1px 2px 0 rgba(0,0,0,.35);--shadow-sm:0 1px 3px 0 rgba(0,0,0,.4);--shadow-md:0 4px 12px 0 rgba(0,0,0,.5);--shadow-lg:0 8px 24px 0 rgba(0,0,0,.55);--shadow-xl:0 12px 32px 0 rgba(0,0,0,.6);--shadow-spread-sm:0 0 8px 0 rgba(0,0,0,.5);--shadow-spread-md:0 0 24px 0 rgba(0,0,0,.6);
color-scheme:dark;
}}
:root[data-theme="dark"]{
--bg-canvas:var(--neutral-900);--bg-surface:var(--neutral-850);--bg-subtle:var(--neutral-800);--bg-panel:var(--neutral-800);--bg-inverse:var(--neutral-50);
--text-primary:var(--neutral-50);--text-strong:var(--neutral-0);--text-secondary:var(--neutral-200);--text-tertiary:var(--neutral-300);--text-disabled:var(--neutral-400);--text-inverse:var(--neutral-950);
--fill-normal:rgba(112,115,124,.22);--fill-strong:rgba(112,115,124,.28);--fill-alternative:rgba(112,115,124,.12);
--line-normal:rgba(112,115,124,.32);--line-neutral:rgba(112,115,124,.28);--line-alternative:rgba(112,115,124,.22);
--border-default:var(--neutral-700);--border-strong:var(--neutral-600);--border-subtle:var(--neutral-800);--border-focus:var(--accent-400);
--interaction-inactive:var(--neutral-600);--interaction-disable:var(--neutral-800);--dimmer:rgba(23,23,25,.74);
/* 다크 accent — 기준만 한 단계 다르다(라이트 600 · 다크 500). 글자색(--accent-on)과 밝은 제품 판정은 라이트와 공유한다.
흰 글자 제품은 상태가 짙은 쪽(600·700), 밝은 제품은 밝은 쪽 — 그쪽은 아래 [data-accent-light] 다크 블록이 덮는다.
텍스트는 300(400 은 파랑 계열에서 패널 위 4.08 로 미달. 상태색 400 은 이미 밝아 통과한다),
선택 배경은 제품과 무관한 중립 700(제품 램프 900 은 캔버스와 1.05~1.16),
글자 없는 지표는 400(기준 500 이면 패널 위 2.47 로 3:1 미달), 반전 표면 위 Primary 는 800(--bg-inverse 가 다크에서는 밝다). 포커스 링도 400(위 줄) */
--accent:var(--accent-500);--accent-hover:var(--accent-600);--accent-pressed:var(--accent-700);
--accent-subtle:var(--neutral-700);--accent-text:var(--accent-300);--accent-inverse:var(--accent-800);--accent-graphic:var(--accent-400);
--brand-subtle:#3A2410;
/* 다크 상태색 — 기준 500. 다크에서는 네 가족 모두 면 위에 어두운 글자를 쓰므로 hover·pressed 가 밝은 쪽(400·300)이다.
짙은 쪽으로 보내면 red-600 위의 어두운 글자가 3.71 로 깨진다. -text 는 400(캔버스 6.16~10.22) */
--danger:var(--red-500);--danger-hover:var(--red-400);--danger-pressed:var(--red-300);--danger-subtle:rgba(255,66,66,.12);--danger-on:var(--neutral-950);--danger-text:var(--red-400);
--warning:var(--orange-500);--warning-hover:var(--orange-400);--warning-pressed:var(--orange-300);--warning-subtle:rgba(255,146,0,.12);--warning-on:var(--neutral-950);--warning-text:var(--orange-400);
--success:var(--green-500);--success-hover:var(--green-400);--success-pressed:var(--green-300);--success-subtle:rgba(0,191,64,.12);--success-on:var(--neutral-950);--success-text:var(--green-400);
--info:var(--cyan-500);--info-hover:var(--cyan-400);--info-pressed:var(--cyan-300);--info-subtle:rgba(0,189,222,.12);--info-text:var(--cyan-400);
/* 다크 심각도 — critical 과 high 가 둘 다 red-400 이라 대비 1.00 으로 같은 색이었다(5단계가 4단계로 읽혔다).
라이트의 관계를 뒤집어 유지한다 — 어느 테마에서든 critical 이 배경과 대비가 더 크다(다크 8.98 vs 4.53). 분리 1.98(라이트는 1.72).
다크 면은 다섯 다 밝아서 -on 은 전부 어두운 글자다. -text 는 면보다 한두 단계 밝다 — 태그가 패널 위에 놓이면 반투명 -bg 합성 바탕이 밝아져,
red-300 은 critical 바탕에서 5.54 로 되지만 high 바탕에서 red-400 은 4.06 으로 미달이다(캔버스 기준으로만 재면 5.18 로 통과해 보인다) */
--sev-critical:var(--red-300);--sev-high:var(--red-500);--sev-medium:var(--orange-400);--sev-low:var(--cyan-400);--sev-info:var(--neutral-300);
--sev-critical-on:var(--neutral-950);--sev-high-on:var(--neutral-950);--sev-medium-on:var(--neutral-950);--sev-low-on:var(--neutral-950);--sev-info-on:var(--neutral-950);
--sev-critical-text:var(--red-200);--sev-high-text:var(--red-300);--sev-medium-text:var(--orange-400);--sev-low-text:var(--cyan-400);--sev-info-text:var(--neutral-300);
--sev-critical-bg:rgba(255,99,99,.16);--sev-high-bg:rgba(255,66,66,.12);--sev-medium-bg:rgba(255,169,56,.12);--sev-low-bg:rgba(40,208,237,.12);--sev-info-bg:rgba(112,115,124,.16);
--code-bg:var(--neutral-950);--code-chip:var(--neutral-800);--code-chip-border:var(--neutral-700);--code-chip-fg:var(--neutral-200);--code-head-fg:var(--neutral-300);
--shadow-xs:0 1px 2px 0 rgba(0,0,0,.35);--shadow-sm:0 1px 3px 0 rgba(0,0,0,.4);--shadow-md:0 4px 12px 0 rgba(0,0,0,.5);--shadow-lg:0 8px 24px 0 rgba(0,0,0,.55);--shadow-xl:0 12px 32px 0 rgba(0,0,0,.6);--shadow-spread-sm:0 0 8px 0 rgba(0,0,0,.5);--shadow-spread-md:0 0 24px 0 rgba(0,0,0,.6);
color-scheme:dark;
}
/* 다크 + 중립 전환 — 램프를 제품 계열 중립(--neutral-*)으로 갈고 메인은 100 단계.
라이트의 900 과 거울이다: 어두운 바탕에서는 밝은 쪽이 주인공이어야 한다(중립 600 #5A5C63 은 캔버스 위에서 2.0).
fill 15.0 → hover 10.25 → pressed 8.26, 캔버스 경계 14.29. subtle·지표·포커스·반전은 램프 교체로 자동이라 덮지 않는다.
특정도 (0,3,0) 으로 다크 블록과 [data-accent-light] 를 모두 이긴다 */
:root[data-theme="dark"][data-accent-neutral]{
--accent-50:var(--neutral-50);--accent-100:var(--neutral-100);--accent-200:var(--neutral-200);--accent-300:var(--neutral-300);--accent-400:var(--neutral-400);--accent-500:var(--neutral-500);--accent-600:var(--neutral-600);--accent-700:var(--neutral-700);--accent-800:var(--neutral-800);--accent-900:var(--neutral-900);
--accent:var(--accent-100);--accent-hover:var(--accent-200);--accent-pressed:var(--accent-300);--accent-on:var(--neutral-950);--accent-text:var(--accent-100);
}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"])[data-accent-neutral]{
--accent-50:var(--neutral-50);--accent-100:var(--neutral-100);--accent-200:var(--neutral-200);--accent-300:var(--neutral-300);--accent-400:var(--neutral-400);--accent-500:var(--neutral-500);--accent-600:var(--neutral-600);--accent-700:var(--neutral-700);--accent-800:var(--neutral-800);--accent-900:var(--neutral-900);
--accent:var(--accent-100);--accent-hover:var(--accent-200);--accent-pressed:var(--accent-300);--accent-on:var(--neutral-950);--accent-text:var(--accent-100);
}}
/* 다크 + 밝은 제품 — 기준 500 위에 어두운 글자를 쓰므로 상태는 밝은 쪽으로 간다(제품A 8.13 → 10.94 → 11.65).
라이트의 500·400 은 기준 600 에서 나온 값이라 그대로 쓸 수 없다. 특정도 (0,3,0) 으로 다크 블록을 이긴다 */
:root[data-theme="dark"][data-accent-light]{
--accent-hover:var(--accent-400);--accent-pressed:var(--accent-300);
}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"])[data-accent-light]{
--accent-hover:var(--accent-400);--accent-pressed:var(--accent-300);
}}Radius
역할별로 반경을 정해 두고, 모든 블록에 같은 반경을 찍지 않습니다. 타일을 클릭하면 토큰이 복사됩니다.
xs 2
sm 4
md 6
lg 8
xl 10
full
CSS
:root {
--radius-xs: 2px;
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-xl: 10px;
--radius-full: 999px;
}Motion
전환·피드백 애니메이션의 지속 시간과 이징입니다. 컴포넌트 CSS 는 --motion-* 토큰만 참조하고, prefers-reduced-motion 일 때 모든 전환을 제거합니다.
| 토큰 | 값 | 용도 |
|---|---|---|
--motion-fast | 120ms ease-out | hover, 토글, 체크 |
--motion-base | 200ms ease-in-out | 드롭다운, 툴팁, 프로그레스 |
--motion-slow | 320ms cubic-bezier(.2,.8,.2,1) | 팝업, 드로어 |
CSS
:root {
--motion-fast: 120ms ease-out;
--motion-base: 200ms ease-in-out;
--motion-slow: 320ms cubic-bezier(.2,.8,.2,1);
}.switch input{transition:background var(--motion-fast)}
.popup{animation:popup-in var(--motion-slow)}
@keyframes popup-in{from{opacity:0;transform:translateY(8px) scale(.98)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}Accessibility
WCAG 2.1 AA 를 기준으로 합니다.
- 텍스트 대비 4.5:1, 대형 텍스트·UI 요소 3:1. Neutral 500 이상만 텍스트에 사용
- 모든 인터랙션 키보드 접근, 포커스 링 항상 표시(
:focus-visible2px--border-focus) - 색만으로 상태를 전달하지 않음(태그 라벨, 아이콘 병행)
- 클릭 타깃 최소 32px
- 동적 알림은
aria-live, 아이콘 전용 버튼은aria-label
:focus-visible{outline:2px solid var(--border-focus);outline-offset:2px}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}Accent 스케일 생성 (JS)
제품 메인 컬러 HEX 하나로 --accent-50~900 을 만들어 :root 에 주입합니다. 메인 컬러가 상태색과 색상환 15도 미만이면 스케일 대신 무채색 램프를 넣고 data-accent-neutral 을, 메인 컬러가 밝아 흰 글자가 뜨면 data-accent-light 를 켭니다 — 이유는 Colors › 중립 전환과 밝은 메인 컬러.
function hexToHsl(hex){const n=parseInt(hex.slice(1),16);const r=((n>>16)&255)/255,g=((n>>8)&255)/255,b=(n&255)/255;const mx=Math.max(r,g,b),mn=Math.min(r,g,b);let h=0,s=0;const l=(mx+mn)/2;if(mx!==mn){const d=mx-mn;s=l>.5?d/(2-mx-mn):d/(mx+mn);switch(mx){case r:h=(g-b)/d+(g<b?6:0);break;case g:h=(b-r)/d+2;break;default:h=(r-g)/d+4;}h/=6;}return [h*360,s*100,l*100];}
function hslToHex(h,s,l){s/=100;l/=100;const k=n=>(n+h/30)%12;const a=s*Math.min(l,1-l);const f=n=>l-a*Math.max(-1,Math.min(k(n)-3,Math.min(9-k(n),1)));return "#"+[f(0),f(8),f(4)].map(x=>Math.round(x*255).toString(16).padStart(2,"0")).join("").toUpperCase();}
export function accentScale(hex){const [h,s,l6]=hexToHsl(hex);const up=t=>l6+(88-l6)*t,dn=t=>l6*t;const L={50:96,100:90,200:80,300:up(.55),400:up(.32),500:up(.13),700:dn(.78),800:dn(.58),900:dn(.38)};const S={50:.7,100:.7,200:.7,300:.8,400:.85,500:.9};const out={};for(const [k,l] of Object.entries(L)) out[k]=hslToHex(h,Math.min(100,s*(S[k]||1)),Math.max(0,Math.min(100,l)));out[600]=hex.toUpperCase();return out;}
const NEUTRAL_RAMP={50:"#F7F7F8",100:"#EAEBEC",200:"#C2C4C8",300:"#AEB0B6",400:"#989BA2",500:"#70737C",600:"#5A5C63",700:"#37383C",800:"#2E2F33",900:"#1B1C1E"};
const STATUS_HUES=[0,38,160,199]; // danger · warning · success · info
function clashesWithStatus(hex){const [h,s]=hexToHsl(hex);if(s<20) return false;return STATUS_HUES.some(t=>{const d=Math.abs(h-t)%360;return Math.min(d,360-d)<15;});}
function relLum(hex){const c=[0,1,2].map(i=>parseInt(hex.slice(1+i*2,3+i*2),16)/255).map(v=>v<=.03928?v/12.92:Math.pow((v+.055)/1.055,2.4));return .2126*c[0]+.7152*c[1]+.0722*c[2];}
function ratio(a,b){const [x,y]=[relLum(a)+.05,relLum(b)+.05];return Math.max(x,y)/Math.min(x,y);}
// 메인 컬러 위에 흰 글자가 뜨는지 — 어두운 글자(#171719 = --neutral-950)가 더 대비가 높으면 밝은 제품이다
export function isLightAccent(hex){return ratio(hex,"#FFFFFF")<ratio(hex,"#171719");}
export function applyAccent(hex){const neutral=clashesWithStatus(hex);const root=document.documentElement;
/* --accent-* 가 아니라 --prod-* 로 주입한다 — --prod-* 는 제품이 넣는 입력, --accent-* 는 테마가 해석한 결과 */
const sc=neutral?NEUTRAL_RAMP:accentScale(hex);
for(const [k,v] of Object.entries(sc)) root.style.setProperty("--prod-"+k,v);
if(neutral) root.dataset.accentNeutral=""; else delete root.dataset.accentNeutral;
/* 밝은 제품은 글자를 어둡게 쓰고 상태가 밝은 쪽으로 간다. 무채색 램프는 늘 흰 글자라 함께 켜지지 않는다 */
if(!neutral && isLightAccent(sc[600])) root.dataset.accentLight=""; else delete root.dataset.accentLight;}Tailwind 매핑
// tailwind.config.ts
export default { theme: { extend: {
colors: {
canvas: "var(--bg-canvas)", surface: "var(--bg-surface)", subtle: "var(--bg-subtle)", panel: "var(--bg-panel)",
fg: { DEFAULT: "var(--text-primary)", secondary: "var(--text-secondary)", tertiary: "var(--text-tertiary)", disabled: "var(--text-disabled)" },
line: { DEFAULT: "var(--border-default)", strong: "var(--border-strong)", focus: "var(--border-focus)" },
accent: { DEFAULT: "var(--accent)", hover: "var(--accent-hover)", pressed: "var(--accent-pressed)", subtle: "var(--accent-subtle)", on: "var(--accent-on)", graphic: "var(--accent-graphic)" },
brand: { DEFAULT: "var(--brand)", subtle: "var(--brand-subtle)" },
danger: "var(--danger)", warning: "var(--warning)", success: "var(--success)", info: "var(--info)",
sev: { critical: "var(--sev-critical)", high: "var(--sev-high)", medium: "var(--sev-medium)", low: "var(--sev-low)", info: "var(--sev-info)" },
},
borderRadius: { xs: "var(--radius-xs)", sm: "var(--radius-sm)", md: "var(--radius-md)", lg: "var(--radius-lg)", xl: "var(--radius-xl)", full: "var(--radius-full)" },
boxShadow: { 1: "var(--shadow-1)", 2: "var(--shadow-2)", 3: "var(--shadow-3)" },
fontFamily: { sans: "var(--font-sans)", mono: "var(--font-mono)" },
transitionDuration: { fast: "120ms", base: "200ms", slow: "320ms" },
}}}