/* frontend-v3 — 디자인 토큰 + 기본 레이아웃
   원본: Task-001 scaffold (DEC-002) · 토큰 3티어화: P2-1 (DEC-005 §3.1)

   ┌─ 토큰 규율 (DEC-005 §3.1) ────────────────────────────────────────────┐
   │ T1 원시(램프)  : --v3-{neutral|brand|success|warning|danger}-{N}      │
   │ T2 의미        : --v3-{fg|bg|border|accent|fs|sp|radius|shadow|z|…}-* │
   │ T3 컴포넌트/viz: --v3-{viz|chart|table|heat|btn|kpi|field}-*          │
   │                                                                       │
   │ 컴포넌트 CSS·템플릿·차트는 **T2·T3 만 참조**한다. T1 직접 참조 금지.  │
   │ 다크모드는 T1 램프를 그대로 두고 T2 매핑만 교체한다(P2-7).            │
   └───────────────────────────────────────────────────────────────────────┘

   ⚠️ P2-1 불변식: 아래 "레거시 별칭"의 해석값은 2026-08-06 이전 값과 **완전히 동일**하다.
      이 단계의 목표는 화면 변화 0. 값 변경은 P2-2(리터럴 제거)·P2-3(배너 AA 수리)에서 한다.
      가드: tests/test_v3_tokens.py */

