/* =============================================================================
   base 프론트 공통 스타일 (디자인 시스템)

   프론트 3종(web-tmf · web-vue · web-rct)이 같은 파일을 쓴다.
   화면을 만들 때는 여기 정의된 클래스만 조합하고, 인라인 style 을 쓰지 않는다.
   그래야 세 프론트의 화면이 서로 어긋나지 않고, 색·간격을 한 곳에서 바꿀 수 있다.

   전체 목록과 실제 모양은 /styleguide 화면에서 확인한다.

   구성
     1. 토큰
        1a. 원시 토큰 — 색 램프·크기 계단. 규칙에서 직접 쓰지 않는다
        1b. 의미 토큰 — 화면이 실제로 참조하는 이름(--c-*, --f-*, --s-*, --r-*, --shadow-*, --dur-*)
        1c. 색 테마 12종 — 의미 토큰만 갈아끼운다
     2. 기본      — body, 타이포 규칙(한글 보정), 제목, 링크
     3. 레이아웃  — header, main, card
     4. 컴포넌트  — button, form, table, alert, badge, pager, toolbar, empty
     5. 유틸리티  — 자주 쓰는 한 줄짜리 보정 (u-)
     6. 접근성    — 움직임 줄이기·포커스 링·상태
     7. 반응형    — 헤더가 좁은 폭에 들어가기 위한 최소 조정뿐

   모바일 우선으로 짠다. 현장 화면은 휴대폰 접속 비중이 높은데 데스크톱만 보고
   만들면 표가 화면을 넘쳐 글자가 세로로 쪼개진다.

   값을 정할 때 지킨 규칙(왜 이 숫자인가)
     - 글자 크기는 7단계(12·13·15·16·20·24·32)만 쓴다. 단계 밖 숫자를 쓰기
       시작하면 13과 14가 섞여 위계가 아니라 노이즈가 된다.
     - 간격은 4의 배수 토큰만 쓴다. 10·14·6 같은 값은 한 번 쓰면 옆 화면이
       따라 하고, 결국 어디가 한 덩어리인지 여백으로 못 읽게 된다.
     - 층은 3단계뿐이다. 카드는 선, 떠 있는 목록은 shadow-2, 모달은 shadow-3.
       "이 카드만 강조" 목적의 그림자 승급은 하지 않는다.
     - 글자 대비는 본문 4.5:1, 테두리·포커스 링은 3:1 을 12테마 전부에서 지킨다.
       계산해서 통과한 값만 토큰에 넣었다(주석에 실측값을 적어 둔다).
   ============================================================================= */

/* ---------------------------------------------------------------------------
   1a. 원시 토큰 — 색 램프

   여기 값은 "색 그 자체"일 뿐이라 의미가 없다. 규칙(2~7장)에서는 절대
   직접 쓰지 않는다 — --n-600 이 '보조 글자'인지 '테두리'인지 코드만 봐서는
   알 수 없기 때문이다. 아래 1b 의 의미 이름을 거쳐서만 쓴다.
   --------------------------------------------------------------------------- */
:root {
  /* 중성 — 푸른기를 뺀 순수 회색 계열. 단계 사이가 고르게 벌어지도록 잡았다
     (기존 6단계는 f5↔e5 가 붙어 있고 e5↔87 사이가 비어 중간 톤을 못 만들었다) */
  --n-0:   #ffffff;
  --n-25:  #f7f7f8;   /* 페이지 바닥 */
  --n-50:  #f2f2f3;   /* 표 머리·hover */
  --n-100: #ebebec;
  --n-200: #e4e4e6;   /* 면 구분선 */
  --n-300: #d0d0d3;
  --n-400: #a1a1a4;
  --n-500: #767676;   /* 컨트롤 테두리 */
  --n-600: #616161;   /* 보조 글자 */
  --n-700: #454548;
  --n-800: #26262a;
  --n-900: #111111;   /* 본문 글자 */

  /* 상태 — 배경 50 / 글자 700 짝으로만 쓴다. 이 짝이라야 배경 위 글자가
     4.5:1 을 넘는다(500 단계를 글자로 쓰면 3.5:1 대로 떨어진다) */
  --red-50:   #fef3f2;  --red-500: #dc2626;  --red-700: #b42318;  --red-800: #8f1c13;
  --green-50: #ecfdf3;  --green-700: #027a48;
  --amber-50: #fffaeb;  --amber-700: #b54708;
}

/* ---------------------------------------------------------------------------
   1b. 의미 토큰 — 화면이 참조하는 이름

   브랜드를 바꿀 때는 --c-primary / --c-accent / --c-focus 계열만 손댄다.
   나인소프트는 차콜을 주 색으로, 주황을 강조로 쓴다.

   주황(#F37723) 위에 흰 글자를 얹으면 대비가 2.9:1 로 기준(4.5:1)에 못 미친다.
   그래서 '흰 글자로 채운 면'에는 차콜(--c-primary)을 쓰고, 주황은 글자를 얹지
   않는 브랜드 선·아이콘에만 쓴다.

   포커스 링은 --c-accent 를 쓰지 않는다 — 주황은 흰 배경 대비 2.81:1 로
   비텍스트 기준(3:1)에 미달이고, teal·green·navy 테마는 2.2~2.5 로 더 나쁘다.
   링 전용 --c-focus 를 따로 두고 12테마 전부 3:1 이상으로 계산해 넣었다.
   --------------------------------------------------------------------------- */
:root {
  /* --- 색: 브랜드 --- */
  --c-primary: #222222;         /* 차콜 — 흰 글자를 얹는 면(버튼·헤더). 흰 글자 15.91:1 */
  --c-primary-dark: #000000;    /* hover */
  --c-primary-light: #fbe3d2;   /* 선택된 칩·행의 옅은 바탕 (본문 글자 15.30:1) */
  --c-accent: #f37723;          /* 주황 — 브랜드 선·장식. 글자·링에는 쓰지 않는다 */
  --c-accent-dark: #d9631a;
  --c-focus: #c2410c;           /* 포커스 링·선택 테두리 전용. 흰 배경 5.18:1 */
  --c-gold: #f6ab00;            /* 확장 팔레트 */

  /* --- 색: 면과 글자 --- */
  --c-text: var(--n-900);
  --c-muted: var(--n-600);      /* 흰 배경 6.19:1 / 옅은 면 5.57:1 — 둘 다 통과 */
  /* 고르는 칸(select·.pick-face)의 표시기. 데이터 URI 안에는 CSS 변수를 넣을 수
     없어 테마마다 한 줄씩 따로 둔다 — 색은 --c-muted 와 같은 값이다.
     테마를 새로 만들면 이 한 줄을 같이 옮겨야 한다. 빠뜨리면 밝은 테마의
     진회색 삼각형이 어두운 배경 위에 그대로 남는다.
     크기(7×6)는 예전에 쓰던 글자 ▾ 의 실제 잉크 크기를 브라우저에서 재어 옮긴
     값이다. 두 곳이 참조하므로 토큰으로 묶는다 — 한쪽만 고치면 어긋난다. */
  --sel-arrow-w: 7px;
  --sel-arrow-h: 6px;
  --sel-arrow: url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%277%27 height=%276%27 viewBox=%270 0 7 6%27%3E%3Cpath d=%27M0 0h7L3.5 6z%27 fill=%27%23616161%27/%3E%3C/svg%3E");
  --c-border: var(--n-200);     /* 면 구분선. 여기는 대비 대상이 아니다(면 나누기) */
  /* 컨트롤(버튼·입력·칩) 테두리 — "누를 수 있다"는 표시라 3:1 이상이어야 한다.
     흰 배경 4.54:1 / 페이지 바닥 4.24:1 / 옅은 면 4.06:1. 면 구분에는 계속
     --c-border 를 쓴다(카드 경계까지 진하면 화면이 무거워진다). */
  --c-border-strong: var(--n-500);
  /* 페이지 바닥과 카드를 색으로 가른다. 예전에는 둘 다 순백이라 카드 경계가
     1.26:1 선 하나에 걸려 있었고, 그래서 라이트와 다크의 깊이 모델이 서로
     달랐다(다크만 바닥≠카드). 이제 두 모드가 같은 구조다. */
  --c-bg: var(--n-25);
  --c-surface: var(--n-0);
  --c-subtle: var(--n-50);      /* 표 머리·hover·코드 배경 */

  /* --- 색: 상태 --- */
  --c-danger: var(--red-700);          /* 글자·테두리용. 흰 배경 6.57:1 */
  --c-danger-bg: var(--red-50);        /* 배지 바탕 (위 글자와 6.05:1) */
  --c-danger-solid: var(--red-700);    /* 채운 버튼 바탕 (흰 글자 6.57:1) */
  --c-danger-solid-hover: var(--red-800);
  --c-ok: var(--green-700);            /* 흰 배경 5.41:1 */
  --c-ok-bg: var(--green-50);          /* 배지 (5.13:1) */
  --c-warn: var(--amber-700);          /* 흰 배경 5.43:1 */
  --c-warn-bg: var(--amber-50);        /* 배지 (5.20:1) */
  /* 채운 면 위의 글자색. 테마가 다크로 바뀌어도 이 값만 보면 된다 */
  --c-on-primary: #ffffff;
  --c-on-danger: #ffffff;

  /* --- 간격 — 4의 배수. 임의의 숫자를 쓰지 않는다 ---
     큰 쪽(24·32·40·48)을 실제로 쓰는 것이 이번 개편의 핵심이다. 전에는 8·12·16
     세 개가 전체의 83% 라 화면 전체가 한 가지 밀도로 균질했고, 어디가 큰 덩어리
     경계인지 여백만으로는 보이지 않았다.
     관계 규칙: 라벨↔값 4~8 / 컴포넌트 내부 12~16 / 컴포넌트 사이 20~24 /
     구획 사이 32~40. 컴포넌트 사이 간격 ≥ 내부 패딩 × 1.5 를 지킨다
     (카드 패딩 16 → 카드 사이 24). 역전되면 카드 셋이 하나로 보인다. */
  --s-1: 4px;
  --s-2: 8px;
  --s-3: 12px;
  --s-4: 16px;
  --s-5: 20px;
  --s-6: 24px;
  --s-8: 32px;
  --s-10: 40px;
  --s-12: 48px;

  /* --- 글자 크기 — 7단계(비율 1.2 를 짝수로 스냅) ---
     전에는 토큰 4단계인데 화면에는 10종이 렌더됐다(11·14·22·28 이 토큰 밖).
     단계 밖 크기를 쓰지 않기로 하고, 금액·화살표까지 이 계단 안으로 넣었다. */
  --f-xs:   12px;   /* 배지·각주·단위 */
  --f-sm:   13px;   /* 보조 설명, 표 셀, 오류 문구 */
  --f-base: 15px;   /* 본문 기본 */
  --f-md:   16px;   /* 입력 필드 하한(iOS 확대 방지), 타일 제목 */
  --f-lg:   20px;   /* 섹션 제목(h2), 카드 제목 */
  --f-xl:   24px;   /* 상세 금액 */
  --f-hero: 28px;   /* 화면 제목(h1). h2(20)와 확실히 갈려야 "이것이 이 화면의
                       제목" 으로 읽힌다 — 24 는 h2 와 너무 가까웠다.
                       --f-2xl(32)은 KPI 숫자 자리라 제목에 쓰면 과하다 */
  --f-2xl:  32px;   /* 대시보드 KPI 숫자 */

  /* --- 행간·자간 ---
     한글은 라틴보다 획이 많아 같은 px 에서 더 답답하게 읽힌다. 본문은 넉넉히,
     제목은 큰 글자일수록 좁게. 12px 이하는 조이지 않는다(받침이 붙어 보여
     6과 8, 0과 O 오독이 늘어난다 — 현장에서 자재코드 오독으로 직결된다). */
  --lh-body: 1.6;
  --lh-head: 1.3;
  --lh-tight: 1.2;   /* 버튼·라벨·칩처럼 한 줄짜리 */
  --ls-body: -0.01em;
  --ls-head: -0.025em;
  --ls-flat: 0;

  --fw-normal: 400;
  --fw-medium: 500;    /* '약간 강조' 자리. 없으면 강조할 때마다 700 으로 점프한다 */
  --fw-semibold: 600;
  --fw-bold: 700;

  /* --- 모서리 — 5단계 ---
     중첩 역전(카드 8 안에 타일 12)을 없애려고 카드를 12 로 올렸다. 앱이 이미
     12 기준(BorderRadius.circular(12) 27회)이라 웹·앱이 같은 얼굴이 된다.
     반경은 요소 높이의 1/4 을 넘기지 않는다(40 버튼에 16을 주면 알약처럼
     보여 '누를 수 있는 것'과 '상태 배지'가 구분되지 않는다). */
  --r-xs:   4px;    /* 영수증 박스·아주 작은 표식 */
  --r-sm:   8px;    /* 버튼·입력·셀렉트·페이저 */
  --r-md:  12px;    /* 카드·타일·패널·떠 있는 목록 */
  --r-lg:  16px;    /* 모달·메뉴 카드 */
  --r-full: 999px;  /* 배지·칩 */
  /* 구식 이름 — 기존 규칙이 참조하던 별칭. 새 규칙은 --r-* 를 쓴다 */
  --radius: var(--r-sm);
  --radius-sm: var(--r-xs);

  /* --- 층 3단계 ---
     예전 정책은 --shadow: none 이었는데 이미 두 곳(FAB·pick 목록)이 각자 다른
     값으로 그림자를 쓰고 있었다. 정책이 아니라 예외가 규칙이 된 상태였다.
     이제 층을 셋으로 못 박는다.
       0 페이지 바닥 — 그림자 없음
       1 카드·타일   — 선만. 목록에 카드가 200개 있으면 blur 는 저사양 단말의
                       스크롤을 끊고, 직사광선 아래서는 어차피 안 보인다
       2 떠 있는 목록(콤보·pick·팝오버) — shadow-2
       3 모달·FAB    — shadow-3
     그림자 색은 순수 검정이 아니라 남색 계열 어두운 값이다(검정은 회색 얼룩처럼
     보인다). blur ≤ 24px, y ≤ 12px, 불투명도 ≤ 12%. */
  /* 층 1 은 지금 쓰는 곳이 없다 — 카드·타일을 선으로 나누기로 했기 때문이다.
     계단을 문서로 남겨야 나중에 "이건 2인가 3인가" 를 다시 논쟁하지 않는다 */
  --shadow-1: 0 1px 2px rgba(16, 24, 40, .06), 0 1px 3px rgba(16, 24, 40, .10);
  --shadow-2: 0 2px 4px rgba(16, 24, 40, .06), 0 4px 8px rgba(16, 24, 40, .08);
  --shadow-3: 0 4px 8px rgba(16, 24, 40, .06), 0 12px 24px rgba(16, 24, 40, .12);
  --shadow: none;   /* 구식 이름 — 남겨만 둔다. 새 규칙은 --shadow-1/2/3 */

  /* --- 모션 ---
     100ms 이하는 '즉시', 100~300ms 는 '자연스러운 반응', 400ms 이상은 '기다림'
     으로 인식된다. 업무 화면은 같은 전환을 하루 수백 번 본다 — 300ms × 300회는
     하루 90초의 순수 대기다. 그래서 상한을 250ms 로 둔다.
     진입은 ease-out(이미 도착해 있는 느낌), 이탈은 ease-in(끝까지 안 본다). */
  --dur-instant: 100ms;   /* hover·색 변화 */
  --dur-fast:    150ms;   /* 눌림·체크 */
  --dur-base:    200ms;   /* 드롭다운·아코디언 */
  --dur-slow:    250ms;   /* 모달 */
  --ease-out:    cubic-bezier(0, 0, .2, 1);
  --ease-in:     cubic-bezier(.4, 0, 1, 1);
  --ease-in-out: cubic-bezier(.4, 0, .2, 1);

  /* --- 리듬(높이) — 서로 배수·짝 관계가 되게 잡는다 ---
     전에는 한 폼 안에 48·41·40·33·32 다섯 종이 섞여 세로로 붙으면 어긋남이
     눈에 보였다. 이제 입력 48 / 버튼 40 / 배지·칩 32·24 / 터치 최소 44 다.
     입력과 버튼이 한 줄에 설 때는 그 줄에 align-items:stretch 를 줘 버튼이
     입력 높이(48)까지 늘어난다 — 값은 그대로 두고 줄에서만 맞춘다. */
  --h-input: 48px;
  --h-tap:   44px;   /* 손가락 최소 목표물 */
  --h-btn:   40px;
  --h-chip:  32px;
  --h-lg:    56px;   /* FAB·메뉴 아이콘 원 */

  /* 조건 칩의 가로. 높이가 한 값으로 통일돼 있듯 가로도 같아야 한 줄의 칩들이
     같은 종류로 읽히고, 줄이 여러 개일 때 둘째·셋째 열까지 맞는다.

     값의 근거는 두 개다.
     ① 가장 긴 라벨이 다섯 글자("카드번호순" 64.3px)다. 여기에 좌우 여백을
        더해도 80 안에 든다.
     ② 375px 화면에서 한 줄에 셋이 들어가야 한다 — 카드 안쪽 310 에서
        라벨 열(58)과 열 간격(4)을 빼면 248 이고, 80×3 + 칩 간격 4×2 = 248 로
        정확히 들어맞는다. 이보다 크면 마지막 칩이 잘려 밀어야 보인다. */
  --w-chip:  80px;

  /* 모든 화면의 콘텐츠 폭 — 휴대폰으로 봤을 때와 같은 인상이 나는 값
     (2026-08-24 사용자 확정 480. 휴대폰 실측 폭 390~430 에 약간의 여유).

     폭은 반드시 이 토큰 하나에서 온다. 전에는 960/640/560/400 네 값이
     화면마다 흩어져 있었고, header 는 main 의 960 을 복제해 들고 있었다 —
     로그인(400)에서 아이디 찾기(640)로 넘어가면 카드가 넓어졌다.
     "웹=모바일 완전 동일 레이아웃" 규약의 폭 절반이 그때는 빈말이었다. */
  --w-page: 480px;
  /* 개발자 문서(/styleguide) 전용 — 표본 격자·비교 표가 많아 480 으로는
     비교가 무너진다. 사용자가 보는 화면이 아니므로 예외로 둔다. */
  --w-doc:  960px;

  --o-disabled: .55;  /* 비활성 불투명도 — 전에는 .5/.55/.65 셋이었다 */
}

/* ---------------------------------------------------------------------------
   1c. 색 테마

   시스템 전역 색 분위기 — 스타일 설정 화면에서 관리자가 정하면 모든 사용자의
   웹·앱이 같은 분위기가 된다. 서버 설정(tb_style_config)을 layout.html 의
   head 스크립트가 CSS 적용 전에 html[data-theme] 로 세팅한다.

   키 목록은 서버(StyleConfigServiceImpl.THEMES)·설정 화면(StyleAdminController)
   ·앱 팔레트와 일치해야 한다 — 한쪽에만 추가하면 "저장했는데 안 바뀐다" 가 된다.

   라이트 테마는 primary(채운 면)·accent(강조)·focus(링) 세 계열만 갈아끼운다.
   primary 는 흰 글자를 얹는 면이라 전 테마 4.5:1 이상(최저 teal 5.47),
   focus 는 비텍스트 3:1 이상(최저 default 4.66, 옅은 면 기준)을 계산해 지켰다.
   다크 테마는 배경·글자·상태색·층까지 통째로 갈아끼운다.
   --------------------------------------------------------------------------- */
html[data-theme="indigo"] {
  --c-primary: #3730a3; --c-primary-dark: #312e81; --c-primary-light: #e0e7ff;
  --c-accent: #6366f1;  --c-accent-dark: #4f46e5;
  --c-focus: #4f46e5;                       /* 흰 배경 6.29:1 */
}
html[data-theme="blue"] {
  --c-primary: #1e40af; --c-primary-dark: #1e3a8a; --c-primary-light: #dbeafe;
  --c-accent: #3b82f6;  --c-accent-dark: #1d4ed8;
  --c-focus: #1d4ed8;                       /* 6.70:1 */
}
html[data-theme="teal"] {
  --c-primary: #0f766e; --c-primary-dark: #115e59; --c-primary-light: #ccfbf1;
  --c-accent: #14b8a6;  --c-accent-dark: #0d9488;
  --c-focus: #0f766e;                       /* accent(#14b8a6)는 2.49:1 이라 못 쓴다 → 5.47:1 */
}
html[data-theme="green"] {
  --c-primary: #047857; --c-primary-dark: #065f46; --c-primary-light: #d1fae5;
  --c-accent: #10b981;  --c-accent-dark: #059669;
  --c-focus: #047857;                       /* accent 2.54:1 → 5.48:1 */
}
html[data-theme="purple"] {
  --c-primary: #6d28d9; --c-primary-dark: #5b21b6; --c-primary-light: #ede9fe;
  --c-accent: #8b5cf6;  --c-accent-dark: #6d28d9;
  --c-focus: #6d28d9;                       /* 7.10:1 */
}
html[data-theme="rose"] {
  --c-primary: #9f1239; --c-primary-dark: #881337; --c-primary-light: #ffe4e6;
  --c-accent: #f43f5e;  --c-accent-dark: #be123c;
  --c-focus: #be123c;                       /* 6.29:1 */
}
html[data-theme="navy"] {
  --c-primary: #1e293b; --c-primary-dark: #0f172a; --c-primary-light: #fde9c8;
  --c-accent: #f59e0b;  --c-accent-dark: #b45309;
  --c-focus: #a15c07;                       /* accent 2.15:1 → 5.19:1 */
}
/* 스톤 — 따뜻한 무채색 미니멀. 바닥도 살짝 웜 톤이다 */
html[data-theme="stone"] {
  --c-primary: #44403c; --c-primary-dark: #292524; --c-primary-light: #e7e5e4;
  --c-accent: #78716c;  --c-accent-dark: #57534e;
  --c-focus: #57534e;                       /* 7.63:1 */
  --c-bg: #f7f6f4; --c-surface: #ffffff; --c-subtle: #f0efed; --c-border: #e4e2df;
}
/* 모노 — 색을 뺀 블랙 미니멀 */
html[data-theme="mono"] {
  --c-primary: #111111; --c-primary-dark: #000000; --c-primary-light: #e8e8e8;
  --c-accent: #525252;  --c-accent-dark: #262626;
  --c-focus: #262626;                       /* 15.13:1 */
}
/* 다크 — 차콜 배경 + 주황 강조. 상태색은 어두운 배경에서 보이도록 밝힌다.
   다크에서 층은 그림자가 아니라 면 밝기가 만든다(검정 위 검정 그림자는
   물리적으로 안 보인다). 그래서 shadow 토큰에 옅은 흰 테두리를 섞는다. */
html[data-theme="dark"] {
  --c-primary: #262a33; --c-primary-dark: #0d0f13; --c-primary-light: #3a2c1e;
  --c-accent: #f37723;  --c-accent-dark: #f59250;
  --c-focus: #f37723;                       /* 바닥 6.67:1 / 카드 6.20:1 */
  --c-text: #e7e9ee; --c-muted: #9aa1af;
  --sel-arrow: url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%277%27 height=%276%27 viewBox=%270 0 7 6%27%3E%3Cpath d=%27M0 0h7L3.5 6z%27 fill=%27%239aa1af%27/%3E%3C/svg%3E");
  --c-bg: #101216; --c-surface: #171a20; --c-subtle: #1f232b; --c-border: #2b303a;
  --c-border-strong: #8a919c;
  /* 글자용 danger 는 밝게(카드 위 5.84:1), 채운 삭제 버튼은 어둡게 —
     전에는 버튼도 #f87171 이라 흰 글자 2.77:1 로 AA 실패였다. 되돌릴 수 없는
     동작의 버튼이 가장 안 읽히는 상태였다. */
  --c-danger: #f87171; --c-danger-bg: #3a1518;
  --c-danger-solid: var(--red-500); --c-danger-solid-hover: var(--red-700);  /* 흰 글자 4.83:1 */
  --c-ok: #34d399; --c-ok-bg: #0c2f26;
  --c-warn: #fbbf24; --c-warn-bg: #3a2c10;
  --shadow-1: 0 0 0 1px rgba(255, 255, 255, .04);
  --shadow-2: 0 0 0 1px rgba(255, 255, 255, .06), 0 6px 16px rgba(0, 0, 0, .55);
  --shadow-3: 0 0 0 1px rgba(255, 255, 255, .08), 0 16px 32px rgba(0, 0, 0, .65);
}
/* 미드나이트 — 깊은 네이비 배경 + 밝은 블루 강조 */
html[data-theme="midnight"] {
  /* 선택 바탕(--c-primary-light)을 예전(#1e3a5f)보다 한 단계 어둡게 잡았다 —
     그 위에 얹히는 보조 글자(--c-muted)가 4.49:1 로 아슬아슬하게 미달이었다 */
  --c-primary: #1d2b47; --c-primary-dark: #0e1526; --c-primary-light: #1b3355;
  --c-accent: #60a5fa;  --c-accent-dark: #93c5fd;
  --c-focus: #60a5fa;                       /* 바닥 7.41:1 / 카드 6.83:1 */
  --c-text: #e2e8f0; --c-muted: #94a3b8;
  --sel-arrow: url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%277%27 height=%276%27 viewBox=%270 0 7 6%27%3E%3Cpath d=%27M0 0h7L3.5 6z%27 fill=%27%2394a3b8%27/%3E%3C/svg%3E");
  --c-bg: #0b1120; --c-surface: #101a2e; --c-subtle: #16233c; --c-border: #223250;
  --c-border-strong: #8a919c;
  --c-danger: #f87171; --c-danger-bg: #3a1518;
  --c-danger-solid: var(--red-500); --c-danger-solid-hover: var(--red-700);
  --c-ok: #34d399; --c-ok-bg: #0c2f26;
  --c-warn: #fbbf24; --c-warn-bg: #3a2c10;
  --shadow-1: 0 0 0 1px rgba(255, 255, 255, .04);
  --shadow-2: 0 0 0 1px rgba(255, 255, 255, .06), 0 6px 16px rgba(0, 0, 0, .55);
  --shadow-3: 0 0 0 1px rgba(255, 255, 255, .08), 0 16px 32px rgba(0, 0, 0, .65);
}