:root {
  color-scheme: light;

  /* ══ T1 원시 — 중립 램프 ══ (기존 bg/surface/border/text/text-muted 를 좌표로 흡수) */
  --v3-neutral-0: #ffffff;
  --v3-neutral-25: #fcfcfd;
  --v3-neutral-50: #f6f8f9;
  --v3-neutral-100: #eef1f4;
  --v3-neutral-200: #e1e6ea;
  --v3-neutral-300: #d0d5dd;
  --v3-neutral-400: #b0b7c3;
  --v3-neutral-500: #98a1b0;
  /* DEC-009: 흰 3.67 · 캔버스 3.45 · sunken 3.24 — **세 배경 모두** 1.4.11 통과.
     이전 #8a92a0 은 흰 배경(3.13)에서만 통과하고 캔버스 2.94 · sunken 2.76 이었다
     — 2026-08-31 배지 4.28:1 과 같은 유형(자기 배경을 안 본 것)이다. */
  --v3-neutral-550: #7d8695;
  --v3-neutral-600: #596473;
  --v3-neutral-700: #4a5464;
  --v3-neutral-800: #333c4a;
  --v3-neutral-900: #16202b;
  --v3-neutral-950: #121821;
  /* 다크 표면 3단 (P2-7/Task-M022). T1 은 **추가만** 한다 — 기존 좌표의 값을
     바꾸면 라이트 화면이 함께 움직인다. 다크는 T2 매핑 교체로만 만든다. */
  --v3-neutral-975: #0f151c;   /* 다크 캔버스 */
  --v3-neutral-925: #19212b;   /* 다크 표면 */
  --v3-neutral-875: #232d3a;   /* 다크 융기 표면 */

  /* ══ T1 원시 — 브랜드 램프 ══ (500=기존 primary, 50=기존 primary-weak, 600=기존 hover 리터럴) */
  /* DEC-009 D2 — 600 = A안 accent. 명도 단조(흰 배경 1.10 → 17.61)를
     테스트가 고정한다. 600 만 바꾸면 `.v3-btn:hover`(=brand-500)가 밝은 파랑으로
     남아 같은 컴포넌트 안에서 두 파랑이 싸운다 → 램프 전체를 다시 만든다. */
  --v3-brand-50: #eef4fe;
  --v3-brand-100: #dbe8fc;
  --v3-brand-200: #b7d0f8;
  --v3-brand-300: #89b1f0;
  --v3-brand-400: #4e87e4;
  --v3-brand-500: #2163cd;
  --v3-brand-600: #1f4f9c;
  --v3-brand-700: #173f80;
  --v3-brand-800: #133263;
  --v3-brand-900: #10274a;
  --v3-brand-950: #0c192d;   /* 다크 info 배경 (P2-7) */

  /* ══ T1 원시 — 상태 램프 ══ (50=기존 배너 배경 리터럴, 500=기존 전경, 700=AA 수리용) */
  --v3-success-50: #e8f6ee;
  --v3-success-200: #a7e3c6;   /* 다크 전경 (P2-7) */
  --v3-success-500: #1a7f52;
  --v3-success-700: #157a4a;   /* DEC-009 — 배너 배경 위 4.81:1 (이전 6.52) */
  --v3-success-900: #0d3f2a;
  --v3-success-950: #0a2b1d;   /* 다크 배경 (P2-7) */
  --v3-warning-50: #fdf5e2;
  --v3-warning-200: #f0d493;
  --v3-warning-500: #b5860b;
  --v3-warning-700: #8a6508;
  --v3-warning-900: #5c4405;
  --v3-warning-950: #33260a;
  --v3-danger-50: #fdecea;
  --v3-danger-200: #f5b7ae;
  --v3-danger-500: #c0392b;
  --v3-danger-700: #b8322a;    /* DEC-009 — 배너 배경 위 5.21:1 (이전 7.09) */
  --v3-danger-900: #611710;
  --v3-danger-950: #3a120c;
  /* info 는 brand 램프 재사용 — 별도 램프 만들지 않는다(기존 --v3-info 중복 해소) */

  /* ══ T1 원시 — 지표 성격 램프 ══ (DEC-009 D2)
     기존 색은 **증감 방향**(늘었나/줄었나)만 말했다. 지표 **자체의 성격**
     (볼륨 / 금액 / 비율)을 말하는 축을 추가한다. 두 축은 겹치지 않는다 —
     성격은 카드의 띠·아이콘에, 방향은 델타 텍스트에.
     600=라이트, 300=다크. 실측: 라이트 최악 5.53 / 다크 최악 7.29 (모두 AA).
     ⚠️ 차트 범주형 6색(`--v3-viz-cat-*`)과 **다른 축**이다. 섞지 말 것. */
  --v3-volume-600: #0b6478;
  --v3-volume-300: #6ecfe6;
  --v3-money-600: #8a5406;
  --v3-money-300: #efc069;
  --v3-rate-600: #5a3ea6;
  --v3-rate-300: #b7a2ef;

  /* ══ T2 의미 — 전경 ══ */
  --v3-fg-default: var(--v3-neutral-900);
  --v3-fg-muted: var(--v3-neutral-600);
  /* ⚠️ fg-subtle 은 흰 배경 2.61:1 로 AA 미달 — **비텍스트 전용**(구분선·비활성 아이콘).
     본문·라벨 등 읽어야 하는 텍스트에는 fg-muted(4.97:1) 이상을 쓸 것. 가드 있음. */
  --v3-fg-subtle: var(--v3-neutral-500);
  --v3-fg-on-accent: var(--v3-neutral-0);

  /* ══ T2 의미 — 표면 ══ (카드 위 카드 / 모달 배경 확보) */
  --v3-bg-canvas: var(--v3-neutral-50);
  --v3-bg-surface: var(--v3-neutral-0);
  --v3-bg-surface-raised: var(--v3-neutral-0);
  --v3-bg-surface-sunken: var(--v3-neutral-50);
  --v3-bg-surface-emphasis: var(--v3-neutral-100);   /* 합계행 등 강조 표면 (P2-5) */
  --v3-bg-inverse: var(--v3-neutral-900);            /* 툴팁 등 반전 표면 (P2-5) */
  --v3-bg-overlay: rgba(18, 24, 33, 0.48);

  /* ══ T2 의미 — 경계 ══
     장식용(카드·구분선)과 **폼 경계**를 분리한다(DEC-005 §3.2).
     WCAG 1.4.11 은 UI 컴포넌트를 식별하는 경계에 3:1 을 요구하는데,
     subtle/default/strong 은 흰 배경 위 1.25 / 1.47 / 2.02:1 로 전부 미달이다.
     → 폼 컨트롤에는 border-form(3.13:1)만 쓴다. 나머지는 장식용이라 대비 요건 없음. */
  --v3-border-subtle: var(--v3-neutral-200);
  --v3-border-default: var(--v3-neutral-300);
  --v3-border-strong: var(--v3-neutral-400);
  --v3-border-form: var(--v3-neutral-550);
  --v3-border-accent: var(--v3-brand-500);
  --v3-border-inverse: var(--v3-neutral-700);   /* 반전 표면 위 경계 (P2-5) */

  /* ══ T2 의미 — 상태 3종세트(fg/bg/border) ══ (P2-3 에서 배너에 적용)
     실측: success 6.52 / warning 4.90 / danger 7.09 / info 5.91:1 — 전부 AA.
     ⚠️ *-border 3종은 아직 어떤 규칙도 참조하지 않는다(bg 대비 1.4~1.5:1 로 장식 수준).
        배너에 테두리를 넣기로 하면 그때 값을 다시 잡을 것. */
  --v3-success-fg: var(--v3-success-700);
  --v3-success-bg: var(--v3-success-50);
  --v3-success-border: #a8d9c0;
  --v3-warning-fg: var(--v3-warning-700);
  --v3-warning-bg: var(--v3-warning-50);
  --v3-warning-border: #e8cf8a;
  --v3-danger-fg: var(--v3-danger-700);
  --v3-danger-bg: var(--v3-danger-50);
  --v3-danger-border: #f0b6ae;
  --v3-info-fg: var(--v3-brand-700);
  --v3-info-bg: var(--v3-brand-50);
  --v3-info-border: var(--v3-brand-200);

  /* ══ T2 의미 — 지표 성격 ══ (DEC-009 D2) */
  --v3-nature-volume: var(--v3-volume-600);
  --v3-nature-money: var(--v3-money-600);
  --v3-nature-rate: var(--v3-rate-600);

  /* ══ T2 의미 — 인터랙션 ══ (accent=brand-600: 흰 글자 6.35:1, 기존 500 은 4.57:1) */
  --v3-accent: var(--v3-brand-600);
  --v3-accent-hover: var(--v3-brand-700);
  --v3-accent-active: var(--v3-brand-800);
  --v3-accent-subtle: var(--v3-brand-50);
  /* ⚠️ `--v3-focus-ring` 은 **box-shadow 값**이다(`0 0 0 3px …`). `outline:` 에 넣으면
     computed-value time 에 무효가 되어 **링이 아예 사라진다**(초기값으로 떨어진다).
     현재 소비자 0건 — 포커스 링은 `:where(…):focus-visible` 의 `outline: 2px solid
     var(--v3-accent)` 가 그린다. 이 토큰을 쓸 일이 생기면 `box-shadow:` 로만 써라. */
  --v3-focus-ring: 0 0 0 3px rgba(31, 79, 156, 0.40);   /* = brand-600 */
  --v3-focus-ring-offset: 2px;
  /* DEC-009: n400 은 disabled-bg 위 **1.78:1** 로 실렌더에서 읽히지 않는다.
     다크 쪽은 2026-08-29 실측 후 4.27 로 올렸는데 **라이트는 그대로였다** —
     같은 사유가 양쪽에 적용된다. n600 → 5.30:1. 본문 14.52 와의 격차가
     '비활성' 신호를 그대로 남긴다(다크 14.82 vs 4.27 과 같은 구도). */
  --v3-disabled-fg: var(--v3-neutral-600);
  --v3-disabled-bg: var(--v3-neutral-100);

  /* ══ T2 의미 — 타이포 스케일 ══ (기존 9개 임의값 → 8단계 정규화. 15/17/22px 폐기 예정) */
  --v3-fs-2xs: 11px;
  --v3-fs-xs: 12px;
  --v3-fs-sm: 13px;
  --v3-fs-md: 14px;
  --v3-fs-lg: 16px;
  --v3-fs-xl: 20px;
  --v3-fs-2xl: 24px;
  --v3-fs-3xl: 30px;

  --v3-lh-tight: 1.2;
  --v3-lh-snug: 1.35;
  --v3-lh-normal: 1.5;
  --v3-lh-loose: 1.7;
  --v3-tracking-wide: .04em;
  --v3-fw-regular: 400;
  --v3-fw-medium: 500;
  --v3-fw-semibold: 600;
  --v3-fw-bold: 700;

  /* ══ T2 의미 — 서체 ══ (DEC-009 D1 — 이전은 OS 기본 스택이라 서체 결정이 없었다)
     `--v3-font-num` 을 별도 monospace 로 두지 않는다: Pretendard 가 `tnum`·`lnum` 을
     가지고 있어 자릿수 정렬 목적이면 패밀리를 바꿀 이유가 없고, 서체가 둘이면
     KPI 라벨과 값의 회색도가 어긋난다. 폴백 스택은 그대로 남긴다. */
  --v3-font-sans: "Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont,
    "Segoe UI", "Malgun Gothic", "맑은 고딕", Roboto, sans-serif;
  --v3-font-num: var(--v3-font-sans);
  --v3-font-feature-num: "tnum" 1, "lnum" 1;

  /* ══ T2 의미 — 간격 ══ (sp-5 결번 보완 + 섹션/페이지 리듬용 상위 4단계 추가) */
  --v3-sp-0: 0;
  --v3-sp-px: 1px;
  --v3-sp-0-5: 2px;
  --v3-sp-1: 4px;
  --v3-sp-1-5: 6px;
  --v3-sp-2: 8px;
  --v3-sp-2-5: 10px;
  --v3-sp-3: 12px;
  --v3-sp-3-5: 14px;
  --v3-sp-4: 16px;
  --v3-sp-5: 20px;
  --v3-sp-6: 24px;
  --v3-sp-8: 32px;
  --v3-sp-10: 40px;
  --v3-sp-12: 48px;
  --v3-sp-16: 64px;

  /* ══ T2 의미 — 반경 / 그림자 ══ */
  --v3-radius-xs: 4px;
  --v3-radius-sm: 6px;
  --v3-radius-md: 10px;
  --v3-radius-lg: 14px;
  --v3-radius-full: 9999px;

  --v3-shadow-xs: 0 1px 2px rgba(16, 24, 40, 0.05);
  --v3-shadow-sm: 0 1px 3px rgba(16, 24, 40, 0.06), 0 1px 2px rgba(16, 24, 40, 0.04);
  --v3-shadow-md: 0 4px 12px rgba(16, 24, 40, 0.08);
  --v3-shadow-lg: 0 12px 32px rgba(16, 24, 40, 0.14);

  /* ══ T2 의미 — z-index ══ (표 28개에 고정 헤더+고정 열이 들어가면 필수) */
  --v3-z-base: 0;
  --v3-z-sticky: 100;
  --v3-z-sticky-col: 110;
  --v3-z-header: 200;
  --v3-z-dropdown: 300;
  --v3-z-overlay: 400;
  --v3-z-modal: 500;
  --v3-z-toast: 600;
  --v3-z-tooltip: 700;

  /* ══ T2 의미 — 모션 ══ */
  --v3-dur-instant: 80ms;
  --v3-dur-fast: 140ms;
  --v3-dur-normal: 220ms;
  --v3-dur-slow: 320ms;
  --v3-ease-standard: cubic-bezier(.2, 0, 0, 1);
  --v3-ease-emphasized: cubic-bezier(.05, .7, .1, 1);
  --v3-ease-exit: cubic-bezier(.3, 0, 1, 1);

  /* ══ T2 의미 — 레이아웃 ══ (헤더 56px 중복 제거용 단일 출처) */
  --v3-header-h: 56px;
  /* ⚠️ `--v3-sidebar-w`(240px)·`--v3-sidebar-w-collapsed`(64px) 제거 (DEC-009 D4).
     레일이 없어졌다. collapsed 쪽은 **정의만 되고 아무도 참조하지 않던 죽은 토큰**이었다. */
  --v3-content-max: 1600px;
  --v3-gutter: var(--v3-sp-6);
  /* 브레이크포인트는 @media 에서 var() 를 쓸 수 없으므로 값만 문서화:
     sm 640px / md 900px / lg 1100px / xl 1440px — 현행 리터럴 900·1100 이 이 어휘 */

  /* ══ T3 컴포넌트 — 데이터 시각화 ══
     범주형 **6색 + 형태 순환**. P2-1 의 12색을 폐기하고 6색으로 줄였다 — 이유는 실측이다.

     한 팔레트로 "라이트·다크 양쪽에서 비텍스트 3:1" 과 "서로 구분됨" 을 동시에 만족시킬 수 없다.
     상대휘도가 [0.127, 0.300] 안에 있어야 두 테마 모두 3:1 을 넘는데(흰 배경 3:1 → L≤0.297,
     #121821 배경 3:1 → L≥0.127), 그 좁은 띠 안에 12색을 넣으면 명도 채널을 못 쓰게 되어
     색끼리 붙는다. 실측 최소 ΔE(CIELAB):
       현행 12색           20.9  (라이트 9/12 · 다크 8/12 — 3:1 미달 7색)
       균등휘도 12색       18.8  (12/12 · 12/12 — 대비는 되지만 구분이 더 나빠짐)
       라이트 전용 12색    38.4  (12/12 · 7/12 — 다크에서 절반이 깨짐)
       **6색(60° 간격)     49.0  (6/6 · 6/6)**  ← 채택
     6색은 현행보다 구분도가 2.3배고 양 테마를 다 만족한다. 계열이 7개 이상이면
     색을 재사용하고 **line.dash / marker.symbol** 로 구분한다(색만으로 정보 전달 금지 = 1.4.1 에도 부합).
     부수 효과: 다크용 범주형 트윈이 필요 없어 P2-7 이 단순해진다. */
  --v3-viz-cat-1: #927d15;   /* 황토 */
  --v3-viz-cat-2: #2a9215;   /* 초록 */
  --v3-viz-cat-3: #148f7a;   /* 청록 */
  --v3-viz-cat-4: #5f76e8;   /* 파랑 */
  --v3-viz-cat-5: #c73de3;   /* 자보라 */
  --v3-viz-cat-6: #e4415c;   /* 적분홍 */
  --v3-viz-cat-count: 6;     /* charts.py 가 순환 주기를 알기 위한 값 */
  /* T1 직접 참조 금지 — 다크는 T2 매핑 교체로 따라와야 한다(아래 P2-5 주석 참조) */
  --v3-viz-neutral: var(--v3-border-default);
  --v3-viz-highlight: var(--v3-accent);
  /* 데이터가 색을 정하는 표면 위의 글자 (SCR-008 RFM 세그먼트 칩).
     세그먼트 7색은 **테마와 무관한 고정 값**이라 --v3-fg-default 를 쓰면 다크에서
     밝은 글자가 밝은 칩 위에 얹혀 대비가 무너진다. 그래서 이 토큰은 다크에서도
     뒤집히지 않는다. 7색 전부 이 값과 4.5:1 이상이다(실측 최저 #4A90D9 = 5.6:1). */
  --v3-viz-on-light: #121821;

  /* ══ T3 컴포넌트 — 차트 크롬 ══ (charts.py::_layout_base 가 읽을 값, P2-5) */
  --v3-chart-paper-bg: var(--v3-bg-surface);
  --v3-chart-plot-bg: transparent;
  --v3-chart-grid: var(--v3-border-subtle);
  --v3-chart-axis-fg: var(--v3-fg-muted);
  --v3-chart-label-fg: var(--v3-fg-default);
  --v3-chart-font-size: 13px;
  --v3-chart-title-size: 15px;
  /* 툴팁 — 어두운 표면 위 밝은 글자(관례). Plotly 가 자동 산출하던 색을 토큰으로 고정 */
  --v3-chart-hover-bg: var(--v3-bg-inverse);
  --v3-chart-hover-fg: var(--v3-fg-on-accent);
  --v3-chart-hover-border: var(--v3-border-inverse);
  --v3-chart-h-sm: 320px;
  --v3-chart-h-md: 420px;
  --v3-chart-h-lg: 500px;
  --v3-chart-h-xl: 600px;

  /* ══ T3 컴포넌트 — 테이블 밀도 ══ (P2-6 에서 data-density 속성으로 수렴) */
  --v3-table-cell-py-compact: 4px;
  --v3-table-cell-py-cozy: 8px;
  --v3-table-cell-py-comfy: 12px;
  --v3-table-cell-px: 10px;
  --v3-table-fs-compact: var(--v3-fs-xs);
  --v3-table-fs-cozy: var(--v3-fs-sm);
  --v3-table-head-bg: var(--v3-bg-surface-sunken);
  --v3-table-head-fg: var(--v3-fg-muted);
  --v3-table-row-hover-bg: var(--v3-bg-surface-sunken);
  --v3-table-row-selected-bg: var(--v3-accent-subtle);
  --v3-table-border: var(--v3-border-subtle);
  --v3-table-total-row-bg: var(--v3-bg-surface-emphasis);

  /* ══ T3 컴포넌트 — 히트맵 셀 텍스트 ══ (P2-5)
     ⚠️ **이름의 light/dark 는 테마가 아니라 글자 밝기다**(P2-5 시절 어휘). 다크모드가
     생긴 뒤로 이 이름은 오해를 부른다 — `--v3-heat-text-dark`(짙은 글자)가 **다크
     테마에서 밝은 글자**로 뒤집히는 것이 정상 동작이다(`--v3-fg-default` 를 따른다).

     `--v3-heat-text-light` 는 셀별 글자색 뒤집기가 DEV-001 §5 에서 폐기된 뒤로
     **참조 0건**이다. 그래도 남겨 둔다 — `tests/test_v3_tokens.py` 가 T3 티어 계약으로
     고정하고 있어 지우려면 토큰 계약을 함께 손봐야 한다(Task-M022 범위 밖).
     ⚠️ 쓰지 말 것: 다크에서 `--v3-fg-on-accent` 를 따라 **어두운 색**이 되므로 이름과
     반대로 동작한다. 밝은 글자가 필요하면 `--v3-fg-default` 를 써라. */
  --v3-heat-text-light: var(--v3-fg-on-accent);
  --v3-heat-text-dark: var(--v3-fg-default);

  /* ══ T3 컴포넌트 — 대표 치수 ══ */
  --v3-btn-h-sm: 28px;
  --v3-btn-h-md: 34px;
  --v3-btn-h-lg: 40px;
  --v3-field-h: 34px;
  --v3-tab-indicator-h: 2px;
  --v3-toast-w: 360px;

  /* ==== DEC-008 계층 2 : motion.csv 두 티어 (Task-M024) ==================
     ⚠️ `overshoot` 를 **밀집 데이터 표에 쓰지 말 것** — motion.csv 의 Don't 를
        그대로 옮긴다: "정보성 UI 에서 오버슛은 sloppy 하게 읽힌다".
        코드로도 강제한다: `viz_catalog.motion_tier_for('table-row') == 'subtle'`.
     기존 --v3-dur-* 는 **상태 전이**(hover 등)용이고, 아래는 **등장**용이라 따로 둔다. */
  --v3-dur-enter: 400ms;                            /* Standard 티어 */
  --v3-dur-enter-subtle: 300ms;                     /* Subtle 티어 */
  --v3-ease-overshoot: cubic-bezier(.34, 1.45, .64, 1);   /* ≈ back.out(1.4) */
  --v3-ease-decel: cubic-bezier(.25, .46, .45, .94);      /* ≈ power1.out */
  --v3-stagger-standard: 60ms;
  --v3-stagger-subtle: 30ms;
  /* 스태거를 이 개수까지만 준다. 300행 표에 30ms 씩 주면 마지막 행이 9초 뒤에 온다. */
  --v3-stagger-cap: 10;

  /* ==== DEC-008 계층 3 : 아트디렉션 T3 (Task-M025) ======================
     「환승 통로」 띠 색은 **채널 범주가 아니라 변화 방향**이다 — 범주색이면 예쁘기만
     하고 인사이트를 못 푼다. KPI 델타(is-up/is-down)와 **같은 문법**을 쓰도록 같은
     T2 토큰을 참조한다.
     ⚠️ 다크 오버라이드가 **필요 없다** — 참조하는 T2 가 이미 뒤집힌다
        (success 11.02:1 / danger 9.36:1 / fg-muted 7.94:1). 다크 블록 두 벌을
        건드리지 않는 것이 이 설계의 이득이다. */
  --v3-ribbon-up: var(--v3-success);
  --v3-ribbon-down: var(--v3-danger);
  --v3-ribbon-flat: var(--v3-fg-muted);
  --v3-ribbon-sep: var(--v3-bg-surface);      /* charts.csv "white separator borders" */
  --v3-share-fill: var(--v3-accent);
  --v3-share-track: var(--v3-bg-surface-emphasis);

  /* ══════════════════════════════════════════════════════════════════════
     레거시 별칭 — 기존 컴포넌트 CSS 가 참조하는 토큰.
     해석값은 P2-1 이전과 완전히 동일하다.
     후속 단계에서 컴포넌트 규칙을 T2/T3 로 옮기며 하나씩 걷어낸다.
     제거 이력: --v3-info, --v3-warning (P2-3 — 배너가 상태 3종세트로 옮겨가며 참조 0)
     ══════════════════════════════════════════════════════════════════════ */
  --v3-bg: var(--v3-neutral-50);              /* #f5f6f8 */
  --v3-surface: var(--v3-neutral-0);          /* #ffffff */
  --v3-border: var(--v3-neutral-200);         /* #e3e6ea */
  --v3-text: var(--v3-neutral-900);           /* #1f2733 */
  --v3-text-muted: var(--v3-neutral-600);     /* #667085 */
  --v3-primary: var(--v3-brand-500);          /* #2f6feb — 활성 탭·브랜드 강조에 잔존 */
  --v3-primary-weak: var(--v3-brand-50);      /* #eaf1fe */
  --v3-success: var(--v3-success-500);        /* #1a7f52 — KPI 상승(흰 배경 5.00:1) */
  --v3-danger: var(--v3-danger-500);          /* #c0392b — KPI 하락(흰 배경 5.44:1) */
  --v3-radius: var(--v3-radius-md);           /* 10px */
  --v3-font: var(--v3-font-sans);
  --v3-shadow: var(--v3-shadow-sm);
  /* --v3-sp-1..sp-6 / --v3-radius-sm 은 T2 에서 동일 이름·동일 값으로 정의됨 */

}

/* ══════════════════════════════════════════════════════════════════════════
   다크 테마 (DEC-005 §3.3 · Task-M022)

   T1 램프는 그대로 두고 **T2 매핑만 교체**한다. 컴포넌트 규칙은 한 줄도 바뀌지
   않는다 — 실제로 컴포넌트 CSS 에는 색 리터럴이 0건이라 이 교체만으로 전 화면이 뒤집힌다.

   ## 왜 같은 블록이 두 번 나오는가

   `prefers-color-scheme` 은 `@media` 안에서만 물을 수 있고, 사용자의 명시적 선택
   (`data-theme`)은 밖에서만 걸 수 있다. CSS 에는 이 둘을 하나의 규칙으로 묶는 방법이 없다.

   그래서 **의도적으로 복제하되, 두 블록이 글자 단위로 같은지 테스트가 단언한다**
   (`tests/test_v3_dark_mode.py::test_two_dark_blocks_are_identical`). 같은 사실을 두 곳에
   적으면 한 곳만 고치게 된다 — 2026-08-29 SELF-CHECK 가 그 유형의 결함을 3건 잡았다.
   여기서는 복제를 없앨 수 없으니 **드리프트를 기계가 막게** 한다.

   3상태다: `data-theme` 없음 = 시스템 설정 따름 / `light` = 강제 라이트 / `dark` = 강제 다크.
   ══════════════════════════════════════════════════════════════════════════ */

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
  color-scheme: dark;

  /* 전경 — 램프를 뒤집는다. n50 on surface = 14.82:1 (실측) */
  --v3-fg-default: var(--v3-neutral-50);
  --v3-fg-muted: var(--v3-neutral-400);      /* surface 7.94:1 */
  --v3-fg-subtle: var(--v3-neutral-600);     /* 비텍스트 전용(라이트와 같은 규율) */
  /* ⚠️ 다크의 accent 는 **밝은 파랑**이라 그 위 글자는 어두워야 한다.
     n0(흰색)을 얹으면 3.50:1 로 AA 미달이다(실측). 이 토큰은 뒤집힌다. */
  --v3-fg-on-accent: var(--v3-neutral-950);

  /* 표면 */
  --v3-bg-canvas: var(--v3-neutral-975);
  --v3-bg-surface: var(--v3-neutral-925);
  --v3-bg-surface-raised: var(--v3-neutral-875);
  --v3-bg-surface-sunken: var(--v3-neutral-950);
  --v3-bg-surface-emphasis: var(--v3-neutral-875);
  --v3-bg-inverse: var(--v3-neutral-100);    /* 반전이므로 다크에서는 밝다 */
  --v3-bg-overlay: rgba(0, 0, 0, 0.62);

  /* 경계 — form 은 1.4.11 대상이라 surface 대비 6.15:1 을 확보한다 */
  --v3-border-subtle: var(--v3-neutral-800);
  --v3-border-default: var(--v3-neutral-700);
  --v3-border-strong: var(--v3-neutral-600);
  --v3-border-form: var(--v3-neutral-500);
  --v3-border-accent: var(--v3-brand-300);
  --v3-border-inverse: var(--v3-neutral-300);

  /* 상태 3종세트 — 전경은 밝게, 배경은 짙게. 실측 9.50~10.50:1 */
  --v3-success-fg: var(--v3-success-200);
  --v3-success-bg: var(--v3-success-950);
  --v3-success-border: var(--v3-success-700);
  --v3-warning-fg: var(--v3-warning-200);
  --v3-warning-bg: var(--v3-warning-950);
  --v3-warning-border: var(--v3-warning-700);
  --v3-danger-fg: var(--v3-danger-200);
  --v3-danger-bg: var(--v3-danger-950);
  --v3-danger-border: var(--v3-danger-700);
  --v3-info-fg: var(--v3-brand-200);
  --v3-info-bg: var(--v3-brand-950);
  --v3-info-border: var(--v3-brand-800);

  /* 지표 성격 — 어두운 표면에선 밝은 쪽(300)을 쓴다 */
  --v3-nature-volume: var(--v3-volume-300);
  --v3-nature-money: var(--v3-money-300);
  --v3-nature-rate: var(--v3-rate-300);

  /* 인터랙션 — hover 가 **밝아진다**(라이트에서는 어두워진다) */
  --v3-accent: var(--v3-brand-400);          /* surface 4.58:1 · 글자 5.09:1 */
  --v3-accent-hover: var(--v3-brand-300);
  --v3-accent-active: var(--v3-brand-200);
  --v3-accent-subtle: var(--v3-brand-950);
  --v3-focus-ring: 0 0 0 3px rgba(137, 177, 240, 0.55);  /* = brand-300 */
  /* 비활성 컨트롤은 1.4.3 대비 요건에서 면제되지만, n600/n800 은 2.24:1 로 실렌더에서
     읽히지 않았다(2026-08-29 브라우저 실측). n500 으로 올려 4.27:1 을 준다 — 본문
     14.82:1 보다 확연히 흐려서 '비활성' 신호는 그대로 남는다. */
  --v3-disabled-fg: var(--v3-neutral-500);
  --v3-disabled-bg: var(--v3-neutral-800);

  /* 그림자 — 어두운 배경에서는 검게, 더 진하게 깔아야 층이 보인다 */
  --v3-shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.40);
  --v3-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.46), 0 1px 2px rgba(0, 0, 0, 0.34);
  --v3-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.52);
  --v3-shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.60);

  /* T3 — viz 범주형 6색은 **바꾸지 않는다.** 두 테마 모두 3:1 을 넘도록 고른 값이다
     (DEC-005 §3.1 실측표: 6/6 · 6/6). 차트 크롬 토큰은 전부 T2 참조라 자동으로 따라온다. */
  --v3-viz-neutral: var(--v3-border-strong);

  /* 레거시 별칭 — 컴포넌트 CSS 가 아직 이걸 참조한다. 함께 뒤집지 않으면
     화면 절반만 다크가 된다. */
  --v3-bg: var(--v3-bg-canvas);
  --v3-surface: var(--v3-bg-surface);
  --v3-border: var(--v3-border-subtle);
  --v3-text: var(--v3-fg-default);
  --v3-text-muted: var(--v3-fg-muted);
  --v3-primary: var(--v3-brand-300);         /* surface 6.54:1 */
  --v3-primary-weak: var(--v3-brand-950);
  --v3-success: var(--v3-success-200);       /* KPI 상승 — surface 11.02:1 */
  --v3-danger: var(--v3-danger-200);         /* KPI 하락 — surface 9.36:1 */
  --v3-shadow: var(--v3-shadow-sm);
}
}

:root[data-theme="dark"] {
  color-scheme: dark;

  /* 전경 — 램프를 뒤집는다. n50 on surface = 14.82:1 (실측) */
  --v3-fg-default: var(--v3-neutral-50);
  --v3-fg-muted: var(--v3-neutral-400);      /* surface 7.94:1 */
  --v3-fg-subtle: var(--v3-neutral-600);     /* 비텍스트 전용(라이트와 같은 규율) */
  /* ⚠️ 다크의 accent 는 **밝은 파랑**이라 그 위 글자는 어두워야 한다.
     n0(흰색)을 얹으면 3.50:1 로 AA 미달이다(실측). 이 토큰은 뒤집힌다. */
  --v3-fg-on-accent: var(--v3-neutral-950);

  /* 표면 */
  --v3-bg-canvas: var(--v3-neutral-975);
  --v3-bg-surface: var(--v3-neutral-925);
  --v3-bg-surface-raised: var(--v3-neutral-875);
  --v3-bg-surface-sunken: var(--v3-neutral-950);
  --v3-bg-surface-emphasis: var(--v3-neutral-875);
  --v3-bg-inverse: var(--v3-neutral-100);    /* 반전이므로 다크에서는 밝다 */
  --v3-bg-overlay: rgba(0, 0, 0, 0.62);

  /* 경계 — form 은 1.4.11 대상이라 surface 대비 6.15:1 을 확보한다 */
  --v3-border-subtle: var(--v3-neutral-800);
  --v3-border-default: var(--v3-neutral-700);
  --v3-border-strong: var(--v3-neutral-600);
  --v3-border-form: var(--v3-neutral-500);
  --v3-border-accent: var(--v3-brand-300);
  --v3-border-inverse: var(--v3-neutral-300);

  /* 상태 3종세트 — 전경은 밝게, 배경은 짙게. 실측 9.50~10.50:1 */
  --v3-success-fg: var(--v3-success-200);
  --v3-success-bg: var(--v3-success-950);
  --v3-success-border: var(--v3-success-700);
  --v3-warning-fg: var(--v3-warning-200);
  --v3-warning-bg: var(--v3-warning-950);
  --v3-warning-border: var(--v3-warning-700);
  --v3-danger-fg: var(--v3-danger-200);
  --v3-danger-bg: var(--v3-danger-950);
  --v3-danger-border: var(--v3-danger-700);
  --v3-info-fg: var(--v3-brand-200);
  --v3-info-bg: var(--v3-brand-950);
  --v3-info-border: var(--v3-brand-800);

  /* 지표 성격 — 어두운 표면에선 밝은 쪽(300)을 쓴다 */
  --v3-nature-volume: var(--v3-volume-300);
  --v3-nature-money: var(--v3-money-300);
  --v3-nature-rate: var(--v3-rate-300);

  /* 인터랙션 — hover 가 **밝아진다**(라이트에서는 어두워진다) */
  --v3-accent: var(--v3-brand-400);          /* surface 4.58:1 · 글자 5.09:1 */
  --v3-accent-hover: var(--v3-brand-300);
  --v3-accent-active: var(--v3-brand-200);
  --v3-accent-subtle: var(--v3-brand-950);
  --v3-focus-ring: 0 0 0 3px rgba(137, 177, 240, 0.55);  /* = brand-300 */
  /* 비활성 컨트롤은 1.4.3 대비 요건에서 면제되지만, n600/n800 은 2.24:1 로 실렌더에서
     읽히지 않았다(2026-08-29 브라우저 실측). n500 으로 올려 4.27:1 을 준다 — 본문
     14.82:1 보다 확연히 흐려서 '비활성' 신호는 그대로 남는다. */
  --v3-disabled-fg: var(--v3-neutral-500);
  --v3-disabled-bg: var(--v3-neutral-800);

  /* 그림자 — 어두운 배경에서는 검게, 더 진하게 깔아야 층이 보인다 */
  --v3-shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.40);
  --v3-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.46), 0 1px 2px rgba(0, 0, 0, 0.34);
  --v3-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.52);
  --v3-shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.60);

  /* T3 — viz 범주형 6색은 **바꾸지 않는다.** 두 테마 모두 3:1 을 넘도록 고른 값이다
     (DEC-005 §3.1 실측표: 6/6 · 6/6). 차트 크롬 토큰은 전부 T2 참조라 자동으로 따라온다. */
  --v3-viz-neutral: var(--v3-border-strong);

  /* 레거시 별칭 — 컴포넌트 CSS 가 아직 이걸 참조한다. 함께 뒤집지 않으면
     화면 절반만 다크가 된다. */
  --v3-bg: var(--v3-bg-canvas);
  --v3-surface: var(--v3-bg-surface);
  --v3-border: var(--v3-border-subtle);
  --v3-text: var(--v3-fg-default);
  --v3-text-muted: var(--v3-fg-muted);
  --v3-primary: var(--v3-brand-300);         /* surface 6.54:1 */
  --v3-primary-weak: var(--v3-brand-950);
  --v3-success: var(--v3-success-200);       /* KPI 상승 — surface 11.02:1 */
  --v3-danger: var(--v3-danger-200);         /* KPI 하락 — surface 9.36:1 */
  --v3-shadow: var(--v3-shadow-sm);
}

/* 모션 토큰을 쓰는 규칙이 아직 없어 현재 영향은 없지만, 모션 도입(P2-4/P2-7) 전에 선행 배치 */
@media (prefers-reduced-motion: reduce) {
  :root {
    --v3-dur-instant: 0.01ms;
    --v3-dur-fast: 0.01ms;
    --v3-dur-normal: 0.01ms;
    --v3-dur-slow: 0.01ms;
  }
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: var(--v3-font);
  color: var(--v3-text);
  background: var(--v3-bg);
  font-size: var(--v3-fs-md);
  line-height: var(--v3-lh-normal);
  /* TR-004 T-8: 커닝·합자 지시가 **0건**이었다 → 서체를 바꿔도 조판이 따라오지 않는다.
     A2 시안이 검증한 조합 그대로. */
  font-feature-settings: "kern" 1;
  text-rendering: optimizeLegibility;
}

/* 헤더 */
.v3-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: var(--v3-header-h);
  padding: 0 var(--v3-sp-6);
  background: var(--v3-surface);
  border-bottom: 1px solid var(--v3-border);
}
.v3-brand {
  display: inline-flex; align-items: center; gap: var(--v3-sp-2);
  font-weight: var(--v3-fw-bold);
  font-size: var(--v3-fs-lg);
  color: var(--v3-text);
  text-decoration: none;
}
.v3-brand em { color: var(--v3-primary); font-style: normal; }
/* SELPER 로고(2026-09-08) — 투명 배경 PNG, 파랑 #2B98FF 는 라이트 3.0:1 / 다크 서피스 5.3:1 (로고는 텍스트 대비 기준 대상이
   아니지만 다크에서 더 잘 보인다). 표시 높이 28px, 파일은 2배(56px)로 레티나 대응. */
.v3-brand-logo { display: block; height: 28px; width: auto; }
.v3-header-right { display: flex; align-items: center; gap: var(--v3-sp-3); color: var(--v3-text-muted); }
/* 테마 토글 (Task-M022) — 자동/라이트/다크 3상태를 한 버튼으로 돈다.
   JS 가 꺼져 있으면 아무 일도 일어나지 않으므로 aria-live 로 상태를 알리지 않는다
   (값은 버튼 라벨 자체가 말한다). */
.v3-theme-toggle { display: inline-flex; align-items: center; gap: var(--v3-sp-1); }
.v3-theme-toggle .v3-theme-icon { font-size: var(--v3-fs-md); line-height: 1; }
.v3-theme-label { font-size: var(--v3-fs-sm); }
/* 좁은 화면에서는 아이콘만 — 헤더가 두 줄이 되는 것을 막는다.
   ⚠️ 라벨만 숨기면 모자란다: 375px 실측에서 버튼 여백만으로 10px 가 넘쳤다
   (scrollWidth 385 vs 375). 여백까지 줄여야 오버플로 0 이 유지된다(Task-M023). */