/* ---------------------------------------------------------------------------
   2. 기본
   --------------------------------------------------------------------------- */
* { box-sizing: border-box; }

/* 모바일 브라우저의 글자 자동 확대(font boosting)를 끈다.
   부스팅은 좁은 화면에서 '일부' 텍스트만 골라 키우는데, 표에서 줄바꿈 가능한
   셀만 확대되면 행 높이는 커진 텍스트를 따르고 nowrap 셀은 확대 전 높이로
   칠해져 — 같은 행의 구분선이 열마다 다른 높이에 그려진다. 픽셀 측정으로
   확인한 실제 사고다(2줄 행에서만 작업 열 선이 34px 위에 있었다).
   text-size-adjust 는 표준·구형 표기를 함께 둔다. */
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }

body {
  margin: 0;
  /* 위 text-size-adjust 를 무시하는 크롬 안드로이드 부스팅까지 막는 관용 처방.
     유한한 max-height 가 있는 블록은 부스팅 대상에서 빠진다 — 값이 커서
     레이아웃에는 아무 영향이 없다. */
  max-height: 999999px;
  background: var(--c-bg);
  color: var(--c-text);
  /* 폰트는 사내망·CSP 때문에 외부 CDN 을 쓸 수 없다. 그래서 "깔려 있으면 쓰고
     없으면 다음" 순서만 정비한다. Pretendard 가 로컬에 있는 PC 는 Pretendard,
     맥·아이폰은 Apple SD Gothic Neo, 윈도우는 맑은 고딕, 안드로이드는
     Noto Sans KR 로 떨어진다 — 어느 경우에도 한글 본문용 얼굴이 잡힌다. */
  font-family: "Pretendard Variable", Pretendard, "Apple SD Gothic Neo",
               "Malgun Gothic", "Noto Sans KR", system-ui, sans-serif;
  font-size: var(--f-base);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-body);
  /* 한글은 어절 단위로 끊어야 읽힌다. keep-all 이 없으면 "발주\n서 승인" 처럼
     음절에서 잘린다 — 실제로 타일 제목·부제에서 그랬다.
     대신 URL·해시처럼 띄어쓰기 없는 긴 덩어리는 어디서든 끊는 탈출구가 필요해
     break-word 를 함께 둔다(전역을 anywhere 로 두면 flex 항목의 최소 폭 계산이
     바뀌어 나란히 선 칸들이 예고 없이 찌그러진다 — 탈출구는 필요한 곳에만). */
  word-break: keep-all;
  overflow-wrap: break-word;
  /* 어떤 요소가 폭을 넘겨도 가로 스크롤을 만들지 않는다. 가로로 밀리면
     헤더 배경(뷰포트 폭)이 짧아 보여 오른쪽에 흰 띠가 드러난다.
     clip 은 hidden 과 달리 스크롤 컨테이너를 만들지 않아 sticky 를 깨지 않는다.
     지원하지 않는 브라우저는 앞의 hidden 을 쓴다. */
  overflow-x: hidden;
  overflow-x: clip;
}

/* 숫자가 세로로 정렬되어야 검산이 되는 자리 — 금액·건수·표.
   글리프 폭이 균일해져 자릿수가 줄맞춤되고, 값이 바뀔 때 폭이 흔들리지 않아
   레이아웃 떨림도 사라진다. 한글 문장 속 숫자까지 전역으로 적용하면 글자
   사이가 벌어져 어색하므로 여기 적힌 자리에만 준다. */
.stat .num, .tree-count, .badge,
th, td, .u-num,
input[type="number"], input[type="date"],
input[inputmode="numeric"], input[inputmode="decimal"] {
  font-variant-numeric: tabular-nums;
}

/* 제목 위계 — 20 / 18 / 15(본문과 동일) 이던 것을 24 / 20 / 16 으로 벌렸다.
   전에는 h3 가 본문과 같은 크기에 회색이라 제목 레벨 셋 중 실질 차이가
   20↔18 뿐이었다. 큰 글자일수록 자간을 좁히고 행간을 줄인다. */
h1, h2, h3 {
  line-height: var(--lh-head);
  letter-spacing: var(--ls-head);
}
/* 화면 제목은 구획 제목(h2, 20)보다 확실히 커야 한다. 24 대 20 은 20% 차이라
   나란히 놓이면 같은 급으로 읽힌다 — 글 제목("덤벼")과 구획 제목("답변")이
   실제로 그렇게 보였다(2026-08-23 제보). h2 는 그대로 둔다: 구획 제목은
   본문(15)과의 위계에서 지금 크기가 맞다. */
h1 { font-size: var(--f-hero); font-weight: var(--fw-bold); margin: 0 0 var(--s-4); }
h2 { font-size: var(--f-lg); font-weight: var(--fw-bold); margin: var(--s-8) 0 var(--s-3); }
h3 { font-size: var(--f-md); font-weight: var(--fw-semibold); color: var(--c-text);
     margin: var(--s-6) 0 var(--s-2); }
/* 카드의 첫 제목 — 구획용 위 여백이 카드 패딩 위에 겹치면 머리가 비어 보인다 */
.card > h2:first-child, .card > h3:first-child { margin-top: 0; }
code {
  background: var(--c-subtle); padding: var(--s-1) var(--s-2); border-radius: var(--r-xs);
  font-size: var(--f-sm); font-family: Consolas, Monaco, monospace;
  letter-spacing: var(--ls-flat);
  /* 줄 끝에서 쪼개질 때 조각마다 패딩과 모서리를 다시 그린다. 이것이 없으면
     첫 조각의 오른쪽과 둘째 조각의 왼쪽이 잘린 채 붙어, 옅은 바탕이 두 개의
     서로 다른 이름처럼 보인다(.auto- | filled).
     nowrap 으로 막지 않는 이유 — 여기에는 45자짜리 함수 서명도 들어가는데,
     한 줄로 붙들면 좁은 화면에서 페이지 전체가 가로로 넘친다. 쪼개지되
     쪼개진 티가 나지 않게 하는 쪽이 맞다. */
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
}

/* ---------------------------------------------------------------------------
   3. 레이아웃
   --------------------------------------------------------------------------- */
header {
  background: var(--c-primary);
  border-bottom: 3px solid var(--c-accent);   /* 브랜드 강조선 */
  color: var(--c-on-primary);
  /* 배경은 화면 전체를 덮되 안쪽 내용은 본문(main)과 좌우 끝을 맞춘다.
     전체 폭 padding 으로 두면 넓은 화면에서 로고와 메뉴만 화면 양 끝에
     붙어 가운데 모인 본문과 어긋나 보인다.
     좁은 화면에서는 max() 가 기본 여백을 택한다(계산값이 음수가 된다).
     폭은 반드시 main 과 같은 토큰(--w-page)이어야 한다 — 전에는 960 을
     여기 한 번 더 적어 두어, main 만 바꾸면 헤더와 본문이 어긋났다. */
  padding: var(--s-3) max(var(--s-4), calc((100% - var(--w-page)) / 2 + var(--s-3)));
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  flex-wrap: wrap;                 /* 좁은 화면에서 제목과 메뉴가 겹치지 않게 */
}
header a { color: var(--c-on-primary); text-decoration: none; }
header .brand { font-weight: var(--fw-bold); font-size: var(--f-md); }

/* 브랜드 — 헤더 로고와 로그인 화면 심벌.
   이미지 파일은 /branding 아래에 두고 여기서는 크기만 정한다.
   로고는 원본 비율을 유지한다(높이만 지정하고 폭은 auto). */
header .brand { display: inline-flex; align-items: center; gap: var(--s-2); }
/* 로고 폭을 제한한다. SVG 는 원본 비율대로 늘어나므로 좁은 화면에서
   메뉴를 밀어내고, 헤더가 화면 밖으로 넘쳐 오른쪽에 흰 띠가 드러난다. */
header .brand img { height: var(--s-6); width: auto; max-width: 40vw; display: block; }
/* 브랜드 이름은 글자다 — 그림에 박아 두면 이름을 바꿀 때 그림을 다시 그려야 한다.
   좁은 화면에서 줄바꿈을 막는다: 두 글자가 아래로 떨어지면 헤더가 두 줄이 된다. */
header .brand .brand-name { white-space: nowrap; letter-spacing: var(--ls-head); }

.auth-brand { text-align: center; margin-bottom: var(--s-6); }
.auth-brand img { width: 72px; height: 72px; display: block; margin: 0 auto; }
.auth-brand .name {
  font-weight: var(--fw-bold); font-size: var(--f-lg); margin-top: var(--s-2);
  letter-spacing: var(--ls-head);
}
.auth-brand .tagline { color: var(--c-muted); font-size: var(--f-sm); }
/* 메뉴가 한 줄에 안 들어가면 다음 줄로 넘어간다.
   nowrap 으로 두면 줄바꿈 대신 화면 밖으로 밀려 가로 스크롤이 생기고,
   그때 헤더 배경 바깥의 흰 바탕이 보인다. */
header nav { display: flex; gap: var(--s-4); flex-wrap: wrap; justify-content: flex-end; min-width: 0; }
/* 헤더 링크도 손가락 목표물이다 — 글자 높이(약 22px)만큼만 눌리면 좁은 화면에서
   자주 빗나간다. 44 까지 키우면 헤더가 통째로 두꺼워지므로 칩과 같은 32 로 둔다
   (헤더는 모든 화면 맨 위를 차지해 1px 도 비싸다). */
header nav a { display: inline-flex; align-items: center; min-height: var(--h-chip); }

/* 데스크톱과 모바일은 완전히 같은 레이아웃을 그린다(단일 레이아웃 원칙의 확장,
   2026-08-20 사용자 확정) — 뷰포트로 배치·크기를 갈라놓지 않고, 넓은 화면은
   같은 화면을 가운데 폭 제한 안에 놓을 뿐이다. 두 벌은 반드시 어긋난다.
   위 여백 20 / 아래 40 — 아래를 크게 두는 이유는 목록 끝과 화면 끝이 붙으면
   더 있는지 끝인지 구분이 안 되기 때문이다. */
main { max-width: var(--w-page); margin: var(--s-5) auto var(--s-10); padding: 0 var(--s-4); }
/* 로그인 계열. 폭은 main 과 같다 — 전에는 여기만 400 이라 로그인에서
   아이디 찾기(640)로 넘어가면 카드가 넓어졌다. 남는 것은 위 여백뿐이다. */
main.auth { margin-top: var(--s-12); }
/* 개발자 문서만 예외 — 이유는 --w-doc 토큰 주석에 */
main.sg-doc { max-width: var(--w-doc); }

.card {
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);   /* 앱 카드(12)와 동일 — 안의 타일과 역전되지 않는다 */
  padding: var(--s-4);          /* 앱 화면 패딩(16)과 동일 */
}
/* 카드 사이 24 — 내부 패딩(16)의 1.5배. 16 이던 예전 값은 패딩과 같아
   카드 여러 장이 한 덩어리로 읽혔다 */
.card + .card { margin-top: var(--s-6); }

/* ---------------------------------------------------------------------------
   4. 컴포넌트
   --------------------------------------------------------------------------- */

/* 버튼 — 기본(primary) / 보조(plain) / 위험(danger)
   높이 40 을 기준으로 잡고, 글자는 flex 로 가운데 놓는다(패딩만으로 맞추면
   글꼴이 바뀔 때 위아래가 어긋난다). 입력(48)과 한 줄에 설 때는 그 줄에
   align-items:stretch 를 줘 버튼이 늘어난다. */
button, .btn {
  padding: var(--s-2) var(--s-4);
  border: 0;
  border-radius: var(--r-sm);
  cursor: pointer;
  background: var(--c-primary);
  color: var(--c-on-primary);
  font: inherit;
  font-weight: var(--fw-medium);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-body);
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);                 /* 아이콘+글자를 같이 넣어도 붙지 않는다 */
  vertical-align: middle;
  white-space: nowrap;
  min-height: var(--h-btn);        /* 손가락으로 누를 수 있는 최소 크기 */
  text-align: center;
  transition: background-color var(--dur-instant) linear,
              border-color var(--dur-instant) linear,
              transform var(--dur-fast) var(--ease-out);
}
/* hover 로 배경을 바꾸는 규칙은 전부 (hover: hover) 안에 둔다 — iOS 사파리는
   탭한 요소에 :hover 를 끈적하게 남겨서, 밖에 두면 클래스로 밝게 칠한 버튼
   (예: 카드 선택 .pick-face, 0-1-0)이 button:hover(0-1-1)에 져 탭하는 순간
   검게 뒤집힌다(2026-08-27 아이폰 실측 제보). 터치의 눌림 신호는 아래
   :active(scale)가 맡는다. */
@media (hover: hover) {
  button:hover, .btn:hover { background: var(--c-primary-dark); }
}
/* 눌림 — 터치 기기에는 hover 가 없다. :active 가 없으면 휴대폰에서 버튼을
   눌렀을 때 아무 신호가 없어 "눌렸나?" 하고 두 번 누르게 된다.
   크기는 transform 으로만 바꾼다(width/height 는 레이아웃을 다시 계산한다). */
button:active, .btn:active { transform: scale(.98); }
button:disabled, .btn:disabled,
button[aria-disabled="true"], .btn[aria-disabled="true"] {
  opacity: var(--o-disabled); cursor: not-allowed; transform: none;
}
@media (hover: hover) {
  button:disabled:hover, .btn:disabled:hover { background: var(--c-primary); }
}

.btn-plain {
  background: var(--c-surface); color: var(--c-text);
  border: 1px solid var(--c-border-strong);
}
@media (hover: hover) {
  .btn-plain:hover { background: var(--c-subtle); }
  .btn-plain:disabled:hover { background: var(--c-surface); }
}
.btn-danger { background: var(--c-danger-solid); color: var(--c-on-danger); }
@media (hover: hover) {
  .btn-danger:hover { background: var(--c-danger-solid-hover); }
  .btn-danger:disabled:hover { background: var(--c-danger-solid); }
}

/* 목록 안의 삭제 — 조용한 위험 버튼

   같은 삭제 버튼이라도 자리에 따라 무게가 달라야 한다. 확인 창의 삭제는 그
   화면의 결론이라 채워야 하지만, 목록 타일의 삭제는 스무 줄에 스무 개가
   반복된다. 채운 빨강을 스무 번 쌓으면 화면에서 가장 먼저 눈에 들어오는 것이
   정작 읽어야 할 카드번호가 아니라 삭제 버튼이 된다.

   그래서 평상시에는 테두리와 글자만 위험색으로 두고, 손이 닿는 순간(hover·
   focus·누름) 채운다 — 색이 말하는 뜻(위험)은 그대로고, 목소리 크기만 낮췄다.
   모바일 앱도 같은 자리에서 조용한 테두리 아이콘(delete_outline)을 쓴다.
   웹과 앱은 같은 화면이어야 하므로 여기를 앱에 맞춘 것이다. */
.btn-danger-quiet {
  background: var(--c-surface);
  color: var(--c-danger);
  /* 45% — 원색 테두리는 채운 버튼만큼 튄다. 기능 카드 색조와 같은 농도 */
  border: 1px solid color-mix(in srgb, var(--c-danger) 45%, transparent);
}
/* focus-visible 채움은 가드 밖에 남긴다 — 키보드 신호는 터치와 무관하다 */
.btn-danger-quiet:focus-visible {
  background: var(--c-danger-solid);
  color: var(--c-on-danger);
  border-color: var(--c-danger-solid);
}
@media (hover: hover) {
  .btn-danger-quiet:hover {
    background: var(--c-danger-solid);
    color: var(--c-on-danger);
    border-color: var(--c-danger-solid);
  }
  .btn-danger-quiet:disabled:hover { background: var(--c-surface); color: var(--c-danger); }
  /* 실행되지 않는 위험 버튼(aria-disabled — 눌러도 안내만) — 손이 닿아도
     채우지 않는다: 채워지면 "실행 가능"으로 읽힌다 */
  .btn-danger-quiet[aria-disabled="true"]:hover {
    background: var(--c-surface); color: var(--c-danger);
  }
}
.btn-sm {
  padding: var(--s-1) var(--s-3); min-height: var(--h-chip); font-size: var(--f-sm);
}
/* 작은 버튼은 보이는 크기(32)보다 손가락 목표물(44)이 커야 한다.
   투명한 세로 확장만 준다 — 가로로 넓히면 옆 버튼을 덮어 오터치가 는다. */
.btn-sm { position: relative; }
.btn-sm::after {
  content: ''; position: absolute; left: 0; right: 0;
  top: 50%; transform: translateY(-50%); height: var(--h-tap);
}
.btn-block { display: flex; width: 100%; }

/* 폼 */
/* type 을 생략한 input 도 텍스트 입력이다. 속성 선택자만 나열하면 그런 입력이
   스타일 없이 남아 폭이 어긋난다 — 실제로 SPA 로그인 화면에서 그랬다.
   :not() 은 리스트 한 번으로 쓴다 — :not() 을 여섯 번 이으면 특이도가 (0,6,1)이
   되어 .is-invalid·.auto-filled 같은 상태색 클래스(0,1,1)가 전부 진다.
   실제로 완료색이 셀렉트에만 먹고 텍스트 입력엔 안 먹던 사고의 원인이다. */
/* 파일 선택 — 브라우저가 그리는 기본 버튼은 회색 네이티브 모양이라, 우리 버튼들과
   나란히 서면 혼자 다른 시대의 물건처럼 보인다. 안쪽 버튼만 골라 보조 버튼과 같은
   모양으로 바꾼다(::file-selector-button).

   입력 자체를 통째로 감싸지 않는 이유: 파일 입력은 값을 스크립트로 넣을 수 없어
   "가짜 버튼 + 숨긴 입력" 으로 바꾸면 키보드·보조기술 동작을 전부 다시 만들어야 한다.
   기본 요소를 그대로 두고 껍데기만 갈아입히는 편이 안전하다.

   파일 이름 글자는 보조 정보라 작게·흐리게 둔다 — 버튼이 먼저 눈에 들어와야 한다. */
input[type=file] {
  max-width: 100%;
  font: inherit; font-size: var(--f-sm); color: var(--c-muted);
}
input[type=file]::file-selector-button {
  margin-right: var(--s-2);
  /* .btn-sm 과 <b>같은 값</b>이다. 같은 줄에 나란히 서므로 여백이 한 칸만
     달라도 높이가 어긋나 보인다(2026-08-23 지적). */
  min-height: var(--h-chip); padding: var(--s-1) var(--s-3);
  background: var(--c-surface); color: var(--c-text);
  border: 1px solid var(--c-border-strong); border-radius: var(--r-sm);
  font: inherit; font-size: var(--f-sm); font-weight: var(--fw-semibold);
  cursor: pointer;
  transition: background-color var(--dur-instant) linear;
}
@media (hover: hover) {
  input[type=file]::file-selector-button:hover { background: var(--c-subtle); }
}
input[type=file]::file-selector-button:active { background: var(--c-border); }

input:not([type=checkbox], [type=radio], [type=file], [type=hidden], [type=submit], [type=button]),
textarea, select {
  width: 100%;
  padding: var(--s-3);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--r-sm);
  font: inherit;
  background: var(--c-surface);
  color: var(--c-text);
  font-size: var(--f-md);          /* iOS 는 16px 미만이면 입력 시 화면을 확대한다 */
  line-height: 1.4;
  min-height: var(--h-input);      /* 12 + 12 + 22.4 + 2 ≈ 48 */
  transition: border-color var(--dur-instant) linear;
}
/* 링크

   기본값을 두는 이유: 이 규칙이 없으면 클래스가 없는 <a> 가 브라우저 기본
   스타일로 그려진다 — 파란 글자에 보라색 방문 표시다. 12테마를 계산해 맞춘
   화면 한가운데에 브랜드와 무관한 색이 두 종류 얹힌다(2026-08-22 실제로 났다.
   로그인 화면의 가입·찾기 링크).

   색은 물려받는다(inherit). 링크 전용 색을 새로 두지 않은 이유:
   주황(--c-accent)은 흰 배경 대비 2.81:1 이라 글자에 쓸 수 없고(팔레트 주석),
   차콜은 본문 글자색과 같아 색만으로는 어차피 구분되지 않는다. 그래서 구분은
   밑줄이 맡는다 — 색을 못 가리는 사람에게도 남는 신호다.

   밑줄은 글자보다 옅게 깔고 손이 닿으면 진해진다. 글자와 같은 굵기로 두면
   문장 속 링크가 취소선처럼 무겁게 읽힌다.

   text-underline-offset 이 3px 인 것은 한글 때문이다. 기본값은 받침(ㄱ·ㅇ)에
   밑줄이 붙어 글자가 뭉개진다. */
a {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--c-border-strong);
  text-underline-offset: 3px;
  border-radius: var(--r-xs);
}
a:hover { text-decoration-color: currentColor; }
a:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 2px; }

/* 링크처럼 안 보여야 하는 링크 — 버튼·칩·타일·카드.

   .btn / .chip-link / .tile-main / .pager a / header a 는 각자 이미
   text-decoration: none 을 갖고 있다. .menu-card 만 없어서 여기서 함께 끈다
   (홈 카드 제목에 밑줄이 생겼다). 새 링크형 컴포넌트를 만들 때도 같은 자리다. */
.menu-card { text-decoration: none; }

/* 글 속의 링크 — 문장에 섞여 있어 밑줄만으로는 눈에 잘 안 든다.
   묶음으로 늘어놓는 보조 링크(로그인 화면의 가입·찾기)가 이것이다.
   기본 <a> 보다 진한 밑줄과 조금 굵은 글자로 "누를 수 있는 것" 을 분명히 한다. */
.link {
  color: var(--c-text);
  font-weight: var(--fw-medium);
  text-decoration-color: var(--c-muted);
}
.link:hover { text-decoration-thickness: 2px; }
/* 라벨 줄에 얹는 글자 버튼 — 상자 없이 링크 모양이다(2026-08-25, 계산하기).
   라벨(--f-sm)과 같은 크기라 줄이 두꺼워지지 않는다. 눈에 보이는 높이는
   글자뿐이지만 손가락 목표물은 투명 확장으로 44 를 지킨다(.btn-sm 과 같은 처방). */
button.link {
  background: none; border: 0; padding: 0; cursor: pointer;
  /* 기본 button 의 최소 높이(40)를 끈다 — 상자가 보이지 않아도 40 상자가
     남으면 라벨 줄 높이와 글자 밑선을 흔든다. 탭 목표물은 ::after 가 맡는다 */
  min-height: 0;
  font-size: var(--f-sm); line-height: inherit;
  text-decoration: underline; text-decoration-color: var(--c-muted);
  text-underline-offset: 3px; position: relative;
}
/* 기본 button:hover 의 채움 배경이 글자 버튼에 묻지 않게 — hover 신호는
   .link:hover 의 밑줄 굵기가 맡는다 */
button.link:hover { background: none; }
button.link::after {
  content: ''; position: absolute;
  left: calc(-1 * var(--s-2)); right: calc(-1 * var(--s-2));
  top: 50%; transform: translateY(-50%); height: var(--h-tap);
}

/* 포커스는 :focus 가 아니라 :focus-visible 로 잡는다. 전에는 입력만 :focus 라
   마우스 클릭에도 링이 떴고(전역 원칙과 정면으로 어긋났다), 링 색이
   --c-primary-light(#fbe3d2) 라 흰 배경 대비 1.23:1 — 계산상 보이지 않았다.
   브라우저는 텍스트 입력에 대해서는 클릭 포커스도 focus-visible 로 쳐 주므로
   "클릭해도 안 보인다" 문제는 생기지 않는다. */