@media (max-width: 40rem) {
  .v3-theme-label { display: none; }
  .v3-theme-toggle { padding: var(--v3-sp-1) var(--v3-sp-2); }
  /* 브랜드는 줄여서라도 한 줄에 남는다 — 잘리는 편이 헤더가 무너지는 것보다 낫다 */
  .v3-brand { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
}

/* 본문 — 단일 컬럼 (DEC-009 D4: 좌측 240px 필터 레일 제거).
   레일은 1440 화면의 17% 를 상시 점유하면서 세션당 한 번 만지는 컨트롤 3개만
   담고 있었고, 375px 가로 스크롤 결함(TR-002 ④)의 원인이기도 했다
   — "레일+본문 2단이 안 쌓여 본문이 135px 로 눌림". 원인 자체가 사라졌다. */
.v3-body { min-width: 0; }
.v3-main { padding: 0 var(--v3-sp-6) var(--v3-sp-6); min-width: 0; }

/* 카드(스캐폴드 기본) */
.v3-card {
  background: var(--v3-surface);
  border: 1px solid var(--v3-border);
  border-radius: var(--v3-radius);
  box-shadow: var(--v3-shadow);
  padding: var(--v3-sp-4);
}

/* 인사이트 배너 4종(DEC-003 — 클래스만 정의, 내용은 tab_metrics 에서)
   P2-3: 전경을 상태 3종세트 *-fg 로 교체해 WCAG AA 를 만족시킨다(DEV-001).
   배경은 불변, 글자만 어두워진다:
     success 4.49 → 6.52 / warning 3.03 → 4.90 / info 4.03 → 5.91 / error 4.76 → 7.09 */
.v3-banner { border-radius: var(--v3-radius-sm); padding: var(--v3-sp-3) var(--v3-sp-4); }
.v3-banner--success { background: var(--v3-success-bg); color: var(--v3-success-fg); }
.v3-banner--warning { background: var(--v3-warning-bg); color: var(--v3-warning-fg); }
.v3-banner--info    { background: var(--v3-info-bg);    color: var(--v3-info-fg); }
.v3-banner--error   { background: var(--v3-danger-bg);  color: var(--v3-danger-fg); }
/* 데이터 커버리지 안내 (Task-M017) — 배너 위에 리듬만 얹는다. 색은 level 이 정한다. */
.v3-notice { margin-bottom: var(--v3-sp-4); display: flex; flex-direction: column; gap: var(--v3-sp-1); }

/* ── Overview 셸 (Task-009, G3) ───────────────────────────────────────── */

/* 헤더 사용자/버튼 */
.v3-user { font-size: var(--v3-fs-sm); color: var(--v3-text-muted); }
.v3-inline-form { display: inline; margin: 0; }

/* 버튼 */
/* 보조 버튼은 흰 배경 위 테두리만으로 식별되므로 경계가 곧 컴포넌트 식별 정보다
   → 1.4.11 에 따라 border-form(3.13:1). 카드 경계(장식)와 다른 토큰을 쓰는 이유. */
.v3-btn {
  font: inherit; cursor: pointer; border-radius: var(--v3-radius-sm);
  padding: var(--v3-sp-1-5) var(--v3-sp-3-5); border: 1px solid var(--v3-border-form);
  background: var(--v3-surface); color: var(--v3-text);
  /* P2-4: 리터럴 .12s → 모션 토큰. 120→140ms 로 20ms 늘지만 정지 화면은 동일하고,
     이걸로 :root 의 prefers-reduced-motion 블록이 비로소 실제 효력을 갖는다. */
  transition: background var(--v3-dur-fast) var(--v3-ease-standard),
              border-color var(--v3-dur-fast) var(--v3-ease-standard),
              color var(--v3-dur-fast) var(--v3-ease-standard);
}
.v3-btn:hover { border-color: var(--v3-primary); }
/* P2-3: 기본색을 brand-500 → accent(brand-600) 로 상향 → 흰 글자 4.57 → 6.35:1.
   hover 는 accent-hover(brand-700) 로 밀어 상태 구분을 유지한다. */
.v3-btn--primary { background: var(--v3-accent); border-color: var(--v3-accent); color: var(--v3-fg-on-accent); }
.v3-btn--primary:hover { background: var(--v3-accent-hover); border-color: var(--v3-accent-hover); }
/* ghost 는 **경계에 의존하지 않는** 텍스트 버튼이다(라벨이 곧 식별 정보) —
   1.4.11 은 경계를 식별에 쓰는 컴포넌트에만 3:1 을 요구하므로, 경계를 없애는 것이
   기존의 1.25:1 짜리 흐린 테두리를 유지하는 것보다 올바른 해결이다.
   대신 경계가 사라진 만큼 hover 배경으로 조작 가능성을 남긴다(muted 글자 4.60:1). */
.v3-btn--ghost { background: transparent; border-color: transparent; color: var(--v3-text-muted); }
/* border-color 도 다시 지정해야 한다 — 위 .v3-btn:hover(같은 0,2,0)가 primary 색을
   넣어버려서, --ghost 의 transparent 만으로는 hover 때 파란 테두리가 살아난다. */
.v3-btn--ghost:hover { background: var(--v3-bg-surface-sunken); border-color: transparent; color: var(--v3-text); }

/* ── 필터 바 (DEC-009 D4) — 레일을 대신한다 ─────────────────────────────
   본문 최상단 sticky. 레일이 주던 유일한 장점("스크롤해도 기간이 보인다")은
   **접힘**으로 유지한다 — 스크롤하면 한 줄로 접히고 맨 위로 오면 펼쳐진다.
   240px 을 42px 로 되산다.
   ⚠️ 음수 마진으로 본문 패딩을 뚫어 전폭 경계선을 만든다. 안쪽 패딩으로 되돌린다. */
.v3-fbar {
  position: sticky; top: 0; z-index: var(--v3-z-sticky);
  margin: 0 calc(-1 * var(--v3-sp-6)) var(--v3-sp-5);
  padding: var(--v3-sp-3-5) var(--v3-sp-6) var(--v3-sp-3);
  background: var(--v3-bg-surface); border-bottom: 1px solid var(--v3-border);
}
/* 폼 자체는 배치를 갖지 않는다 — 행이 배치를 갖는다(접힘 시 form 째 hidden). */
.v3-fb-form { display: block; }
/* 컨트롤은 왼쪽부터 한 문장처럼 이어진다 — 월 → 개수 → 적용.
   `flex:1 1 auto` 였을 때 칩이 남는 폭을 다 먹어 버튼이 화면 끝으로 밀려났다. */
.v3-fb-grp--months { flex: 0 1 auto; }
/* 제출 버튼 라벨은 줄바꿈하지 않는다 — '기간 적용' 이 두 줄이 되면 바가 흔들린다 */
.v3-apply { white-space: nowrap; }
.v3-fb-row { display: flex; align-items: flex-start; gap: var(--v3-sp-6); flex-wrap: wrap; }
.v3-fb-grp { min-width: 0; border: 0; margin: 0; padding: 0; }
.v3-fb-grp--act .v3-fb-lab { visibility: hidden; }
.v3-fb-lab {
  display: block; padding: 0; font-size: var(--v3-fs-xs);
  font-weight: var(--v3-fw-semibold); color: var(--v3-text-muted);
  margin: 0 0 var(--v3-sp-1-5);
}
.v3-fb-hint { margin: var(--v3-sp-1) 0 0; font-size: var(--v3-fs-2xs); color: var(--v3-text-muted); }

/* 월 칩 — 21개월을 스크롤 0 으로 전부. 실물은 `size="8"` 이라 8개만 보였다. */
.v3-fb-years { display: flex; flex-direction: column; gap: var(--v3-sp-1-5); }
.v3-fb-year { display: flex; align-items: flex-start; gap: var(--v3-sp-2); }
.v3-fb-yl {
  flex: 0 0 2rem; padding-top: var(--v3-sp-2); font-size: var(--v3-fs-2xs);
  font-weight: var(--v3-fw-bold); color: var(--v3-text-muted);
  font-variant-numeric: tabular-nums lining-nums;
}
/* ⚠️ `position: relative` — 안의 절대배치 체크박스가 이 상자를 **탈출**하면
   페이지가 옆으로 스크롤된다(2026-08-31 `.v3-sr-only` 실측 235px). */
.v3-fb-chips { position: relative; display: flex; flex-wrap: wrap; gap: var(--v3-sp-1); min-width: 0; }
.v3-fb-chk { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
/* 경계가 컨트롤을 식별하는 유일한 정보 → 1.4.11 3:1 (border-form). 장식용 경계 금지. */
.v3-chip {
  display: inline-block; min-width: 2.125rem; text-align: center; cursor: pointer;
  padding: var(--v3-sp-1-5) var(--v3-sp-2); border: 1px solid var(--v3-border-form);
  border-radius: var(--v3-radius-sm); background: var(--v3-bg-surface);
  color: var(--v3-text); font-size: var(--v3-fs-xs); font-weight: var(--v3-fw-semibold);
  line-height: 1; font-variant-numeric: tabular-nums lining-nums;
  transition: background var(--v3-dur-fast) var(--v3-ease-standard),
              border-color var(--v3-dur-fast) var(--v3-ease-standard);
}
.v3-chip:hover { border-color: var(--v3-accent); }
.v3-fb-chk:checked + .v3-chip {
  background: var(--v3-accent); border-color: var(--v3-accent);
  color: var(--v3-fg-on-accent); font-weight: var(--v3-fw-bold);
}
/* 체크박스가 시각적으로 숨겨져 있으므로 포커스 링을 라벨이 대신 그린다 */
.v3-fb-chk:focus-visible + .v3-chip {
  outline: 2px solid var(--v3-accent); outline-offset: var(--v3-focus-ring-offset);
}
/* 최대 개수 도달 — `opacity` 로 흐리면 대비가 무너진다. 표면을 낮추고 글자는 muted. */
.v3-chip[aria-disabled="true"] {
  background: var(--v3-bg-surface-emphasis); color: var(--v3-text-muted);
  cursor: not-allowed;
}
.v3-chip[aria-disabled="true"]:hover { border-color: var(--v3-border-form); }

.v3-fb-num { display: flex; align-items: center; gap: var(--v3-sp-2); }
.v3-fb-num .v3-range { width: 8.5rem; }
.v3-fb-out {
  font-size: var(--v3-fs-lg); font-weight: var(--v3-fw-bold); min-width: 1.75rem;
  text-align: right; font-variant-numeric: tabular-nums lining-nums;
}
.v3-fb-unit { font-size: var(--v3-fs-xs); color: var(--v3-text-muted); }

/* 접힌 한 줄 — 레일 240px 을 42px 로 되사는 장치 */
.v3-fb-mini {
  display: flex; align-items: center; gap: var(--v3-sp-2); flex-wrap: wrap;
  font-size: var(--v3-fs-sm); color: var(--v3-text-muted);
}
.v3-fb-mini b { color: var(--v3-text); font-variant-numeric: tabular-nums lining-nums; }
.v3-fb-edit {
  margin-left: var(--v3-sp-1); font: inherit; font-size: var(--v3-fs-xs);
  font-weight: var(--v3-fw-semibold); padding: var(--v3-sp-1) var(--v3-sp-3);
  border: 1px solid var(--v3-border-form); border-radius: var(--v3-radius-full);
  background: var(--v3-bg-surface); color: var(--v3-accent); cursor: pointer;
}
.v3-fb-edit:hover { border-color: var(--v3-accent); }
/* ⚠️ `hidden` 속성은 UA 규칙(특이도 0,1,0)이라 뒤에 오는 클래스 규칙에 진다.
   `.v3-fb-mini{display:flex}` 가 실제로 이겨서 접힌 줄이 항상 보였다(A2 실측). */
[hidden] { display: none !important; }

@media (max-width: 40rem) {
  .v3-main { padding: 0 var(--v3-sp-4) var(--v3-sp-4); width: 100%; }
  .v3-fbar { margin: 0 calc(-1 * var(--v3-sp-4)) var(--v3-sp-4);
    padding: var(--v3-sp-3) var(--v3-sp-4); }
  .v3-fb-row { gap: var(--v3-sp-3); }
  .v3-fb-grp { width: 100%; }
  .v3-fb-grp--act .v3-btn { width: 100%; }
  .v3-fb-num .v3-range { flex: 1 1 auto; width: auto; }
  .v3-kpi-grid,
  .v3-kpi-section .v3-kpi-grid { grid-template-columns: 1fr; }
  /* 탭이 8개까지 있어 375px 를 넘는다 — 내비(.v3-nav)와 같은 처방으로 가로 스크롤.
     탭을 줄바꿈하면 활성 표시(하단 보더)가 여러 줄에 걸쳐 읽히지 않는다. */
  .v3-tabbar { position: relative;
    overflow-x: auto; flex-wrap: nowrap;
    -webkit-overflow-scrolling: touch;
  }
  .v3-tab { flex: 0 0 auto; white-space: nowrap; }
  /* 헤더가 375px 에서 눌려 '관리'·'로그아웃'이 글자 단위로 세로 줄바꿈됐다.
     높이를 풀고 줄바꿈만 막는다(이메일은 길어서 숨긴다 — 계정 식별은 어드민 화면에 있다). */
  .v3-header { height: auto; padding: var(--v3-sp-2) var(--v3-sp-4); gap: var(--v3-sp-2); }
  .v3-header-right { gap: var(--v3-sp-2); flex-shrink: 0; }
  .v3-header-right > * { white-space: nowrap; }
  .v3-header-right .v3-user { display: none; }
  .v3-brand { font-size: var(--v3-fs-md); white-space: nowrap; }
  .v3-brand-text { display: none; } /* 좁은 화면: 로고만 — 링크 이름은 img alt(SELPER) 가 유지한다 */
}
.v3-field-label { font-size: var(--v3-fs-sm); font-weight: var(--v3-fw-semibold); margin-top: var(--v3-sp-3); }
.v3-muted { color: var(--v3-text-muted); font-weight: var(--v3-fw-regular); }
.v3-select, .v3-range { width: 100%; }
/* 폼 컨트롤: 경계가 컴포넌트를 식별하는 유일한 시각 정보 → 1.4.11 3:1 (P2-3) */
.v3-select {
  border: 1px solid var(--v3-border-form); border-radius: var(--v3-radius-sm);
  padding: var(--v3-sp-1); font: inherit; color: var(--v3-text); background: var(--v3-surface);
}
.v3-select option:checked { background: var(--v3-primary-weak); }

/* KPI 그리드 8장 */
.v3-kpi-section { margin-bottom: var(--v3-sp-6); }
.v3-kpi-grid {
  display: grid; gap: var(--v3-sp-3);
  grid-template-columns: repeat(4, minmax(0, 1fr));
}
@media (max-width: 1100px) { .v3-kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.v3-kpi-card {
  background: var(--v3-surface); border: 1px solid var(--v3-border);
  border-radius: var(--v3-radius); box-shadow: var(--v3-shadow);
  padding: var(--v3-sp-4);
}
.v3-kpi-label { font-size: var(--v3-fs-xs); color: var(--v3-text-muted); }
/* 22px 는 타이포 스케일 밖(폐기 예정 3종 중 하나) — P3 리테마에서 --v3-fs-2xl 로 흡수 */
/* TR-004 T-4/T-5: 제품에서 가장 많이 보는 숫자 8개가 자릿수마다 흔들렸다.
   `.v3-kpi .v3-kpi-value`(아래)만 tabular 를 받고 **카드 쪽은 빠져 있었다** —
   같은 이름의 두 컴포넌트가 다르게 조판되던 것을 공용 규칙으로 합친다.
   T-6: 22px 는 타이포 스케일 밖이었다(CSS 주석이 스스로 "폐기 예정"이라 적어 뒀다). */
.v3-kpi-value {
  font-size: var(--v3-fs-xl); font-weight: var(--v3-fw-bold);
  margin-top: var(--v3-sp-0-5);
  font-variant-numeric: tabular-nums lining-nums;
  letter-spacing: -.015em;
}
.v3-kpi-delta { font-size: var(--v3-fs-xs); font-weight: var(--v3-fw-semibold); margin-top: var(--v3-sp-0-5); }
.v3-kpi-delta.is-up { color: var(--v3-success); }
.v3-kpi-delta.is-down { color: var(--v3-danger); }
.v3-kpi-delta.is-none { color: var(--v3-text-muted); font-weight: var(--v3-fw-regular); }
.v3-kpi-error { display: flex; flex-direction: column; gap: var(--v3-sp-1); }
.v3-hint { font-size: var(--v3-fs-xs); opacity: .85; }

/* 탭 바 */
.v3-tabbar {
  display: flex; gap: var(--v3-sp-0-5); border-bottom: 1px solid var(--v3-border);
  margin-bottom: var(--v3-sp-4);
}
.v3-tab {
  font: inherit; cursor: pointer; border: none; background: transparent;
  padding: var(--v3-sp-2-5) var(--v3-sp-4); color: var(--v3-text-muted);
  border-bottom: var(--v3-tab-indicator-h) solid transparent;
  margin-bottom: -1px;
}
.v3-tab:hover { color: var(--v3-text); }
/* P2-3: primary(brand-500)는 **캔버스 배경 위에서** 4.23:1 로 AA 미달이다.
   탭바가 흰 카드가 아니라 캔버스(#f5f6f8) 위에 있어 흰 배경 기준 4.57:1 이 적용되지 않는다.
   토큰 대비 계산만으로는 놓치는 종류라 브라우저 실측 감사에서 잡았다. → accent 로 5.87:1. */
.v3-tab.is-active { color: var(--v3-accent); border-bottom-color: var(--v3-accent); font-weight: var(--v3-fw-semibold); }

/* 탭 콘텐츠 */
.v3-tab-content { min-height: 200px; }
.v3-loading { color: var(--v3-text-muted); padding: var(--v3-sp-6); text-align: center; }
.v3-tab-placeholder { padding: var(--v3-sp-6); }
.v3-tab-title { margin: 0 0 var(--v3-sp-2); }
.v3-empty { color: var(--v3-text-muted); padding: var(--v3-sp-6); text-align: center; }
.v3-fragment-error { display: flex; flex-direction: column; gap: var(--v3-sp-2); align-items: flex-start; }

/* ── 탭 공통 컨트롤 / 차트 / 표 (Task-010~012, G4) ────────────────────── */
.v3-tab-controls { display: flex; gap: var(--v3-sp-4); flex-wrap: wrap; margin-bottom: var(--v3-sp-4); }
.v3-control { display: flex; flex-direction: column; gap: var(--v3-sp-1); min-width: 180px; }
/* 15px 는 타이포 스케일 밖(폐기 예정) — P3 리테마에서 --v3-fs-lg 로 흡수 */
/* T-7: 15px → 스케일 안(16px). h3 이므로 lg. */
.v3-section-title { margin: var(--v3-sp-4) 0 var(--v3-sp-2); font-size: var(--v3-fs-lg); }
.v3-hr { border: none; border-top: 1px solid var(--v3-border); margin: var(--v3-sp-6) 0; }
.v3-caption { color: var(--v3-text-muted); font-size: var(--v3-fs-xs); margin: var(--v3-sp-2) 0 0; }

/* 차트 래퍼 — 가로 스크롤 격리(본문 가로 스크롤 방지) */
.v3-chart-wrap { overflow-x: auto; min-width: 0; }
.v3-chart { width: 100%; }

/* 순위: 트리맵 2열 + 드릴다운 2열 */
.v3-chart-grid-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--v3-sp-4); }
.v3-drilldown-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--v3-sp-4); margin-top: var(--v3-sp-4); }
@media (max-width: 900px) {
  .v3-chart-grid-2, .v3-drilldown-grid { grid-template-columns: 1fr; }
}
.v3-stat { padding: var(--v3-sp-2) 0; border-bottom: 1px dashed var(--v3-border); }
.v3-stat:last-child { border-bottom: none; }
.v3-stat-label { font-size: var(--v3-fs-xs); color: var(--v3-text-muted); }
.v3-stat-value { font-size: var(--v3-fs-xl); font-weight: var(--v3-fw-bold); }
.v3-stat-delta { font-size: var(--v3-fs-xs); color: var(--v3-text-muted); }