input:focus-visible, textarea:focus-visible, select:focus-visible {
  outline: 2px solid var(--c-focus);
  outline-offset: 2px;
  border-color: var(--c-focus);
}
input:disabled, textarea:disabled, select:disabled {
  opacity: var(--o-disabled); cursor: not-allowed; background: var(--c-subtle);
}
/* 고르는 칸의 표시기는 우리가 직접 그린다.

   브라우저 기본 표시기는 플랫폼마다 다른 그림이다 — 데스크톱 크롬은 홑화살표
   하나, iOS 사파리는 위아래 두 개를 그린다. 그런데 같은 폼 안의 리치 선택
   (.pick-face)은 우리가 찍은 삼각형이라, 나란히 선 두 개의 "고르는 칸"이 서로
   다른 모양을 달고 있었다. 기본 그림을 끄고(appearance:none) 같은 삼각형을
   배경으로 깐다 — 이제 웹·모바일·리치선택이 모두 한 모양이다.
   데이터 URI 안에는 CSS 변수를 넣을 수 없어, 색만 테마별로 --sel-arrow 에
   한 줄씩 따로 둔다(:root 와 다크 테마 참조). 값은 --c-muted 와 같은 색이다.
   오른쪽 padding 은 글자가 삼각형 밑으로 파고들지 않도록 비워 둔 자리다. */
select {
  appearance: none;
  -webkit-appearance: none;
  background-image: var(--sel-arrow);
  background-repeat: no-repeat;
  background-position: right var(--s-3) center;
  background-size: var(--sel-arrow-w) var(--sel-arrow-h);
  padding-right: var(--s-8);   /* .pick-face 의 오른쪽 여백과 같은 값이다 */
}

/* 날짜·시간 칸 — iOS 사파리 대응.

   iOS 는 이 칸을 네이티브 위젯으로 그리는데 두 가지가 어긋난다.
   (1) 값을 가운데 정렬한다 — 위아래 칸은 전부 왼쪽 정렬이라 이 칸만 축이 다르다.
   (2) 값 길이만큼의 고유 최소 너비를 가져 width:100% 를 무시하고 칸 밖으로
       삐져나간다. 실제로 "2026. 8. 21. 오후 12:45" 가 카드 오른쪽 경계를
       넘어갔다(2026-08-21 제보).
   min-width:0 이 그 고유 너비를 풀어 주고, ::-webkit-date-and-time-value 가
   iOS 가 값을 그리는 실제 상자다 — 바깥 input 의 text-align 만으로는 안 움직인다. */
input[type=date], input[type=datetime-local], input[type=time], input[type=month] {
  appearance: none;
  -webkit-appearance: none;
  min-width: 0;
  max-width: 100%;
  text-align: left;
  /* 크롬은 이 칸을 다른 입력보다 1.7px 높게 그린다(네이티브 위젯의 고유
     content 높이다 — 안쪽 line-height·padding 을 아무리 만져도 안 줄었다).
     같은 폼에서 이 칸만 키가 다르면 테두리 줄이 어긋나 보이므로 높이를
     못 박는다. 값이 잘리지 않는 것은 브라우저에서 확인했다. */
  height: var(--h-input);
}
input[type=date]::-webkit-date-and-time-value,
input[type=datetime-local]::-webkit-date-and-time-value,
input[type=time]::-webkit-date-and-time-value,
input[type=month]::-webkit-date-and-time-value {
  text-align: left;
}
/* 달력 아이콘은 오른쪽 끝에 세운다. 기본은 값 바로 옆에 붙어, 값 길이가
   칸마다 다르므로 아이콘의 x 좌표도 칸마다 달라진다 */
input[type=date]::-webkit-calendar-picker-indicator,
input[type=datetime-local]::-webkit-calendar-picker-indicator,
input[type=time]::-webkit-calendar-picker-indicator,
input[type=month]::-webkit-calendar-picker-indicator {
  margin-left: auto;
}

/* 등록용 QR 을 담는 자리.

   그림 자체가 이미 "여기를 찍으세요" 라고 말하므로 테두리나 그림자로 더
   강조하지 않는다(층은 셋뿐이고 승급은 없다). 하는 일은 두 가지뿐이다 —
   가운데에 세우고, 흰 바탕을 깔아 준다.

   흰 바탕이 필요한 이유: QR 은 흑백 대비로 읽힌다. 어두운 테마에서 어두운 면
   위에 얹히면 카메라가 격자를 못 찾는다. 그래서 여기만 테마와 무관하게 흰색이다
   — 아래 여백(--s-3)까지 흰 판으로 두어 조용한 여백(quiet zone)을 넉넉히 준다. */
.qr-box {
  display: flex;
  justify-content: center;
  margin: var(--s-4) 0 0;
  padding: var(--s-3);
  background: #ffffff;
  border-radius: var(--r-sm);
}
.qr-box img {
  display: block;
  width: 240px;
  height: 240px;
  max-width: 100%;
}

/* 목록 안 검색칸 — 항목이 여덟 개를 넘을 때만 스크립트가 만든다.

   목록 위에 붙어 따라다닌다(sticky). 카드가 스물여섯 장이면 아래로 내리는 동안
   검색칸이 위로 사라지는데, 그러면 "다시 검색하려면 맨 위로" 가 된다.

   면을 깔아 주는 이유도 sticky 때문이다 — 투명하면 밑을 지나가는 항목이 글자
   위로 비쳐 보인다. 아래 선은 "여기부터 목록" 이라는 경계다. */
.pick-search {
  position: sticky;
  /* 목록에 4px 안쪽 여백이 있어 top:0 이면 그 틈으로 항목이 비쳐 지나간다.
     음수 여백으로 목록 가장자리까지 넓히고 그만큼 위로 올려 딱 붙인다
     (.dialog-actions 의 sticky 와 같은 처방). */
  top: calc(var(--s-1) * -1);
  z-index: 1;
  margin: calc(var(--s-1) * -1) calc(var(--s-1) * -1) var(--s-1);
  padding: var(--s-2);
  background: var(--c-surface);
  border-bottom: 1px solid var(--c-border);
}
.pick-search input {
  width: 100%;
  /* 목록 안이라 한 단계 낮춘다 — 바깥 입력(48)과 같은 높이면 목록보다
     검색칸이 더 무거워 보인다. 16px 글꼴 하한은 그대로다(iOS 확대 방지) */
  min-height: var(--h-btn);
  padding: var(--s-2) var(--s-3);
}
/* 목록은 overflow 를 자르므로 outline 이 잘린다 — 안쪽 그림자로 링을 그린다
   (공용 규약: .filter-chips·.combo-list 와 같은 처방) */
.pick-search input:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--c-surface), 0 0 0 4px var(--c-focus);
}
/* 걸러 낸 결과가 없을 때. 빈 목록을 그냥 두면 "고장" 으로 읽힌다 */
.pick-none {
  padding: var(--s-4) var(--s-3);
  color: var(--c-muted);
  font-size: var(--f-sm);
  text-align: center;
}
/* 수동 입력용 키 — 사람이 눈으로 읽어 옮겨 적는 값.

   등폭 글꼴을 쓰는 이유가 본문 글꼴과 다르다. 이 문자열에는 0·O, 1·I 가 섞여
   있고(Base32 라 걸러 낼 수도 없다), 비례 글꼴에서는 그 둘의 폭까지 달라
   한 글자씩 짚어 가며 읽기 어렵다.

   자간을 벌린 것도 같은 이유다 — 붙어 있으면 네 글자 묶음의 경계가 흐려진다.
   12px 이하에서 자간을 조이지 않는다는 규약의 반대 방향이고, 여기서는 의도적이다.

   user-select: all — 한 번만 눌러도 전체가 잡힌다. 32자를 손가락으로 드래그해
   범위를 잡는 것은 휴대폰에서 특히 성가시다. */
.otp-key {
  /* 아래 여백을 비워 두면 뒤따르는 설명(.detail-rows)이 상자에 딱 붙어
     상자의 일부처럼 읽힌다 — 컴포넌트 사이 간격은 16 이다(사용자 지적, 2026-08-21) */
  margin: var(--s-3) 0 var(--s-4);
  padding: var(--s-3);
  border: 1px solid var(--c-border);
  border-radius: var(--r-sm);
  background: var(--c-subtle);
  color: var(--c-text);
  font-family: Consolas, Menlo, 'Courier New', monospace;
  font-size: var(--f-md);
  line-height: var(--lh-body);
  letter-spacing: .08em;
  /* 32자 + 공백이라 좁은 화면에서는 반드시 줄이 바뀌어야 한다. 안 꺾이면
     카드 밖으로 나가 가로 스크롤이 생긴다 */
  overflow-wrap: anywhere;
  user-select: all;
  -webkit-user-select: all;
}
/* 네 글자 묶음. 간격을 <b>여백으로만</b> 준다 — 공백 문자를 넣으면 복사한 값에
   그대로 따라붙어, 공백을 무시하지 않는 인증 앱에서 붙여넣기가 조용히 실패한다.
   inline-block 이라 줄이 바뀌어도 묶음 가운데가 잘리지 않는다. */
.otp-key > span { display: inline-block; }
.otp-key > span + span { margin-left: .7em; }

/* 파일 고르기와 [올리기] 를 한 줄에 둔다.

   전에는 [파일 선택] 줄 → 안내문 → [올리기] 순서였다. 파일을 고르고 나면
   다음 할 일(올리기)이 안내문 아래에 있어, 시선이 한 번 끊긴 뒤에야 보였다
   (2026-08-21 제보). 고르는 일과 올리는 일은 사실 한 동작이므로 한 줄에 두고,
   버튼은 줄 오른쪽 끝 — 목록의 [받기]·[삭제] 와 같은 세로 축이다.

   파일 이름은 얼마든지 길 수 있다. 왼쪽이 줄어들게 두고(min-width:0) 버튼만
   고정이다 — 이 min-width 를 빠뜨리면 긴 이름이 줄을 밀어 가로 스크롤이 생긴다
   (이 프로젝트에서 반복해 겪은 그 사고다).
   버튼 높이는 --h-chip 으로, ::file-selector-button 과 같은 값이다. */
/* 메일 주소 — [아이디] @ [도메인] 한 줄.

   한 칸에 전부 치게 두지 않는 이유: 회사 메일이 대부분이라 뒷부분이 늘 같은데,
   매번 @ninesoft.biz 를 손으로 친다. 휴대폰에서는 @ 와 . 이 기호 자판에 있어
   자판을 두 번 바꿔야 한다.

   둘을 같은 비율(flex:1 1 0)로 두어 375px 화면에서도 나란히 선다. 도메인을
   더 넓게 잡으면 아이디 칸이 좁아져 긴 아이디가 잘려 보인다.

   직접 입력 칸은 이 줄에 끼우지 않고 아래에 따로 둔다 — 셋이 한 줄에 서면
   좁은 화면에서 각각이 손가락보다 작아진다. */
.email-row { display: flex; align-items: center; gap: var(--s-2); }
.email-row > input,
.email-row > select { flex: 1 1 0; min-width: 0; }
.email-row > .at { flex: 0 0 auto; color: var(--c-muted); }

.upload-row {
  display: flex;
  align-items: center;
  gap: var(--s-2);
}
.upload-row > input[type=file] { flex: 1 1 auto; min-width: 0; }
.upload-row > .btn { flex: 0 0 auto; }

/* 라벨 줄 + 보조 동작(계산하기 등) — 글자 밑선을 맞춘다. 가운데 정렬로 두면
   라벨의 위 여백(16)이 라벨 글자만 아래로 끌어 두 글자가 어긋난다(2026-08-25 지적). */
.label-row { display: flex; justify-content: space-between; align-items: baseline; gap: var(--s-2); }

label {
  display: block; margin: var(--s-4) 0 var(--s-1);
  font-weight: var(--fw-medium); font-size: var(--f-sm); color: var(--c-muted);
}

/* 표 안의 체크박스 — 손가락으로 누를 수 있는 크기로 키운다.
   기본 크기(13px)는 모바일에서 정확히 누르기 어렵다. */
.u-check {
  width: var(--s-5); height: var(--s-5); margin: 0; vertical-align: middle;
  accent-color: var(--c-primary);
}
.u-check:disabled { opacity: var(--o-disabled); cursor: not-allowed; }
.field-error { color: var(--c-danger); font-size: var(--f-sm); margin-top: var(--s-1); }

/* 잘못 입력된 칸은 테두리로도 표시한다. 다만 색만으로 알리지 않는다 —
   색을 구분하지 못하는 사용자에게는 아무 신호가 아니다. 그래서 칸 아래
   .field-error 문구가 함께 있어야 한다. */
input.is-invalid, select.is-invalid, textarea.is-invalid { border-color: var(--c-danger); }
input.is-invalid:focus-visible, select.is-invalid:focus-visible, textarea.is-invalid:focus-visible {
  outline-color: var(--c-danger);
  border-color: var(--c-danger);
}

/* 체크박스 묶음 — 기본 fieldset 테두리는 지우고 legend 만 라벨처럼 쓴다 */
/* 위 여백은 legend 가 아니라 fieldset 이 갖는다. legend 는 fieldset 의 테두리
   선 위에 얹히는 특별한 상자라 위쪽 마진이 앞 요소를 밀어내지 못한다 —
   legend 에 16 을 줬더니 앞 칸과의 간격이 0 이 되어, 롤 묶음의 이름이 바로 위
   셀렉트에 달라붙었다(다른 라벨은 모두 16). 그래서 fieldset 이 대신 띄운다. */
.u-fieldset { border: 0; padding: 0; margin: var(--s-4) 0 0; min-width: 0; }
.u-fieldset legend {
  font-weight: var(--fw-medium); font-size: var(--f-sm); color: var(--c-muted);
  margin: 0 0 var(--s-1); padding: 0;
}
/* 라벨 전체가 눌리는 영역이 된다. 체크박스만 노리게 하면 모바일에서 자주 빗나간다 */
.u-check-label {
  display: inline-flex; align-items: center; gap: var(--s-2);
  font-weight: var(--fw-normal); font-size: var(--f-base); color: var(--c-text);
  margin: 0; min-height: var(--h-tap); cursor: pointer;
}
.u-check-label:hover { color: var(--c-text); }
/* 항목이 여럿인 체크·라디오 묶음은 격자로 세운다.

   전에는 flex-wrap 이었다. 그러면 항목 폭이 글자 수를 따라가(57~100px) 줄마다
   둘째 칸이 121·136·150 처럼 다른 자리에서 시작한다. 스물한 개가 그렇게 쌓이면
   왼쪽 끝이 톱니처럼 보여 "무엇이 몇 개인지"가 한눈에 안 들어온다(사용자 지적,
   2026-08-21 — 실측으로 확인).

   auto-fill + minmax 라 열 수를 고정하지 않는다 — 좁은 창에서는 세 열, 넓어지면
   저절로 늘어나고, 어느 폭에서든 열은 정확히 맞는다.

   최소 폭 120 의 근거: 가장 긴 항목이 상자를 누르지 않고 들어가는 값이다.
   전에는 104 였는데, "시스템관리자"(글자 89 + 상자 20 + 간격 8 = 117)가 셀
   115.2 를 넘자 flex 가 줄일 수 있는 유일한 항목인 체크박스를 20 → 18.1px 로
   눌렀다. 그 한 칸만 상자가 작아 보인다는 지적이 실제로 나왔다(2026-08-21,
   브라우저 실측으로 확인). 컨트롤 크기가 옆 글자 길이에 따라 달라지는 것은
   어느 경우에도 옳지 않아, 아래 두 규칙으로 상자를 고정하고 최소 폭을
   117 위의 4의 배수(120)로 올렸다.
   행 간격 4 / 열 간격 8 — 세로로 더 촘촘해야 한 덩어리로 읽힌다. */
.check-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: var(--s-1) var(--s-2);
}
/* 격자 항목의 기본 min-width 는 내용 크기라, 긴 항목이 제 열을 밀어내면
   열 폭이 서로 달라진다 — 정렬을 지키려면 0 이어야 한다 */
.check-grid > .u-check-label { min-width: 0; }
/* 체크박스는 줄어들지 않는다. 손가락으로 누르는 크기가 옆 글자 길이에 따라
   달라지면 긴 이름의 항목일수록 누르기 어려워진다 — 정확히 거꾸로다. */
.u-check-label > .u-check { flex: 0 0 auto; }
/* 대신 글자가 줄어든다. 칸보다 긴 이름이 오면 상자를 누르는 대신 줄을 바꾼다.
   한글은 띄어쓰기가 없으면 안 꺾이므로 anywhere 가 필요하다. */
.check-grid > .u-check-label > span { min-width: 0; overflow-wrap: anywhere; }
/* 폼 버튼 줄은 앞 필드와 크게 띄운다(구획 사이 24) */
.form-actions { margin-top: var(--s-6); display: flex; gap: var(--s-2); }
/* 폼 버튼은 어느 폭에서든 가로를 나눠 꽉 채운다 — 모바일과 동일한 크기감 */
.form-actions .btn, .form-actions button { flex: 1; }

/* 떠 있는 등록 버튼(FAB) — 앱의 FloatingActionButton 과 같은 역할. 화면
   오른쪽 아래에 고정되어 목록이 길어도 대표 동작이 항상 손 닿는 곳에 있다.
   iOS 홈 바(safe-area)를 피하고, 흰 타일 위에서도 떠 보이게 층 3(모달과 같은
   급)을 쓴다 — 면 구분은 선이 원칙이지만 FAB 는 면이 아니라 떠 있는 것이다.
   글씨 + 는 아이콘 대용 — 이미지가 없어 어느 프론트에도 그대로 복사된다. */
/* 본문 흐름의 끝에 놓고 sticky 로 띄운다 — 내용이 화면보다 짧으면 내용 끝
   바로 아래에 놓이고(창 높이만큼 뚝 떨어지지 않는다), 내용이 길면 스크롤을
   따라 화면 하단에 붙는다. main 안에 있으므로 가로 위치도 본문 컬럼을
   저절로 따라간다(넓은 화면에서 뷰포트 구석으로 달아나지 않는다). */
.fab {
  position: sticky;
  bottom: calc(var(--s-5) + env(safe-area-inset-bottom, 0px));
  margin-left: auto;               /* 본문 컬럼의 오른쪽 끝 */
  margin-top: var(--s-4);
  width: var(--h-lg); height: var(--h-lg);
  padding: 0;
  display: flex; align-items: center; justify-content: center;
  /* 마크업의 + 글자가 폭 0 인 flex 항목으로 남는다 — 버튼 기본 gap 을 그대로
     두면 그 빈 항목 옆에 8px 가 붙어 아이콘이 원 중심에서 4px 비껴 놓인다 */
  gap: 0;
  border-radius: 50%;
  background: var(--c-primary); color: var(--c-on-primary);
  /* 마크업의 + 글자는 숨기고 선으로 직접 그린다 — 글자는 글꼴 메트릭에 따라
     중심이 미세하게 흔들려 원 안에서 살짝 비껴 보인다 */
  font-size: 0; line-height: 1;
  text-decoration: none;
  box-shadow: var(--shadow-3);
  z-index: 40;
}
.fab::before {
  content: '';
  width: var(--s-5); height: var(--s-5);
  background:
    linear-gradient(currentColor 0 0) center / var(--s-5) 2px no-repeat,
    linear-gradient(currentColor 0 0) center / 2px var(--s-5) no-repeat;
}
@media (hover: hover) {
  .fab:hover { background: var(--c-primary-dark); }
}
.fab:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 2px; }
/* sticky FAB 는 흐름 자리를 차지하므로 큰 하단 여백이 필요 없다 */
main.has-fab { padding-bottom: var(--s-4); }

/* 접이식 폼 구역 — 전부 선택 입력이라 평소엔 접어 두는 구역(부가 정보 등).
   summary 가 구역 제목을 겸한다. 위 구분선과 넉넉한 여백(구획 32)으로 앞
   구역과 분리해, 접혀 있어도 별도 구역임이 보인다. 표식(▸)은 회전만 하므로
   열림/닫힘에 폭이 변하지 않는다. */
.fold { margin-top: var(--s-8); border-top: 1px solid var(--c-border); }
.fold > summary {
  display: flex; align-items: center; gap: var(--s-2);
  min-height: var(--h-tap); padding: var(--s-2) 0;
  font-weight: var(--fw-bold); cursor: pointer; list-style: none;
}
.fold > summary::-webkit-details-marker { display: none; }
.fold > summary::before {
  content: '▸'; color: var(--c-muted);
  transition: transform var(--dur-base) var(--ease-out);
}
.fold[open] > summary::before { transform: rotate(90deg); }
.fold > summary .u-muted { font-weight: var(--fw-normal); }

/* 표 — 좁은 화면에서는 줄이지 않고 가로 스크롤한다.
   억지로 줄이면 칸이 한 글자씩 세로로 쪼개져 오히려 못 읽는다.
   (새 목록 화면은 표가 아니라 아래 .tile-list 를 쓴다 — 표는 남아 있는
   관리 화면용이다.) */
.table-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  margin: 0 calc(var(--s-4) * -1);   /* 카드 패딩과 짝 — 표가 카드 끝까지 닿는다 */
  padding: 0 var(--s-4);
}
/* collapse 를 쓰지 않는다. collapse 는 인접 셀의 경계선을 병합해 그리는데,
   모바일 세로 화면처럼 축소 스케일에서는 서브픽셀 반올림이 셀마다 달라져
   같은 행의 선이 열마다 다른 높이에 계단처럼 그려진다(가로 회전은 정상이라
   한참 헤맸다). separate + spacing 0 은 보이는 모습이 같고 병합 계산만 없다. */
table { width: 100%; border-collapse: separate; border-spacing: 0; min-width: 560px; }
/* 셀은 줄바꿈하지 않는다. 어차피 가로 스크롤이 있으므로, 좁은 화면에서 줄바꿈을
   허용하면 "시 스 템 관 리 자" 처럼 한 글자씩 세로로 쪼개져 오히려 못 읽는다.
   긴 설명만 .desc 로 예외를 둔다.
   패딩 12/8 → 행 높이 약 48(리듬의 comfortable 값). */
th, td {
  padding: var(--s-3) var(--s-2);
  border-bottom: 1px solid var(--c-border);
  text-align: left;
  /* 세로 가운데를 명시한다. 셀마다 배지·버튼·여러 줄 글이 섞이면 기본 정렬이
     baseline 기준으로 제각각 놓여, 행이 높아질 때 배지와 버튼의 선이 어긋난다. */
  vertical-align: middle;
  white-space: nowrap;
}
/* 머리글은 가운데 정렬한다. 본문은 왼쪽 그대로다 — 숫자·문장이 섞인 본문까지
   가운데로 모으면 행마다 시작점이 달라져 훑어 내려가기 어렵다. */
th {
  background: var(--c-subtle); font-weight: var(--fw-semibold); text-align: center;
  font-size: var(--f-sm); color: var(--c-muted);
}
/* 긴 설명은 목록에서 한 줄로 자른다(말줄임). 줄바꿈을 허용하면 그 행만 높아져
   행 리듬이 깨지고, 모바일에서는 폰트 부스팅과 겹쳐 구분선까지 어긋났다.
   전체 내용은 상세 화면에서 본다. */