/* 분포: 라디오 토글 + 지표 섹션(차트 2 : 표 1) */
.v3-viz-toggle { display: flex; align-items: center; gap: var(--v3-sp-4); margin-bottom: var(--v3-sp-4); }
.v3-radio { display: inline-flex; align-items: center; gap: var(--v3-sp-1); cursor: pointer; }
.v3-dist-section { margin-bottom: var(--v3-sp-6); padding-bottom: var(--v3-sp-4); border-bottom: 1px solid var(--v3-border); }
.v3-dist-grid { display: grid; grid-template-columns: 2fr 1fr; gap: var(--v3-sp-4); align-items: start; }
@media (max-width: 900px) { .v3-dist-grid { grid-template-columns: 1fr; } }
.v3-dist-table { position: relative; overflow-x: auto; }

/* 표 — 색·치수를 T3 테이블 토큰으로 이관(해석값 동일).
   세로 패딩 6px 은 밀도 3단(compact 4 / cozy 8 / comfy 12) 사이라 아직 리터럴.
   P2-6 에서 data-density 속성으로 수렴시킨다. */
.v3-table { width: 100%; border-collapse: collapse; font-size: var(--v3-table-fs-cozy); }
.v3-table th, .v3-table td { padding: var(--v3-sp-1-5) var(--v3-table-cell-px); border-bottom: 1px solid var(--v3-table-border); text-align: left; }
.v3-table th { color: var(--v3-table-head-fg); font-weight: var(--v3-fw-semibold); background: var(--v3-table-head-bg); position: sticky; top: 0; }
.v3-table .v3-num, .v3-num { text-align: right; font-variant-numeric: tabular-nums lining-nums; }

/* ── 주요지표 탭 (Task-013, G5) ───────────────────────────────────────── */
/* 17px 는 타이포 스케일 밖(폐기 예정) — P3 리테마에서 --v3-fs-xl 로 흡수 */
/* T-7: 17px → 스케일 안(20px). h2 라 h3(16px) 위 단계를 준다. */
.v3-metrics-head { margin: 0 0 var(--v3-sp-1); font-size: var(--v3-fs-xl); }
.v3-metric-section { margin-bottom: var(--v3-sp-4); }
.v3-subsection-title { margin: var(--v3-sp-4) 0 var(--v3-sp-2); font-size: var(--v3-fs-md); color: var(--v3-text); }
.v3-banners { display: flex; flex-direction: column; gap: var(--v3-sp-2); margin-top: var(--v3-sp-3); }
/* 툴팁 표시용 커서(카드 help) */
.v3-kpi-card[title] { cursor: help; }

/* 지표 성격 띠 (DEC-009 D2). 8장이 전부 같은 표면이라 "이 화면이 뭘 말하는가"가
   안 읽혔다. 왼쪽 4px 띠 + 우상단 텍스트 라벨.
   ⚠️ 1.4.1 — 색만으로 전달하지 않는다. 띠와 **라벨이 함께** 있어야 한다. */
.v3-kpi-card[data-nature] { position: relative; overflow: hidden; }
.v3-kpi-card[data-nature]::before {
  content: ""; position: absolute; inset: 0 auto 0 0; width: 4px;
  background: var(--v3-kpi-nature);
}
.v3-kpi-card[data-nature="volume"] { --v3-kpi-nature: var(--v3-nature-volume); }
.v3-kpi-card[data-nature="money"]  { --v3-kpi-nature: var(--v3-nature-money); }
.v3-kpi-card[data-nature="rate"]   { --v3-kpi-nature: var(--v3-nature-rate); }
.v3-kpi-nature {
  position: absolute; right: var(--v3-sp-2-5); top: var(--v3-sp-2-5);
  font-size: var(--v3-fs-2xs); font-weight: var(--v3-fw-semibold);
  color: var(--v3-text-muted); letter-spacing: var(--v3-tracking-wide);
}

/* ── 상세 데이터 탭 (Task-014, G5) ────────────────────────────────────── */
.v3-detail-search { max-width: 320px; margin-bottom: var(--v3-sp-4); }
/*
  ⚠️ `position: relative` 가 **장식이 아니다.** 이게 없으면 표 안의
  `.v3-sr-only`(position:absolute, offset 없음)가 이 스크롤 래퍼를 **탈출**한다 —
  컨테이닝 블록이 여기가 아니라 html 까지 올라가므로 `overflow-x:auto` 가 클리핑하지
  못하고, 낭독용 1px 스팬이 정적 위치(표 안쪽 x≈600px)에 남아 **문서 전체를 옆으로
  스크롤시킨다.**

  2026-08-31 실측(375px, 감속 모션): `scrollX=235px`.
  · `body.scrollWidth` 는 375 인데 `html.scrollWidth` 가 610 이었다 — 컨테이닝 블록이
    html 이라는 신호였다.
  · `overflow:hidden` 으로는 **안 막혔고** `contain:paint` 로는 막혔다 — 절대 위치
    자손이 원인일 때의 지문이다.
  · **모션이 켜져 있으면 증상이 사라졌다**(표 행 애니메이션이 합성 레이어를 만들어
    가렸다). 감속 모션에서만 보이는 결함이었다 → 두 설정 모두 측정해야 한다.

  순위 표가 **가로 스크롤 표 안에 sr-only 를 넣은 첫 사례**라 그전까지 드러나지 않았다.
*/
.v3-detail-table-wrap { position: relative; overflow-x: auto; max-height: 620px; overflow-y: auto; border: 1px solid var(--v3-border); border-radius: var(--v3-radius-sm); }
.v3-detail-table { font-size: var(--v3-table-fs-compact); white-space: nowrap; }
/* TODO(P2-6): z-index 1 → var(--v3-z-sticky). 값이 바뀌므로 표 작업과 함께. */
.v3-detail-table th { z-index: 1; }
.v3-detail-table td { border-bottom: 1px solid var(--v3-table-border); }
/*
  히트맵 셀 (Task-M022).

  배경색은 값에서 나오므로 **서버가 계산해 인라인으로 준다** — CSS 로는 값→색 보간을
  할 수 없다. 다만 인라인 `background:` 를 그대로 쓰면 다크에서 밝은 파랑 위에 밝은
  글자가 얹혀 대비가 무너진다.

  그래서 서버는 색을 **두 벌**(`--v3-heat` / `--v3-heat-dark`) 넘기고, 어느 쪽을 칠할지는
  여기서 테마가 고른다. 글자색은 T2 를 따라 자동으로 뒤집힌다.

  라이트 램프는 frac ≤ 0.60 으로 압축돼 짙은 글자 하나로 전 구간 4.75:1(DEV-001 §5),
  다크 램프는 밝은 글자 기준 최악 7.17:1 이다(charts.py::HEATMAP_DARK).
*/
.v3-heat-cell { background: var(--v3-heat, transparent); color: var(--v3-heat-text-dark); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .v3-heat-cell { background: var(--v3-heat-dark, transparent); }
}
:root[data-theme="dark"] .v3-heat-cell { background: var(--v3-heat-dark, transparent); }
.v3-detail-footer { display: flex; flex-wrap: wrap; gap: var(--v3-sp-4); margin-top: var(--v3-sp-3); }
.v3-detail-footer .v3-caption { margin: 0; }

/* ── 일별/주별 탭 (Task-015/016, G6) ──────────────────────────────────── */
.v3-control--wide { min-width: 260px; }
.v3-btn--xs { padding: var(--v3-sp-0-5) var(--v3-sp-2); font-size: var(--v3-fs-2xs); margin-left: var(--v3-sp-2); }
.v3-timeseries input[type="date"].v3-select { padding: 5px var(--v3-sp-2); }
.v3-timeseries select[multiple].v3-select { padding: var(--v3-sp-0-5); }

/* ══════════════════════════════════════════════════════════════════════════
   P2-4 — 상태·포커스·접근성 (DEC-005 §3.2 나머지 항목)
   ══════════════════════════════════════════════════════════════════════════ */

/* ── 보조 유틸 ─────────────────────────────────────────────────────────── */

/* 시각적으로 숨기되 스크린리더에는 읽히는 텍스트.
   display:none / visibility:hidden 은 낭독에서도 빠지므로 쓰면 안 된다. */
.v3-sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* 2.4.1 블록 건너뛰기 — 헤더·사이드바를 지나 본문으로. 포커스 받을 때만 나타난다. */
.v3-skip {
  position: absolute; left: var(--v3-sp-2); top: var(--v3-sp-2);
  z-index: var(--v3-z-toast);
  transform: translateY(-250%);
  padding: var(--v3-sp-2) var(--v3-sp-3);
  background: var(--v3-bg-surface); color: var(--v3-accent);
  border: 1px solid var(--v3-border-form); border-radius: var(--v3-radius-sm);
  box-shadow: var(--v3-shadow-md); text-decoration: none;
  font-weight: var(--v3-fw-semibold);
  transition: transform var(--v3-dur-fast) var(--v3-ease-standard);
}
.v3-skip:focus { transform: translateY(0); }

/* ── 2.4.7 / 2.4.11 포커스 가시성 (이전 0건) ───────────────────────────── */

/* :where() 는 0-specificity → 컴포넌트 규칙이 언제든 덮어쓸 수 있다.
   outline-offset 을 두는 이유: 채워진 버튼(accent 배경) 위에 같은 색 링을 그리면
   보이지 않으므로 링을 요소 **바깥**(표면/캔버스)으로 밀어낸다.
   accent 는 표면 6.35:1 / 캔버스 5.87:1 로 비텍스트 3:1 을 넉넉히 넘는다. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--v3-accent);
  outline-offset: var(--v3-focus-ring-offset);
}
/* 포인터 클릭으로 들어온 포커스에는 링을 띄우지 않는다(브라우저 기본과 동일 의도) */
:where(a, button, input, select, textarea, summary, [tabindex]):focus:not(:focus-visible) {
  outline: none;
}

/* ── 버튼 상태 ─────────────────────────────────────────────────────────── */

.v3-btn:active { transform: translateY(1px); }
.v3-btn--primary:active { background: var(--v3-accent-active); border-color: var(--v3-accent-active); }
/* WCAG 는 비활성 컨트롤을 대비 요건에서 제외한다(1.4.3/1.4.11 예외).
   대신 색만으로 구분되지 않도록 커서·투명도를 함께 바꾼다. */
.v3-btn:disabled, .v3-btn[aria-disabled="true"] {
  background: var(--v3-disabled-bg); border-color: var(--v3-disabled-bg);
  color: var(--v3-disabled-fg); cursor: not-allowed; transform: none;
}

/* ── 로딩 / 진행 상태 ──────────────────────────────────────────────────── */

/* htmx 가 요청 중인 버튼 */
.v3-btn.htmx-request { cursor: progress; }
/* 탭 패널 교체 중 — 색이 아니라 투명도+커서로도 알린다. 낭독은 #v3-status 가 담당. */
.v3-tab-content[aria-busy="true"] { opacity: .55; cursor: progress; }
.v3-tab-content { transition: opacity var(--v3-dur-fast) var(--v3-ease-standard); }

.v3-spinner {
  display: inline-block; width: 14px; height: 14px; vertical-align: -2px;
  margin-right: var(--v3-sp-2);
  border: 2px solid var(--v3-border-default); border-top-color: var(--v3-accent);
  border-radius: var(--v3-radius-full);
  animation: v3-spin var(--v3-dur-slow) linear infinite;
}
@keyframes v3-spin { to { transform: rotate(360deg); } }

/* ── 1.4.1 색만으로 정보 전달 금지 — KPI 증감 방향 기호 ────────────────── */

/* 기존에는 빨강/초록 색 하나로만 증감을 구분했다. 기호를 동반시킨다.
   기호는 aria-hidden 이고, 낭독용 텍스트는 .v3-sr-only 로 따로 넣는다
   (▲ 를 그대로 읽히면 "검은색 위쪽 삼각형" 같은 낭독이 나온다). */
.v3-kpi-arrow { font-size: var(--v3-fs-2xs); margin-right: var(--v3-sp-0-5); }

/* ── 표: 고정 헤더가 포커스를 가리지 않도록 (2.4.11) ───────────────────── */
.v3-detail-table-wrap { scroll-padding-top: var(--v3-sp-8); }

/* ══════════════════════════════════════════════════════════════════════════
   로그인 화면 (SCR-001)
   ⚠️ 아래 4개 클래스는 login.html 이 참조하는데 CSS 에 **정의가 없어서**
      로그인 화면이 사실상 무스타일로 렌더되고 있었다. 게다가 템플릿의 제출 버튼은
      v3-btn-primary, CSS 는 v3-btn--primary 라 이름까지 어긋나 있었다(템플릿 쪽 교정).
   ══════════════════════════════════════════════════════════════════════════ */
.v3-login {
  min-height: calc(100vh - var(--v3-header-h));
  display: flex; align-items: center; justify-content: center;
  padding: var(--v3-sp-6);
}
.v3-login-card {
  width: 100%; max-width: 380px;
  display: flex; flex-direction: column; gap: var(--v3-sp-4);
  padding: var(--v3-sp-8);
}
.v3-login-title {
  margin: 0; font-size: var(--v3-fs-xl); font-weight: var(--v3-fw-semibold);
  line-height: var(--v3-lh-tight);
}
.v3-field { display: flex; flex-direction: column; gap: var(--v3-sp-1-5); }
.v3-field > span { font-size: var(--v3-fs-sm); font-weight: var(--v3-fw-semibold); }
.v3-field input {
  font: inherit; font-size: var(--v3-fs-md);
  padding: var(--v3-sp-2) var(--v3-sp-2-5);
  border: 1px solid var(--v3-border-form); border-radius: var(--v3-radius-sm);
  background: var(--v3-bg-surface); color: var(--v3-fg-default);
}
.v3-login-card .v3-btn { margin-top: var(--v3-sp-1); padding: var(--v3-sp-2-5); }

/* ── SCR-003 구독 전환 ──────────────────────────────────────────────────
   :root 밖 색 리터럴 0 제약(DEC-005 §3.1) — 전부 T2/T3 토큰 참조.        */

.v3-page-title { font-size: var(--v3-fs-2xl); font-weight: var(--v3-fw-bold);
  margin: 0 0 var(--v3-sp-1); text-wrap: balance; }
.v3-page-sub { font-size: var(--v3-fs-sm); color: var(--v3-text-muted);
  margin: 0 0 var(--v3-sp-5); max-width: 80ch; }
.v3-section-title { font-size: var(--v3-fs-sm); font-weight: var(--v3-fw-semibold);
  color: var(--v3-text-muted); text-transform: uppercase; letter-spacing: .04em;
  margin: var(--v3-sp-5) 0 var(--v3-sp-2); }

/* 사이드바 날짜 입력 — .v3-select 와 같은 경계(1.4.11 3:1, P2-3) */
.v3-input {
  width: 100%; border: 1px solid var(--v3-border-form); border-radius: var(--v3-radius-sm);
  padding: var(--v3-sp-1); font: inherit; color: var(--v3-text); background: var(--v3-surface);
}
.v3-field-hint { font-size: var(--v3-fs-xs); margin: 0 0 var(--v3-sp-2); }

/* KPI — Overview 의 v3-kpi-card 와 같은 표면, 델타 자리만 없다 */
.v3-kpi {
  background: var(--v3-surface); border: 1px solid var(--v3-border);
  border-radius: var(--v3-radius); box-shadow: var(--v3-shadow);
  padding: var(--v3-sp-4); min-width: 0;
}
/* tabular 는 공용 `.v3-kpi-value` 가 준다(T-5 통합). 여기는 줄바꿈만. */
.v3-kpi .v3-kpi-value { overflow-wrap: anywhere; }
/* 이 화면의 KPI 는 5장·4장 두 행이라 4열 고정 그리드에서 1장이 홀로 떨어진다.
   auto-fit + minmax 로 열 수를 내용에 맞춘다(Overview 의 8장 그리드는 건드리지 않는다). */
/* KPI 2단 위계 (DEC-009 D3). 6열 고정 — hero 2장이 3칸씩 첫 줄을 채우고
   나머지 6장이 둘째 줄을 채운다. `auto-fit minmax(190px,…)` 였을 때는 폭에 따라
   5·6·7열로 흔들려 위계가 성립하지 않았고, **hero 의 긴 금액이 잘렸다**
   (`₩15,079,522,730` 실렌더 확인). */
.v3-kpi-section .v3-kpi-grid { grid-template-columns: repeat(6, minmax(0, 1fr)); }
.v3-kpi-section .v3-kpi-card.is-hero { grid-column: span 3; }
.v3-kpi-card.is-hero .v3-kpi-value { font-size: var(--v3-fs-3xl); letter-spacing: -.025em; }
.v3-kpi-card.is-hero .v3-kpi-label { font-size: var(--v3-fs-sm); }
@media (max-width: 1100px) {
  .v3-kpi-section .v3-kpi-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .v3-kpi-section .v3-kpi-card.is-hero { grid-column: span 3; }
}
@media (max-width: 40rem) {
  .v3-kpi-section .v3-kpi-grid { grid-template-columns: 1fr; }
  .v3-kpi-section .v3-kpi-card.is-hero { grid-column: span 1; }
  /* 375px 에서 30px 금액이 카드 폭에 거의 붙는다. 카드는 성격 띠 때문에
     overflow:hidden 이라 넘치면 **조용히 잘린다**(22→24px 때 실제로 잘렸다).
     좁은 화면에서는 카드가 세로로 쌓여 위계를 순서·띠가 이미 전달하므로
     크기를 한 단계 내려 여유를 확보한다. */
  .v3-kpi-card.is-hero .v3-kpi-value { font-size: var(--v3-fs-2xl); }
}
.v3-kpi-help {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; margin-left: var(--v3-sp-1);
  border: 1px solid var(--v3-border-form); border-radius: 50%;
  font-size: var(--v3-fs-2xs); color: var(--v3-text-muted);
  cursor: help; vertical-align: middle;
}
/* ⚠️ 여기 있던 `outline: 2px solid var(--v3-focus-ring)` 은 **포커스 링을 지우고 있었다.**
   `--v3-focus-ring` 은 `0 0 0 3px rgba(...)` 즉 **box-shadow 값**이라 outline 문법에 맞지 않는다.
   var() 를 쓴 선언은 파싱 시점에 검증되지 않고 치환 후 "computed-value time 에 무효"가 되는데,
   그때 그 속성은 앞선 규칙으로 **되돌아가지 않고 초기값**(= outline-style:none)이 된다.
   그래서 이 요소만 링이 사라졌다 — 파일 격리 실측: `.v3-kpi-help` outline-style=**none**,
   같은 페이지의 `.v3-btn`·`<a>` 는 solid 2px rgb(37,89,196).

   `.v3-kpi-help` 는 `tabindex="0"` 이라 위의 `:where(…[tabindex]):focus-visible` 규칙이
   이미 같은 링(+ 같은 2px offset)을 준다. 규칙을 지우는 것이 곧 수정이다.
   가드: `tests/test_v3_a11y.py::test_focus_ring_token_is_never_used_as_an_outline` */

.v3-cohort { display: flex; flex-direction: column; gap: var(--v3-sp-5); }

/* 한글 줄바꿈 — ui-typography "수치+단위를 붙여 둘 것".
   CSS 기본값은 CJK 를 글자 단위로 끊어서 "30일" 이 "30" / "일" 로 갈라질 수 있다.
   23곳에 &nbsp; 를 심는 대신 keep-all 로 한 번에 막는다 — KPI 라벨은 Python 에서
   오므로(cohort.kpis) nbsp 방식이면 계산 계층까지 마크업이 스며든다.
   긴 값이 넘칠 자리는 각 규칙이 overflow-wrap 으로 따로 푼다. */
.v3-cohort, .v3-page-sub, .v3-chart-sub, .v3-footnote,
.v3-field-hint, .v3-table-caption, .v3-kpi .v3-kpi-label { word-break: keep-all; }
.v3-chart-title { font-size: var(--v3-fs-md); font-weight: var(--v3-fw-semibold);
  margin: 0 0 var(--v3-sp-1); }
.v3-chart-sub { font-size: var(--v3-fs-xs); color: var(--v3-text-muted);
  margin: 0 0 var(--v3-sp-3); max-width: 80ch; }

/* 라디오 그룹 — fieldset 기본 테두리 제거, 라벨과 컨트롤이 한 히트영역 */
.v3-radio-group { border: 0; padding: 0; margin: 0; }
.v3-radio-group .v3-field-label { margin-top: 0; padding: 0; }
.v3-radio {
  display: inline-flex; align-items: center; gap: var(--v3-sp-1);
  margin-right: var(--v3-sp-3); cursor: pointer;
  font-size: var(--v3-fs-sm); min-height: 24px;   /* WCAG 2.2 대상 크기 */
}
.v3-radio input { cursor: pointer; }

.v3-table-caption {
  caption-side: top; text-align: left;
  font-size: var(--v3-fs-xs); color: var(--v3-text-muted);
  padding-bottom: var(--v3-sp-2);
}
.v3-footnote { font-size: var(--v3-fs-xs); color: var(--v3-text-muted);
  margin-top: var(--v3-sp-5); max-width: 90ch; }

/* ── 어드민 화면 (SCR-004 사용자관리 · SCR-005 IP 허용목록) ─────────────────
   색·간격·타이포는 전부 기존 토큰 참조 — :root 밖 리터럴 0 규약(DEC-005 §3.1). */