td.desc {
  color: var(--c-muted); min-width: 180px; max-width: 260px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
td.actions { white-space: nowrap; }
td.actions > * + * { margin-left: var(--s-1); }

/* 알림 — 테두리는 상태색을 바탕에 섞어 만든다. 예전에는 #fecaca 처럼 라이트
   전용 파스텔이 박혀 있어 다크에서 테두리가 글자보다 밝게 빛났다(11.29:1). */
.alert {
  padding: var(--s-3) var(--s-4); border-radius: var(--r-md);
  margin-bottom: var(--s-4); border: 1px solid transparent;
}
.alert-error {
  background: var(--c-danger-bg); color: var(--c-danger);
  border-color: color-mix(in srgb, var(--c-danger) 35%, var(--c-danger-bg));
}
.alert-ok {
  background: var(--c-ok-bg); color: var(--c-ok);
  border-color: color-mix(in srgb, var(--c-ok) 35%, var(--c-ok-bg));
}
.alert-info { background: var(--c-subtle); color: var(--c-text); border-color: var(--c-border); }
.alert-warn {
  background: var(--c-warn-bg); color: var(--c-warn);
  border-color: color-mix(in srgb, var(--c-warn) 35%, var(--c-warn-bg));
}

/* 로고 탭 — 숨은 설정으로 들어가는 통로.
   버튼이지만 로고 그대로 보여야 하므로 기본 버튼 모양을 모두 걷어낸다.
   커서를 default 로 두는 것도 같은 이유다(눌러 보라는 신호를 주지 않는다). */
.brand-tap {
  display: block; margin: 0 auto; padding: 0;
  border: 0; background: none; box-shadow: none;
  cursor: default; min-height: 0;
  -webkit-tap-highlight-color: transparent;
}
.brand-tap:hover, .brand-tap:focus { background: none; box-shadow: none; }
.brand-tap:active { transform: none; }

/* 알약 라벨 3종(.badge / .badge-tap / .tree-count)은 같은 문법을 쓴다 —
   패딩 4/8, 반경 full. 전에는 3·1·1 / 10·6·7 로 제각각이라 같은 계열로
   보이지 않았다. 누를 수 있는 알약(칩)은 32 높이로 따로 구분한다. */
.badge-tap {
  display: inline-block; margin-left: var(--s-2); padding: var(--s-1) var(--s-2);
  border: 1px solid var(--c-border); border-radius: var(--r-full);
  background: var(--c-subtle); color: var(--c-muted);
  font-size: var(--f-xs); line-height: var(--lh-tight); letter-spacing: var(--ls-flat);
}

/* 설정 창.
   폭을 뷰포트 기준으로 잡는다 — 고정 px 로 두면 좁은 화면에서 창이 잘린다.
   내용이 길어질 수 있어 세로도 제한하고 안에서 스크롤시킨다.
   모달은 층 3 — 유일하게 "지금 이것만 다루라"는 뜻이라 깊이를 준다.
   전에는 그림자가 0 이라 배경 위에 종이 한 장이 그냥 놓인 모양이었다. */
.dialog {
  width: min(420px, calc(100vw - var(--s-8)));
  /* 휴대폰에서 100vh 는 **주소창·하단 바 뒤 영역까지** 센다. 창은 그 큰 높이를
     기준으로 잡히는데 화면 가운데 정렬이라, 실제로 보이는 영역보다 커진 만큼
     위아래가 브라우저 바에 잘린다 — 창 제목이 주소창에 먹혔다(사용자 지적,
     2026-08-21). 100dvh 는 지금 실제로 보이는 높이라 잘리지 않는다.
     100vh 줄을 위에 남겨 두는 이유: dvh 를 모르는 브라우저가 그 값을 쓴다. */
  max-height: calc(100vh - var(--s-12));
  max-height: calc(100dvh - var(--s-12));
  overflow-y: auto;
  /* 크기 조절(2026-08-27 사용자 지시) — 오른쪽 아래 모서리를 끈다.
     최소 크기를 두는 이유: 손잡이 줄보다 작게 줄이면 되돌릴 수 없다.
     닫으면 스크립트(fragments/layout 창 크롬)가 인라인 크기를 버린다. */
  resize: both;
  min-width: 240px;
  min-height: 140px;
  padding: var(--s-5);
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  background: var(--c-surface);
  color: var(--c-text);
  box-shadow: var(--shadow-3);
  /* 열림 250ms(ease-out) / 닫힘 150ms(ease-in) — 이탈은 진입보다 30% 짧게.
     사용자는 사라지는 것을 끝까지 지켜보지 않는다.
     옮기는 거리는 8px 이하다. 그보다 크면 '움직임'이 아니라 '이사'로 보인다.
     display·overlay 까지 전환 대상에 넣어야 닫힐 때도 애니메이션이 돈다
     (allow-discrete·@starting-style 을 모르는 브라우저는 그냥 즉시 뜬다 —
     지금과 같은 동작이라 위험이 없다). */
  opacity: 0;
  transform: translateY(8px) scale(.98);
  transition: opacity var(--dur-fast) var(--ease-in),
              transform var(--dur-fast) var(--ease-in),
              overlay var(--dur-fast) allow-discrete,
              display var(--dur-fast) allow-discrete;
}
.dialog[open] {
  opacity: 1;
  transform: none;
  transition-duration: var(--dur-slow);
  transition-timing-function: var(--ease-out);
}
@starting-style {
  .dialog[open] { opacity: 0; transform: translateY(8px) scale(.98); }
}
.dialog::backdrop {
  background: rgba(0, 0, 0, .45);
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease-in),
              overlay var(--dur-fast) allow-discrete,
              display var(--dur-fast) allow-discrete;
}
.dialog[open]::backdrop { opacity: 1; transition-duration: var(--dur-slow); }
@starting-style {
  .dialog[open]::backdrop { opacity: 0; }
}
.dialog h2 { margin: 0 0 var(--s-2); font-size: var(--f-lg); }

/* 창 맨 위의 그림. 창 폭을 꽉 채우되 비율은 원본 그대로 둔다(height:auto) —
   고정 높이를 주면 사람 얼굴이나 글자가 눌린다.
   창의 좌우 패딩만큼 음수 여백으로 넓혀 가장자리까지 닿게 하고, 위쪽 모서리는
   창의 둥근 모서리를 그대로 따른다 — 각진 사진이 둥근 창 밖으로 튀어나오면
   창이 두 겹으로 보인다.
   블록으로 두는 이유: img 는 기본이 인라인이라 아래에 글자 여백이 붙어
   그림과 제목 사이가 4px 쯤 더 벌어진다. */
/* 그림은 창을 열 때 붙는다(login.html 참고) — 그래서 자리를 미리 잡아 둔다.
   aspect-ratio 가 없으면 창이 열린 직후 높이 0 이었다가 그림이 도착하는 순간
   제목과 입력이 통째로 아래로 밀린다. 값은 원본 비율(760×512)이다. */
.dialog-mark {
  display: block; width: calc(100% + var(--s-5) * 2); height: auto;
  aspect-ratio: 760 / 512;
  margin: calc(var(--s-5) * -1) calc(var(--s-5) * -1) var(--s-4);
  border-radius: calc(var(--r-lg) - 1px) calc(var(--r-lg) - 1px) 0 0;
}

.dialog-meta {
  margin-top: var(--s-6);
  padding-top: var(--s-4);
  border-top: 1px solid var(--c-border);
}
.dialog-meta h3 { margin: 0 0 var(--s-2); font-size: var(--f-sm); color: var(--c-muted); }
/* 창 맨 아래 버튼 줄 — [기본값으로][닫기][저장] 이 한 줄에 선다.
   버튼들이 폭을 균등하게 나눠 갖는다. 위아래로 쌓으면 짝이라는 것이
   드러나지 않고 창이 세로로 길어진다. */
/* 동작 줄은 창 바닥에 붙어 따라다닌다.

   창이 길어지면(계정과목 21종처럼) 이 줄이 스크롤 끝으로 밀려난다. 그러면
   "고르고 → 끝까지 내려서 → 적용" 이 되고, 휴대폰에서는 마지막 줄이 브라우저
   하단 바에 가려 아예 안 보이기도 한다(사용자 지적, 2026-08-21).
   sticky 로 붙여 두면 어디까지 내렸든 [취소][적용]이 늘 손 닿는 곳에 있다.

   바닥에 붙는 순간 뒤 내용이 비쳐 보이므로 면을 깔고, 창의 좌우·아래 패딩만큼
   음수 여백으로 넓혀 바 전체가 내용을 가리게 한다. 위 선은 "여기부터 동작"이라는
   경계다 — 없으면 마지막 체크박스가 버튼 위에 떠 있는 것처럼 보인다.

   아래 패딩에 safe-area 를 더한다 — iOS 홈 바와 안드로이드 제스처 바 위로
   버튼을 올려야 눌린다(FAB 와 같은 처리). */
.dialog-actions {
  display: flex; gap: var(--s-2); margin-top: var(--s-6);
  position: sticky; bottom: calc(var(--s-5) * -1);
  margin-left: calc(var(--s-5) * -1); margin-right: calc(var(--s-5) * -1);
  padding: var(--s-3) var(--s-5) calc(var(--s-3) + env(safe-area-inset-bottom, 0px));
  background: var(--c-surface);
  border-top: 1px solid var(--c-border);
}
.dialog-actions .btn { flex: 1 1 0; min-width: 0; }

/* 라벨 폭을 고정해 값이 세로로 정렬되게 한다 */
.dialog-meta dl {
  display: grid; grid-template-columns: 56px 1fr; gap: var(--s-1) var(--s-3);
  margin: 0; font-size: var(--f-sm);
}
.dialog-meta dt { color: var(--c-muted); }
/* 주소·해시는 긴 한 덩어리라 break-all 이 없으면 창을 밀고 나간다 */
.dialog-meta dd { margin: 0; word-break: break-all; }

/* 배지 — 목록의 상태 표시. 색만으로 알리지 않는다(안에 항상 글자가 있다) */
.badge {
  display: inline-block; padding: var(--s-1) var(--s-2); border-radius: var(--r-full);
  font-size: var(--f-sm); font-weight: var(--fw-semibold); line-height: var(--lh-tight);
  white-space: nowrap; vertical-align: middle;
  /* nowrap 만으로는 부족하다 — flex 자식은 기본이 flex-shrink:1 이라 옆 칸이
     길면 배지가 눌려 글자가 잘린다. 배지는 폭이 곧 내용이므로 고정한다. */
  flex: 0 0 auto;
}
/* 상태 3쌍은 배경 50 / 글자 700 짝이라 전부 4.5:1 을 넘는다.
   예전 값은 ok 3.58 / danger 4.41 로 배지 크기에서 AA 미달이었다 */
.badge-ok { background: var(--c-ok-bg); color: var(--c-ok); }
.badge-muted { background: var(--c-subtle); color: var(--c-muted); }
.badge-danger { background: var(--c-danger-bg); color: var(--c-danger); }
.badge-warn { background: var(--c-warn-bg); color: var(--c-warn); }

/* 도구 모음 — 검색·등록.
   입력에 min-width:0 을 주는 이유: flex 자식은 기본 최소 폭이 내용 크기라
   좁은 화면에서 줄어들지 못하고 옆 요소를 밀어낸다. 실제로 검색어 칸이
   찌그러져 빈 칸처럼 보이는 일이 있었다. */
/* 도구 모음·동작 줄 — 모바일과 데스크톱이 완전히 같은 배치: 조건은 한 줄에
   하나씩 쌓이고 버튼은 가로를 꽉 채운다. 앱의 세로 스택 화면과 같은 구성이라
   화면 폭이 어떻든 손이 같은 자리를 찾는다. */
.toolbar { display: block; margin-bottom: var(--s-4); }
.toolbar form { display: grid; grid-template-columns: 1fr; gap: var(--s-2); }
.toolbar input, .toolbar select { min-width: 0; }
.toolbar form > button, .toolbar form > .btn { width: 100%; min-height: var(--h-input); }
.toolbar > .btn, .toolbar > button { display: flex; width: 100%; }
/* 간격은 <b>모든</b> 직계 자식에게 준다. 전에는 .btn 과 button 에만 주어,
   [삭제] 처럼 폼에 싸인 버튼만 위 버튼에 딱 붙어 한 덩어리로 보였다
   (2026-08-22 제보 — 문의 상세의 [수정]과 [삭제]). 폼은 마크업 사정이지
   사용자가 보는 것은 나란히 선 버튼 셋이다. */
.toolbar > .btn, .toolbar > button, .toolbar > form { margin-top: var(--s-2); }
.toolbar > :first-child { margin-top: 0; }

/* 기간 입력 — 시작·종료는 늘 나란히 둔다. 위아래로 떨어지면 한 쌍이라는 것이
   드러나지 않아 어느 쪽이 시작인지 매번 눈으로 확인하게 된다. */
.date-range { display: flex; gap: var(--s-2); align-items: center; min-width: 0; }
.date-range > input { flex: 1 1 0; min-width: 0; }
.date-range .tilde { flex: 0 0 auto; color: var(--c-muted); }

/* 사용내역 상세 — 앱 상세와 같은 구성(영수증형).
   금액이 화면에서 제일 눈에 띄어야 하므로 상호(20)보다 한 단계 큰 24 를 쓴다.
   예전 28px 은 어느 토큰에도 없는 값이었다. */
.detail-head { display: flex; align-items: center; gap: var(--s-2); }
.detail-head .merchant {
  font-size: var(--f-lg); font-weight: var(--fw-semibold); min-width: 0;
  line-height: var(--lh-head); letter-spacing: var(--ls-head);
  overflow-wrap: anywhere;
}
/* 숫자를 줄맞춤한다. 위 토큰 구획의 규칙에 이 이름을 적어 두었었는데, 그러면
   범용 규칙이 업무 선택자를 알게 되고 업무 CSS 를 지워도 이름만 남는다. */
.detail-amount {
  font-size: var(--f-xl); font-weight: var(--fw-bold); margin-top: var(--s-1);
  line-height: var(--lh-head); letter-spacing: var(--ls-head);
  font-variant-numeric: tabular-nums;
}
.detail-amount.canceled { text-decoration: line-through; color: var(--c-muted); }
.detail-sep { border: 0; border-top: 1px solid var(--c-border); margin: var(--s-5) 0; }
/* 상세 본문 면(2026-08-28 사용자 지시) — 글이 어느 영역인지 면으로 말한다.
   메타 줄과 본문을 선으로만 가르면 짧은 글일수록 한 덩어리로 읽힌다(제보).
   은은한 배경 + 모서리 8(카드 12 안이라 더 작게) + 패딩 16. 줄바꿈 보존까지
   이 클래스가 맡는다 — 본문에 u-prewrap 을 따로 붙이지 않는다. */
.detail-body { background: var(--c-subtle); border-radius: var(--r-sm);
  padding: var(--s-4); margin-top: var(--s-4);
  white-space: pre-wrap; overflow-wrap: anywhere; }
.detail-rows {
  display: grid; grid-template-columns: 84px 1fr; gap: var(--s-2) var(--s-3); margin: 0;
}
.detail-rows dt { color: var(--c-muted); font-size: var(--f-sm); }
.detail-rows dd { margin: 0; overflow-wrap: anywhere; }

/* 증빙 파일 리스트 — 이름(보기 링크)·받기·삭제가 한 줄이다 */
.file-list { display: flex; flex-direction: column; gap: var(--s-2); margin-bottom: var(--s-5); }
.file-item {
  display: flex;
  /* 위 맞춤이다. 가운데로 두면 이름이 두 줄인 파일에서만 버튼이 아래로
     내려가, 목록의 버튼들이 계단처럼 어긋난다(2026-08-23 제보).
     이름은 길이가 제각각이고 버튼은 늘 한 줄이라, 기준은 첫 줄이어야 한다. */
  align-items: flex-start;
  gap: var(--s-2);
  padding: var(--s-3);
  min-height: var(--h-tap);
  border: 1px solid var(--c-border); border-radius: var(--r-sm);
  transition: background-color var(--dur-instant) linear;
}
@media (hover: hover) { .file-item:hover { background: var(--c-subtle); } }
.file-item .file-main { flex: 1 1 auto; min-width: 0; display: flex;
  flex-direction: column; text-decoration: none; color: inherit; }
/* 이름 칸은 .file-main 로 감싼 것도 있고 <a> 하나로 끝나는 것도 있다
   (증빙은 앞것, 문의 첨부는 뒷것). 어느 쪽이든 같은 줄에 서므로 같은 규칙을
   받아야 한다 — 뒷것에 이 규칙이 없어서 긴 파일명이 [삭제] 를 카드 밖으로
   밀어냈다(2026-08-22 제보).

   :not(.btn) 이 붙은 이유: [받기] 도 <a> 라서 이 규칙을 함께 받아 남는 폭을
   먹었다. 그래서 이름이 짧은 줄에서는 버튼이 넓고 긴 줄에서는 좁아, 같은
   버튼이 파일마다 다른 크기로 보였다. */
.file-item > a:not(.btn) { flex: 1 1 auto; min-width: 0; }
.file-item .name, .file-item > a:not(.btn) { overflow-wrap: anywhere; color: var(--c-text);
  text-decoration: underline; text-underline-offset: 3px; }
/* 용량과 [받기]·[삭제] 는 줄어들지도 늘어나지도 않는다. 폭이 곧 뜻인 것들이라
   눌리면 "3.1 MB" 가 두 줄로 꺾이고 버튼 글자가 잘린다. */
.file-item > .u-small { flex: 0 0 auto; white-space: nowrap; }
.file-item > .btn, .file-item > button { flex: 0 0 auto; }
.file-item form { margin: 0; flex: 0 0 auto; }
/* 이미지 증빙은 그림을 바로 보인다(2026-08-25 사용자 지시) — 카메라가 지은
   숫자 파일명만으로는 무엇인지 알 수 없다. 누르면 기존 미리보기 창이 커진
   그림을 연다. 96 은 컨트롤 리듬 계단이 아니라 콘텐츠 그림 크기다
   (영수증 캔버스 420 과 같은 갈래) — 영수증 글씨가 알아볼 만한 최소 폭이다. */
.file-main.has-thumb { flex-direction: row; align-items: center; gap: var(--s-3); }
.file-thumb { width: 96px; height: 96px; object-fit: cover; flex: 0 0 auto;
  border: 1px solid var(--c-border); border-radius: var(--r-sm); background: var(--c-subtle); }
.file-main .file-text { display: flex; flex-direction: column; min-width: 0; }
/* 썸네일 항목만 줄바꿈을 허용한다 — 아주 좁은 폰(360px)에서는 96px 그림과
   버튼 셋이 한 줄에 못 서고, wrap 이 없으면 그림이 버튼 밑으로 파고든다.
   좁으면 버튼 줄이 그림 아래로 내려앉는 것이 올바른 모양이다. */
.file-item.has-thumb-item { flex-wrap: wrap; }

/* 계산하기(간단 계산기) 다이얼로그 — 금액 입력 옆 [계산하기]가 연다
   (fragments/calc.html). 숫자는 tabular 로 자리를 맞추고, 키는 손가락
   목표물 높이(--h-tap)다. 식 줄이 길어지면 안에서 꺾인다. */
.calc-expr { min-height: var(--s-5); text-align: right; color: var(--c-muted);
  font-size: var(--f-sm); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
.calc-result { text-align: right; font-size: var(--f-2xl); font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums; margin-bottom: var(--s-3); overflow-wrap: anywhere; }
.calc-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--s-2); }
.calc-grid .btn { height: var(--h-tap); padding: 0; font-size: var(--f-md); }

/* 목록 위 요약 차트 (2026-08-25) — 라이브러리 없이 SVG. 좌표는 서버가 계산한다.
   막대는 브랜드 면(--c-primary), 파이 조각·범례 점은 홈 카드와 같은 색조
   사전(.tint-* 의 --card-tint)을 그대로 쓴다 — 차트만의 새 색을 만들지 않는다. */
/* 통계 접힘 — .fold 의 위 32 마진은 실험실(맨 아래)용이라 여기선 조인다 */
.chart-fold { margin-top: 0; margin-bottom: var(--s-4);
  border-bottom: 1px solid var(--c-border); }
.chart-fold > .chart-row { margin-bottom: 0; padding-top: var(--s-2); }
/* 카드 탭의 전폭 패널(부서 차트)도 펼친 제목과 같은 숨을 쉰다(2026-08-28) */
.chart-fold > .chart-panel { margin-top: var(--s-2); }

.chart-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-2); margin-bottom: var(--s-4); }
/* 높이를 못 박는다(2026-08-25 사용자 지시) — 범례 다섯 줄 + 제목이 기준이고,
   왼쪽 막대는 남는 높이를 받아 바닥에 붙는다. 데이터에 따라 카드가 늘었다
   줄었다 하면 그 아래 목록이 매번 다른 자리에서 시작한다. */
.chart-panel { border: 1px solid var(--c-border); border-radius: var(--r-md);
  padding: var(--s-3); min-width: 0; display: flex; flex-direction: column;
  height: 196px; }
/* 패널 자체가 [전체] 창의 열쇠다(2026-08-25) — 버튼 한 줄을 아끼고 그림 전체가
   과녁이 된다. 누를 수 있다는 신호는 제목 옆 표식(›)과 손 모양·hover 면이다. */
.chart-panel.is-clickable { cursor: pointer; }
@media (hover: hover) { .chart-panel.is-clickable:hover { background: var(--c-subtle); } }
.chart-panel.is-clickable:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 2px; }
.chart-more { color: var(--c-muted); font-size: var(--f-md); line-height: 1; }
.chart-title { font-size: var(--f-sm); color: var(--c-muted);
  font-weight: var(--fw-medium); margin-bottom: var(--s-2); }
/* 두 패널은 격자가 같은 높이로 늘린다. 막대는 남는 높이를 받아 바닥에 붙는다
   (preserveAspectRatio 는 마크업에 있다) — 위에 여백이 남는 편이 자연스럽다 */
.chart-bars { width: 100%; height: auto; display: block; flex: 1 1 auto; min-height: 0; }
.chart-bars rect { fill: var(--c-primary); }
/* 어두운 테마의 --c-primary 는 배경과 거의 같은 면색이라(대비 1.3:1) 막대가
   사라진다 — 버튼은 흰 글자·테두리가 형태를 만들지만 막대는 면 자체가 데이터다.
   그 테마들에서는 이미 검증된 강조색으로 그린다. */
html[data-theme="dark"] .chart-bars rect,
html[data-theme="midnight"] .chart-bars rect { fill: var(--c-accent); }
/* SVG 안 글자는 viewBox(216) 기준이라 화면에서는 패널 폭에 맞춰 함께 준다 */
.chart-bars .val { font-size: var(--f-xs); fill: var(--c-muted); }
.chart-bars .lbl { font-size: var(--f-xs); fill: var(--c-muted); }
/* 파이가 위, 범례가 아래(2026-08-25 사용자 지시). 파이는 남는 높이를 받아
   정원으로 서고, 범례는 상위 셋 + "외 N개" 네 줄로 폭을 다 쓴다. */
.chart-donut-wrap { display: flex; flex-direction: column; gap: var(--s-1);
  align-items: center; min-width: 0; flex: 1 1 auto; min-height: 0; }
.chart-donut { flex: 1 1 auto; min-height: 0; width: auto; max-width: 100%;
  height: auto; aspect-ratio: 1; }
/* 채운 파이 — r10 에 굵기 20 이면 중심까지 찬다(pathLength=100 정규화) */
.chart-slice { fill: none; stroke: var(--card-tint, var(--c-border)); stroke-width: 20; }
/* 차트 패널의 작은 선택(부서 인원별 파이, 2026-08-28) — 입력 기본(48px)은
   차트 패널(196px) 안에서 파이를 잡아먹는다. 칩 높이(--h-chip 32)로 줄여
   제목 아래 한 줄로 둔다(제목 옆은 좁은 화면에서 제목이 꺾여 파이가 짜부라진다).
   모서리 4는 높이 32의 1/4 이하 규칙을 지킨 값이다 */
.chart-select { width: auto; max-width: 100%; min-height: var(--h-chip);
  height: var(--h-chip); padding: 0 var(--s-2); font-size: var(--f-sm);
  border-radius: var(--r-xs); margin-bottom: var(--s-2); }
/* 부서 인원별 파이 판 — 선택이 hidden 만 바꾼다. display:flex 명시가 hidden 을
   덮는 함정을 여기서 막는다(.chart-period 에서 배운 처방) */
.dept-user-pie[hidden] { display: none; }
/* 전폭 큰 차트 패널(부서 차트, 2026-08-28) — 창의 큰 파이 구성을 화면에
   그대로 세운다. 2열 패널의 고정 높이(196)는 내용이 정해진 작은 판의 값이라
   여기서는 내용이 높이를 정한다 */
.chart-panel.is-tall { height: auto; }
/* 부서별 파이의 목록 줄은 누르면 아래 인원별 차트가 바뀐다 — 누를 수 있음을
   손가락 모양으로 말한다(조각과 같은 신호) */
.chart-panel.is-tall .chart-full li { cursor: pointer; }
/* basis 를 고정값으로 주면 그 값만으로 줄바꿈이 판정돼, 자리가 있는데도
   범례가 아래로 내려간다. auto 로 두면 내용 최소치(점+이름 3글자+퍼센트)까지
   줄어들며 한 줄을 지키고, 그마저 못 담는 폭(글꼴 배율이 큰 기기)에서만
   위 flex-wrap 이 발동한다 — 평소 높이는 그대로, 넘침은 없다. */
.chart-legend { display: flex; flex-direction: column; gap: 2px;
  flex: none; width: 100%; min-width: 0; margin: 0; padding: 0; font-size: var(--f-xs); }
.chart-legend li { display: flex; align-items: center; gap: var(--s-1); list-style: none; min-width: 0; }
.chart-legend .dot { width: 8px; height: 8px; border-radius: var(--r-full);
  background: var(--card-tint, var(--c-border)); flex: none; }
/* 이름만 줄어든다 — 퍼센트는 폭이 곧 뜻이라 눌리면 안 된다(파일 목록의
   용량 칸과 같은 처방) */
/* 이름은 적어도 세 글자는 보인다(2026-08-25 지적) — "세…" 두 글자로는 어느
   계정인지 알 수 없다. 3.8em = 세 글자(36px) + 말줄임(9px) 실측값이고,
   그만큼의 자리는 도넛이 먼저 줄어 마련한다(위 min-width 40). */
/* 세로 배치라 이름이 폭을 다 쓴다 — 그래도 넘치면 말줄임. 퍼센트는 오른쪽
   끝에 붙고 눌리지 않는다(폭이 곧 뜻인 값). */
.chart-legend .name { flex: 1 1 auto; min-width: 0; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; }
.chart-legend li > .u-muted { flex: none; margin-left: auto; padding-left: var(--s-1); }

/* 창 안의 큰 차트 — 화면의 작은 그림과 같은 색·같은 조각이다. 창은 폭을 다
   쓰므로 도넛을 키우고(140) 막대는 열두 칸을 편다. flex 자식이 아니므로
   패널용 flex 규칙(위 .chart-bars)의 영향을 받지 않게 크기를 다시 준다. */
/* 라벨·지시선을 도넛 위에 겹쳐 놓는 상자 — 도넛과 같은 정사각이어야
   viewBox 좌표×배율이 곧 화면 좌표가 된다. 라벨은 상자를 넘어도 보인다 */
.chart-donut-lg-wrap { position: relative; width: 180px; margin: var(--s-3) auto 0; }
/* 조각을 누르면 뜨는 안내 — 2초 뒤 사라진다(스크립트). 자리를 미리 잡아 두지
   않는 이유: 늘 비워 두면 도넛과 목록 사이가 그만큼 떠 보인다 */