.v3-admin { display: flex; flex-direction: column; gap: var(--v3-sp-5); }

/* 어드민 행 액션은 `<a class="v3-btn">`(편집·PW)와 `<button class="v3-btn">`
   (비활성화·삭제)이 **한 줄에 나란히** 선다. 공용 `.v3-btn` 은 `text-decoration`
   을 지정하지 않으므로 앵커에만 UA 밑줄이 남아, 같은 컴포넌트가 두 종류로 보인다
   (2026-09-08 실렌더 실측: `a.v3-btn` → `underline`, `button.v3-btn` → `none`).
   경계·배경이 조작 가능성을 이미 전달하므로 밑줄을 지우는 쪽이 옳다(1.4.1 는 본문
   링크 이야기이고 이것은 버튼이다).
   ⚠️ 근본 수리는 공용 `.v3-btn { text-decoration: none; }` 한 줄이지만 그 블록은
   차터 C 소유라 여기서는 어드민 범위로만 맞춘다. 공용 규칙이 들어오면 이 규칙은
   지워도 된다 — 헤더의 `관리` 버튼(base_layout, 전 화면)은 그때 함께 풀린다. */
.v3-admin a.v3-btn { text-decoration: none; }

/* 어드민 힌트 문단은 1440px 에서 **한 줄에 121자**까지 갔다(2026-09-08 실측).
   `.v3-page-sub`(80ch)·`.v3-footnote`(90ch)는 상한이 있는데 `.v3-field-hint` 만
   없어서다 — 가독 상한은 45~90자다. 어드민은 폼 옆 설명이 길어(공인 egress IP
   안내 등) 이 클래스가 실제로 상한에 부딪히는 유일한 화면이다.
   ⚠️ `.v3-field-hint` 는 7개 템플릿 공용이라 공용 블록(차터 C)에 `max-width: 90ch`
   한 줄을 넣는 것이 근본 수리다. 그러면 이 규칙은 지워도 된다. */
.v3-admin .v3-field-hint { max-width: 90ch; }

/* 행마다 버튼이 4개씩 붙는 화면이라 모바일 더블탭 확대 지연(≈300ms)이 그대로
   드러난다. 탭 하이라이트도 브라우저 기본값에 맡기지 않고 명시한다. */
.v3-admin .v3-btn,
.v3-admin .v3-check,
.v3-admin .v3-check input { touch-action: manipulation; -webkit-tap-highlight-color: transparent; }

/* 패널 제목에 가변 길이 값이 들어간다("비밀번호 재설정 · <이메일>"). `.v3-page-title`
   에만 balance 가 있어 좁은 폭에서 마지막 줄에 한 조각만 남는 위도우가 생겼다. */
.v3-admin .v3-panel-title { text-wrap: balance; }

.v3-admin-nav { display: flex; gap: var(--v3-sp-2); margin-bottom: var(--v3-sp-1); }

.v3-admin-toolbar {
  display: flex; justify-content: space-between; align-items: center;
  gap: var(--v3-sp-3); flex-wrap: wrap;
}

.v3-panel {
  padding: var(--v3-sp-5);
  display: flex; flex-direction: column; gap: var(--v3-sp-4);
}
.v3-panel-title {
  font-size: var(--v3-fs-lg); font-weight: var(--v3-fw-semibold); margin: 0;
}

/* auto-fit 이라 좁은 화면에서 1열로 접힌다 — 별도 미디어쿼리 불필요 */
.v3-form-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--v3-sp-3) var(--v3-sp-4);
}

.v3-checks {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: var(--v3-sp-1-5);
}
.v3-check {
  display: flex; align-items: center; gap: var(--v3-sp-2);
  font-size: var(--v3-fs-sm); min-height: 24px;   /* WCAG 2.2 대상 크기 */
  cursor: pointer;
}
.v3-check input { cursor: pointer; }

.v3-row-actions { display: flex; flex-wrap: wrap; gap: var(--v3-sp-1); }
.v3-form-actions { display: flex; flex-wrap: wrap; gap: var(--v3-sp-2); align-items: center; }

.v3-badge {
  display: inline-block; border-radius: var(--v3-radius-full);
  padding: var(--v3-sp-0-5) var(--v3-sp-2);
  font-size: var(--v3-fs-2xs); font-weight: var(--v3-fw-semibold);
}
.v3-badge--on { background: var(--v3-success-bg); color: var(--v3-success-fg); }
.v3-badge--off { background: var(--v3-bg-surface-sunken); color: var(--v3-text-muted); }
.v3-badge--warn { background: var(--v3-warning-bg); color: var(--v3-warning-fg); }

/* 잠긴 액션의 사유. 버튼을 disabled 로만 두면 왜 못 누르는지 알 수 없다. */
.v3-locked { font-size: var(--v3-fs-2xs); color: var(--v3-text-muted); }

/* env 규칙(읽기 전용) — 합집합의 나머지 절반을 숨기지 않기 위한 영역 (SCR-005 §5) */
.v3-env-rules {
  display: flex; flex-wrap: wrap; gap: var(--v3-sp-2);
  list-style: none; margin: 0; padding: 0;
}
.v3-env-rule {
  font-family: var(--v3-font-num); font-size: var(--v3-fs-xs);
  background: var(--v3-bg-surface-sunken); color: var(--v3-text);
  border-radius: var(--v3-radius-sm); padding: var(--v3-sp-0-5) var(--v3-sp-2);
}
.v3-code { font-family: var(--v3-font-num); font-size: var(--v3-fs-sm); }

/* ── 데이터 내보내기 (SCR-006) ─────────────────────────────────────────────
   토큰만 참조한다 — `:root` 밖 색 리터럴 0 (DEC-005 §3.1 가드). */
.v3-export-form {
  display: flex; flex-direction: column; gap: var(--v3-sp-4);
  background: var(--v3-bg-surface); border: 1px solid var(--v3-border-subtle);
  border-radius: var(--v3-radius-md); padding: var(--v3-sp-4);
  max-width: 46rem;
}
.v3-field-row {
  display: flex; flex-wrap: wrap; gap: var(--v3-sp-4); align-items: flex-start;
}
.v3-field-row > .v3-field { flex: 1 1 16rem; min-width: 0; }

/* PDF 리포트 탭 (SCR-006 §10, Task-M011b) */
.v3-textarea {
  width: 100%; resize: vertical; min-height: 6rem;
  font: inherit; line-height: var(--v3-lh-normal);
}
.v3-fieldset {
  border: 1px solid var(--v3-border-subtle); border-radius: var(--v3-radius-sm);
  padding: var(--v3-sp-3) var(--v3-sp-4); margin: 0;
}
.v3-fieldset > legend { padding: 0 var(--v3-sp-1); }
.v3-check-row { display: flex; flex-wrap: wrap; gap: var(--v3-sp-4); }


/* ── SCR-007 사용자 정의 분석 ──────────────────────────────────────────
   탭 5개가 한 패널을 공유한다. 컨트롤 묶음이 길어서 섹션 간격을 명시한다. */
.v3-custom { display: flex; flex-direction: column; gap: var(--v3-sp-5); }

/* 온디맨드 분석의 조건 폼 — '실행' 을 눌러야 조회가 나가는 폼이라 시각적으로 묶어 둔다 */
.v3-run-form {
  background: var(--v3-bg-surface); border: 1px solid var(--v3-border-subtle);
  border-radius: var(--v3-radius-md); padding: var(--v3-sp-4);
}

/* 고급 옵션 접기 — 원본 st.expander("고급 옵션") 대응. <details> 기본 스타일만 정돈한다 */
.v3-details {
  border: 1px solid var(--v3-border-subtle); border-radius: var(--v3-radius-sm);
  padding: var(--v3-sp-2) var(--v3-sp-3);
}
.v3-summary {
  cursor: pointer; font-weight: 600; color: var(--v3-fg-muted);
  padding: var(--v3-sp-1) 0;
}

/* 결과 헤더 — 행 수와 다운로드 버튼을 같은 줄에 */
.v3-result-head {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--v3-sp-3);
}

/* 표시 개수 슬라이더 — 원본 st.slider 대응 */
.v3-range { width: 100%; accent-color: var(--v3-accent); }

/* ── SCR-008 CRM 오디언스 ────────────────────────────────────────────── */
.v3-crm { display: flex; flex-direction: column; gap: var(--v3-sp-5); }

/* PII 상태 배너 — 페이지 최상단. 색만으로 구분하지 않도록 🔓/🔒 를 함께 쓴다 */
.v3-pii-banner { margin-top: var(--v3-sp-3); }

/* 추출된 ID 목록 — 길어서 자체 스크롤 */
.v3-idlist { max-height: 26rem; overflow-y: auto; }

/* 좁은 화면에서 넓게 쓰는 입력 */
.v3-field--wide { flex: 2 1 24rem; }

/* htmx 요청 중에만 보이는 표시 — 1~2분 걸리는 계산이라 진행 중임을 알려야 한다 */
.v3-htmx-indicator {
  display: none; align-items: center; gap: var(--v3-sp-2);
  color: var(--v3-fg-muted); font-size: var(--v3-fs-sm);
}
.htmx-request .v3-htmx-indicator,
.htmx-request.v3-htmx-indicator { display: inline-flex; }

/* RFM 5×6 그리드 + 세그먼트 합계 */
.v3-rfm-layout {
  display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 1fr);
  gap: var(--v3-sp-5); align-items: start;
}
@media (max-width: 60rem) { .v3-rfm-layout { grid-template-columns: 1fr; } }
.v3-rfm-grid-wrap { position: relative; overflow-x: auto; }
.v3-rfm-grid { width: 100%; table-layout: fixed; }
.v3-rfm-corner { font-weight: 600; white-space: nowrap; }
.v3-rfm-cell { padding: 0; }
/* 셀 전체가 버튼이다 — 원본은 차트를 클릭할 수 없어 selectbox 를 따로 뒀다.
   글자색을 짙은 중립으로 고정한다: 세그먼트 7색 모두 밝은 편이라 이 조합이 AA 를 넘는다. */
.v3-rfm-btn {
  display: flex; flex-direction: column; gap: 2px;
  width: 100%; min-height: 3.5rem; padding: var(--v3-sp-2);
  border: 0; background: transparent; cursor: pointer;
  color: var(--v3-viz-on-light); font: inherit; text-align: center;
}
.v3-rfm-btn:hover { outline: 2px solid var(--v3-viz-on-light); outline-offset: -2px; }
.v3-rfm-primary { font-weight: 700; }
.v3-rfm-secondary { font-size: var(--v3-fs-sm); }

.v3-rfm-totals { min-width: 0; }
.v3-seg-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--v3-sp-2); }
.v3-seg-item { display: flex; flex-direction: column; gap: 2px; }
.v3-seg-btn {
  display: flex; align-items: center; gap: var(--v3-sp-2);
  border: 0; background: transparent; cursor: pointer;
  color: var(--v3-fg-default); font: inherit; padding: 2px 0; width: 100%;
}
.v3-seg-swatch { width: 0.75rem; height: 0.75rem; border-radius: var(--v3-radius-xs); flex: none; }
.v3-seg-name { flex: 1; text-align: left; }
.v3-seg-count { font-variant-numeric: tabular-nums lining-nums; }
.v3-seg-bar { display: block; height: 4px; background: var(--v3-bg-surface-sunken); border-radius: var(--v3-radius-full); }
.v3-seg-bar-fill { display: block; height: 100%; border-radius: var(--v3-radius-full); }

.v3-rfm-details:empty { display: none; }
.v3-drilldown { display: flex; flex-direction: column; gap: var(--v3-sp-4); }
.v3-movement { display: flex; flex-direction: column; gap: var(--v3-sp-4); }
.v3-move-list { margin: 0; padding-left: var(--v3-sp-5); display: flex; flex-direction: column; gap: 2px; }

/* 헤더 KPI 를 한 줄로 (result-head 안에서) */
.v3-kpi-grid--inline { display: flex; flex-wrap: wrap; gap: var(--v3-sp-4); }

/* ── SCR-009 페이지 분석 ─────────────────────────────────────────────── */
.v3-pages { display: flex; flex-direction: column; gap: var(--v3-sp-5); }

/* ── SCR-010 지역 분석 ───────────────────────────────────────────────── */
.v3-regional { display: flex; flex-direction: column; gap: var(--v3-sp-5); }
.v3-attribution { display: flex; flex-direction: column; gap: var(--v3-sp-4); }

/* ── SCR-011 마케팅 채널 ─────────────────────────────────────────────── */
.v3-compare { display: flex; flex-direction: column; gap: var(--v3-sp-5); }

/* ── 8화면 내비게이션 ─────────────────────────────────────────────────
   헤더 아래 가로 막대. 좁은 화면에서는 가로 스크롤한다(줄바꿈보다 위치가 안정적). */
.v3-nav {
  border-bottom: 1px solid var(--v3-border-subtle);
  background: var(--v3-bg-surface);
  overflow-x: auto;
}
.v3-nav-list {
  display: flex; gap: var(--v3-sp-1);
  list-style: none; margin: 0; padding: 0 var(--v3-sp-4);
  white-space: nowrap;
}
.v3-nav-link {
  display: inline-block; padding: var(--v3-sp-3) var(--v3-sp-3);
  color: var(--v3-fg-muted); text-decoration: none;
  border-bottom: 2px solid transparent;
}
.v3-nav-link:hover { color: var(--v3-fg-default); }
/* 현재 화면은 색 + 밑줄 + 굵기 3가지로 표시한다 — 색만으로 전달하지 않는다(1.4.1) */
.v3-nav-link.is-current {
  color: var(--v3-fg-default); font-weight: 700;
  border-bottom-color: var(--v3-accent);
}


/* ==========================================================================
   DEC-008 계층 3 — 아트디렉션 (Task-M025)
   ==========================================================================
   색 리터럴 0. 전부 T2/T3 토큰 참조라 다크가 자동으로 따라온다. */

/* ---- 차트 경고 배너 (계층 2 · charts.csv "When NOT to Use") ------------- */
.v3-viz-caveat {
  display: flex; gap: var(--v3-sp-2); align-items: flex-start;
  margin: 0 0 var(--v3-sp-3);
  padding: var(--v3-sp-2-5) var(--v3-sp-3);
  border: 1px solid var(--v3-info-border);
  border-left-width: 3px;
  border-radius: var(--v3-radius-sm);
  background: var(--v3-info-bg);
  color: var(--v3-info-fg);
  font-size: var(--v3-fs-sm);
  line-height: var(--v3-lh-snug);
}
.v3-viz-caveat-mark {
  flex: 0 0 auto;
  width: 18px; height: 18px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--v3-radius-full);
  border: 1px solid currentColor;
  font-size: var(--v3-fs-2xs); font-weight: var(--v3-fw-bold);
  font-style: italic;
}

/* ---- 보조 뷰 표식 ------------------------------------------------------- */
.v3-viz-secondary { margin-top: var(--v3-sp-6); }
/* 「보조 뷰」는 색이 아니라 **글자**로 말한다 — 색만으로 위계를 전달하지 않는다(1.4.1) */
.v3-viz-badge {
  display: inline-block; vertical-align: middle;
  margin-left: var(--v3-sp-2);
  padding: var(--v3-sp-0-5) var(--v3-sp-2);
  border: 1px solid var(--v3-border-default);
  border-radius: var(--v3-radius-full);
  background: var(--v3-bg-surface-sunken);
  color: var(--v3-fg-muted);
  font-size: var(--v3-fs-2xs); font-weight: var(--v3-fw-medium);
}

/* ---- 순위 표 (주 뷰) --------------------------------------------------- */
.v3-rank-table { width: 100%; }
.v3-rank-ord {
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--v3-fg-muted);
  font-weight: var(--v3-fw-semibold);
  width: 3.5em;
}
.v3-rank-name { text-align: left; font-weight: var(--v3-fw-semibold); white-space: nowrap; }

/* 점유율 셀 — 막대(길이) + 숫자 + 순위 배지. 색 외 채널을 3개 겹쳐 둔다. */
.v3-share-cell { position: relative; white-space: nowrap; min-width: 8.5em; }
.v3-share-bar {
  display: inline-block; vertical-align: middle;
  width: 3.2em; height: var(--v3-sp-1-5);
  margin-right: var(--v3-sp-1-5);
  border-radius: var(--v3-radius-full);
  background: var(--v3-share-track);
  overflow: hidden;
  position: relative;
}
.v3-share-bar::before {
  content: ""; position: absolute; inset: 0 auto 0 0;
  width: var(--v3-share, 0%);
  background: var(--v3-share-fill);
  border-radius: inherit;
}
.v3-share-num { font-variant-numeric: tabular-nums lining-nums; }
/* ⚠️ 전경은 fg-muted 가 아니라 **fg-default** 다. muted(#667085)를
   bg-surface-emphasis(#ecEEF1) 위에 올리면 **4.28:1 로 AA 미달**이다 — 흰 배경에서는
   5.66:1 이라 통과하므로 토큰만 보면 안 걸리고, 실렌더 대비 측정에서만 나온다
   (2026-08-31 실측). 자기 배경을 가진 배지는 그 배경 기준으로 재계산해야 한다.
   위계는 색이 아니라 **크기(11px)와 굵기**로 준다. */
.v3-rank-badge {
  display: inline-block;
  margin-left: var(--v3-sp-1-5);
  padding: 0 var(--v3-sp-1);
  border-radius: var(--v3-radius-xs);
  background: var(--v3-bg-surface-emphasis);
  color: var(--v3-fg-default);
  font-size: var(--v3-fs-2xs);
  font-weight: var(--v3-fw-regular);
}
.v3-delta-cell { font-variant-numeric: tabular-nums lining-nums; white-space: nowrap; }
.v3-delta-cell.is-up { color: var(--v3-success-fg); }
.v3-delta-cell.is-down { color: var(--v3-danger-fg); }
.v3-delta-cell.is-flat { color: var(--v3-fg-muted); }

/* ---- 「환승 통로」 (DEC-008 D2) ---------------------------------------- */
.v3-corridor-section { margin-top: var(--v3-sp-6); }
.v3-corridor {
  margin: var(--v3-sp-3) 0 0;
  padding: var(--v3-sp-3);
  border: 1px solid var(--v3-border-subtle);
  border-radius: var(--v3-radius-md);
  background: var(--v3-bg-surface);
}
.v3-corridor-ends {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--v3-sp-2);
  margin-bottom: var(--v3-sp-2);
  font-size: var(--v3-fs-xs); font-weight: var(--v3-fw-semibold);
  color: var(--v3-fg-muted);
}
.v3-corridor-end { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.v3-corridor-end:last-of-type { text-align: right; }
.v3-corridor-arrow { flex: 0 0 auto; color: var(--v3-fg-muted); }
.v3-corridor-svg {
  display: block; width: 100%; height: auto;
  max-height: 380px;
}
/* 라벨 거터(DEV-016) — 좁은 화면에서는 그림을 줄이지 않고 상자 안에서 가로 스크롤한다(표와 같은 규칙,
   페이지 scrollX 0). svg 의 min-width 는 viewBox 폭(charts.py CORRIDOR_*)에서 오므로 템플릿이 inline 으로 준다 —
   같은 숫자를 두 곳에 적지 않는다. */
.v3-corridor-scroll { position: relative; overflow-x: auto; overflow-y: hidden; } /* 컨테이닝 블록 — DEV-013 sr-only 탈출 가드 */
/* 띠 라벨 — currentColor 상속이라 다크모드 블록이 필요 없다. 글자 크기는 viewBox 단위(svg 속성). */
.v3-ribbon-label { fill: currentColor; font-weight: var(--v3-fw-medium); }
.v3-ribbon-label--l { text-anchor: end; }
.v3-ribbon-label--r { text-anchor: start; }
/* 띠 — 두께가 점유율, 색이 방향. stroke 는 인접 띠와의 분리선(charts.csv). */
.v3-ribbon-band, .v3-ribbon-cap {
  stroke: var(--v3-ribbon-sep); stroke-width: 1;
}
.v3-ribbon--up .v3-ribbon-band, .v3-ribbon--up .v3-ribbon-cap { fill: var(--v3-ribbon-up); }
.v3-ribbon--down .v3-ribbon-band, .v3-ribbon--down .v3-ribbon-cap { fill: var(--v3-ribbon-down); }
.v3-ribbon--flat .v3-ribbon-band, .v3-ribbon--flat .v3-ribbon-cap { fill: var(--v3-ribbon-flat); }
.v3-ribbon-band {
  transition: fill-opacity var(--v3-dur-fast) var(--v3-ease-standard);
}
.v3-ribbon:hover .v3-ribbon-band, .v3-ribbon:focus-within .v3-ribbon-band { fill-opacity: .72; }

.v3-corridor-legend {
  display: flex; flex-wrap: wrap; gap: var(--v3-sp-3);
  list-style: none; margin: var(--v3-sp-2) 0 0; padding: 0;
  font-size: var(--v3-fs-2xs); color: var(--v3-fg-muted);
}
.v3-corridor-legend .is-up { color: var(--v3-success-fg); }
.v3-corridor-legend .is-down { color: var(--v3-danger-fg); }
.v3-corridor-legend .is-flat { color: var(--v3-fg-muted); }
.v3-corridor-summary {
  margin: var(--v3-sp-2) 0 0;
  font-size: var(--v3-fs-sm); line-height: var(--v3-lh-snug);
  color: var(--v3-fg-default);
}
/* 절단은 조용히 하지 않는다 — 무엇을 안 보여줬는지 화면에서 말한다. */
.v3-corridor-note {
  margin: var(--v3-sp-1-5) 0 0;
  font-size: var(--v3-fs-2xs); color: var(--v3-fg-muted);
}

/* ==========================================================================
   DEC-008 계층 2 — motion.csv 두 티어 (Task-M024)
   ==========================================================================
   ⚠️ 티어가 갈리는 이유가 미학이 아니다. motion.csv 가 오버슛을 밀집 표에서
      금지한다. KPI 그리드(카드 8장)는 Standard, 표 행은 Subtle 이다.
   ⚠️ 기본 상태 = **최종 상태**로 적는다. 감속 모션이면 애니메이션만 사라지고
      요소는 제자리에 있어야 한다(opacity:0 을 기본값으로 두면 안 보인다). */

@keyframes v3-enter-standard {
  from { opacity: 0; transform: translateY(16px) scale(.92); }
  to   { opacity: 1; transform: none; }
}
@keyframes v3-enter-subtle {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

.v3-kpi-grid > .v3-kpi-card {
  animation: v3-enter-standard var(--v3-dur-enter) var(--v3-ease-overshoot) both;
}
.v3-rank-table tbody > tr {
  animation: v3-enter-subtle var(--v3-dur-enter-subtle) var(--v3-ease-decel) both;
}
.v3-corridor .v3-ribbon {
  animation: v3-enter-subtle var(--v3-dur-enter-subtle) var(--v3-ease-decel) both;
}

/* 스태거는 --v3-stagger-cap(10) 까지만. 그 뒤 행은 10번째와 같은 지연을 쓴다. */
.v3-kpi-grid > .v3-kpi-card:nth-child(1) { animation-delay: calc(0 * var(--v3-stagger-standard)); }
.v3-kpi-grid > .v3-kpi-card:nth-child(2) { animation-delay: calc(1 * var(--v3-stagger-standard)); }
.v3-kpi-grid > .v3-kpi-card:nth-child(3) { animation-delay: calc(2 * var(--v3-stagger-standard)); }
.v3-kpi-grid > .v3-kpi-card:nth-child(4) { animation-delay: calc(3 * var(--v3-stagger-standard)); }
.v3-kpi-grid > .v3-kpi-card:nth-child(5) { animation-delay: calc(4 * var(--v3-stagger-standard)); }
.v3-kpi-grid > .v3-kpi-card:nth-child(6) { animation-delay: calc(5 * var(--v3-stagger-standard)); }
.v3-kpi-grid > .v3-kpi-card:nth-child(7) { animation-delay: calc(6 * var(--v3-stagger-standard)); }
.v3-kpi-grid > .v3-kpi-card:nth-child(n+8) { animation-delay: calc(7 * var(--v3-stagger-standard)); }

.v3-rank-table tbody > tr:nth-child(1) { animation-delay: calc(0 * var(--v3-stagger-subtle)); }
.v3-rank-table tbody > tr:nth-child(2) { animation-delay: calc(1 * var(--v3-stagger-subtle)); }
.v3-rank-table tbody > tr:nth-child(3) { animation-delay: calc(2 * var(--v3-stagger-subtle)); }
.v3-rank-table tbody > tr:nth-child(4) { animation-delay: calc(3 * var(--v3-stagger-subtle)); }
.v3-rank-table tbody > tr:nth-child(5) { animation-delay: calc(4 * var(--v3-stagger-subtle)); }
.v3-rank-table tbody > tr:nth-child(6) { animation-delay: calc(5 * var(--v3-stagger-subtle)); }
.v3-rank-table tbody > tr:nth-child(7) { animation-delay: calc(6 * var(--v3-stagger-subtle)); }
.v3-rank-table tbody > tr:nth-child(8) { animation-delay: calc(7 * var(--v3-stagger-subtle)); }
.v3-rank-table tbody > tr:nth-child(9) { animation-delay: calc(8 * var(--v3-stagger-subtle)); }
.v3-rank-table tbody > tr:nth-child(n+10) { animation-delay: calc(9 * var(--v3-stagger-subtle)); }

.v3-corridor .v3-ribbon:nth-of-type(1) { animation-delay: calc(0 * var(--v3-stagger-subtle)); }
.v3-corridor .v3-ribbon:nth-of-type(2) { animation-delay: calc(1 * var(--v3-stagger-subtle)); }
.v3-corridor .v3-ribbon:nth-of-type(3) { animation-delay: calc(2 * var(--v3-stagger-subtle)); }
.v3-corridor .v3-ribbon:nth-of-type(4) { animation-delay: calc(3 * var(--v3-stagger-subtle)); }
.v3-corridor .v3-ribbon:nth-of-type(5) { animation-delay: calc(4 * var(--v3-stagger-subtle)); }
.v3-corridor .v3-ribbon:nth-of-type(6) { animation-delay: calc(5 * var(--v3-stagger-subtle)); }
.v3-corridor .v3-ribbon:nth-of-type(7) { animation-delay: calc(6 * var(--v3-stagger-subtle)); }
.v3-corridor .v3-ribbon:nth-of-type(n+8) { animation-delay: calc(7 * var(--v3-stagger-subtle)); }

/* 감속 모션 — 등장 애니메이션을 **완전히 끈다**. duration 0.01ms 로 줄이는 것으로는
   `animation-fill-mode: both` 의 from 상태가 한 프레임 보일 수 있다. */
@media (prefers-reduced-motion: reduce) {
  .v3-kpi-grid > .v3-kpi-card,
  .v3-rank-table tbody > tr,
  .v3-corridor .v3-ribbon {
    animation: none !important;
  }
  .v3-ribbon-band { transition: none; }
}

/* ---- 좁은 화면 (Task-M023 블록과 같은 breakpoint) ---------------------- */
@media (max-width: 40rem) {
  .v3-corridor-svg { max-height: 300px; }
  .v3-share-bar { width: 2.2em; }
  .v3-rank-badge { display: none; }   /* 순위는 첫 열에 이미 있다 */
  .v3-corridor-legend { gap: var(--v3-sp-2); }
}