/* 조각을 가리키는 지시선과 라벨 — 도넛 위에 겹쳐 놓는다.
   라벨은 조각 방향 바깥쪽에 서고, 상자를 벗어나지 않게 가장자리에서 멈춘다. */
.chart-leader { position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none; overflow: visible; }
.leader-line { stroke: var(--c-text); stroke-width: 0.5; }
.leader-head { fill: var(--c-text); }
.chart-callout { position: absolute; margin: 0; white-space: nowrap;
  font-size: var(--f-sm); font-weight: var(--fw-semibold);
  background: var(--c-surface); border: 1px solid var(--c-border);
  border-radius: var(--r-sm); padding: var(--s-1) var(--s-2);
  pointer-events: none; }
.slice-hit { cursor: pointer; }
/* 고른 조각은 반지름이 살짝 자라 도드라진다 */
.chart-donut-lg .slice-hit.is-on { stroke-width: 21; }
/* 창의 기간 프리셋 — 여기서는 통계가 주인공이라 라디오를 조인다(2026-08-25
   지적: 기본 탭 타깃 44 가 줄마다 먹혀 여덟 개가 도넛을 화면 밖으로 밀었다).
   높이는 칩(32), 열은 96 부터 — 폰에서 3열×3줄이면 절반 높이로 준다. */
.chart-presets { margin: var(--s-2) 0 0; padding: 0; border: 0; }
.chart-presets .check-grid {
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 0 var(--s-2);
}
.chart-presets .u-check-label { font-size: var(--f-sm); min-height: var(--h-chip); }
.chart-donut-lg { display: block; flex: none; width: 180px; min-width: 0; }
/* 창의 큰 차트는 채운 파이 — 굵기 20 이 반지름 10 을 중심까지 채운다 */
.chart-donut-lg .chart-slice { stroke-width: 20; }
.chart-bars-lg { margin-top: var(--s-3); flex: none; }

/* 창의 기간 프리셋 — 조건 칩과 같은 어휘(.chip-link)를 쓰되 창 안이라
   조금 더 조인다. 넷이 한 줄에 서고, 좁으면 그 줄만 접힌다. */
.chart-chips { margin-top: var(--s-2); gap: var(--s-1); }
.chart-chips .chip-link { min-width: 0; padding: 0 var(--s-3); height: var(--h-chip); }

/* 모달이 열려 있으면 뒤 페이지는 멈춘다 — 배경이 잡혀 움직이면 창이 아니라
   페이지를 만지는 느낌이 된다(2026-08-25 지적). 스크롤은 창 안에서만 구른다 */
body:has(dialog[open]) { overflow: hidden; }
dialog { overscroll-behavior: contain; }

/* 차트 창은 내용이 화면보다 길 수 있다(라디오+차트+목록) — 창 안에서
   구르게 한다. 안 그러면 아래가 잘린 채 닫기도 못 누른다(2026-08-25 지적) */
#chart-dialog, #month-dialog { max-height: 85vh; max-height: 85dvh; overflow-y: auto; }

/* 창 제목 줄 — 제목 왼쪽, 연도 선택 오른쪽. 세로 가운데로 나란히 */
.dialog-head { display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-2); }
/* 제목 글줄과 연도 버튼 테두리가 같은 높이(28)다(2026-08-25 지적) */
.dialog-head h2 { margin: 0; line-height: 28px; }
.chart-year-form { display: flex; flex: 0 0 auto; }
/* 전역 select(풀폭·48 높이)를 이 자리에 맞게 좁힌다 — 제목과 같은 글자 크기,
   화살표 여백만큼만 오른쪽 패딩 */
.chart-year-form select { width: auto; min-height: 0; height: 28px;
  padding: 0 var(--s-6) 0 var(--s-2); font-size: var(--f-sm);
  border-radius: var(--r-sm); background-position: right var(--s-2) center; }

/* 지금 달 — 열두 칸 중 오늘이 속한 달이 한눈에 보인다 */
/* 강조는 색이 아니라 진하기다 — 강조색이 테마에 따라 본색보다 연해 현재
   달이 오히려 흐려 보였다(2026-08-25 지적). 다른 달을 반투명으로 눕히고
   지금 달(과 클릭한 달)만 온전한 농도로 세운다. 화면·창 공통이다 —
   둘 다 올해 1~12월을 펴므로 같은 문법이어야 한다. */
.chart-bars rect { opacity: .4; }
.chart-bars rect.is-now, .chart-bars rect.is-on { opacity: 1; }
.chart-bars .lbl.is-now { font-weight: var(--fw-bold); fill: var(--c-text); }
/* 달을 골랐으면 고른 달이 유일한 강조다(2026-08-25 사용자 지시) — 현재월
   표시는 잠시 내리고, 선택을 풀면 되돌아온다. 고른 달의 라벨도 진하게. */
.chart-bars .lbl.is-on { font-weight: var(--fw-bold); fill: var(--c-text); }
.chart-bars:has(rect.is-on) rect.is-now:not(.is-on) { opacity: .4; }
.chart-bars:has(rect.is-on) .lbl.is-now:not(.is-on) {
  font-weight: var(--fw-normal); fill: var(--c-muted); }

/* 창의 기간 검색 — 보조 도구다. 본문 입력(48)과 같은 크기로 두면 차트보다
   먼저 눈에 들어온다(2026-08-25 지적). 칩 높이(32)·작은 글자로 낮춘다.
   날짜 둘과 조회가 한 줄, 좁으면 접힌다. */
.chart-period { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-1);
  margin-top: var(--s-2); }
/* display: flex 명시가 hidden 의 기본 display:none 을 덮는다 — 속성만으로
   숨었다고 믿으면 안 된다(2026-08-25 실측으로 배움) */
.chart-period[hidden] { display: none; }

/* ── 창 이동 손잡이 (.dlg-drag) ──────────────────────────────────────────
   모든 dialog 첫 줄에 스크립트가 끼우는 여섯 점 손잡이(2026-08-27 사용자
   지시 — 홈 카드의 끌기 손잡이와 같은 어휘). 줄 전체가 끌기 표적이다 —
   점만 노리게 하면 폰에서 못 잡는다. 음수 여백으로 창 패딩을 물려 창의
   맨 위 가장자리에 붙인다. */
.dlg-drag {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  height: 24px;
  margin: calc(-1 * var(--s-5)) calc(-1 * var(--s-5)) 0;
  padding: 0 var(--s-2);
  color: var(--c-muted);
  cursor: move;
  touch-action: none;              /* 끌기가 화면 스크롤로 새지 않게 */
  user-select: none;
  -webkit-user-select: none;
}

/* ── 기간 설정 시트 (.period-sheet) ─────────────────────────────────────
   연·월·일을 휠로 고르는 창(2026-08-27 사용자 지시 — 증권앱 관례).
   네이티브 date 는 브라우저마다 다른 달력을 띄워 웹=모바일 동일이 깨졌다.
   화면 가운데 정렬(2026-08-27 지시 — 처음엔 바닥 시트였다): dialog 기본
   여백(auto)이 가운데를 잡아 주므로 위치 규칙을 더하지 않는다.
   휠 칸 높이 40 은 JS(appPeriodSheet 의 ITEM)와 같은 값이어야 한다 —
   어긋나면 스냅 자리와 판정 인덱스가 갈라진다. 40 인 이유: 5줄 × 두 그룹이
   작은 폰(667px)에도 통째로 들어와야 시트가 안에서 스크롤되며 종료일 그룹만
   잘려 보이는 일(2026-08-27 제보 — 두 그룹 높이가 달라 보였다)이 없다. */
.period-sheet {
  width: min(480px, calc(100vw - var(--s-8)));
  border-radius: var(--r-lg);
}
.period-head { display: flex; align-items: center; justify-content: space-between; }
.period-head .period-x { min-width: 44px; }
/* 위쪽 요약 — 휠을 돌리는 동안 실시간으로 따라온다 */
.period-values { display: flex; gap: var(--s-6); margin: var(--s-2) 0 var(--s-4); }
.period-values > div { display: flex; flex-direction: column; gap: 2px; }
.period-values strong { font-size: var(--f-lg); font-variant-numeric: tabular-nums; }
.period-row { display: grid; grid-template-columns: 56px 1fr; align-items: center; }
.period-label { position: relative; align-self: stretch; display: flex; align-items: center;
  font-weight: var(--fw-medium); }
/* 연초 칩 — 선택 줄(가운데)보다 두 줄 아래 라인에 수직 정렬(참조 이미지).
   라벨 열을 휠 높이만큼 늘여(stretch) 그 안에서 절대 배치한다. */
.period-label .period-yearstart {
  position: absolute;
  left: 0;
  top: calc(50% + 80px);          /* 두 줄 아래 = 2 × 칸 높이 40 */
  transform: translateY(-50%);
  /* chip-link 를 <button> 에 얹으면 버튼 기본 채움색이 이긴다 — 칩 얼굴로 되돌림 */
  background: var(--c-surface);
  color: var(--c-text);
  font-weight: var(--fw-normal);
  /* 라벨 열(56px) 안에 들어와야 한다(2026-08-27 지시) — 칩 공통 min-width 를
     풀고 여백을 줄인다. 두 글자 칩이라 폭 걱정은 없다 */
  min-width: 0;
  max-width: 56px;
  padding: 0 var(--s-2);
}
.wheels { display: grid; grid-template-columns: repeat(3, 1fr); position: relative; }
/* 가운데 줄 표시 — 세 휠을 가로지르는 얇은 두 선. 선택 자리가 눈에 고정된다 */
.wheels::before {
  content: ""; position: absolute; left: 0; right: 0; top: 50%;
  height: 40px; transform: translateY(-50%);
  border-top: 1px solid var(--c-border); border-bottom: 1px solid var(--c-border);
  pointer-events: none;
}
.wheel {
  height: 200px;                          /* 5칸 — 가운데 1 + 위아래 2 */
  overflow-y: auto;
  scroll-snap-type: y mandatory;
  text-align: center;
  scrollbar-width: none;
  /* 위아래로 갈수록 흐려진다 — 이웃 값이 보이되 무게는 가운데에 */
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 30%, #000 70%, transparent);
  mask-image: linear-gradient(to bottom, transparent, #000 30%, #000 70%, transparent);
}
.wheel::-webkit-scrollbar { display: none; }
/* 첫·끝 값도 가운데에 설 수 있게 위아래 빈 칸(2칸) — JS 가 만든다 */
.wheel .wheel-pad { height: 80px; }
.wheel .wheel-item {
  height: 40px; line-height: 40px;
  scroll-snap-align: center;
  color: var(--c-muted);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  /* 절대 접히지 않는다 — 칸 높이가 40 을 넘는 순간 스냅 자리와 판정
     인덱스(scrollTop/40)가 갈라진다. 아주 좁은 화면에서는 잘리는 쪽이 낫다 */
  white-space: nowrap;
}
.wheel .wheel-item.on { color: var(--c-text); font-weight: var(--fw-bold); font-size: var(--f-lg); }
.period-actions { display: flex; gap: var(--s-2); }
.period-actions .btn, .period-actions .btn-plain { flex: 1 1 0; }
/* 좁은 화면에서 날짜가 두 줄로 접히지 않게 */
.period-values strong { white-space: nowrap; }
/* 고급 창의 기간 요약 버튼 — 시트가 고른 기간을 보여 주고, 누르면 다시 연다.
   pick-face(입력 모양)를 빌려 쓰므로 display:block 이 hidden 을 덮는 함정을
   여기서 막는다(.chart-period 와 같은 처방). */
.period-open[hidden] { display: none; }
.chart-period input[type="date"] { flex: 1 1 7em; min-width: 0;
  height: var(--h-chip); min-height: 0; padding: 0 var(--s-2);
  font-size: var(--f-sm); border-radius: var(--r-sm); }
.chart-period .btn { height: var(--h-chip); min-height: 0; padding: 0 var(--s-3); }
.chart-period .btn::after { content: none; }   /* btn-sm 의 투명 확장 — 줄 높이를 키운다 */

/* 막대를 누르면 그 달을 강조 — 막대와 아래 목록 줄이 같은 신호를 낸다 */
.chart-bars rect.bar { cursor: pointer; }
.chart-bars rect.bar.is-on { fill: var(--c-accent); }
.chart-full li.is-on { background: var(--c-subtle); border-radius: var(--r-sm);
  outline: 2px solid var(--c-focus); outline-offset: 2px; }

/* 계정과목 전체 창 — 이름은 줄고 금액·비율은 안 눌린다(폭이 곧 뜻인 값).
   숫자는 tabular 로 자리를 맞춰 세로로 읽힌다. */
.chart-full { display: flex; flex-direction: column; gap: var(--s-2);
  margin: var(--s-3) 0 0; padding: 0; }
.chart-full li { display: flex; align-items: center; gap: var(--s-2);
  list-style: none; min-width: 0; font-size: var(--f-sm); }
.chart-full .dot { width: 10px; height: 10px; border-radius: var(--r-full);
  background: var(--card-tint, var(--c-border)); flex: none; }
.chart-full .name { flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chart-full .amount { flex: none; font-variant-numeric: tabular-nums; }
.chart-full .pct { flex: none; min-width: 3em; text-align: right;
  font-variant-numeric: tabular-nums; }

/* 사용내역 카드 리스트 — 앱의 목록 타일과 같은 구성이다.
   웹과 앱이 같은 정보를 다른 모양으로 보여 주면 사용자가 두 화면을 다시 배운다. */
/* 필수 입력 표시 — 라벨 옆 별표 */
.req { color: var(--c-danger); }

/* 비밀번호 보기 토글 — 입력 오른쪽 끝의 눈 아이콘. 마스킹은 어깨너머를 막지만
   본인 확인도 막는다 — 누르는 동안이 아니라 토글로 유지되어야 길게 확인할 수 있다 */
.pw-wrap { position: relative; }
.pw-wrap input { padding-right: var(--h-tap); width: 100%; }
.pw-toggle {
  position: absolute; right: 2px; top: 50%; transform: translateY(-50%);
  background: none; border: 0; padding: var(--s-2) var(--s-3); min-height: 0;
  color: var(--c-muted); cursor: pointer; line-height: 1;
}
.pw-toggle:hover { background: none; color: var(--c-text); }
.pw-toggle:active { transform: translateY(-50%) scale(.94); }

/* 콤보박스 — 셀렉트처럼 목록이 열리고, 입력하면 즉시 좁혀지는 선택 상자 */
.combo { position: relative; }
/* 떠 있는 목록 두 종(.combo-list · .pick-list)은 같은 층·같은 모서리·같은
   테두리를 쓴다. 전에는 하나는 그림자 없이 8px, 다른 하나는 그림자에 6px 라
   같은 일을 하는 두 목록이 서로 다른 물건처럼 보였다. */
.combo-list, .pick-list {
  position: absolute; top: 100%; left: 0; right: 0; z-index: 30;
  max-height: 320px; overflow-y: auto;
  margin: var(--s-1) 0 0; padding: var(--s-1); list-style: none;
  background: var(--c-surface); border: 1px solid var(--c-border);
  border-radius: var(--r-md); box-shadow: var(--shadow-2);
  animation: pop-in var(--dur-base) var(--ease-out);
}
@keyframes pop-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}
.combo-list li, .pick-list li {
  padding: var(--s-2) var(--s-3); border-radius: var(--r-sm); cursor: pointer;
  transition: background-color var(--dur-instant) linear;
}
/* 한 줄짜리 항목은 손가락 목표물(44)까지 키운다. 여러 줄인 pick 항목은
   내용만으로 이미 그보다 높다 */
.combo-list li { display: flex; align-items: center; min-height: var(--h-tap); }
/* .active 는 고른 상태다 — 손가락에서도 남아야 하므로 hover 와 갈라 둔다.
   함께 묶어 두면 손가락 기기에서 선택 표시까지 사라진다. */
.combo-list li.active { background: var(--c-subtle); }
@media (hover: hover) {
  .combo-list li:hover, .pick-list li:hover { background: var(--c-subtle); }
}
.combo-list li:active, .pick-list li:active { background: var(--c-border); }
.combo-list .combo-none { color: var(--c-muted); cursor: default; }
.combo-list .combo-none:hover { background: none; }

/* 영수증 등록 — 읽기 버튼·미리보기·근거 강조 */
.receipt-pick { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap; }
/* 파일 이름은 남은 폭 안에서 말줄임 — 긴 이름·긴 버튼 문구("읽는 중…")가
   화면을 옆으로 밀고 나가지 않는다 */
.receipt-pick .btn, .receipt-pick button { max-width: 100%; }
.receipt-pick span {
  flex: 1 1 120px; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.receipt-canvas { position: relative; display: inline-block; max-width: 100%; }
.receipt-canvas img { max-width: 100%; max-height: 420px; display: block;
  border: 1px solid var(--c-border); border-radius: var(--r-md); }
/* 근거 위치 표시 — 색은 테마의 danger 를 따른다(전에는 라이트 빨강이 박혀 있어
   다크에서 배경색과 무관한 얼룩이 됐다) */
.receipt-bbox { position: absolute; border: 2px solid var(--c-danger);
  background: color-mix(in srgb, var(--c-danger) 14%, transparent);
  border-radius: var(--r-xs); pointer-events: none; }
.evidence { display: flex; flex-wrap: wrap; gap: var(--s-2); align-items: center;
  margin-top: var(--s-1); }

/* 첨부 파일 목록 — 이름은 말줄임(긴 촬영 파일명이 화면을 밀지 않게),
   이름을 누르면 저장 전에도 미리보기가 열린다 */
/* ── 프로필 사진 ──────────────────────────────────────────────────────
   사람이 서는 자리(사용자 목록의 이름, 사용내역의 가맹점) 왼쪽에 붙는 동그란
   그림이다. 사진이 없어도 서버가 이름 첫 글자로 그린 그림을 주므로 화면은
   "있는가" 를 묻지 않는다 — <img> 하나면 언제나 무언가 그려진다.

   크기를 --avatar 하나로 두는 이유: 자리마다 다른 숫자를 적으면 목록 사이를
   오갈 때 얼굴 크기가 미묘하게 달라 정렬이 흔들려 보인다. */
.avatar {
  --avatar: 32px;
  flex: 0 0 auto;
  width: var(--avatar); height: var(--avatar);
  border-radius: 50%;
  object-fit: cover;                    /* 세로 사진이 찌그러지지 않게 */
  background: var(--c-subtle);          /* 그림이 오기 전 자리를 잡아 준다 */
  border: 1px solid var(--c-border);
  display: block;
}
/* 누를 수 있는 얼굴 — 커서와 초점 테두리로 그것을 말한다.
   버튼으로 감싸는 이유: 키보드로도 확대할 수 있어야 하고, <img> 는 초점을
   받지 못한다. */
.avatar-btn {
  flex: 0 0 auto;
  padding: 0; border: 0; background: none; line-height: 0;
  cursor: zoom-in; border-radius: 50%;
}
.avatar-btn:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 2px; }

/* 내 정보의 큰 얼굴 — 바꾸기 전에 무엇이 걸려 있는지 크게 본다 */
.avatar-lg { --avatar: 96px; }

/* 이름 줄에 얼굴을 붙일 때. 얼굴과 글이 <b>가운데가 아니라 위쪽</b>으로
   맞는다 — 이름이 두 줄로 넘어가면 가운데 정렬은 얼굴을 아래로 끌어내린다
   (파일 목록의 [받기] 버튼에서 같은 일이 있었다, 2026-08-23). */
.with-avatar { display: flex; align-items: flex-start; gap: var(--s-3); min-width: 0; }
.with-avatar > .avatar-body { flex: 1 1 auto; min-width: 0; }

/* 사진 옆 동작 줄 — [사진 지우기]와 [상태 메시지]가 나란히 선다.
   좁은 화면에서는 접힌다(wrap): 한 줄을 고집하면 버튼이 화면 밖으로 나간다.
   폼이 섞여 있어(지우기는 별도 폼) 폼 자체의 여백을 0 으로 눕힌다 — 안 그러면
   폼 하나가 자기 아래 여백을 만들어 두 버튼의 밑선이 어긋난다. */
.avatar-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); }
.avatar-actions > form { margin: 0; flex: 0 0 auto; }
/* 상태 메시지 버튼은 적어 둔 말이 곧 글자라 길어질 수 있다. 줄이지 않으면
   옆 버튼을 화면 밖으로 밀어낸다 — 한 줄로 자르고 남는 자리를 다 쓴다. */
.avatar-actions > button { flex: 0 1 auto; min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }

/* 상태 메시지를 목록에 그리는 규칙은 없앴다(2026-08-23).
   프로필 창의 한 줄은 .preview-note 가 맡는다. */

/* 증빙 목록은 이제 .file-list / .file-item 한 벌뿐이다.
   전에는 등록 화면만 .evidence-files + .attach-row 를 따로 썼는데, 같은 파일
   목록인데 저장을 경계로 다른 물건처럼 보였다(2026-08-21 지적). 공통 목록이
   고정 줄을 품게 고쳐(layout.html) 따로 둘 이유가 없어졌다. */
/* 사진 아래 한 줄 — 확대 창에서 그 사람이 남긴 말.
   가운데로 모으는 이유: 위의 사진이 가운데 축이라 왼쪽에 붙이면 축이 어긋난다.
   긴 말은 두 줄로 접힌다 — 여기는 목록이 아니라 자세히 보는 자리다. */
.preview-note { margin: var(--s-3) 0 0; text-align: center; overflow-wrap: anywhere; }

.attach-preview-img { max-width: 100%; max-height: 60vh; display: block; margin: 0 auto;
  border: 1px solid var(--c-border); border-radius: var(--r-md); }

/* 입력 완료 표시 — 채워진 칸은 초록, 빈 칸은 빨강 테두리. 입력하는 즉시
   갱신된다. 색만으로 알리지 않아도 되는 안내다(비면 저장 때 required 가
   다시 막는다). */
input.auto-filled, select.auto-filled { border-color: var(--c-ok); }
input.needs-input, select.needs-input { border-color: var(--c-danger); }
.pick-face.auto-filled { border-color: var(--c-ok); }
.pick-face.needs-input { border-color: var(--c-danger); }

/* 리치 선택(pick) — 항목이 여러 줄(카드 종류/번호/담당자)인 선택 상자.
   네이티브 셀렉트는 항목을 한 줄로만 그려 웹과 모바일이 다르게 보인다 —
   웹=모바일 동일 원칙에 따라 직접 그린다. 검색 입력이 있는 .combo 와 달리
   단순 선택 전용이다. 값은 hidden input 이 나른다.
   얼굴(.pick-face)은 입력과 같은 물건으로 보여야 하므로 입력과 같은 반경(8)·
   같은 높이(48)·같은 글자(16)를 쓴다. */
.pick { position: relative; }
.pick-face {
  display: block; width: 100%; text-align: left;
  background: var(--c-surface); color: var(--c-text); font-weight: var(--fw-normal);
  border: 1px solid var(--c-border-strong); border-radius: var(--r-sm);
  padding: var(--s-3) var(--s-8) var(--s-3) var(--s-3);
  min-height: var(--h-input); position: relative;
  font-size: var(--f-md); line-height: 1.4;
  transition: border-color var(--dur-instant) linear,
              background-color var(--dur-instant) linear;
}
@media (hover: hover) { .pick-face:hover { background: var(--c-subtle); } }
/* 엑셀 창의 대상 카드 얼굴만 가운데 정렬 + 버튼 높이(2026-08-25 사용자 지시) —
   이 창의 나머지(일괄 업로드·다운로드 버튼)가 전부 가운데 정렬·--h-btn(40)이라
   혼자 왼쪽·48이면 축과 키가 갈린다. 목록 필터의 pick 은 입력 칸들과 나란해
   왼쪽·입력 높이 그대로 둔다. 왼쪽 padding 을 오른쪽(화살표 자리 s-8)과 같게
   두는 이유: 안 그러면 가운데가 내용 상자 기준이라 글자가 왼쪽으로 치우쳐
   보인다. 세로 padding 은 버튼과 같은 s-2 다. */
#excelCardPick .pick-face {
  text-align: center;
  padding: var(--s-2) var(--s-8);
  min-height: var(--h-btn);
}
.pick-face:active { transform: none; }
.pick-face:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 2px;
  border-color: var(--c-focus); }
/* 표시기는 select 와 **같은 그림**을 쓴다.

   전에는 글자(▾)를 찍었는데, 글자는 폰트가 있어야 그 모양이다 —
   Pretendard 가 없는 단말에서는 대체 폰트의 삼각형이 그려져, 바로 아래
   select 의 그림과 크기·굵기가 어긋난다. 그림 하나(--sel-arrow)로 통일하면
   폰트·플랫폼과 무관하게 두 칸이 같은 모양이다.
   크기는 원래 글자의 잉크 크기(7×6, 브라우저에서 실측)를 그대로 옮겼다 —
   토큰으로 묶어 둔 이유는 여기와 select 두 곳이 절대 어긋나면 안 되기 때문이다. */
.pick-face::after {
  content: ''; position: absolute; right: var(--s-3); top: 50%;
  width: var(--sel-arrow-w); height: var(--sel-arrow-h);
  transform: translateY(-50%);
  background: var(--sel-arrow) no-repeat center;
  background-size: var(--sel-arrow-w) var(--sel-arrow-h);
}
.pick-list li[aria-selected="true"] { background: var(--c-primary-light); }
/* 부서원 추가 창 — 선택 목록(.pick-list)은 절대 배치라 창 높이에 안 잡힌다.
   창이 픽 얼굴 높이만큼이면 목록이 창 끝에서 잘려 한 명만 보인다(2026-08-28
   제보). 목록 최대치(위 320) + 제목·얼굴·버튼 줄이 함께 서는 만큼(560) 자리를
   미리 편다 — 작은 화면에서는 .dialog 최대 높이와 같은 식으로 화면 안에 접힌다.
   vh 줄은 dvh 를 모르는 브라우저의 폴백이다. */
#member-add-dialog {
  min-height: min(560px, calc(100vh - var(--s-12)));
  min-height: min(560px, calc(100dvh - var(--s-12)));
}
.pick .t1 { display: block; font-weight: var(--fw-semibold); }
.pick .t2 { display: block; overflow-wrap: anywhere; }
.pick .t3 { display: block; color: var(--c-muted); font-size: var(--f-sm); }
.edited-dot { color: var(--c-warn); font-size: var(--f-sm); font-weight: var(--fw-medium); }

/* 입력 지우기(X) — 값 오른쪽 끝. 비밀번호 토글과 같은 자리 문법이다 */
.clear-wrap { position: relative; }
.clear-wrap input { width: 100%; padding-right: var(--s-10); }
.clear-wrap .clear-x {
  position: absolute; right: 2px; top: 50%; transform: translateY(-50%);
  background: none; border: 0; padding: var(--s-2) var(--s-3); min-height: 0;
  color: var(--c-muted); cursor: pointer; font-size: var(--f-sm);
}
.clear-wrap .clear-x:hover { background: none; color: var(--c-text); }
.clear-wrap .clear-x:active { transform: translateY(-50%) scale(.94); }

/* 제목 행 — 화면 제목과 대표 동작 버튼이 양 끝에 선다 */
.page-head { display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-2); margin-bottom: var(--s-4); }
/* 제목은 사용자가 넣은 값이다. 줄어들 수 있게(min-width:0) 두지 않으면 긴
   제목 하나가 옆의 배지·버튼을 화면 밖으로 밀어낸다. 배지는 반대로 줄면
   안 되는 쪽이라 고정한다 — 한 줄에 서는 것은 늘 이 두 종류뿐이다. */
/* h1 만 걸어 두었더니 h2 를 쓰는 화면(메뉴 권한)에서 같은 넘침이 났다.
   제목 줄에 서는 제목은 태그가 무엇이든 사용자가 넣은 값이다. */
/* 제목은 왼쪽에 붙는다. space-between 만 두면 요소가 셋일 때(손잡이·제목·버튼)
   제목이 가운데로 밀려, 제목 줄의 시작선이 아래 내용과 어긋난다.
   요소가 둘일 때는 결과가 같다 — 첫 요소의 margin-right:auto 가 곧 space-between 이다. */
.page-head h1, .page-head h2 {
  margin: 0 auto 0 0; min-width: 0; overflow-wrap: anywhere;
}
.page-head > .badge, .page-head > .btn, .page-head > form { flex: 0 0 auto; }

/* 제목 옆 화면 탭(2026-08-27) — 칩 문법(.chip-link + .on)을 그대로 쓴다.
   button 에 얹으므로 기본 채움색을 지운다 — 연초 칩에서 배운 함정:
   버튼 기본 배경이 칩 얼굴을 이긴다. 켜진 쪽(.on)은 칩 규칙이 칠한다. */
.page-tabs { display: flex; gap: var(--s-1); flex: 0 0 auto; }
.page-tabs .chip-link {
  background: var(--c-surface);
  color: var(--c-text);
  font: inherit;
  font-size: var(--f-sm);
  cursor: pointer;
}
.page-tabs .chip-link.on {
  background: var(--c-primary-light);
}

/* 탭·검색이 타일을 숨길 때 — .usage-item·.tile 은 display:flex 라 hidden
   속성이 그냥은 지지 않는다(chart-period 에서 배운 함정) */
.usage-item[hidden], .tile[hidden] { display: none; }
/* 이 기간에 안 쓴 카드의 0원 — 문제가 아니라 없음이다. 숫자 카드의 is-zero 와
   같은 처방(흐림 + 보통 굵기) */
.usage-head .amount.zero { color: var(--c-muted); font-weight: var(--fw-normal); }

/* 라벨 붙은 검색 행 — 라벨이 컨트롤 위에 선다. 모바일과 데스크톱이 완전히
   같은 배치다(뷰포트로 갈라놓지 않는다 — 두 벌은 반드시 어긋난다).
   라벨↔컨트롤은 4 로 붙인다(한 덩어리로 읽히게), 조건끼리는 12 로 띄운다. */
.filter-rows { display: grid; grid-template-columns: 1fr; gap: var(--s-1);
  margin-bottom: var(--s-3); }
/* 격자 항목의 기본 min-width 는 auto(=내용의 최소 폭)다. 날짜 칸 두 개가 든
   기간 줄은 브라우저가 정한 최소 폭이 있어, 그 폭이 1fr 을 밀어 올리면 조건
   줄 전체가 카드 밖으로 나가고 페이지가 가로로 넘친다(360px 에서 실측 5px).
   0 으로 두면 칸이 컨테이너에 맞고, 안쪽은 각자의 flex 규칙으로 줄어든다 */
.filter-rows > * { min-width: 0; }
/* 라벨은 작고 옅게 — 라벨이 굵고 크면 필터 세 덩이가 화면 무게를 다 가져가
   목록(본론)이 밀린다. 위계: 화면 제목 > 컨트롤 > 라벨 */
.filter-label {
  font-weight: var(--fw-medium); font-size: var(--f-sm); color: var(--c-muted);
  margin: var(--s-2) 0 0; letter-spacing: var(--ls-flat);
}
.filter-range { display: flex; gap: var(--s-2); align-items: center; flex-wrap: wrap; }
/* 프리셋과 날짜 쌍이 각자 한 줄을 꽉 채운다 — 반 폭으로 남으면 행마다
   끝나는 위치가 달라 통일감이 깨진다 */
.filter-range select { flex: 1 1 100%; min-width: 120px; }
.filter-range .date-range { flex: 1 1 240px; }
/* 앱형 검색 줄 — 라벨 없이 화면 맨 위. 돋보기가 "무엇을 하는 칸인지"를 말하므로
   라벨을 따로 두지 않는다(앱 화면과 같은 구성). 실행은 오른쪽 화살표 버튼.
   stretch — 버튼이 입력(48) 높이까지 늘어난다. 40 인 채로 두면 나란히 선
   두 컨트롤의 아래선이 8px 어긋난다. */
.search-top { position: relative; display: flex; gap: var(--s-2); align-items: stretch;
  margin-bottom: var(--s-3); }
.search-top .ico { position: absolute; left: var(--s-3); top: 50%;
  transform: translateY(-50%); color: var(--c-muted); pointer-events: none; }
.search-top input { flex: 1 1 auto; min-width: 0; padding-left: var(--s-10); }
/* 실행 버튼은 두지 않는다 — 글자를 치고 손을 멈추면 저절로 조회된다(공용 스크립트).
   버튼이 있으면 "쳤는데 왜 안 되지 → 버튼을 찾는다" 는 한 걸음이 매번 끼어든다.
   엔터는 폼 기본 제출로 그대로 살아 있어, 스크립트가 없는 환경에서도 검색이 된다.
   이 규칙이 남아 있는 이유: 옛 화면이 버튼을 되살릴 때 줄이 깨지지 않게 한다. */
.search-top button { flex: 0 0 auto; }

/* 필터 칩 줄 — [작은 라벨 | 칩들]. 라벨은 옅게, 칩이 값을 말한다.
   한 조건은 한 줄에 둔다 — 줄바꿈을 허용하면 좁은 화면에서 마지막 칩 하나만
   아래로 떨어져 다른 조건처럼 보인다. 넘치면 그 줄만 가로로 민다.

   밀도: 이 줄이 네 개 쌓이면 375px 화면에서 목록이 시작되기도 전에 뷰포트의
   1/3 을 필터가 먹는다. 칩 높이를 32 로 낮추고 줄 간격을 4 로 줄여 한 줄당
   약 41px → 36px 로 내렸다(마크업은 그대로 두고 CSS 로만 조인다).

   넘침 단서: 스크롤바를 숨겼기 때문에 칩이 더 있다는 사실이 보이지 않았다.
   양 끝의 페이드는 background-attachment 로 만든다 — 덮개(local)는 내용과
   함께 움직여 끝에 닿으면 그림자를 가리고, 그림자(scroll)는 칸에 붙어 있다.
   즉 "더 밀 수 있을 때만" 페이드가 보인다(JS 없이). */
.filter-chips {
  display: flex; align-items: center; gap: var(--s-1);
  /* overflow-x 만 auto 로 두면 스펙상 세로도 스크롤 컨테이너가 된다 — 칩의
     투명 탭 확장(44)이 32 줄을 세로로 넘쳐 이 줄만 위아래로 긁혔다
     (2026-08-25 지적). 세로는 잠근다. */
  flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden; margin-bottom: var(--s-1);
  -webkit-overflow-scrolling: touch; scrollbar-width: none;
  /* 양 끝 페이드로 "더 밀 수 있다"를 말한다(JS 없이). 원리는 두 겹이다 —
     그림자 두 장(3·4번)은 스크롤 창에 고정(scroll)돼 늘 양 끝에 있고,
     덮개 두 장(1·2번)은 내용과 함께 움직여(local) 끝에 닿았을 때 그림자를 가린다.

     덮개는 반드시 **단색**이어야 한다. 예전에는 덮개도 그라데이션이라
     가장자리에서만 불투명하고 안쪽으로 갈수록 비쳐, 밀 것이 없는 넓은 화면에서도
     오른쪽 끝에 회색 띠가 남아 있었다(사용자 지적, 2026-08-21). 덮개는 가리는
     역할만 하므로 색이 변할 이유가 없다. */
  background-image:
    linear-gradient(var(--c-surface), var(--c-surface)),
    linear-gradient(var(--c-surface), var(--c-surface)),
    linear-gradient(to right, color-mix(in srgb, var(--c-text) 22%, transparent), transparent),
    linear-gradient(to left,  color-mix(in srgb, var(--c-text) 22%, transparent), transparent);
  background-position: left center, right center, left center, right center;
  background-repeat: no-repeat;
  background-size: var(--s-6) 100%, var(--s-6) 100%, var(--s-4) 100%, var(--s-4) 100%;
  background-attachment: local, local, scroll, scroll;
}
.filter-chips::-webkit-scrollbar { display: none; }
/* 칩 줄이 연달아 서면 사이를 한 단 벌린다 — 4 로는 실기기 배율에서 맞붙어 보인다 */
.filter-chips + .filter-chips { margin-top: var(--s-2); }
.filter-chips .filter-label { margin: 0; flex: 0 0 auto; }
.filter-chips .chip-link, .filter-chips .btn, .filter-chips button { flex: 0 0 auto; }
/* 도구 줄의 버튼이 폼(POST + data-confirm)에 싸여 올 때 — 폼도 고정이어야
   옆 칸이 길 때 밀려나지 않는다(부서 상세의 [모두 비우기]) */
.filter-chips form { flex: 0 0 auto; margin: 0; }
/* 도구 줄(번호 보기·고급)도 조건 칩과 같은 줄 문법을 쓰므로 폭도 같이 맞춘다.
   여기만 제 폭대로 서면 바로 위 세 줄과 다른 물건처럼 보인다 */
.filter-chips .btn, .filter-chips button { min-width: var(--w-chip); padding: 0 var(--s-2); }

/* 조건 줄이 둘 이상이면 이 격자로 묶는다.

   줄마다 라벨을 안에 두면 라벨 길이가 제각각이라("정렬" 26 · "매칭 여부" 57 ·
   "전체 22장" 58) 칩이 줄마다 다른 자리에서 시작한다. 눈은 그 들쭉날쭉한
   왼쪽 끝을 먼저 읽어서, 세 줄이 한 덩어리가 아니라 흩어진 것으로 보인다.

   그래서 라벨을 줄 밖으로 꺼내 격자의 첫 열에 세운다. 첫 열을 max-content 로
   두면 브라우저가 **모든 줄의 라벨 중 가장 긴 것**에 맞춰 열 너비를 잡는다 —
   고정 px 을 박지 않으므로 라벨 문구가 바뀌거나 건수가 네 자리가 되어도
   정렬이 깨지지 않는다.

   둘째 열은 minmax(0,1fr) 이다. 그냥 1fr 이면 최소 너비가 내용 크기라
   칩이 많은 줄이 열을 밀어내고 결국 화면이 가로로 넘친다. 0 을 최소로 줘야
   칩 줄이 자기 안에서 가로 스크롤된다. */
.filter-grid {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  /* 4 — 줄 사이도, 라벨과 칩 사이도 4 다. 8 이면 375px 화면에서 칩 셋이
     들어갈 자리가 4px 모자란다(계산은 --w-chip 주석 참고). */
  gap: var(--s-1);
  align-items: center;
}
.filter-grid > .filter-label { margin: 0; }
/* 줄마다 여백을 따로 주지 않는다. 예전에는 도구 줄만 u-mt-3(12)으로 띄웠는데,
   위 리셋이 특이도로 그 유틸리티를 눌러 라벨 쪽에만 여백이 안 붙었고, 격자가
   줄을 가운데 맞추면서 라벨이 버튼보다 6px 위로 떴다(사용자 지적, 실측 6px).
   특이도를 올려 막을 수도 있었지만, 예외를 하나 두면 다음 줄도 예외를 갖는다.
   지금은 **모든 조건 줄이 gap 하나(4)로 붙고**, 조건 블록과 결과 사이만
   아래 margin(16)으로 벌어진다 — 규칙이 하나라 어긋날 자리가 없다. */
/* 줄 간격은 격자의 gap 이 준다 — 줄이 제 아래 여백을 또 가지면 두 번 벌어진다.
   대신 조건 블록 전체와 결과(목록) 사이는 격자가 책임진다(16). 전에는 이 여백을
   마지막 줄의 u-mb-4 가 갖고 있었는데, 여기서 0 으로 덮어써 목록이 버튼에
   달라붙었다(사용자 지적, 2026-08-21). 여백의 주인을 격자로 옮겨 해결한다. */
.filter-grid > .filter-chips { margin-bottom: 0; }
.filter-grid { margin-bottom: var(--s-4); }

/* 조건이 두세 개뿐이고 컨트롤 자체가 무엇인지 말할 때(카드 선택 상자, "전체/오늘/
   이번 달" 칩)는 라벨 열을 쓰지 않는다. 라벨이 이름값을 못 하면서 가로를 40~60px 씩
   먹고, 좁은 화면에서는 그만큼이 본론(목록)을 밀어내기 때문이다.

   그때도 라벨을 **지우지는** 않는다 — .u-sr-only 로 숨긴다. 보이는 사람에게만
   자명할 뿐, 소리로 듣는 사람에게는 "이 상자가 무엇을 고르는 것인지"가 여전히 필요하다.
   여러 개가 한 묶음일 때(기간 칩 셋)는 role="group" + aria-labelledby 로 잇는다.

   그렇게 하면 줄마다 컨트롤 하나씩이라 따로 감쌀 그릇이 필요 없다 — 카드 사용내역이
   그 표본이다(카드 선택 / 기간 칩 / 도구, 세 줄). */
/* 줄은 열을 꽉 채운다 — 그래야 카드 선택(입력형 컨트롤)·칩 줄·도구 줄의
   왼쪽과 오른쪽 끝이 모두 같은 자리에 서서 한 덩어리로 읽힌다.
   한때 내용 폭만큼만 넓힌 적이 있는데, 그건 덮개가 그라데이션이라 빈 끝에
   회색 띠가 서던 때의 임시 처방이었다. 덮개를 단색으로 고친 지금은 페이드가
   "밀 수 있을 때"만 뜨므로 열을 채워도 띠가 생기지 않는다(1280px 실측 확인). */
.filter-grid > .filter-chips { justify-self: stretch; }
/* 격자 항목의 기본 min-width 는 내용의 최소 폭이라, 칩 줄이 아닌 컨트롤(.pick 등)을
   둘째 열에 넣으면 그 내용이 minmax(0,1fr) 트랙을 밀어내고 화면이 가로로 넘친다.
   조건 줄(.filter-rows)에서 이미 같은 이유로 5px 넘침을 겪었다(app.css 위쪽 참고). */
.filter-grid > * { min-width: 0; }
/* overflow 컨테이너 안에서는 outline 이 잘린다 — 링을 box-shadow 로 그린다 */
.filter-chips .chip-link:focus-visible,
.filter-chips .btn:focus-visible,
.filter-chips button:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--c-surface), 0 0 0 4px var(--c-focus);
}

.filter-keyword { display: flex; gap: var(--s-2); align-items: stretch; }
.filter-keyword input { flex: 1 1 auto; min-width: 0; }
.filter-keyword button { flex: 0 0 auto; }
/* 검색줄에 콤보를 넣을 때 — 콤보 래퍼가 입력 역할로 늘어난다 (담당자 매칭 줄) */
.filter-keyword .combo { flex: 1 1 auto; min-width: 0; }
.filter-keyword .combo input { width: 100%; }

/* 입력+버튼 한 줄(.input-row, 2026-08-28) — 즉석 등록(부서명)·이름 저장·
   선택+추가처럼 필드 하나와 실행 버튼이 나란한 줄. 검색줄(.filter-keyword)과
   같은 골격이지만 검색이 아니라 쓰기라 어휘를 가른다. 규약대로 stretch —
   버튼(40)이 입력·픽(48) 높이로 늘어난다(값을 고치지 않는다). 유틸 두엇을
   겹쳐 조립하던 것을 컴포넌트로 승격(styleguide 7-2: 유틸 두세 개가 겹치면
   컴포넌트를 만들 때다). */
.input-row { display: flex; gap: var(--s-2); align-items: stretch; }
.input-row > input, .input-row > .pick { flex: 1 1 auto; min-width: 0; }
.input-row > .btn, .input-row > button { flex: 0 0 auto; }

/* 목록 타일 — 항목 사이 12. 타일 내부 패딩(16)보다 좁아야 "한 장 한 장"으로
   읽힌다. 예전 10px 은 어느 토큰에도 없는 값이었다. */
.usage-list { display: flex; flex-direction: column; gap: var(--s-3); }
.usage-item {
  display: flex; align-items: center; gap: var(--s-2);
  padding: var(--s-4);
  border: 1px solid var(--c-border); border-radius: var(--r-md);
  background: var(--c-surface);
  color: inherit; text-decoration: none;
  transition: background-color var(--dur-instant) linear,
              border-color var(--dur-instant) linear;
}
@media (hover: hover) { .usage-item:hover { background: var(--c-subtle); } }
.usage-item:active { background: var(--c-subtle); border-color: var(--c-border-strong); }
.usage-main { flex: 1 1 auto; min-width: 0; color: inherit; text-decoration: none; }
.usage-head { display: flex; align-items: baseline; gap: var(--s-2); }
.usage-head .merchant {
  flex: 1 1 auto; min-width: 0; font-weight: var(--fw-bold); font-size: var(--f-md);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.usage-head .amount {
  flex: 0 0 auto; font-weight: var(--fw-bold); font-size: var(--f-md);
  font-variant-numeric: tabular-nums;
}
.usage-head .amount.canceled { text-decoration: line-through; color: var(--c-muted); }
.usage-time { color: var(--c-muted); font-size: var(--f-sm); margin-top: var(--s-1); }
.usage-meta {
  color: var(--c-muted); font-size: var(--f-sm); margin-top: var(--s-1);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.usage-warn { color: var(--c-danger); font-size: var(--f-sm); margin-top: var(--s-1); }
/* 타일 오른쪽의 방향 표식(›) — 사용내역 타일과 카드 타일이 함께 쓴다.
   "이 줄을 누르면 다음 화면이 있다"를 말한다. 삭제 버튼만 오른쪽에 서 있으면
   타일 본문이 눌리는 것인지 알 길이 없다.
   크기는 20(--f-lg) — 금액도 화살표도 글자 7단계 안에서 고른다.
   색은 muted 다: 본론이 아니라 방향 표식이다.
   마크업에서는 aria-hidden 을 붙인다 — 링크 자체가 이미 읽히므로 "오른쪽
   홑화살괄호"까지 읽히면 문장이 끊긴다. */
.usage-item .chev,
.tile .chev { flex: 0 0 auto; color: var(--c-muted); font-size: var(--f-lg); }

/* 공용 타일 리스트 — 모든 목록 화면의 기본 모양이다(사용자·카드 등).
   웹의 모든 화면은 앱과 같은 스타일·레이아웃을 쓴다는 원칙에 따라
   표 대신 이 타일을 쓴다. 본문(a.tile-main)을 누르면 상세·수정으로 가고,
   오른쪽(.tile-side)에 부가 동작 버튼이 선다. */
.tile-list { display: flex; flex-direction: column; gap: var(--s-3); }
.tile {
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-4);
  border: 1px solid var(--c-border); border-radius: var(--r-md);
  background: var(--c-surface);
  transition: background-color var(--dur-instant) linear,
              border-color var(--dur-instant) linear;
}
@media (hover: hover) { .tile:hover { background: var(--c-subtle); } }

/* 가입 승인 타일 — 타일 안에서 값을 고쳐 바로 처리하는 유일한 목록이다.

   보통 타일은 가로 한 줄(본문 + 오른쪽 버튼)이지만, 여기는 직급 선택 상자
   둘과 버튼 둘이 들어가 그 폭에 서지 못한다. 세로로 쌓고 폼이 가로를 꽉
   채운다 — 좁은 화면과 넓은 화면이 같은 배치다(뷰포트로 갈라놓지 않는다).

   hover 배경을 끄는 이유: 이 타일은 누르는 대상이 아니라 <b>작업하는 자리</b>다.
   손이 지나갈 때마다 배경이 바뀌면 "누르면 어디로 간다" 로 읽힌다. */
.signup-tile {
  flex-direction: column; align-items: stretch; gap: var(--s-2);
}
@media (hover: hover) { .signup-tile:hover { background: var(--c-surface); } }
.signup-tile:active { background: var(--c-surface); border-color: var(--c-border); }
/* 타일 본문이 링크가 아니므로 손가락 모양을 주지 않는다 */
.signup-tile > .tile-main { cursor: default; }
/* 폼 안의 라벨은 첫 것만 위 여백을 줄인다 — 본문 바로 아래에 붙는다 */
.signup-approve > label:first-of-type { margin-top: var(--s-3); }
.signup-approve > button { width: 100%; }
.tile:active { background: var(--c-subtle); border-color: var(--c-border-strong); }
.tile-main { flex: 1 1 auto; min-width: 0; display: block; color: inherit; text-decoration: none; }
/* 제목이 왼쪽을 온전히 갖고 배지는 오른쪽 끝에 선다.
   전에는 배지가 제목 바로 옆에 붙어 "덤벼 대기" 가 한 낱말처럼 읽혔다
   (2026-08-23 제보 — 목록에서 제목과 배지에 함께 밑줄을 그었다). */
.tile-head { display: flex; align-items: center; gap: var(--s-2); min-width: 0; }
.tile-head > .tile-title { margin-right: auto; }
.tile-head > .badge { flex: 0 0 auto; }
/* 제목 16/700 — 앱의 titleMedium(16sp)과 같은 급이다. 예전 18px 은 본문(15)과
   3px 차이라 위계는 약한데 목록 높이만 키웠다 */
.tile-title { font-weight: var(--fw-bold); font-size: var(--f-md); min-width: 0;
  line-height: var(--lh-head); overflow-wrap: anywhere; }
.tile-sub { color: var(--c-muted); margin-top: var(--s-1); overflow-wrap: anywhere; }
.tile-meta { color: var(--c-muted); font-size: var(--f-sm); margin-top: var(--s-1);
  overflow-wrap: anywhere; }
.tile-tags { display: flex; gap: var(--s-2); flex-wrap: wrap; margin-top: var(--s-2); }
.tile-side { flex: 0 0 auto; display: flex; flex-direction: column; gap: var(--s-2); }
.tile-side form { margin: 0; }
.tile-side .btn, .tile-side button { width: 100%; }
/* 테두리만 있는 무채색 태그 — 앱 태그의 기본(plain) 톤 */
.badge-plain { background: transparent; border: 1px solid var(--c-border); color: var(--c-text); }

/* 메뉴 권한 — 앱의 권한 시트와 같은 구성. 롤마다 카드 하나, 안에 체크 칩.
   체크 표시(✓)는 자리를 항상 차지하고 표시만 켜고 끈다 — 체크할 때마다
   칩 폭이 변해 옆 칩들이 밀리는 것을 막는다(앱과 같은 처리). */
.perm-card { border: 1px solid var(--c-border); border-radius: var(--r-md);
  padding: var(--s-4); }
.perm-card + .perm-card { margin-top: var(--s-3); }
.perm-head { display: flex; align-items: center; gap: var(--s-2); margin-bottom: var(--s-3); }
.perm-role { font-weight: var(--fw-bold); min-width: 0; overflow-wrap: anywhere; }
.chip-row { display: flex; gap: var(--s-2); flex-wrap: wrap; }
/* 칩은 label 이다 — 위의 폼 라벨 규칙(위아래 여백·작은 회색 글자)이 그대로
   내려오면 칩 줄이 세로로 벌어지고 글자가 흐려진다. 여기서 되돌린다 */
.chip {
  position: relative; cursor: pointer; margin: 0;
  font-size: var(--f-sm); font-weight: var(--fw-normal); color: var(--c-text);
}
/* 실제 체크박스는 숨긴다 — 상태·전송·키보드 조작은 그대로 남는다 */
.chip input { position: absolute; opacity: 0; pointer-events: none; }
/* 누를 수 있는 알약은 32 높이로 통일한다(배지 24 와 구분된다).
   보이는 크기는 32 지만 손가락 목표물은 44 여야 한다 — 라벨 전체에 투명한
   세로 확장을 얹는다. 라벨을 누르면 안의 체크박스가 켜지므로 그대로 동작한다. */
.chip::after {
  content: ''; position: absolute; left: 0; right: 0;
  top: 50%; transform: translateY(-50%); height: var(--h-tap);
}
.chip-face {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-1);
  min-height: var(--h-chip); padding: 0 var(--s-3);
  font-size: var(--f-sm); line-height: var(--lh-tight);
  border: 1px solid var(--c-border-strong); border-radius: var(--r-full);
  user-select: none;
  transition: background-color var(--dur-instant) linear,
              border-color var(--dur-instant) linear;
}
@media (hover: hover) { .chip:hover .chip-face { background: var(--c-subtle); } }
.chip:active .chip-face { background: var(--c-border); }
.chip-check { visibility: hidden; font-weight: var(--fw-bold); }
/* 선택 표시 테두리는 --c-accent 가 아니라 --c-focus 다 — 주황은 옅은 바탕
   위에서 2.4:1 이라 "골랐다"는 신호가 되지 못했다 */
.chip input:checked + .chip-face {
  background: var(--c-primary-light); border-color: var(--c-focus);
  font-weight: var(--fw-semibold);
}
.chip input:checked + .chip-face .chip-check { visibility: visible; }
.chip input:disabled + .chip-face { opacity: var(--o-disabled); cursor: default; }
.chip input:focus-visible + .chip-face { outline: 2px solid var(--c-focus); outline-offset: 2px; }

/* 칩 안의 아이콘 — 글자 대신 그림으로 고르는 자리.

   SVG 는 viewBox 만으로는 크기가 정해지지 않는다. 이 규칙이 없으면 브라우저가
   기본 300x150 으로 그리거나(글자가 밀려난다) 부모가 0 이면 아무것도 안 보인다 —
   실제로 아이콘 고르는 칩이 <b>빈 원 열다섯 개</b>로 보였다.

   글자와 나란히 설 때도 같은 크기다(--s-5 20). 아이콘만 있는 칩은 좌우 여백을
   줄여 정사각에 가깝게 — 알약 안에 그림 하나만 있으면 가로로 길어 어색하다. */
.chip-face svg { width: var(--s-5); height: var(--s-5); flex: 0 0 auto; }
.chip-face:has(> svg:only-child) { padding: 0 var(--s-2); }

/* 색 견본 칩 — 색 자체가 정보라 동그라미로 보여 준다.

   select 로는 "violet" 이라는 글자만 읽히고 실제 색을 볼 수 없다. 색을 고르는
   일에 색이 안 보이면 매번 저장하고 결과를 보러 가야 한다. */
.chip-swatch {
  width: var(--s-5); height: var(--s-5); border-radius: 50%;
  background: var(--card-tint, var(--c-border));
  flex: 0 0 auto;
}

/* 스타일 설정 — 테마 카드. 라디오를 숨기고 카드 전체가 목표물이다(권한 칩과 같은 방식) */
.theme-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
  gap: var(--s-3);
  margin-bottom: var(--s-5);
}
/* 테마 카드도 label 이다 — 칩과 같은 이유로 폼 라벨 규칙을 되돌린다 */
.theme-card {
  position: relative; cursor: pointer; margin: 0;
  font-size: var(--f-base); font-weight: var(--fw-normal); color: var(--c-text);
}
.theme-card input { position: absolute; opacity: 0; pointer-events: none; }
.theme-face {
  display: flex; flex-direction: column; gap: var(--s-2);
  padding: var(--s-4);
  border: 1px solid var(--c-border-strong); border-radius: var(--r-md);
  background: var(--c-surface);
  transition: background-color var(--dur-instant) linear,
              border-color var(--dur-instant) linear;
}
@media (hover: hover) { .theme-card:hover .theme-face { background: var(--c-subtle); } }
.theme-card:active .theme-face { background: var(--c-border); }
.theme-card input:checked + .theme-face { border-color: var(--c-focus); border-width: 2px; }
.theme-card input:focus-visible + .theme-face { outline: 2px solid var(--c-focus); outline-offset: 2px; }
.swatches { display: flex; gap: var(--s-2); }
.swatches i { width: var(--s-5); height: var(--s-5); border-radius: 50%; }
.swatches i.bordered { border: 1px solid var(--c-border); }
.theme-name { font-weight: var(--fw-bold); }
.theme-tags { display: flex; gap: var(--s-2); flex-wrap: wrap; }

/* 정렬 칩 — 앱의 선택 칩과 같은 모양의 링크. 누르는 즉시 적용된다 */
.sort-row { margin-bottom: var(--s-3); }

/* 목록 보조 줄 — 왼쪽에 정렬 칩, 오른쪽에 보조 도구(엑셀 등)를 한 줄로 놓는다.
   줄을 따로 쌓으면 목록 시작이 자꾸 내려가고, 같은 급으로 보이는 두 줄이
   서로 경쟁한다. 좁은 화면에서는 자연히 두 줄로 꺾인다. */
.list-tools {
  display: flex; justify-content: space-between; align-items: center;
  gap: var(--s-2); flex-wrap: wrap;
  margin: var(--s-5) 0;
}
.list-tools .sort-row { margin-bottom: 0; }
.list-tools .tools { display: flex; gap: var(--s-2); flex-wrap: wrap; }

/* 요약 숫자 줄 — 검사·처리 결과의 건수를 한눈에. 문장으로 늘어놓으면
   숫자가 글 속에 묻힌다. 0 은 흐리게 — 눈이 문제 있는 숫자로 먼저 간다. */
.stat-row { display: flex; gap: var(--s-2); margin: var(--s-4) 0; }
.stat {
  flex: 1 1 0; min-width: 0; text-align: center;
  padding: var(--s-3) var(--s-2);
  border: 1px solid var(--c-border); border-radius: var(--r-sm);
}
.stat .num {
  display: block; font-size: var(--f-xl); font-weight: var(--fw-bold);
  line-height: var(--lh-head); letter-spacing: var(--ls-head);
}
.stat .lbl { display: block; color: var(--c-muted); font-size: var(--f-sm);
  white-space: nowrap; letter-spacing: var(--ls-flat); }
.stat-ok .num { color: var(--c-ok); }
.stat-warn .num { color: var(--c-warn); }
.stat-danger .num { color: var(--c-danger); }
.stat.is-zero .num { color: var(--c-muted); font-weight: var(--fw-normal); }

/* 정렬·필터 칩 링크 — 권한 칩(.chip-face)과 같은 알약 문법·같은 높이(32) */
.chip-link {
  display: inline-flex; align-items: center; justify-content: center;
  /* 여백은 4 로 조였다. 지금 쓰는 라벨은 전부 min-width 안에 들어와 여백이
     폭을 좌우하지 않지만, 다섯 글자를 넘는 라벨이 오면 이 값이 그대로 폭에
     더해진다 — 8 이면 375px 화면에서 셋째 칩이 잘린다. */
  min-height: var(--h-chip); padding: 0 var(--s-1);
  /* 글자 수가 달라도 칩 폭은 같게 둔다. 2글자("전체")와 5글자("카드번호순")가
     제 폭대로 서면 나란히 선 칩들이 크기가 다른 물건처럼 보이고, 줄이 여러
     개일 때 둘째·셋째 열이 줄마다 어긋나 격자로 읽히지 않는다.
     min-width 라서 더 긴 글자가 오면 잘리지 않고 그 칩만 넓어진다. */
  min-width: var(--w-chip);
  font-size: var(--f-sm); line-height: var(--lh-tight);
  border: 1px solid var(--c-border-strong); border-radius: var(--r-full);
  color: inherit; text-decoration: none;
  transition: background-color var(--dur-instant) linear,
              border-color var(--dur-instant) linear;
}
@media (hover: hover) { .chip-link:hover { background: var(--c-subtle); } }
.chip-link:active { background: var(--c-border); }
/* 고른 칩은 ✓ 로 표시한다(앱의 선택 칩과 같은 처리).

   ✓ 는 고른 칩에만 넣는다. 전에는 안 고른 칩에도 같은 크기의 빈 자리를 좌우로
   비워 두었다 — 고를 때 폭이 변하지 않게, 그리고 글자가 가운데 오게 하려던
   것이다. 그런데 그 빈 자리가 칩마다 34px 을 먹어 375px 화면에서 셋째 칩이
   잘렸다(사용자 지적, 2026-08-21).

   지금은 두 목적을 다른 수단으로 지킨다.
   · 폭이 변하지 않는 것 — min-width(80)가 잡아 준다. 라벨이 다섯 글자여도
     ✓ 를 넣은 폭이 80 안에 들어, 골라도 칩이 커지지 않는다.
   · 글자가 가운데 오는 것 — 안 고른 칩에는 ✓ 가 아예 없으므로 저절로 가운데다.
     예전처럼 한쪽만 비워 "오른쪽 정렬"처럼 보이는 일이 없다.

   고른 칩의 좌우 여백을 0 으로 두는 이유: ✓ 자리는 min-width 가 남긴 여유
   안에서 꺼내 쓴다. 여백까지 함께 세면 다섯 글자 칩이 80 을 넘어 그 줄만
   밀어야 보이게 된다. */
.chip-link.on::before { content: '✓'; margin-right: var(--s-1); }
.chip-link.on { padding: 0; }
.chip-link.on {
  background: var(--c-primary-light); border-color: var(--c-focus);
  font-weight: var(--fw-semibold);
}

/* 페이저 — 버튼과 같은 40 높이·같은 반경. 전에는 계산 높이가 38.5 라
   옆에 선 버튼과 미묘하게 어긋났다 */
.pager {
  display: flex; gap: var(--s-1); justify-content: center; align-items: center;
  margin-top: var(--s-6); flex-wrap: wrap;
}
.pager a, .pager span {
  /* 페이지 수가 많아지면 번호가 눌려 40 미만이 된다 — 손가락 크기 아래로
     내려가므로 줄지 않게 고정하고 넘치면 다음 줄로 접는다(flex-wrap). */
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--h-btn); padding: 0 var(--s-3); border-radius: var(--r-sm);
  text-decoration: none; color: var(--c-text); min-width: var(--h-btn); text-align: center;
  transition: background-color var(--dur-instant) linear;
}
@media (hover: hover) { .pager a:hover { background: var(--c-subtle); } }
.pager a:active { background: var(--c-border); }
.pager .on { background: var(--c-primary); color: var(--c-on-primary); font-weight: var(--fw-semibold); }

/* 트리 — 계층 구조를 접었다 펼 수 있게 보인다.
   접기(화살표 버튼)와 선택(이름 링크)을 갈라 놓았다. 한 요소가 둘 다 하면
   무엇을 눌러야 할지 알 수 없고, details/summary 로 묶으면 이름을 누를 때도 접힌다. */
.tree ul { list-style: none; margin: 0; padding: 0; }

/* 자식 목록은 들여쓰고 왼쪽에 세로선을 둔다 — 어느 부모에 속하는지 눈으로 잇는다 */
.tree ul ul { margin-left: var(--s-4); padding-left: var(--s-3); border-left: 1px solid var(--c-border); }

.tree li { margin: var(--s-1) 0; }
.tree li.collapsed > ul { display: none; }

.tree-node {
  display: flex; align-items: center; gap: var(--s-2);
  padding: var(--s-1) var(--s-2);
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  min-height: var(--h-tap);      /* 손가락으로 누를 수 있는 크기 */
  transition: background-color var(--dur-instant) linear,
              border-color var(--dur-instant) linear;
}
@media (hover: hover) { .tree-node:hover { background: var(--c-subtle); } }

/* 지금 편집 중인 노드 */
.tree-node.on { border-color: var(--c-focus); background: var(--c-primary-light); }
.tree-node.on .tree-name { font-weight: var(--fw-bold); }

/* 접기 버튼. 접힌 li 안에서는 화살표가 오른쪽을 향한다 */
.tree-caret {
  flex: 0 0 var(--h-chip); width: var(--h-chip); min-height: var(--h-chip); padding: 0;
  background: none; border: 0; color: var(--c-muted);
  font-size: var(--f-md); line-height: 1; cursor: pointer;
  transform: rotate(90deg);
  transition: transform var(--dur-fast) var(--ease-out),
              background-color var(--dur-instant) linear;
}
@media (hover: hover) { .tree-caret:hover { background: var(--c-border); border-radius: var(--r-xs); } }
.tree-caret:active { transform: rotate(90deg) scale(.9); }
li.collapsed > .tree-node > .tree-caret { transform: rotate(0deg); }
li.collapsed > .tree-node > .tree-caret:active { transform: rotate(0deg) scale(.9); }
span.tree-caret { cursor: default; }   /* 자식 없는 노드의 빈 자리 */

/* 이름이 곧 선택 링크다. 줄 전체를 차지해 누르기 쉽게 한다 */
.tree-name {
  flex: 1; min-width: 0;
  color: var(--c-text); text-decoration: none;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  padding: var(--s-2) 0;
}

/* 하위 개수 */
.tree-count {
  flex: 0 0 auto; min-width: var(--s-6); text-align: center;
  background: var(--c-border); color: var(--c-text);
  border-radius: var(--r-full); font-size: var(--f-xs);
  padding: var(--s-1) var(--s-2); line-height: var(--lh-tight);
  letter-spacing: var(--ls-flat);
}

/* 기능 카드 격자 — 모바일 앱 홈과 같은 구성.
   목록(한 줄짜리 링크)이 아니라 카드를 쓰는 이유는 목표물 크기다. 한 손으로
   쓰는 화면에서 카드 하나는 손가락보다 훨씬 크고, 색과 위치로 글자를 읽기 전에
   먼저 찾을 수 있다. */
/* 제목과 카드 묶음을 한 덩어리로 가운데 놓는다.
   폭을 여기서 정하고 격자는 그 안을 채운다 — 둘이 각자 폭을 가지면
   화면이 넓어질 때 서로 다른 위치에서 시작한다.
   폭 상한(560)이 열 폭도 함께 묶으므로 넓은 화면용 분기가 필요 없다 —
   예전의 @media(min-width:720px) 블록은 지웠다(단일 레이아웃 원칙). */
.menu-section {
  /* main(--w-page)이 이미 제한하므로 시각적으로는 없어도 같다. 그래도 같은
     토큰을 적어 둔다 — 숫자가 남아 있으면 다음 사람이 "여기가 폭을 정하나"
     하고 헤맨다. */
  max-width: var(--w-page);
  margin-left: auto;
  margin-right: auto;
}

.menu-grid {
  display: grid;
  /* 2열 고정 — 앱 홈(crossAxisCount: 2)과 같은 구성이다.
     auto-fill+minmax 로 두면 375px 화면에서 열당 180px 이 되어 최소 폭에
     못 미치고, 그러면 1열로 떨어져 카드 하나가 화면을 가득 채운다. */
  grid-template-columns: repeat(2, 1fr);
  gap: var(--s-4);
}

.menu-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: var(--s-2);
  padding: var(--s-5) var(--s-3);
  background: var(--c-surface);
  /* 테두리에만 색을 쓴다. 면 전체를 칠하면 카드마다 글자 대비를 다시 맞춰야
     하고, 흰 바탕의 차분한 인상도 깨진다. 색은 --card-tint 로 각자 정한다.
     45% 로 옅게 — 원색 그대로면 테두리가 내용보다 먼저 눈에 들어온다(앱과 같은 값). */
  border: 1px solid color-mix(in srgb, var(--card-tint, var(--c-border)) 45%, transparent);
  border-radius: var(--r-lg);
  text-decoration: none;
  color: var(--c-text);
  /* 폭보다 높이를 조금 낮게 — 세로로 길면 카드 안이 비어 보인다 */
  aspect-ratio: 1.1;
  /* 격자 항목의 기본 min-width 는 auto(=내용의 최소 폭)라, 제목이 긴 카드가
     칸보다 넓어지면 칸을 밀어내고 결국 페이지 전체가 가로로 넘친다.
     실제로 좁은 폭(카드 안의 격자)에서 40px 넘침이 나왔다. 0 으로 두면
     카드가 칸에 맞춰 줄고 제목이 줄바꿈된다 — 넘치는 것보다 접히는 게 낫다 */
  min-width: 0;
  transition: background-color var(--dur-instant) linear,
              transform var(--dur-fast) var(--ease-out);
}
@media (hover: hover) { .menu-card:hover { background: var(--c-subtle); } }
.menu-card:active { transform: scale(.98); }

/* 아이콘을 감싸는 원. 옅은 같은 계열 색으로 채워 카드끼리 구분된다 */
.menu-card .icon {
  width: var(--h-lg); height: var(--h-lg);
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: color-mix(in srgb, var(--card-tint, var(--c-primary)) 12%, transparent);
  color: var(--card-tint, var(--c-primary));
}
.menu-card .icon svg { width: var(--s-6); height: var(--s-6); }
.menu-card .title { font-weight: var(--fw-bold); font-size: var(--f-md); }
.menu-card .desc { color: var(--c-muted); font-size: var(--f-sm); }
/* 칸이 좁아지면 줄바꿈으로 견딘다 — 한글은 단어 사이가 아니면 잘 안 꺾여서
   min-width:0 만으로는 긴 이름이 그대로 삐져나올 수 있다 */
.menu-card .title, .menu-card .desc { overflow-wrap: anywhere; }

/* 빈 상태 / 로딩 — 구획 여백(40)으로 목록과 확실히 갈라 놓는다.
   빈 화면에는 반드시 복구 경로(등록 버튼·조건 초기화)가 함께 있어야 한다.
   그 버튼은 마크업 쪽에서 넣는다(아래 '후속' 주석 참고). */
.empty { text-align: center; padding: var(--s-10) var(--s-4); color: var(--c-muted);
  /* 콘텐츠 영역임을 표시하는 선(2026-08-25 사용자 지시) — 빈 상태가 경계 없이
     떠 있으면 "여기에 목록이 온다"는 자리 자체가 안 보인다. 점선인 이유:
     실선은 카드·타일(실재하는 것)의 말이고, 점선은 "아직 비어 있는 자리"다. */
  border: 1px dashed var(--c-border); border-radius: var(--r-md); }

/* 로딩 — "로딩과 빈 목록을 반드시 구분한다"는 규칙에 CSS 가 없어서,
   지금까지는 버튼을 비활성화하고 회색 문장 한 줄을 붙이는 것이 전부였다.
   앱에는 이미 스피너가 13곳 있다. 아래 두 클래스로 웹도 같은 말을 한다.
   (새 클래스라 기존 화면에는 영향이 없다 — 쓰는 화면부터 붙이면 된다) */
.u-spinner {
  display: inline-block; width: var(--s-4); height: var(--s-4);
  border: 2px solid color-mix(in srgb, var(--c-text) 25%, transparent);
  border-top-color: var(--c-text);
  border-radius: 50%;
  vertical-align: middle;
  animation: spin 800ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* 뼈대 — 실제 콘텐츠와 높이가 같아야 로드 완료 시 화면이 튀지 않는다 */
.u-skeleton {
  background: var(--c-subtle);
  border-radius: var(--r-sm);
  min-height: var(--s-4);
  animation: skeleton 1.5s var(--ease-in-out) infinite;
}
@keyframes skeleton { 50% { opacity: .55; } }

/* 끌어서 자리 바꾸기 — 목록·격자 어디에나 얹는 어휘.

   메뉴 관리에서 홈 카드를 끌어 옮기려고 만들었지만, 이름을 그 화면에 매지
   않는다(.menu-edit-* 가 아니다). 순서를 손으로 정하는 화면은 또 생기고,
   그때마다 같은 모양을 다시 만들면 화면마다 조금씩 달라진다.

   쓰는 쪽:
     .sortable          끌 수 있는 항목들을 담는 상자
     .sortable > *      항목. JS 가 [data-sort-id] 로 식별한다
     .is-dragging       지금 손에 들려 화면을 따라다니는 사본
     .is-ghost          들린 항목이 원래 있던 자리(옅게 남겨 어디서 왔는지 보인다)
     .is-drop-target    놓으면 여기 들어간다는 표시(상자에 붙는다)

   왜 원본을 감추고 사본을 띄우는가: 원본을 그대로 움직이면 격자가 매 프레임
   다시 계산되어 다른 항목들이 덜컹인다. 사본은 흐름 밖(fixed)에 있어 아무것도
   밀지 않는다. */
/* 항목 — 손잡이를 얹기 위한 자리. 안의 내용이 크기를 정한다 */
.sort-item { position: relative; min-width: 0; }
/* 격자 안에서는 래퍼가 칸을 채우고 그 안의 카드가 래퍼를 채워야 한다.
   구획처럼 흐름 안에 서는 항목에는 주지 않는다 — grid 로 만들면 자식의
   위아래 여백이 부모 밖으로 못 나가, 구획을 갈라 주던 간격이 사라진다. */
.menu-grid > .sort-item { display: grid; }

/* 정렬 가능한 세로 목록 — 항목 사이를 스스로 띄운다.

   홈에서는 구획 제목(h2)의 위 여백 32 가 구획을 갈랐다. 편집 화면에서는 제목이
   .page-head 안에 있어 그 여백이 0 이라(제목 줄의 규칙이다) 구획이 딱 붙는다.
   여백을 되살리는 대신 상자가 간격을 갖는다 — 여백은 안쪽 규칙에 따라 사라지지만
   gap 은 그렇지 않다.

   값은 <b>16</b> 이다. 제목 줄 아래(.page-head margin-bottom)도 16, 카드 사이
   (.menu-grid gap)도 16 — 화면 전체가 한 간격으로 선다.

   32 로 두었더니 제목 위 32 · 아래 16 이 되어 제목이 위로 떠 보였다. 한쪽만
   넓으면 "제목이 어느 구획 것인지" 가 흔들리고, 눈은 그 어긋남을 정렬 문제로
   읽는다. 첫 항목은 gap 이 없으므로 margin-top 으로 같은 값을 준다 — 안 주면
   첫 구획만 앞 카드에 딱 붙는다. */
.sortable[data-sort-vertical] {
  display: grid;
  gap: var(--s-4);
  margin-top: var(--s-4);
}

/* 손잡이. 이것만 끌 수 있다.

   카드 전체를 끌게 하면 "누르면 열리고 끌면 옮겨진다" 를 손이 매번 가늠해야
   한다. 손가락에서는 더 나쁘다 — 길게 누르기로 가르면 목록 스크롤과 겹쳐,
   넘기려다 카드가 들리거나 옮기려다 화면이 스크롤된다.

   손잡이가 따로 있으면 그 애매함이 사라진다. 카드는 눌러서 여는 것, 손잡이는
   잡아서 옮기는 것 — 손이 닿기 전에 눈으로 갈린다. */
.sort-handle {
  position: absolute;
  top: var(--s-1); right: var(--s-1);
  z-index: 1;
  /* 보이는 크기는 작게, 만지는 크기는 손가락에 맞게(--h-tap 44) */
  width: var(--h-tap); height: var(--h-tap);
  display: flex; align-items: center; justify-content: center;
  padding: 0;
  border: 0;
  background: none;
  color: var(--c-muted);
  /* 잡으면 곧바로 끌기다. 브라우저가 스크롤을 가져가지 않게 여기서만 막는다 —
     카드 나머지는 그대로라 목록을 넘길 수 있다 */
  touch-action: none;
  cursor: grab;
  border-radius: var(--r-sm);
}
@media (hover: hover) { .sort-handle:hover { color: var(--c-text); background: var(--c-subtle); } }
.sort-handle:active { cursor: grabbing; }
.sort-handle svg { width: var(--s-5); height: var(--s-5); }

/* 구획 손잡이는 제목 줄에 선다 — 절대배치가 아니라 흐름 안이다 */
.sort-handle.is-inline { position: static; width: var(--h-tap); flex: 0 0 auto; }

/* <a> 는 기본으로 끌 수 있다. 그대로 두면 브라우저의 링크 드래그가 먼저
   시작해 카드가 안 따라오고 주소 그림자만 끌린다 */
.sort-item a { -webkit-user-drag: none; }

.is-dragging {
  position: fixed;
  z-index: 60;
  pointer-events: none;
  box-shadow: var(--shadow-3);
  opacity: .92;
  /* 손끝보다 살짝 커져서 "들렸다" 가 보인다. 크게 하면 놓을 자리를 가린다 */
  transform: scale(1.03);
}

/* 원래 자리 — 지우지 않고 옅게 남긴다. 없애면 격자가 즉시 메워져,
   놓기를 취소했을 때 어디로 돌아가는지 알 수 없다 */
.is-ghost { opacity: .35; }

/* 놓을 상자. 색만으로 알리지 않도록 테두리 굵기도 함께 바뀐다 */
.is-drop-target {
  outline: 2px dashed var(--c-focus);
  outline-offset: var(--s-1);
  border-radius: var(--r-lg);
}

/* 저장 중 표시 — 옮긴 결과가 서버에 닿았는지 알려 준다.
   끌어 놓고 아무 신호가 없으면 "저장된 건가" 를 매번 묻게 된다 */
.sort-status {
  position: sticky;
  bottom: var(--s-4);
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-4);
  border-radius: var(--r-full);
  background: var(--c-text);
  color: var(--c-surface);
  font-size: var(--f-sm);
  box-shadow: var(--shadow-2);
}
.sort-status[hidden] { display: none; }

/* ---------------------------------------------------------------------------
   5. 유틸리티 — 인라인 style 대신 쓴다
   --------------------------------------------------------------------------- */
/* 이제 main 과 같은 폭이다(--w-page 로 통일). 클래스는 지우지 않고 별칭으로
   남긴다 — 템플릿 21곳이 쓰고 있고, 특이성이 main 보다 높아 값을 안 맞추면
   여기 적은 값이 이긴다. 값이 다르면 그 21개 화면만 폭이 갈라진다. */
.u-narrow { max-width: var(--w-page); margin-left: auto; margin-right: auto; }
/* 사람이 친 글을 그대로 보이는 자리 — 공지 본문, 문의 내용·답변.

   줄바꿈을 살리려고 th:utext 로 <br> 을 넣거나 nl2br 을 만들지 않는다.
   본문에 섞인 태그가 그대로 살아나 화면이 깨지거나 남의 스크립트가 실행된다.
   글은 th:text 로 이스케이프해 넣고, 줄바꿈은 CSS 가 살린다.

   overflow-wrap 이 함께 있어야 한다 — 띄어쓰기 없는 긴 주소 한 줄이
   카드를 가로로 밀어낸다. */
.u-prewrap { white-space: pre-wrap; overflow-wrap: anywhere; }
.u-muted { color: var(--c-muted); }
/* 결과를 말하는 한 줄 글씨 — 아이디 중복 확인처럼 칸 옆에서 바로 답하는 자리.
   .field-error 와 다른 점은 이쪽이 "성공" 도 말한다는 것이다.
   색만으로 알리지 않는다 — 문구가 그 자체로 결과를 말하고 색은 거들 뿐이다. */
.u-ok { color: var(--c-ok); }
.u-danger { color: var(--c-danger); }
.u-small { font-size: var(--f-sm); }
.u-center { text-align: center; }
.u-flex { display: flex; gap: var(--s-2); align-items: center; }
.u-flex-between { display: flex; gap: var(--s-2); align-items: center; justify-content: space-between; }
/* flex 줄에서 이 하나만 오른쪽 끝으로 — 한 줄에 동작 버튼 하나를 우측 정렬할 때
   (부서·그룹 상세의 비우기 버튼, 2026-08-28). 밀려나지 않게 고정도 함께 */
.u-ml-auto { margin-left: auto; flex: 0 0 auto; }
.u-wrap { flex-wrap: wrap; }
.u-mt-2 { margin-top: var(--s-2); }
.u-mt-3 { margin-top: var(--s-3); }
.u-mt-4 { margin-top: var(--s-4); }
.u-mt-5 { margin-top: var(--s-5); }
.u-mb-1 { margin-bottom: var(--s-1); }
.u-mb-3 { margin-bottom: var(--s-3); }
.u-mb-4 { margin-bottom: var(--s-4); }
/* 구획을 크게 끊을 때 — 24/32/40 을 쓰라고 이름을 만들어 둔다 */
.u-mt-6 { margin-top: var(--s-6); }
.u-mt-8 { margin-top: var(--s-8); }
.u-mt-10 { margin-top: var(--s-10); }
.u-mb-6 { margin-bottom: var(--s-6); }
/* 숫자 칸 — 금액·수량은 오른쪽 정렬 + 고정폭 숫자라야 자릿수가 세로로 맞는다 */
.u-num { text-align: right; }

/* ---------------------------------------------------------------------------
   6. 접근성 보정
   --------------------------------------------------------------------------- */

/* 움직임을 줄이도록 설정한 사용자에게는 전환을 없앤다.
   전정 장애가 있는 사람에게 화면 이동은 어지러움을 유발한다. OS 설정을
   존중하는 것이 유일한 신호다 — 앱 안에 별도 스위치를 두면 아무도 못 찾는다.
   토큰 자체를 1ms 로 내려 개별 규칙까지 한 번에 잡고, 혹시 토큰을 안 쓴
   선언이 남아 있어도 아래 전역 !important 가 덮는다.
   다만 완전 무음으로 만들지는 않는다 — 색 변화는 즉시 일어나므로
   "눌렸나?" 를 유발하지 않는다. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-instant: 1ms; --dur-fast: 1ms; --dur-base: 1ms; --dur-slow: 1ms;
  }
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
  /* 회전하는 스피너만은 남기되 아주 느리게 — 로딩 중이라는 사실은 알려야 한다 */
  .u-spinner { animation-duration: 2s !important; animation-iteration-count: infinite !important; }
  .u-skeleton { animation: none !important; }
}

/* 키보드로 이동할 때만 포커스 링을 보인다. 마우스 클릭에도 링이 남으면
   지저분해 보여 결국 outline:none 으로 지우게 되고, 그러면 키보드 사용자가
   지금 어디에 있는지 알 수 없게 된다.
   두께 2px + 오프셋 2px 이 최적점이다 — 요소에 붙으면 테두리와 구분되지 않고,
   4px 이상이면 촘촘한 목록에서 인접 행을 덮어 오히려 헷갈린다.
   색은 --c-focus 하나뿐이다(전에는 전역·입력·FAB·칩 네 가지 처방이 있었고
   그중 둘은 대비 1.23:1 로 사실상 보이지 않았다). */
:focus-visible {
  outline: 2px solid var(--c-focus);
  outline-offset: 2px;
}
/* overflow 를 자르는 컨테이너 안에서는 outline 이 잘려 나간다 — 그런 자리는
   box-shadow 로 같은 두께의 링을 그린다(바깥 흰 띠 2px + 링 2px) */
.table-wrap :focus-visible,
.combo-list :focus-visible,
.pick-list :focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--c-surface), 0 0 0 4px var(--c-focus);
}

/* 화면에는 없지만 보조기술은 읽는 텍스트 */
.u-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;
}

/* ---------------------------------------------------------------------------
   7. 반응형
   --------------------------------------------------------------------------- */
/* 좁은 화면 잔여 조정 — 배치·크기는 전 폭 공통이고(위 단일 레이아웃 원칙),
   여기 남는 것은 헤더가 좁은 폭에 물리적으로 들어가기 위한 최소 조정과,
   표의 동작 열이 좁은 폭에서 겹치지 않게 하는 처리뿐이다. */
@media (max-width: 640px) {
  header { padding: var(--s-2) var(--s-3); }
  header .brand { font-size: var(--f-base); }
  header nav { gap: var(--s-3); font-size: var(--f-sm); }
  td.actions { display: flex; gap: var(--s-1); align-items: center; }
  td.actions form { margin: 0; }
  td.actions > * + * { margin-left: 0; }
}

/* ---------------------------------------------------------------------------
   기능 카드 색조 — 홈 화면

   메뉴가 열 개를 넘으면 글자만으로는 "아까 그 카드"를 다시 찾기 어렵다.
   색을 하나 더 얹으면 위치보다 색으로 먼저 찾게 된다(색은 보조 신호일 뿐이라
   제목·아이콘이 언제나 같은 뜻을 말한다 — 색만으로 구분되는 것은 없다).

   실제 색칠은 .menu-card 가 --card-tint 하나로 처리한다(테두리 45% · 아이콘
   원 12%). 여기서는 그 값만 갈아끼운다. 원색을 면에 바르지 않으므로 글자
   대비는 카드마다 다시 잴 필요가 없다.

   색은 <b>열넷</b>이다. 원래 다섯이었고, 메뉴가 늘면서 같은 색이 한 격자 안에
   두 번 서기 시작해 늘렸다(2026-08-23).

   고른 기준 둘.
     · 흰 바탕 3:1 이상 — 테두리와 아이콘이 보여야 한다. 괄호 안이 실측값이다
     · 어두운 테마(dark·midnight)에서도 재 두었다. --card-tint 는 테마를 타지
       않는 고정값이라 두 배경에서 모두 서야 한다. blue 가 2.66 으로 가장
       낮은데 이것은 원래 있던 값이라 그대로 둔다 — 새로 넣는 색은 그보다
       낮지 않게 골랐다(slate 는 600 이 2.36 이라 500 으로 내렸다)

   <b>색이 많다고 좋은 것은 아니다.</b> 열 개를 넘으면 색으로 찾는 일 자체가
   흐려진다 — 색은 보조 신호이고 제목·아이콘이 언제나 같은 뜻을 말한다.
   한 격자 안에서 겹치지 않게 고르는 것이 개수보다 중요하다. */
/* 흰 5.70 · 다크 3.13 */  .tint-violet  { --card-tint: #7c3aed; }
/* 흰 3.56 · 다크 5.02 */  .tint-orange  { --card-tint: #ea580c; }
/* 흰 5.02 · 다크 3.56 */  .tint-green   { --card-tint: #15803d; }
/* 흰 6.70 · 다크 2.66 */  .tint-blue    { --card-tint: #1d4ed8; }
/* 흰 5.02 · 다크 3.56 */  .tint-rust    { --card-tint: #b45309; }
/* 흰 4.83 · 다크 3.70 */  .tint-red     { --card-tint: #dc2626; }
/* 흰 4.60 · 다크 3.88 */  .tint-pink    { --card-tint: #db2777; }
/* 흰 4.71 · 다크 3.79 */  .tint-fuchsia { --card-tint: #c026d3; }
/* 흰 6.29 · 다크 2.84 */  .tint-indigo  { --card-tint: #4f46e5; }
/* 흰 3.68 · 다크 4.85 */  .tint-cyan    { --card-tint: #0891b2; }
/* 흰 5.47 · 다크 3.26 */  .tint-teal    { --card-tint: #0f766e; }
/* 흰 4.99 · 다크 3.58 */  .tint-lime    { --card-tint: #4d7c0f; }
/* 흰 4.92 · 다크 3.63 */  .tint-yellow  { --card-tint: #a16207; }
/* 흰 4.76 · 다크 3.75 */  .tint-slate   { --card-tint: #64748b; }

/* ---------------------------------------------------------------------------
   영수증 미리보기 묶음

   .receipt-canvas(그림)와 그 아래 보조 동작 줄(회전·다시 선택)을 한 덩어리로
   묶는다. 묶지 않으면 버튼이 다음 입력 칸에 딸린 것처럼 붙어 보인다.
   --------------------------------------------------------------------------- */
.receipt-preview {
  padding: var(--s-3);
  background: var(--c-subtle);
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
}

/* 읽은 원문처럼 줄바꿈이 뜻을 갖는 글. 가로로 넘치면 잘라 내지 말고 접는다 —
   영수증 원문은 한 줄이 길어 가로 스크롤이 생기면 읽는 자리를 잃는다 */
.u-pre {
  margin: var(--s-2) 0 0;
  padding: var(--s-3);
  background: var(--c-subtle);
  border-radius: var(--r-sm);
  white-space: pre-wrap;
  word-break: break-all;
  color: var(--c-muted);
  max-height: 240px;
  overflow-y: auto;
}

/* ---------------------------------------------------------------------------
   스타일 가이드 화면 전용
   --------------------------------------------------------------------------- */

/* --- 문서 제목 단계 ------------------------------------------------------ */
/* 이 문서는 장(7) → 절(34) → 항 세 겹이라 업무 화면보다 제목이 한 겹 깊다.
   그렇다고 장 제목을 h1 으로 올리면 문서의 최상위가 여덟 갈래로 갈라져,
   제목만 훑어 읽는 사람(보조기술 사용자 포함)에게는 여덟 개의 다른 문서처럼
   들린다 — 홈 화면에서 h1 을 하나로 줄인 것과 같은 이유다.

   그래서 구조(h1 > h2 > h3 > h4)는 문서 그대로 두고 크기만 업무 화면의 계단으로
   되돌린다. 문서와 화면이 같은 글자 계단을 쓰게 하는 것이 목적이다. */
.sg-doc h2 { font-size: var(--f-xl); letter-spacing: var(--ls-head); }
.sg-doc h3 { font-size: var(--f-lg); font-weight: var(--fw-bold); letter-spacing: var(--ls-head);
             margin: var(--s-8) 0 var(--s-3); }
.sg-doc h4 { font-size: var(--f-md); font-weight: var(--fw-semibold); color: var(--c-text);
             margin: var(--s-6) 0 var(--s-2); }
/* 카드의 첫 제목은 카드 패딩 위에 여백을 겹치지 않는다(업무 화면과 같은 처리) */
.sg-doc .card > h3:first-child, .sg-doc .card > h4:first-child { margin-top: 0; }
.sg-section { padding: var(--s-5) 0; border-bottom: 1px dashed var(--c-border); }
.sg-section:last-child { border-bottom: 0; }
.sg-row { display: flex; gap: var(--s-2); flex-wrap: wrap; align-items: center; }
.sg-swatch {
  width: 84px; height: var(--h-lg); border-radius: var(--r-sm);
  border: 1px solid var(--c-border); display: flex; align-items: flex-end;
  justify-content: center; padding-bottom: var(--s-1);
  font-size: var(--f-xs); letter-spacing: var(--ls-flat);
  color: #fff; text-shadow: 0 1px 2px rgba(0, 0, 0, .4);
}
.sg-note { color: var(--c-muted); font-size: var(--f-sm); margin: var(--s-1) 0 var(--s-3); }
/* 가이드 표본용 — 떠 있는 컴포넌트를 문서 흐름 안에 세워 보여 준다 */
.sg-fab-demo .fab { position: static; margin-left: 0; margin-top: 0; }

/* --- 목차 -------------------------------------------------------------- */
/* 문서가 34개 표본으로 길어졌다. 맨 위에서 원하는 절로 바로 뛰게 한다 */
.sg-toc {
  padding: var(--s-4);
  background: var(--c-subtle);
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
  margin-bottom: var(--s-6);
}
.sg-toc .filter-label { margin-top: var(--s-3); }
.sg-toc .filter-label:first-child { margin-top: 0; }

/* --- 색 견본 ------------------------------------------------------------ */
/* 견본 이름은 흰 글자 + 그림자로 얹는다(.sg-swatch). 옅은 면 위에서는 흰 글자가
   읽히지 않으므로 그 견본만 글자색을 본문색으로 되돌린다 */
.sg-sw-primary       { background: var(--c-primary); }
.sg-sw-primary-dark  { background: var(--c-primary-dark); }
.sg-sw-primary-light { background: var(--c-primary-light); }
.sg-sw-accent        { background: var(--c-accent); }
.sg-sw-accent-dark   { background: var(--c-accent-dark); }
.sg-sw-focus         { background: var(--c-focus); }
.sg-sw-gold          { background: var(--c-gold); }

.sg-sw-bg            { background: var(--c-bg); }
.sg-sw-surface       { background: var(--c-surface); }
.sg-sw-subtle        { background: var(--c-subtle); }
.sg-sw-border        { background: var(--c-border); }
.sg-sw-border-strong { background: var(--c-border-strong); }
.sg-sw-muted         { background: var(--c-muted); }
.sg-sw-text          { background: var(--c-text); }

.sg-sw-ok            { background: var(--c-ok); }
.sg-sw-ok-bg         { background: var(--c-ok-bg); }
.sg-sw-warn          { background: var(--c-warn); }
.sg-sw-warn-bg       { background: var(--c-warn-bg); }
.sg-sw-danger        { background: var(--c-danger); }
.sg-sw-danger-bg     { background: var(--c-danger-bg); }
.sg-sw-danger-solid  { background: var(--c-danger-solid); }

/* 옅은 면 견본은 흰 글자가 읽히지 않는다 — 이 칸만 본문색으로 되돌린다 */
.sg-sw-primary-light,
.sg-sw-bg, .sg-sw-surface, .sg-sw-subtle, .sg-sw-border,
.sg-sw-ok-bg, .sg-sw-warn-bg, .sg-sw-danger-bg {
  color: var(--c-text);
  text-shadow: none;
}

/* --- 글자 계단 ---------------------------------------------------------- */
/* 표본은 실제 토큰 그대로 — 여기 숫자를 손으로 적으면 토큰을 바꿔도 문서는
   옛 값을 계속 보여 준다 */
.sg-t-2xl  { font-size: var(--f-2xl);  font-weight: var(--fw-bold); line-height: var(--lh-tight); letter-spacing: var(--ls-head); margin: 0; }
.sg-t-hero { font-size: var(--f-hero); font-weight: var(--fw-bold); line-height: var(--lh-head);  letter-spacing: var(--ls-head); margin: 0; }
.sg-t-xl   { font-size: var(--f-xl);   font-weight: var(--fw-bold); line-height: var(--lh-head);  letter-spacing: var(--ls-head); margin: 0; }
.sg-t-lg   { font-size: var(--f-lg);   font-weight: var(--fw-bold); line-height: var(--lh-head);  letter-spacing: var(--ls-head); margin: 0; }
.sg-t-md   { font-size: var(--f-md);   font-weight: var(--fw-semibold); line-height: var(--lh-head);  margin: 0; }
.sg-t-base { font-size: var(--f-base); line-height: var(--lh-body); letter-spacing: var(--ls-body); margin: 0; }
.sg-t-sm   { font-size: var(--f-sm);   line-height: var(--lh-body); margin: 0; }
.sg-t-xs   { font-size: var(--f-xs);   line-height: var(--lh-body); letter-spacing: var(--ls-flat); margin: 0; }

/* --- 눈금(간격·모션·리듬 공용) ------------------------------------------ */
/* 이름(코드) · 막대 · 설명 세 칸. 이름 칸을 고정폭으로 잡아야 막대의 시작점이
   줄마다 같아지고, 그래야 길이 차이가 눈에 들어온다 */
.sg-scale { display: flex; flex-direction: column; gap: var(--s-2); }
/* QR 견본 자리 — 실제 QR 대신 같은 크기의 뼈대를 보여 준다(240 = --sel 과 무관한
   QR 전용 크기로, .qr-box img 와 같은 값이다) */
.sg-qr-sample { width: 240px; height: 240px; }
.sg-scale-row { display: flex; align-items: center; gap: var(--s-3); }
.sg-scale-row > code { flex: 0 0 88px; font-size: var(--f-xs); }
.sg-scale-row > span:last-child { flex: 1 1 auto; }

.sg-bar { height: var(--s-3); background: var(--c-accent); border-radius: var(--r-xs); flex: 0 0 auto; }
.sg-bar-1  { width: 4px; }
.sg-bar-2  { width: 8px; }
.sg-bar-3  { width: 12px; }
.sg-bar-4  { width: 16px; }
.sg-bar-5  { width: 20px; }
.sg-bar-6  { width: 24px; }
.sg-bar-8  { width: 32px; }
.sg-bar-10 { width: 40px; }
.sg-bar-12 { width: 48px; }

/* --- 모서리 ------------------------------------------------------------- */
.sg-shape {
  width: var(--s-12); height: var(--s-12);
  display: flex; align-items: center; justify-content: center;
  background: var(--c-subtle); border: 1px solid var(--c-border-strong);
  font-size: var(--f-xs); color: var(--c-muted);
}
.sg-r-xs   { border-radius: var(--r-xs); }
.sg-r-sm   { border-radius: var(--r-sm); }
.sg-r-md   { border-radius: var(--r-md); }
.sg-r-lg   { border-radius: var(--r-lg); }
.sg-r-full { border-radius: var(--r-full); }

/* --- 층 ----------------------------------------------------------------- */
/* 그림자는 옆 칸과 나란히 놓아야 차이가 보인다. 바닥을 --c-bg 로 깔아
   흰 카드가 실제 화면과 같은 조건에서 떠 보이게 한다 */
.sg-layers { background: var(--c-bg); padding: var(--s-4); border-radius: var(--r-md); }
.sg-layer {
  flex: 1 1 132px;
  display: flex; flex-direction: column; gap: var(--s-1);
  padding: var(--s-3);
  background: var(--c-surface);
  border-radius: var(--r-md);
  font-size: var(--f-sm); font-weight: var(--fw-semibold);
}
.sg-layer span { font-weight: 400; font-size: var(--f-xs); color: var(--c-muted); }
.sg-layer-0    { background: var(--c-bg); }
.sg-layer-line { border: 1px solid var(--c-border); }
.sg-layer-1    { box-shadow: var(--shadow-1); }
.sg-layer-2    { box-shadow: var(--shadow-2); }
.sg-layer-3    { box-shadow: var(--shadow-3); }

/* --- 모션 --------------------------------------------------------------- */
/* 시간은 글로 읽어서는 감이 오지 않는다. 눌러 보면 그 시간만큼 공이 간다.
   button 이라 터치·키보드(포커스)에서도 작동한다 — hover 만 걸면 휴대폰에서는
   아무 일도 일어나지 않는다 */
.sg-track {
  flex: 1 1 auto;
  position: relative;
  height: var(--s-6);
  padding: 0;
  background: var(--c-subtle);
  border: 1px solid var(--c-border);
  border-radius: var(--r-full);
  cursor: pointer;
}
@media (hover: hover) { .sg-track:hover { background: var(--c-subtle); } }
.sg-ball {
  position: absolute; top: 50%; left: 4px;
  width: var(--s-4); height: var(--s-4);
  margin-top: calc(var(--s-2) * -1);
  border-radius: 50%;
  background: var(--c-accent);
  transition-property: transform;
  transition-timing-function: var(--ease-out);
}
/* 공이 트랙 끝까지 가도록 — 트랙 폭은 화면마다 다르므로 폭 기준으로 민다 */
.sg-track:hover .sg-ball,
.sg-track:focus-visible .sg-ball,
.sg-track:active .sg-ball { transform: translateX(calc(var(--s-12) * 3)); }
.sg-d-instant { transition-duration: var(--dur-instant); }
.sg-d-fast    { transition-duration: var(--dur-fast); }
.sg-d-base    { transition-duration: var(--dur-base); }
.sg-d-slow    { transition-duration: var(--dur-slow); }
.sg-e-out     { transition-timing-function: var(--ease-out); }
.sg-e-in      { transition-timing-function: var(--ease-in); }
.sg-e-in-out  { transition-timing-function: var(--ease-in-out); }

/* --- 리듬(높이) --------------------------------------------------------- */
.sg-hbar { width: var(--s-12); background: var(--c-primary-light); border: 1px solid var(--c-border-strong); border-radius: var(--r-xs); flex: 0 0 auto; }
.sg-hb-lg    { height: var(--h-lg); }
.sg-hb-input { height: var(--h-input); }
.sg-hb-tap   { height: var(--h-tap); }
.sg-hb-btn   { height: var(--h-btn); }
.sg-hb-chip  { height: var(--h-chip); }

/* --- 상태 표본 ---------------------------------------------------------- */
/* hover·active·focus 는 손을 올려야만 보이는 상태라 문서로는 비교가 안 된다.
   그래서 각 상태를 강제로 고정한 표본을 나란히 세운다(실제 버튼 규칙을 손으로
   다시 적은 것이므로, 버튼 규칙을 바꾸면 여기도 함께 고친다) */
.sg-states { align-items: flex-start; }
.sg-state { flex: 1 1 120px; }
.sg-state button {
  width: 100%;
  height: var(--h-btn);
  border: 0; border-radius: var(--r-sm);
  background: var(--c-primary); color: var(--c-on-primary);
  font-size: var(--f-base); font-weight: var(--fw-semibold);
}
.sg-is-hover  { background: var(--c-primary-dark); }
.sg-is-active { transform: scale(.98); }
.sg-is-focus  { outline: 2px solid var(--c-focus); outline-offset: 2px; }

/* --- 포커스 링 ---------------------------------------------------------- */
.sg-w-sm { max-width: 200px; }
.sg-ring {
  display: inline-flex; align-items: center;
  height: var(--h-btn); padding: 0 var(--s-3);
  border-radius: var(--r-sm);
  background: var(--c-surface);
  font-size: var(--f-sm);
  outline: 2px solid var(--c-focus); outline-offset: 2px;
}

/* --- 스켈레톤 표본 ------------------------------------------------------ */
.sg-sk-1 { height: var(--s-4); width: 60%; }
.sg-sk-2 { height: var(--s-4); width: 40%; }
