/* >>> BEGIN GENERATED TOKENS — design/tokens.css 가 SSOT. 직접 수정 금지 >>> */
/* ══════════════════════════════════════════════════════════════════
   Hyphen 관리자 웹 디자인 토큰 — SSOT (단일 원본)

   이 파일이 정본이다. static/style.css 의 :root 블록은
   `python design/sync.py` 가 이 파일에서 생성한 사본이며 직접 수정 금지.

   값 변경 절차: 이 파일 수정 → design/gallery.html 로 데모 확인
   → 사용자 승인 → `python design/sync.py` 배포 → 커밋.
   상세 = design/SSOT.md
   ══════════════════════════════════════════════════════════════════ */

/* ──────────────────────────────────────────────────────────────────
   2026-08-12 압축 (사용자 지시) — 토큰 40개 → 정본 12개

   표면·텍스트 3 · 브랜드·상태 3 · 타이포 6. 이 아래 §정본 이 전부다.
   위계는 **색이 아니라 크기와 굵기**로 만든다.

   2026-08-13: **폐기 별칭 블록 전부 삭제 완료** (사용자 지시).
   --muted 194 · --accent 42 · --danger 33 · --surface 29 … 총 362곳을 정본
   이름으로 바꿨다. 별칭은 이미 정본을 가리키고 있었으므로 계산된 색·크기는
   한 곳도 바뀌지 않는다 — 이름만 사라진다 (전 화면 3,954노드 실측 대조).
   되살리지 말 것. lint §4.1 이 옛 이름 사용을 위반으로 잡는다.
   ────────────────────────────────────────────────────────────────── */
:root {
  /* ══ 정본 — 표면·텍스트 3 ══ */
  --bg: #FFFFFF;        /* 모든 면 — 페이지·카드·모달·입력 */
  --fg: #18181B;        /* 모든 글자 */
  --border: #E4E4E7;    /* 모든 선 + 옅은 바탕(hover·표 머리) */

  /* ══ 정본 — 브랜드·상태 3 ══ */
  --primary: #CC1F1F;   /* 브랜드 · 강조 · 위험(삭제) 겸용 — 앱·홈페이지와 같은 값 (2026-08-28) */
  --success: #10B981;   /* 완료 · 정상 */
  --warning: #F59E0B;   /* 주의 · 임박 */

  /* ══ 정본 — 타이포 6단 ══ */
  --fs-h1: 22px;        /* 페이지 제목 */
  --fs-h2: 18px;        /* 모달 제목 */
  --fs-h3: 16px;        /* 카드 제목 */
  --fs-body: 14px;      /* 본문 */
  --fs-sm: 13px;        /* 표 · 버튼 */
  --fs-xs: 12px;        /* 라벨 · 보조 설명 · 뱃지 */

  /* ── 글꼴 (§2-B-모노 차단이 이 토큰을 쓴다 — 없으면 규칙이 무효가 된다) ──
     ※ Pretendard 를 1순위로 올리는 것은 별도 판단 항목 (SSOT §5 D-3) ── */
  --font: -apple-system, BlinkMacSystemFont, "Pretendard", "Segoe UI", sans-serif;

  /* ── 한글 자간 (design-block §2.4 — 항상 음수) ── */
  --ls-title: -0.02em;   /* 18px+ 제목 */
  --ls-body: -0.01em;    /* 12~16px 본문 */

  /* ── radius (design-block §4.3) ── */
  --r-sm: 4px;
  --r-md: 8px;
  --r-lg: 12px;

  /* ── spacing (4px 그리드) ── */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-7: 40px;

  /* 보조 설명 글자의 흐림 정도 (2026-08-13 사용자 결정 — 갤러리 §15 B안).
     색을 늘리지 않고 위계를 만든다. 흰 바탕 대비 5.5:1 로 WCAG AA 통과. */
  --dim: 0.65;

  /* ── 컨트롤 높이 — 하나 (2026-09-04 D115 · 사용자 "버튼 크기나 그런것도 좀 통일") ──
     한 줄에 나란히 서는 것은 전부 이 높이다: 버튼 · 입력칸 · 검색칸 · 칩 · 탭칩 ·
     페이저 화살표. 실측해 보니 33 / 35 / 29 / 26 / 34 / 28 여섯 가지였고,
     그래서 액션바·필터바의 밑선이 컨트롤마다 어긋나 있었다.
     34 인 이유 = 가장 많이 쓰이는 셋(버튼 33 · 입력칸 35 · 탭칩 34)의 한가운데라
     대다수가 ±1 만 움직인다. 글자 13px 기준으로 위아래 여백도 넉넉하다. */
  --ctrl-h: 34px;

  /* ── 자리 예약 높이 (D115 · 레이아웃 시프트 차단) ──
     로딩·빈·에러가 같은 자리에서 갈아 끼워질 때 **셋이 같은 바닥**을 갖게 하는 값.
     없으면 스켈레톤 → 문구 → 실제 내용으로 바뀔 때마다 아래가 튄다.
     정본 규칙 = design/SSOT.md §17. */
  --slot-msg: 76px;          /* .table-msg · .panel-msg — 한 줄 문구 자리 */
  --slot-msg-roomy: 132px;   /* .is-roomy — 두 줄 + 다시 시도 링크가 들어가는 자리 */
  --slot-banner: 37px;       /* .page-banner — 없을 때도 이 높이를 비워 둔다 */

  /* 카카오 브랜드 색 — 우리 팔레트가 아니라 **남의 브랜드 규정** 값이라
     의미 토큰(primary 등)에 섞지 않고 이름을 그대로 쓴다.
     2026-08-23 — 랜딩과 로그인이 같은 hex 를 각자 적고 있었다 (§0-B). */
  --kakao: #FEE500;
  --kakao-hover: #F2D900;
  --kakao-fg: #191919;

  --shadow: 0 1px 3px rgba(0,0,0,0.08);
  /* 떠 있는 것(모달·토스트·고정 배너) 전용. 2026-08-12 신설 —
     같은 성격의 그림자가 화면마다 .15 / .12 / .08 로 제각각이었다. */
  --shadow-lift: 0 8px 24px rgba(0,0,0,0.15);
}
/* <<< END GENERATED TOKENS <<< */

* { box-sizing: border-box; margin: 0; padding: 0; }

html, body {
  height: 100%;
  font-family: -apple-system, BlinkMacSystemFont, "Pretendard", "Segoe UI", sans-serif;
  background: var(--bg);
  color: var(--fg);
  -webkit-font-smoothing: antialiased;
}

.app {
  display: grid;
  grid-template-columns: 220px 1fr;
  min-height: 100vh;
}

/* ── 사이드바 ── */
.sidebar {
  background: var(--bg);
  border-right: 1px solid var(--border);
  padding: 24px 0;
  display: flex;
  flex-direction: column;
}

/* (구 `.brand { display:none }` — 2026-09-05 D118 삭제. 마크업은 2026-08-19 에
   이미 지웠고 "숨기는 규칙" 만 남아 있었다. 아무것도 안 하는 정의다.) */

/* 머리 카드 — 2026-09-30 사용자 선택 'A3 한 카드 + 번호 띠'.
   높이는 어떤 값이 와도 같다(자리 예약): 줄마다 line-height 고정 + 한 줄 말줄임,
   번호 띠는 늘 있다. 구 `.gym-row` 는 삭제 (조건부 줄이라 메뉴가 밀렸다). */
.gym-meta {
  padding: 0 var(--sp-3) var(--sp-3);
  border-bottom: 1px solid var(--border);
}

.gym-card {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  overflow: hidden;
}

.gym-card-body {
  padding: var(--sp-2) var(--sp-3);
}

.gym-brand {
  height: 16px;
}

/* 체육관 HYPHEN 로고 (2026-10-02 — 폰 앱과 같은 원본 docs/brand/hyphen-logo.svg 의 생성물 static/brand-logo.svg).
   글자 워드마크 'HYPHEN' 을 대신한다. 크기는 자리마다 --brand-logo-h 하나로 정한다. */
.brand-logo {
  display: block;
  height: var(--brand-logo-h, 16px);
  width: auto;
}

.gym-meta .name,
.gym-meta .boss {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.gym-meta .name {
  margin-top: 2px;
  font-size: var(--fs-body);
  font-weight: 700;
  line-height: 20px;
  color: var(--fg);
}

.gym-meta .boss {
  font-size: var(--fs-xs);
  line-height: 16px;
  color: var(--fg); opacity: var(--dim); }

.gym-code {
  display: flex;
  justify-content: space-between;
  align-items: center;
  height: 26px;
  padding: 0 var(--sp-3);
  background: var(--border);
  font-size: var(--fs-xs);
}

.gym-code b {
  font-size: var(--fs-sm);
  font-weight: 800;
}

.nav {
  padding: 16px 0;
  flex: 1;
}

.nav a {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 3px 20px;  /* 사용자 요청 2026-05-26 — a 들 사이 간격 반으로 축소 (6→3px) */
  color: var(--fg);
  text-decoration: none;
  font-size: 13px;
  line-height: 1.3;  /* 1.5→1.3 (행 사이 압축) */
  min-height: 22px;  /* 32→22 (반으로 줄임, PC 마우스이므로 클릭 영역 충분) */
  border-left: 2px solid transparent;
  transition: all 0.15s;
}

.nav a:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: -2px;
}

.btn:focus-visible, .chip:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

/* (prefers-contrast: more 오버라이드는 생성 토큰 블록으로 이동 — design/tokens.css) */

.nav a:hover {
  background: var(--border);
  color: var(--fg);
}

.nav a.is-active {
  color: var(--fg);
  background: var(--border);
  border-left-color: var(--primary);
}

.nav .group {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: var(--ls-body);
  color: var(--fg);
  padding: 14px 20px 4px;  /* 사용자 요청 2026-05-26 — 그룹 사이 간격 2배 (7→14px) */
}

.sidebar .footer {
  padding: 12px 16px;
  border-top: 1px solid var(--border);
  font-size: 11px;
  color: var(--fg); opacity: var(--dim); }

/* (2026-08-26 '처음 시작하기' nav 링크 삭제로 .nav-onboarding 규칙 제거 —
   /onboarding 라우트·템플릿은 존치, 링크 복구 시 규칙도 같이 되살릴 것) */

/* ── 메인 ── */
/* D213 — 그리드 칸(1fr)의 기본 `min-width: auto` 때문에 넓은 표가 들어오면 main 이
   칸 밖으로 자라고, 페이지 전체가 가로로 밀렸다 (회원 명단 1280px 화면 실측: main
   1352px). 0 으로 내려 칸 안에 가두면 넘치는 표만 제 그릇에서 가로로 흐른다 —
   머리·필터 줄은 제자리에 남는다. */
.main { padding: 32px 40px; min-width: 0; }

/* D286 — 처음 설정 안내 띠. 체크리스트의 [바로 가기] 로 온 화면 맨 위 한 줄.
   높이 고정 — 문장이 길어도 한 줄(넘치면 …)이라 본문 시작 자리가 늘 같다. */
.setup-band {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  height: 48px;
  box-sizing: border-box;
  padding: 0 var(--sp-4);
  margin-bottom: var(--sp-5);
  background: var(--border);
  border: 1px solid transparent;
  border-radius: var(--r-md);
  font-size: var(--fs-sm);
  font-weight: 700;
}
.setup-band-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.setup-band .btn { flex: none; }
/* D287 — [다음 단계] 를 눌렀는데 그 단계가 안 끝나 돌아온 띠. 자리·높이는 그대로, 글자와 테두리만 빨강. */
.setup-band.is-warn { color: var(--primary); border-color: var(--primary); background: var(--bg); }
/* 빨간 * — 처음 설정에서 '여기가 안 됐다' 는 표시. 안 된 칸·버튼 옆, 경고 띠, 체크리스트 줄에 같은 것 하나. */
.need-mark { color: var(--primary); font-weight: 800; }

/* D289 — 도움말. 화면 오른쪽 아래에 고정된 버튼과 그 위에 뜨는 작은 판.
   둘 다 본문 위에 떠 있어 배치를 밀지 않는다. 모달(100)보다 아래 — 모달이 열리면 가려진다. */
.help-btn {
  position: fixed; right: 24px; bottom: 24px; z-index: 90;
  padding: var(--sp-2) var(--sp-4);
  background: var(--bg); color: var(--fg);
  border: 1px solid var(--border); border-radius: var(--r-md);
  font: inherit; font-size: var(--fs-sm); font-weight: 700;
  cursor: pointer;
}
.help-btn:hover, .help-btn[aria-expanded="true"] { border-color: var(--fg); }
/* 토스트는 도움말 버튼 **위에서** 쌓인다 (2026-10-01 — 같은 오른쪽 아래 자리라 토스트가 버튼을 덮었다).
   그릇의 자리(static/toast.js `bottom-24`)를 이 한 줄이 버튼 높이만큼 올린다. */
#toastContainer { bottom: 72px; }
.help-panel {
  position: fixed; right: 24px; bottom: 72px; z-index: 90;
  width: 320px; max-width: calc(100vw - 48px);
  padding: var(--sp-4);
  background: var(--bg); color: var(--fg);
  border: 1px solid var(--border); border-radius: var(--r-lg);
  box-shadow: var(--shadow-lift);
}
.help-head { display: flex; align-items: center; justify-content: space-between; font-weight: 800; margin-bottom: var(--sp-2); }
.help-head .modal-close { padding: 0 var(--sp-1); line-height: 1; }
.help-what { font-size: var(--fs-sm); line-height: 1.6; }
.help-label { margin-top: var(--sp-3); font-size: var(--fs-xs); font-weight: 700; opacity: var(--dim); }
.help-panel ul { margin: var(--sp-1) 0 0; padding-left: 18px; font-size: var(--fs-sm); line-height: 1.7; }
.help-link { display: inline-block; margin-top: var(--sp-3); font-size: var(--fs-sm); font-weight: 700; color: var(--primary); text-decoration: none; }

.page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 24px;
}

.page-title {
  font-size: 22px;
  font-weight: 800;
  letter-spacing: -0.3px;
}

.page-sub {
  font-size: 12px;
  color: var(--fg);
  margin-top: 4px;
  letter-spacing: var(--ls-body); opacity: var(--dim); }

/* ══ 버튼 — 단 하나 (2026-08-12 압축, 사용자 지시) ══
   변형도 작은 크기도 없다. 예외 없음.
   옛 이름(.btn-primary·.btn-ghost·.btn-danger·.btn-sm·.btn-danger-outline)은
   2026-08-12 마크업 59곳에서 전부 지웠고, 여기 선택자 별칭도 함께 없앴다.
   되살리지 말 것 — 변형이 필요해 보이면 글자와 순서로 구분한다 (SSOT §8.3). */
.btn {
  display: inline-flex;
  align-items: center;
  /* 높이는 토큰 하나 — 한 줄에 서는 컨트롤이 전부 같은 밑선을 갖는다 (D115) */
  height: var(--ctrl-h);
  box-sizing: border-box;
  background: var(--primary);
  color: var(--bg);
  border: 1px solid transparent;
  padding: 0 16px;
  border-radius: var(--r-md);
  font-size: var(--fs-sm);
  font-weight: 700;
  letter-spacing: var(--ls-body);
  cursor: pointer;
  transition: opacity 0.15s;
  text-decoration: none;
  white-space: nowrap;
}

.btn:hover { opacity: 0.86; background: var(--primary); }

/* ── 기간 페이저 (주/월 이동 화살표 — 수업 관리·일정 달력 공용 SSOT) ──
   2026-08-21 사용자 지시: 수업 관리의 큰 빨간 .btn 화살표가 글자 대비 비대했고,
   일정 달력은 폐기된 .btn-ghost 를 참조해 무스타일로 떴다 — 한 컴포넌트로 통일.
   화살표 문자도 ‹ › 로 고정 (한쪽은 ‹ 이전, 한쪽은 아이콘만 쓰는 이원화 금지). */
.pager-btn {
  /* 정사각 — 한 변이 컨트롤 높이 (D115. 구 28×28 은 옆 버튼보다 5px 낮았다) */
  width: var(--ctrl-h); height: var(--ctrl-h); padding: 0;
  box-sizing: border-box;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--bg); color: var(--fg);
  border: 1px solid var(--border); border-radius: var(--r-md);
  font-size: var(--fs-body); line-height: 1; cursor: pointer;
}
.pager-btn:hover { background: var(--border); }
/* 페이지 번호 페이저 (2026-09-14 D151 — 회원 목록 10명씩). 화살표·번호는 위 .pager-btn 그대로,
   현재 페이지는 상태 클래스 하나(is-active — 채움). 1페이지뿐이면 그릇은 자리(높이 --ctrl-h)를
   두고 단추만 비운다 — display:none 이면 아래가 34px 튄다 (§17 visibility). 왼쪽 집계 글자는
   유틸(fs-12 mr-auto)로 서고, 단추는 오른쪽 끝에 붙는다. */
.pager {
  display: flex; align-items: center; justify-content: flex-end; gap: 4px;
  margin-top: 12px; height: var(--ctrl-h);
}
.pager.is-empty .pager-btn { visibility: hidden; }
/* 단추 하나도 같은 규칙 — 자리는 두고 보이기만 끈다 (D194 홈 '오늘' 단추: 오늘을 보고 있으면
   누를 일이 없지만 display:none 이면 옆 화살표가 밀린다). */
.btn.is-empty { visibility: hidden; pointer-events: none; }
.pager-btn.is-active { background: var(--fg); color: var(--bg); border-color: var(--fg); cursor: default; }
.pager-btn:disabled { opacity: 0.35; cursor: default; }

/* ── 필터바 ── */
.filterbar {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 16px;
  padding: 10px 16px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}

.filterbar input[type="search"],
.row-between input[type="search"] {   /* D153 — 동작 사전 도구 줄의 검색 칸도 같은 부품 (정의는 여기 하나) */
  flex: 0 0 240px;
  height: var(--ctrl-h);            /* D115 — 구 29 (옆 칩 26 · 버튼 33 과 다 달랐다) */
  box-sizing: border-box;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--fg);
  padding: 0 12px;
  border-radius: var(--r-md);
  font-size: 13px;
  font-family: inherit;
}

.chip {
  display: inline-flex; align-items: center;
  height: var(--ctrl-h);            /* D115 — 구 26 */
  box-sizing: border-box;
  padding: 0 12px;
  border-radius: var(--r-lg);   /* 12px — filter chip 전용 pill (6글자 이내 라벨) */
  font-size: 12px;
  font-weight: 700;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--fg);
  cursor: pointer;
  transition: background 0.12s, color 0.12s, border-color 0.12s;
}

.chip.is-active {
  background: var(--primary);
  color: var(--bg);
  border-color: var(--primary);
}

/* 회원 상세 탭 — `.chip` 전역 핸들러와 분리 (test p6·p8 충돌 방지) */
.tab-chip {
  display: inline-flex; align-items: center;
  height: var(--ctrl-h);            /* D115 — 컨트롤 높이 하나 */
  box-sizing: border-box;
  padding: 0 14px;
  font-size: 13px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--fg);
  cursor: pointer;
  border-bottom: 2px solid transparent;
  font-weight: 700;
}
.tab-chip.is-active {
  color: var(--primary);
  border-bottom-color: var(--primary);
}
.tab-chip:hover { color: var(--fg); }

/* ── 탭 줄 그릇 (2026-09-04 D115) ──────────────────────────────────────
   탭칩을 담는 자리는 이것 하나다. 지금까지 그릇이 둘이었다:
   `.filterbar gap-0`(5곳 — 이름이 '필터바'인데 탭을 담고 있었다)와
   **정의가 아예 없는 `.tabs`**(2곳 — 유령 클래스라 브라우저 기본 블록으로 떴다).
   `.filterbar` 는 검색·칩을 담는 테두리 있는 바로 남기고, 탭은 밑줄만 쓴다. */
.tab-row {
  display: flex;
  align-items: flex-end;
  gap: 0;
  margin-bottom: var(--sp-4);
  border-bottom: 1px solid var(--border);
}

/* (구 `.stat` — 필터바 오른쪽 집계 한 줄. 2026-09-05 D118 삭제: 제품 마크업
   사용처 0 이고 갤러리 시안에서만 살아 있었다. 같은 일은 `.head-stat`.) */

/* ── 테이블 ── */
.card {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  overflow: hidden;
}

/* (구 `.card.has-rail` — 케어 필요 목록의 좌측 색 띠 카드. 2026-09-05 D118 삭제:
   2026-08-26 사용자 지시로 그 화면이 카드 그리드 → 표로 바뀌면서 사용처가 0 이
   됐다. 표의 좌측 띠는 `tr.is-urgent td:first-child` 가 맡는다.) */

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}

thead th {
  text-align: left;
  padding: 12px 16px;
  background: var(--border);
  border-bottom: 1px solid var(--border);
  color: var(--fg);
  font-weight: 700;
  font-size: 11px;
  letter-spacing: var(--ls-body);
  text-transform: uppercase;
}

tbody td {
  padding: 14px 16px;
  border-bottom: 1px solid var(--border);
}

tbody tr:last-child td { border-bottom: 0; }

tbody tr:hover { background: var(--border); }

/* ══ 뱃지 — 셋뿐 (2026-08-12 압축, 사용자 지시) ══
   pill 도 배경도 테두리도 없다. **글자만** 보인다. 뜻은 색 하나로 읽는다.
     .badge            기본  — 그대로의 상태 (글자색 --fg)
     .badge.is-warn    주의  — 임박·확인 필요 (--warning)
     .badge.is-danger  위험  — 만료·해지·이탈 (--primary)
   등급(RX·Scaled)은 뱃지가 아니라 그냥 굵은 글자다 — .level 하나.
   2026-08-13 §9 C-5·N-2 완료: 옛 이름(.lv·.lv-*·.churn-*·.stat-*) 전량 삭제.
   초록 뱃지는 규격에 없다 — 정상은 조용하고 문제만 눈에 띄어야 한다. */
.badge,
.level {
  display: inline;
  background: none;
  border: 0;
  padding: 0;
  border-radius: 0;
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: var(--ls-body);
  color: var(--fg);
}

.badge.is-warn   { color: var(--warning); }
.badge.is-danger { color: var(--primary); }

/* ── Skeleton loader (shimmer, 그라디언트 금지 — 단색+위치 이동) ── */
.skeleton {
  background: var(--border);
  position: relative;
  overflow: hidden;
  border-radius: 4px;
}
.skeleton::after {
  content: '';
  position: absolute;
  inset: 0;
  background-color: rgba(0, 0, 0, 0.06);
  transform: translateX(-100%);
  animation: skeleton-shimmer 1.5s infinite;
}
@keyframes skeleton-shimmer {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(100%); }
}
.skeleton-line        { height: 14px; margin: 8px 0; }
.skeleton-line.wide   { width: 80%; }
.skeleton-line.mid    { width: 60%; }
.skeleton-line.short  { width: 40%; }
/* (구 `.skeleton-block` 48px — 2026-09-05 D118 삭제. 사용처 0. 로딩 자리는
   `.table-msg.table-skel-cell` + `.skeleton-line` 조합 하나로 통일돼 있다.) */

/* 로딩 자리 (2026-09-04 D115) — 빈·에러와 **같은 클래스(.table-msg)** 위에 얹어
   높이를 공유한다. 그래서 로딩이 끝나도 아래 내용이 튀지 않는다.
   공용 로더 `fetchListInto`·`fetchTableInto` 가 이 조합을 만든다 (app.js). */
.table-skel-cell { opacity: 1; }
.table-skel-cell .skeleton-line {
  display: block; margin: 6px auto; max-width: 100%;
}
/* 줄 길이는 고정값 — 퍼센트로 두면 넓은 표에서 셋 다 상한에 걸려 같은 길이로 보인다 */
.table-skel-cell .skeleton-line.wide  { width: 420px; }
.table-skel-cell .skeleton-line.mid   { width: 320px; }
.table-skel-cell .skeleton-line.short { width: 200px; }

.actions a {
  color: var(--fg);  /* muted -> fg 가독성 보정 (test C5) */
  text-decoration: none;
  font-size: 13px;   /* 12 -> 13 */
  margin-right: 12px;
}

.actions a:hover { color: var(--primary); text-decoration: underline; }

/* 글자만으로 서는 인라인 동작 버튼 — 표 밖에서 쓰는 `.actions a` 의 <button> 판.
   2026-08-12: 연필 픽토그램(✏) 자리를 "편집" 글자로 바꾸며 신설 (§1.4). */
.link-btn {
  background: none; border: 0; padding: 0;
  color: var(--fg); font-family: inherit;
  font-size: var(--fs-sm); letter-spacing: var(--ls-body);
  cursor: pointer;
}
.link-btn:hover { color: var(--primary); text-decoration: underline; }
/* 모달 바닥의 '취소' 는 글자로 두되(2026-08-13 사용자 결정 — 갤러리 §16),
   누를 곳이 너무 작아지지 않게 여백을 준다. 빨간 버튼은 여전히 하나뿐이다. */
.modal-footer .link-btn, .modal-actions .link-btn { padding: 8px 12px; }

/* 행 클릭 가능 표시 (test C2 — 회원 행 클릭 무반응) */
tbody tr.clickable { cursor: pointer; }
tbody tr.clickable:hover td:first-child b { color: var(--primary); }

/* (구 `.inline-edit` + `.saving`·`.saved` — 표 안에서 바로 고치는 입력칸.
   2026-09-05 D118 삭제: 제품에 그런 화면이 없다. 짝이던 `makeInlineEditable` 은
   2026-08-19 에 호출처 0 으로 이미 지워졌고 CSS 만 남아 있었다. 값 수정은 모달 폼
   (`.form-input`), 저장 표시는 토스트가 맡는다. `.saving`·`.saved` 는 맨몸
   상태어라 §7.3 에도 어긋났다 — 되살리면 `is-saving`·`is-saved`.) */

/* ── 토스트 — 한 벌 (2026-09-04 D116) ─────────────────────────────────────
   2026-08-29 사용자 지시 "우측 하단 토스트 모든 크기 전부 2배로 더 크게".

   **D116 이원화 정리.** 여기까지 이 블록은 *아무도 안 쓰는 사본* 이었다.
   실물은 `_layout.html` 의 `showToast`·`showSyncToast` 가 `style.cssText` 로
   각자 그렸고, 둘끼리도 값이 달랐다 —
     showSyncToast  패딩 20×28 · 간격 16 · 최대폭 680 · 테두리 전체가 톤색
     showToast      패딩 28×32 · 간격 24 · 최대폭 640 · 테두리는 --border
     이 CSS         패딩 24×36 · 왼쪽선 8 · 위로 솟는 애니메이션
   갤러리는 이 사본을 보여 주고 있어서 **시안이 실물과 달랐다**.
   이제 셋을 하나로 합쳤다 — 두 함수가 이 클래스를 달고, 인라인 style 은 0 이다.

   자리 잡기(fixed·우하단·간격·z-index)는 그릇 `#toastContainer` 가 한다.
   여기서 다시 position 을 잡으면 그릇 밖으로 튀어 겹친다. */
.toast {
  --toast-tone: var(--fg);           /* 톤은 상태 클래스가 이 변수만 바꾼다 */
  display: flex; align-items: flex-start; gap: var(--sp-3);
  min-width: 408px; max-width: 578px;
  padding: 17px 24px;
  background: var(--bg); color: var(--fg);
  border: 1px solid var(--border);
  border-left: 5px solid var(--toast-tone);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-lift);
  font-size: var(--fs-h2); line-height: 1.4;
  pointer-events: auto; cursor: default;
  animation: toast-in 0.2s ease;
}
/* 초록 뱃지는 없지만(§8.4) 토스트는 **사건 알림**이라 톤을 쓴다 —
   화면에 상시 떠 있는 것이 아니라 방금 일어난 일을 5초 알리고 사라진다. */
.toast.is-success { --toast-tone: var(--success); }
.toast.is-warn    { --toast-tone: var(--warning); }
.toast.is-error   { --toast-tone: var(--primary); }
.toast.is-out     { opacity: 0; transition: opacity 0.3s; }

/* 화면 맨 위 실시간 연결 끊김 띠 (D116 — 구 style.cssText 를 부품으로).
   `.page-banner` 와 다른 부품이다: 그쪽은 본문 흐름 안의 한 줄, 이쪽은 창 맨 위
   고정이라 자리 예약 대상이 아니다 (본문을 밀지 않고 위에 얹힌다). */
.sse-banner {
  position: fixed; top: 0; left: 0; right: 0; z-index: 10000;
  display: flex; align-items: center; justify-content: center; gap: var(--sp-3);
  padding: 6px 16px;
  background: var(--bg); color: var(--fg);
  border-bottom: 2px solid var(--warning);
  box-shadow: var(--shadow-lift);
  font-size: var(--fs-xs); text-align: center;
}

.toast-body { flex: 1; min-width: 0; }
.toast-msg  { font-weight: 700; }
.toast-sub  { font-size: var(--fs-h3); margin-top: 5px; line-height: 1.5; }
.toast-time { flex-shrink: 0; font-size: var(--fs-h3); font-variant-numeric: tabular-nums; }

@keyframes toast-in {
  from { transform: translateX(20px); opacity: 0; }
  to   { transform: translateX(0);    opacity: 1; }
}

.note { color: var(--fg); font-size: 12px; opacity: var(--dim); }
/* 안내 한 줄이 '주의' 를 말할 때 — 상태는 클래스 한 단어로 (2026-09-14). */
.note.is-warn { color: var(--warning); opacity: 1; }

/* 2026-08-07 — 모노스페이스(§2.6 절대 금지) 대체. 토큰·타임스탬프·로그처럼
   세로 자릿수를 맞춰야 하는 곳은 Pretendard 유지 + 고정폭 숫자로 처리한다.
   규격 데모 = design/gallery.html §2. */
.tnum {
  font-family: inherit;
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--ls-body);
}

/* §2.6 모노스페이스 절대 금지 — 브라우저 기본값 차단 (2026-08-12).
   code·pre·kbd·samp 는 CSS 를 한 줄도 안 써도 브라우저가 monospace 로 그린다.
   그래서 "monospace 라고 쓴 곳이 없다" 는 검사만으로는 룰이 지켜지지 않았다.
   실제로 coaches.html 의 페어링 코드와 classes.html 의 WOD 본문이 고정폭으로
   나가고 있었다. 자릿수 정렬은 tabular-nums 로 대체한다 (룰이 지시한 방식). */
/* 스타일이 안 붙은 <a> 는 브라우저 기본 파랑(#0000EE)으로 나온다 — 정본 팔레트에
   없는 색이고, 압축 점검에서 12건 잡혔다 (2026-08-12). 클래스 없는 링크만 잡는다
   (.btn·.nav a 처럼 이미 규격을 가진 링크는 건드리지 않는다). */
a:not([class]) { color: var(--primary); text-decoration: underline; }
a:not([class]):hover { opacity: 0.8; }

code, pre, kbd, samp {
  font-family: var(--font);
  font-variant-numeric: tabular-nums;
}

/* 폼 컨트롤도 같은 사각지대다 (2026-08-13 실측). input·select·textarea·button 은
   글꼴을 **상속하지 않고** 브라우저 기본을 쓴다 — `<input type="date">` 와 클래스
   없는 `<textarea>` 가 실제로 monospace 로 그려지고 있었다(§2.6 위반 2건).
   한 줄로 막는다. 개별 글꼴 지정이 필요하면 그 자리에서 덮어쓴다. */
input, select, textarea, button {
  font-family: inherit;
}

/* ══ (구 `.stack` / `.is-tight` / `.is-loose` — 2026-09-05 D118 철회) ══
   2026-08-13 갤러리 §17 에서 두 안을 그려 A안(.stack)으로 승인까지 받았으나,
   그 뒤 3주 동안 **도입한 자리가 0곳**이었다. 승인 당시 이미 "바꿀 수 있는
   자리를 17개 화면에서 찾아보니 0곳" 이라고 적혀 있었고(SSOT.md §9-C), 그
   상태가 그대로 굳었다. 승인받은 이름이라도 안 쓰이면 다음 사람이 "쓰는 것"
   으로 오해하는 사문이 된다 — 그래서 정의를 지운다.
   기록은 지우지 않는다: 결정 경위는 SSOT.md §9-C, 두 안 비교 그림은 갤러리 §17.
   다시 필요해지면 그 절에서 되살린다 (이름 3개도 그대로). */

/* ══ 꼬리표 — 하나뿐 (2026-08-13 사용자 승인, 갤러리 §18) ══
   이름·줄에 붙는 작은 표시(메모·앱 가입·PR·대리). 상태 뱃지(§8.4)와 일이 다르다 —
   뱃지는 "지금 어떤 상태인가", 꼬리표는 "여기 뭐가 붙어 있나" 를 말한다.
   **배경 없음.** 바탕 칠한 라벨은 pill 이라 §2-B 금지고, 꼬리표만 색을 쓰면
   화면에서 제일 눈에 띄는 것이 '메모 있음' 이 되어 만료·정지보다 강해진다.
   2026-08-13 이전에는 같은 일을 다섯 모양(빨간 바탕·테두리·회색 바탕·반투명
   흰 바탕·글자만)으로, 크기도 10·11·12px 로 갈라 그리고 있었다. 되살리지 말 것. */
.tag {
  display: inline-block;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: var(--ls-body);
  padding: 1px 6px;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  color: var(--fg);
  white-space: nowrap;
}
/* D212 — 배지가 **상태 표시**임을 모양으로 말한다. 코치 테스트: "태그 글자를 하나하나 읽어야
   구분되지, 색이나 모양으로는 한눈에 안 갈린다" · "눌러봤는데 반응이 없어 버튼인지 헷갈린다".
   색 토큰이 셋뿐이라 **채움 vs 외곽선**으로 가른다 (앱 D127 행동 배지와 같은 결). 커서는 기본. */
.tag { cursor: default; }
/* `.tag.is-solid`(채움 배지) 삭제 — D250 (2026-09-22). 쓰던 곳(편집기 메모 칸 배지)이 D215 에서
   메모 칸과 함께 없어져 사용처 0 (lint §7.8). */

/* 로그 줄 (2026-09-14 D151 — 가입 관리 '기록' 탭. 사용자 "2배 이상 컴팩트하게, 단순 로그 쌓이는 정도").
   표 한 행(약 44)의 절반 아래 — 한 줄 24 에 시각·구분(.tag)·이름·연락처·아이디를 가로로 놓는다.
   시각은 고정 폭 92 + tabular-nums 라 뒤 칸이 줄마다 흔들리지 않는다. 줄 사이는 border-bottom 하나. */
.log-line {
  display: flex; align-items: center; gap: 8px;
  height: 24px; box-sizing: border-box; padding: 0 16px;
  font-size: var(--fs-xs); white-space: nowrap;
  border-bottom: 1px solid var(--border);
}
.log-line:last-child { border-bottom: 0; }
.log-line time { flex: 0 0 92px; font-variant-numeric: tabular-nums; }

/* ══ 공용 유틸 (2026-08-13 — member_detail 인라인 139건 정리에서 뽑음) ══
   같은 모양이 파일마다 인라인으로 다시 그려지고 있던 것들. 여기 하나만 둔다. */

/* 카드·구역 제목. h3 를 맨몸으로 쓰면 브라우저 기본값(약 18.7px)이 나와
   같은 화면 안에서 16px 짜리와 섞여 있었다 (11곳 인라인 + 4곳 맨몸). */
h3 { font-size: var(--fs-h3); font-weight: 700; letter-spacing: var(--ls-body); }

/* 탭 안 첫 제목은 아래 내용과 붙지 않게 (.row-between 안의 h3 는 제외 —
   그쪽은 한 줄 안에 나란히 놓이므로 아래 여백이 필요 없다) */
.tab-panel > h3 { margin-bottom: var(--sp-4); }
/* 모달 안 입력칸은 칸 전체를 쓴다 */
.modal-body .form-input { width: 100%; margin-bottom: var(--sp-3); }

/* 제목 왼쪽 · 버튼 오른쪽 한 줄 */
.row-between {
  display: flex; justify-content: space-between; align-items: center;
  margin-bottom: var(--sp-4);
}

/* 숫자 칸 — 오른쪽 정렬 + 자릿수 고정폭 (§2.6 모노 대체) */
.num { text-align: right; font-variant-numeric: tabular-nums; }
/* 화면 머리의 라벨+수치 한 덩이 (회원 상세 포인트).
   D115 자리 예약 — 값이 늦게 도착해 자릿수만큼 넓어지면 옆 버튼을 민다.
   최소 폭을 잡아 '— P' 일 때와 '12,300 P' 일 때 폭이 같다. */
.head-stat {
  min-width: 96px; text-align: right;
  font-variant-numeric: tabular-nums;
}
/* 가운데 칸 — 활성 표시·액션처럼 폭이 짧은 열 (.cell-sub 와 같은 계열 이름) */
.cell-center { text-align: center; }

/* 탭 한 칸 — 카드 안에서 내용이 갈릴 때. 내용이 적어도 높이가 무너지지 않게 */
.tab-panel.card { padding: var(--sp-5); min-height: 400px; }
/* 탭 안 안내·빈 상태 문구 (가운데, 넉넉한 여백).
   D115 — 로딩/빈/에러가 같은 바닥을 갖도록 --slot-msg-roomy 예약 */
.panel-msg {
  min-height: var(--slot-msg-roomy); box-sizing: border-box;
  display: flex; align-items: center; justify-content: center;
  text-align: center; padding: 16px; line-height: 1.7; opacity: var(--dim);
}
/* 내용이 들어오는 그릇 — 비었을 때만 안내처럼 보인다. 본문이 채워지므로
   흐리게 하지 않는다 (.panel-msg 를 씌웠다가 WOD 본문이 흐려졌다). */
.panel-box {
  min-height: var(--slot-msg-roomy); box-sizing: border-box;
  text-align: center; padding: 16px; line-height: 1.7;
  display: flex; align-items: center; justify-content: center; flex-direction: column;
}
/* 표 칸 기본 여백 — WOD 리더보드처럼 표를 직접 그리는 곳 */
.cell-pad { padding: var(--sp-2); font-size: var(--fs-xs); }

/* 화면 머리 설명문 — 표·목록 위에 한 줄 놓이는 안내 */
p.note { margin-bottom: var(--sp-4); }

/* 모달 안 라벨은 앞 입력칸과 붙지 않게. 첫 라벨은 위 여백 없음 */
.modal-body .form-label { margin-top: var(--sp-3); }
.modal-body .form-label:first-child { margin-top: 0; }

/* `.consent-row`(동의 체크 한 줄) 삭제 — D250 (2026-09-22). 유일한 사용처 '결제 동의 갱신' 이
   D240 에서 삭제돼 사용처 0 (lint §7.8). */

/* ── 락커 ── */
.locker-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 10px;
}
/* 구역별 행 (2026-09-13 코치 보고 "거울앞·남탈·입구가 한곳에 몰려 보기 불편") —
   한 화면에 다 보이되 구역마다 머리줄 + 자기 격자. 구역은 서버 zone 한 값으로 가른다. */
.locker-groups { display: flex; flex-direction: column; gap: var(--sp-5); }
.locker-group-head {
  /* 수는 이름 바로 옆 — 넓은 화면에서 오른쪽 끝으로 보내면 어느 행의 수인지 눈이 한 번 더 간다. */
  display: flex; align-items: baseline; justify-content: flex-start;
  gap: var(--sp-3); margin-bottom: var(--sp-2);
  padding-bottom: var(--sp-1); border-bottom: 1px solid var(--border);
}
.locker-group-head .section-label { margin-bottom: 0; font-size: var(--fs-sm); }
.locker-group-count {
  font-size: var(--fs-xs); letter-spacing: var(--ls-body); color: var(--fg);
  opacity: var(--dim); font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* 빈 상태 문구가 120px 셀 하나에 갇혀 세로로 쪼개지던 문제 (2026-08-21 사용자 보고 R1) */
.locker-grid > .table-msg { grid-column: 1 / -1; }

/* 락커 타일 — 한 줄에 하나씩 (2026-08-12 수정, 사용자 보고).
   전에는 상태를 이름 줄에 `::after` 로 붙여 "송준혁 · 만료임박" 이 한 덩어리가 됐다.
   120px 타일에서 그 줄이 "만료임" / "박" 으로 쪼개졌고 그 타일만 키가 커져
   격자가 어긋났다. 이제 번호 / 이름 / 상태 / 날짜가 각각 제 줄을 갖는다.
   높이를 고정해 내용이 몇 줄이든 격자가 흐트러지지 않는다. */
.locker {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 12px 10px;
  text-align: center;
  min-height: 104px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  word-break: keep-all;      /* 한글 단어 중간에서 자르지 않는다 */
  overflow-wrap: anywhere;   /* 아주 긴 이름만 예외적으로 넘긴다 */
}

/* 상태는 색보다 '채움/비움' 으로 가른다 (2026-09-13 코치 "빨간 점선은 오히려 구분이 안 된다" —
   빈 칸이 다수인 락커장에서 빈 칸에 색을 주면 전부 그 색이 돼 아무것도 안 띈다).
   빈 칸 = 속이 빈 칸(점선·연한 바탕·흐린 글자) / 쓰는 칸 = 상태색으로 채운 칸(옅은 바탕 +
   실선 2px + 진한 이름). 채운 칸이 블록으로, 빈 칸이 틈으로 읽힌다. 틴트는 토큰 color-mix
   (요금제 표와 같은 방식). danger 역할 = --primary (2026-08-13 토큰 압축). */
.locker.vacant {
  border: 1px dashed var(--border);
  background: color-mix(in srgb, var(--border) 30%, var(--bg));
}
.locker.vacant .no { font-weight: 400; }   /* 쓰는 칸 800 과 굵기로도 갈린다 (§2.2 — 500/600 금지) */
.locker.occupied { border: 2px solid var(--success); background: color-mix(in srgb, var(--success) 12%, var(--bg)); }
.locker.expiring { border: 2px solid var(--warning); background: color-mix(in srgb, var(--warning) 14%, var(--bg)); }
.locker.expired  { border: 2px solid var(--primary); background: color-mix(in srgb, var(--primary) 10%, var(--bg)); }
.locker.occupied .no, .locker.expiring .no, .locker.expired .no,
.locker.occupied .name, .locker.expiring .name, .locker.expired .name { opacity: 1; }
.locker.occupied .name, .locker.expiring .name, .locker.expired .name { font-weight: 700; }

/* 줄 높이를 못 박는다 — 기본값(normal)은 글꼴·글자에 따라 들쭉날쭉해서
   같은 4줄 타일인데도 키가 104·105·107px 로 갈렸다. 1.3 으로 고정하면
   가장 긴 타일도 99px 라 전부 min-height(104px)에 딱 맞는다. */
.locker .no,
.locker .name,
.locker .state,
.locker .end { line-height: 1.3; opacity: var(--dim); }

.locker .no { font-size: var(--fs-h3); font-weight: 800; letter-spacing: var(--ls-title); }
.locker .name { font-size: var(--fs-sm); color: var(--fg); letter-spacing: var(--ls-body); }
.locker .state { font-size: var(--fs-xs); letter-spacing: var(--ls-body); white-space: nowrap; }
.locker.expiring .state { color: var(--warning); font-weight: 700; }
.locker.expired .state { color: var(--primary); font-weight: 700; }
.locker .end { font-size: var(--fs-xs); letter-spacing: var(--ls-body);
               font-variant-numeric: tabular-nums; white-space: nowrap; }

/* ── 계약서 ── */
.contract-card {
  padding: 24px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  margin-bottom: 16px;
}

.contract-card h3 { font-size: 14px; font-weight: 800; margin-bottom: 8px; }
.contract-card .meta { font-size: 12px; color: var(--fg); margin-bottom: 16px; opacity: var(--dim); }
.contract-card .body {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 16px;
  font-size: 12px;
  color: var(--fg);
  line-height: 1.7;
  max-height: 200px;
  overflow-y: auto;
  margin-bottom: 16px;
}

/* (구 `.signature-box` · `.signature-pad` — 2026-09-05 D118 삭제. 실제 서명은
   `<canvas id="sigCanvas">` 하나이고 그 모양은 유틸 클래스가 잡는다
   (contracts.html · app.js `createSignaturePad`). 이 두 이름은 사용처 0 이었다.) */

/* ── 모달 ── */
/* 모달 뒤 어두운 판 — 정의는 여기 하나다.
   2026-08-12: 옛 `.modal-overlay` 를 `.modal-backdrop` 으로 합치면서, 두 벌이
   display 기본값(none vs flex)과 z-index(100 vs 999)까지 어긋난 채 겹쳐 있었다.
   기본은 감춤이고, 여는 방식은 두 가지 다 받는다:
     (1) `.is-open` 클래스 토글     — dashboard·member_detail
     (2) 인라인 style.display 조작  — classes·class_templates·announcements 등
   (2)는 인라인이 더 세서 이 규칙과 충돌하지 않는다.
   2026-08-13 §9 N-3: 맨몸 상태어(`.open`·`.active`·`.signed`)를 `is-*` 로. */
.modal-backdrop {
  position: fixed; inset: 0;
  background: rgba(0,0,0,0.7);
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 100;
  padding: 24px;
}
.modal-backdrop.is-open { display: flex; }
.modal {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  width: 100%;
  max-width: 720px;
  max-height: 90vh;
  overflow-y: auto;
  padding: 24px 28px;
}
.modal.modal-sm { max-width: 460px; }
.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 20px;
}
.modal-header h2 { font-size: 18px; font-weight: 800; letter-spacing: -0.3px; }
.modal-close {
  background: transparent; border: 0; color: var(--fg);
  font-size: 24px; cursor: pointer; padding: 4px 10px;
}
.modal-close:hover { color: var(--fg); }
.modal-actions {
  display: flex; justify-content: flex-end; gap: 8px;
  margin-top: 24px; padding-top: 20px;
  border-top: 1px solid var(--border);
}

/* ── 구분선형 모달 (.modal.is-boxed) ──
   머리·본문·바닥이 선으로 나뉘는 형태. 수업 관리·수업 종류가 각자 자기
   <style> 안에 같은 것을 한 벌씩 갖고 있던 것을 2026-08-12 여기로 올렸다.
   기본 .modal 은 통짜 padding, 이쪽은 .modal-body 가 padding 을 맡는다. */
.modal.is-boxed { padding: 0; }
.modal-title { font-weight: 700; font-size: var(--fs-h3); letter-spacing: var(--ls-body); }
.modal-body { padding: 20px; }
.modal-footer {
  display: flex; justify-content: flex-end; gap: var(--sp-2);
  padding: 12px 20px; border-top: 1px solid var(--border);
}
.modal.is-boxed .modal-header {
  padding: var(--sp-4) 20px; margin-bottom: 0;
  border-bottom: 1px solid var(--border);
}
.form-label {
  font-size: var(--fs-xs); color: var(--fg); display: block;
  margin-bottom: var(--sp-1); letter-spacing: var(--ls-body); opacity: var(--dim); }
.form-input {
  background: var(--bg); border: 1px solid var(--border); color: var(--fg);
  /* 높이는 컨트롤 토큰 하나 (D115 — 구 35, 옆 버튼 33 과 어긋났다).
     textarea 는 여러 줄이라 예외 (바로 아래 규칙에서 높이를 푼다). */
  height: var(--ctrl-h);
  padding: 0 10px; border-radius: var(--r-sm);
  font-size: var(--fs-sm); font-family: inherit; box-sizing: border-box;
}
textarea.form-input { height: auto; padding: var(--sp-2) 10px; }

/* 세로 나열형 설정 폼 — 입력 폭을 컨테이너 폭에 맞춘다.
   (체육관 프로필 등에서 입력이 내용 크기대로 들쭉날쭉하던 문제 —
    2026-08-20 전 화면 playwright 점검) */
.form-stack .form-input { width: 100%; }
/* 2026-08-23 — 이 규칙이 classes.html 의 <style> 안에 갇혀 있었다.
   입력칸 포커스는 전 화면 공통이라 여기로 옮긴다 (§3). */
.form-input:focus { outline: none; border-color: var(--primary); }

/* ── 폼 ── */
.form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 14px 18px;
}
.form-row { display: flex; flex-direction: column; gap: 6px; }
.form-row.form-full { grid-column: 1 / -1; }
/* 자간: 라벨 내용이 전부 한글이라 양수 트래킹은 §2-B-자간 위반이다.
   (uppercase 트래킹 예외는 실제 영문 대문자에만 해당) */
.form-row label {
  font-size: var(--fs-xs); font-weight: 700;
  color: var(--fg); opacity: var(--dim);
  letter-spacing: var(--ls-body);
}
.form-row input, .form-row select, .form-row textarea {
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--fg);
  padding: 10px 12px;
  border-radius: var(--r-md);
  font-size: 13px;
  font-family: inherit;
}
.form-row input:focus, .form-row select:focus, .form-row textarea:focus {
  outline: none; border-color: var(--primary);
}
.form-row textarea { resize: vertical; min-height: 60px; }
.form-static {
  font-size: 13px; color: var(--fg);
  padding: 10px 0;
}

/* ══ 토글 — 켜고 끄는 유일한 컨트롤 (2026-08-12 신설) ══
   이 파일이 정본이다. 페이지 <style> 안에서 다시 정의하지 말 것
   (알림 설정이 그렇게 자기만의 토글을 갖고 있었다).

   켜짐이 --success 초록인 이유: 정본 3색 중 --primary 는 '브랜드 겸 삭제·
   위험' 이라 켜짐 표시에 쓰면 정상 동작이 경고로 읽힌다. 실제로 그랬다.
   --warning 도 같은 이유로 못 쓴다. 남는 상태색은 '완료·정상' 하나뿐이다.

   트랙이 둥근 것은 §3.1 pill 위반이 아니다 — pill 금지는 라벨·뱃지가
   알약 모양을 흉내내는 것을 막는 규칙이고, 스위치의 둥근 트랙은
   '미는 것' 을 알리는 컨트롤 자체의 모양이다. 예외는 여기 하나뿐.
   ══════════════════════════════════════════════════════════ */
.toggle {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  cursor: pointer; user-select: none;
}
.toggle-track {
  flex: none; width: 40px; height: 24px; border-radius: 12px;
  background: var(--border); position: relative;
  transition: background 0.18s;
}
.toggle-track::after {
  content: ''; position: absolute; top: 3px; left: 3px;
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--bg); box-shadow: var(--shadow);
  transition: left 0.18s;
}
.toggle-track.is-on { background: var(--success); }
.toggle-track.is-on::after { left: 19px; }
/* 상태 글자는 토글 오른쪽에 하나만 — '사용 안 함 / 사용함' 을 양쪽에
   두면 좁은 칸에서 '사용 안\n함' 으로 쪼개진다 (실제로 쪼개져 있었다). */
.toggle-state {
  font-size: var(--fs-sm); color: var(--fg);
  white-space: nowrap; min-width: 52px;
}
.toggle:focus-visible { outline: none; }
.toggle:focus-visible .toggle-track {
  outline: 2px solid var(--fg); outline-offset: 2px;
}
.toggle[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; }

/* ── 락커 클릭 + 클리어 ── */
.locker { position: relative; cursor: pointer; transition: transform 0.12s; }
.locker:hover { transform: translateY(-1px); border-color: var(--primary); }
.locker-clear {
  position: absolute; top: 4px; right: 4px;
  background: transparent; border: 0; color: var(--fg);
  font-size: 14px; cursor: pointer; padding: 2px 6px;
  border-radius: var(--r-sm);
}
.locker-clear:hover { background: rgba(238,43,43,0.2); color: var(--primary); }

/* 선택 모드 (2026-09-13 D140 — 코치 "일괄 삭제·수정"). '선택' 을 누르면 그릇(#lockerGrid)에
   is-picking 이 붙고 타일마다 체크 상자가 보인다. 배정 중 타일(is-locked)은 고를 수 없어
   흐리게 두고 눌리지 않는다. 고른 타일(is-picked)은 primary 외곽선. 선택 모드에서는 × 를
   숨긴다 — 한 화면에 두 종류의 지우기가 같이 서지 않게. */
.locker-pick { display: none; position: absolute; top: var(--sp-1); left: var(--sp-1); }
.locker-pick input { margin: 0; width: 16px; height: 16px; cursor: pointer; }
.is-picking .locker-pick { display: block; }
.is-picking .locker-clear { display: none; }
.is-picking .locker.is-locked { pointer-events: none; opacity: var(--dim); }
.locker.is-picked { outline: 2px solid var(--primary); outline-offset: 2px; }
/* 잠긴 버튼 — '선택 삭제 (0)'·'추가 중...' 처럼 지금 못 누르는 .btn 은 흐리게 (종전엔 빨간 채 그대로였다) */
.btn:disabled { opacity: 0.35; cursor: default; }

/* ══ 설정 화면 (2026-08-12) ══
   '이름 + 설명 + 컨트롤' 한 줄이 반복되는 형태. 알림 설정이 처음이지만
   요금제·포인트 설정도 같은 모양이라 여기 공용으로 둔다. */
.page-narrow { max-width: 640px; display: flex; flex-direction: column; gap: var(--sp-5); }
/* 같은 그릇, 넓은 것 — 표가 5~6열인 설정 화면(회원권·포인트, D154 2026-09-14). 640 에는
   횟수권 6열·자동 적립 조건 문장이 안 들어간다. 폭 말고는 .page-narrow 와 같다. */
.page-wide { max-width: 960px; display: flex; flex-direction: column; gap: var(--sp-5); }
/* 카드 안 설명문은 아래 내용과 붙지 않게 — 붙으면 라벨과 겹쳐 읽힌다 */
.card > .note { margin-bottom: var(--sp-3); }
.setting-row {
  display: flex; justify-content: space-between; align-items: center;
  gap: var(--sp-4); padding: 14px 0; border-bottom: 1px solid var(--border);
}
.setting-row:last-child { border-bottom: none; }
.setting-name { font-size: var(--fs-body); font-weight: 700; color: var(--fg); }
/* 2026-08-27 — 한글 자간 음수 누락 보정 (글로벌 §2-B-자간. 형제인 .page-sub·
   .cell-sub 는 진작 --ls-body 를 쓰고 있었다). 쓰는 화면: 알림 설정·공지·체육관 프로필. */
.setting-desc { font-size: var(--fs-xs); color: var(--fg); opacity: var(--dim);
  margin-top: 2px; letter-spacing: var(--ls-body); line-height: 1.6; }
/* ── 회원 쪽지 (messages) ──
   2026-08-23 — 목록 행·말풍선·시각을 JS 문자열 안에 인라인 style 로 통째
   적고 있었다. 값이 전부 고정이고 갈리는 것은 '선택됨'·'내가 보냄' 두
   상태뿐이라 클래스로 옮긴다 (§3 코드·클래스 SSOT). */
.thread-row {
  display: block;
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
  border-left: 2px solid transparent;
  text-decoration: none;
  color: inherit;
  cursor: pointer;   /* D141 — <a> 였던 줄이 체크·삭제를 품으며 <div role=button> 이 됐다 */
}
.thread-row.is-active {
  background: var(--border);
  border-left-color: var(--primary);
}
/* D141 — 줄 앞 선택 체크. 브라우저 기본 margin 을 지워 이름과 같은 축에 앉힌다. */
.thread-check { margin: 0; flex: none; }

.msg-line { display: flex; justify-content: flex-start; margin-bottom: 8px; }
.msg-line.is-mine { justify-content: flex-end; }

.msg-bubble {
  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 8px 12px;
  font-size: 13px;
  line-height: 1.6;
  white-space: pre-wrap;
  word-break: break-word;
}
.is-mine .msg-bubble {
  background: var(--primary);
  color: var(--bg);
  border-color: var(--primary);
}

.msg-meta { font-size: 11px; margin-top: 4px; text-align: left; }
.is-mine .msg-meta { text-align: right; }

/* 꺼진 항목 — 흐리게. 2026-08-23: `.setting-list.is-off` 로만 쓰던 것을
   요소를 안 가리게 풀었다. 업적 설정이 같은 표현(opacity:.45)을 표 행에
   인라인으로 다시 적고 있었다 (§3 코드·클래스 SSOT). */
.is-off { opacity: 0.45; }
/* 클릭까지 막아야 하는 묶음(설정 목록)만 추가로 차단 */
.setting-list.is-off { pointer-events: none; }

/* 시간대 범위 입력 (공지 예약 노출 기간) */
.time-range { display: flex; align-items: flex-end; gap: var(--sp-3); flex-wrap: wrap; }
.time-range .note { padding-bottom: 10px; }
.time-sep { padding-bottom: 10px; color: var(--fg); }

/* (구 `.log-row` — 알림 발송 로그 한 줄. 2026-09-04 D114 로 삭제.
   `display:flex; justify-content:space-between` 이라 칸 폭이 줄마다 달라져
   시각·항목·회원·제목이 세로로 안 맞았다. 보관함은 표(SSOT §6)로 바꿨다.) */

/* ── 동작 줄 (2026-09-04 D114 · 갤러리 §19) ──────────────────────────────
   카드·편집 칸 바닥의 버튼 묶음. §8.6(모달 바닥) 규칙을 카드 안으로 넓힌 것 —
   **빨간 `.btn` 은 줄에 하나**, 나머지는 `.link-btn` 글자다. 되돌리기·기본값처럼
   자주 안 쓰는 것은 `.ml-auto` 로 줄 끝에 둔다.
   이 이름이 생기기 전에는 화면마다 d-flex+gap+mt 유틸 조합을 따로 적었고,
   알림 설정 노쇼 정책은 `.row-between` 이라 저장과 되돌리기가 640px 양 끝으로
   찢어져 있었다 (사용자 "버튼들이 아무 규칙 없이 제멋대로 배열"). */
.form-actions {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--sp-3); margin-top: var(--sp-4);
}
/* 줄 안의 글자 버튼도 누를 곳을 확보한다 (모달 바닥과 같은 값) */
.form-actions .link-btn { padding: 8px 12px; }
/* 표 바로 밑에 오면 표의 마지막 줄과 붙지 않게 */
table + .form-actions { margin-top: var(--sp-3); }

/* 표 안에서 고르는 칸 — 셀 폭을 넘지 않는다 (규칙 표: 알림 설정 노쇼 정책) */
tbody td > .form-input { max-width: 100%; }

/* ── 섹션 라벨 유틸 — notifications, contracts 등 공용 ── */
.section-label {
  font-size: var(--fs-xs); font-weight: 700;
  letter-spacing: var(--ls-body); color: var(--fg);
  margin-bottom: var(--sp-4);
}

/* ── 공통 차트 컴포넌트 (dashboard + stats 공유) ── */
.kpi-strip { display: grid; grid-template-columns: repeat(5, 1fr); padding: 12px 8px; margin-bottom: 12px; gap: 0; }
.kpi-strip.kpi-6 { grid-template-columns: repeat(6, 1fr); }
.kpi-cell { padding: 4px 14px; border-right: 1px solid var(--border); display: flex; flex-direction: column; gap: 2px; }
.kpi-cell:last-child { border-right: none; }
.kpi-label { font-size: 10px; color: var(--fg); text-transform: uppercase; font-weight: 700; }
.kpi-num { font-size: 22px; font-weight: 800; line-height: 1.1; color: var(--fg); }
.kpi-unit { font-size: 12px; color: var(--fg); font-weight: 700; margin-left: 2px; opacity: var(--dim); }
/* 수업 종류 색 점 — 색만 인라인으로 받고 모양은 여기서 (2026-08-23). */
.color-dot {
  width: 12px; height: 12px;
  border-radius: 50%;
  flex-shrink: 0;
}

.kpi-delta { font-size: 11px; font-weight: 700; margin-left: 6px; }
/* 증감 부호별 색. 2026-08-23 — stats 가 Jinja 조건문으로 색을 인라인으로
   칠하고 있었다. 부호는 세 갈래뿐이라 클래스로 뺀다 (§3). */
.kpi-delta.is-up { color: var(--success); }
.kpi-delta.is-down { color: var(--primary); }
.chart-card { padding: 14px 16px; }
.chart-title { font-size: 10px; color: var(--fg); text-transform: uppercase; margin-bottom: 10px; font-weight: 700; }
/* 도넛 자리는 그림 높이(130)로 고정 — 값이 0이라 '데이터 없음' 한 줄일 때도, 불러오기 전에도
   카드 높이가 같다 (D194 · 실측 76 ↔ 130 으로 아래가 54px 튀었다). */
.donut-wrap { display: flex; align-items: center; gap: 14px; min-height: 130px; }
.donut-wrap svg { flex-shrink: 0; }
.donut-legend { font-size: 12px; flex: 1; }
.donut-legend .row { display: flex; justify-content: space-between; padding: 3px 0; }
.donut-legend .dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; margin-right: 6px; vertical-align: middle; }

/* 2026-08-06 — 사이드바 "회원 쪽지" 안읽음 배지. 완전 원형 pill 금지 룰에 따라 r4 사각. */
.nav-badge {
  margin-left: auto;
  margin-right: 4px;
  min-width: 18px;
  padding: 0 5px;
  border-radius: 4px;
  background: var(--primary);
  color: var(--bg);
  font-size: 11px;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
}

/* ── 계약서 화면 (2026-08-12) ──────────────────────────────
   전자계약서 표가 칸이 좁아 이름·액션이 "신규/회원", "대리 서/명" 처럼
   잘리고 있었다. 칸마다 무엇을 줄이고 무엇을 지킬지 정해 준다. */
.table-contracts { table-layout: fixed; width: 100%; }
.table-contracts th, .table-contracts td { vertical-align: middle; }
/* 날짜·코드·상태처럼 쪼개지면 안 되는 칸 */
.table-contracts .nowrap { white-space: nowrap; }
/* 액션 링크는 한 줄에 붙여 두고, 좁으면 줄을 바꾸되 단어는 지킨다 */
.table-contracts .actions { white-space: nowrap; text-align: right; }
.table-contracts .actions a { margin-left: 10px; word-break: keep-all; }
/* 이름 아래 작은 보조줄 (코드·분류) — 색이 아니라 크기로 위계를 만든다 */
.cell-sub { font-size: var(--fs-xs); opacity: var(--dim); letter-spacing: var(--ls-body); margin-top: 2px; }

/* 계약서 종류 상세 — 필수 입력 항목 목록 */
.tpl-fields { display: flex; flex-wrap: wrap; gap: 6px; }
.tpl-field {
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: var(--ls-body);
  border: 1px solid var(--border); border-radius: var(--r-sm); padding: 3px 8px;
  white-space: nowrap;
}
/* ── 로딩·빈·에러 문구 자리 (2026-09-04 D115 — 자리 예약) ──────────────
   셋은 같은 자리에서 갈아 끼워진다. 종전에는 패딩만 있고 높이가 없어
   스켈레톤(약 80) → 문구(약 74) → 실제 행(무제한) 으로 바뀔 때마다 아래가 튀었다.
   이제 셋 다 --slot-msg 를 바닥으로 갖는다 (문구가 길면 그 위로 자란다).
   정본 규칙 = design/SSOT.md §17. */
.table-msg  {
  height: var(--slot-msg); padding: 0 16px; box-sizing: border-box;
  text-align: center; opacity: var(--dim);
}
/* td 는 height 를 최소값으로 해석한다 — div 자리에서도 같게 min-height 로 못박는다 */
div.table-msg { height: auto; min-height: var(--slot-msg); display: flex;
  align-items: center; justify-content: center; }
/* 빈 상태가 넉넉한 여백을 쓰는 자리(목록 전체가 비었을 때 · 다시 시도 링크 포함) */
.table-msg.is-roomy { height: var(--slot-msg-roomy); padding: 0 16px; line-height: 1.7; }
div.table-msg.is-roomy { height: auto; min-height: var(--slot-msg-roomy); }
/* 에러는 붉게 — 흐리게 처리하지 않는다 (읽혀야 하는 문구) */
.table-msg.is-error { color: var(--primary); opacity: 1; }
/* 표 공용 — 날짜·전화·액션처럼 짧은 셀은 한 줄 고정 (긴 텍스트 컬럼만 줄바꿈).
   회원 리스트 '수정/삭제'가 두 줄로 꺾이던 문제 (2026-08-20 점검). */
td.nowrap   { white-space: nowrap; }
/* 케어 필요 표 긴급 행 (2026-08-26 카드 그리드 → 표) — 첫 칸 좌측 4px rail.
   border 가 아닌 inset 그림자 1겹이라 글자 시작 위치가 다른 행과 같다. */
tr.is-urgent td:first-child { box-shadow: inset 4px 0 0 var(--primary); }
td.actions  { white-space: nowrap; }
/* `.table-skel` 삭제 — D250. 로딩 자리는 D189 부터 `.table-msg.table-skel-cell` 하나라 사용처 0. */
/* 계약서 종류 편집기 — 넣을 수 있는 칸 팔레트 (D172 · 2026-09-15).
   코치가 `{{ plan_name }}` 문법을 외우지 않게, 이름을 누르면 원문에 꽂힌다. */
.tpl-palette { border: 1px solid var(--border); border-radius: var(--r-sm);
  padding: 8px 10px; margin-bottom: 6px; max-height: 148px; overflow-y: auto; }
.tpl-palette-group { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  margin: 4px 0; }
.tpl-palette-name { font-size: var(--fs-xs); font-weight: 800; opacity: var(--dim);
  min-width: 56px; }
button.tpl-chip {
  font-family: inherit; font-size: var(--fs-xs); font-weight: 700;
  letter-spacing: var(--ls-body); color: var(--fg); background: var(--bg);
  border: 1px solid var(--border); border-radius: var(--r-sm); padding: 3px 8px;
  white-space: nowrap; cursor: pointer;
}
button.tpl-chip:hover { border-color: var(--fg); }

.tpl-section {   /* 모달 안 소제목 — 인라인 margin 대신 클래스로 (§2 절차) */
  font-size: var(--fs-xs); font-weight: 800; letter-spacing: var(--ls-body);
  margin-top: 18px; margin-bottom: 8px;
}
/* 계약서 원문은 A4 문서라 좁은 모달에서 표 칸이 "{박스 이/름}" 처럼 쪼개진다.
   문서를 읽을 수 있는 폭을 준다 (2026-08-12). */
.modal-doc { max-width: 960px; width: 94vw; }
.tpl-doc { border: 1px solid var(--border); border-radius: var(--r-sm); overflow: hidden; }
/* 계약서 종류 만들기 — 회원권 고르기 (D196 · 2026-09-16). 한 줄 = 체크 · 이름 · 값 · (지금 묶인 종류).
   줄 부품은 여기 한 곳, 목록·값은 서버 것. */
.tpl-plans { border: 1px solid var(--border); border-radius: var(--r-sm); padding: 2px 12px; }
.tpl-plan { display: flex; align-items: center; gap: 10px; padding: 9px 0;
            border-bottom: 1px solid var(--border); cursor: pointer; font-size: var(--fs-body); }
.tpl-plan:last-child { border-bottom: none; }
.tpl-plan input { margin: 0; }
.tpl-doc-frame { width: 100%; height: 62vh; min-height: 420px; border: 0; display: block; background: var(--bg); }
/* (구 `.tpl-clauses` — 계약서 조항 목록. 2026-09-05 D118 삭제: 조항은 표 안
   개수(`legal_clauses.length`)로만 보이고 목록으로 펼치는 화면이 없다.) */

/* ── 일정 달력 간트 (2026-08-13 사용자 지시) ──
   공지 기간을 월간 가로 막대로. 페이지 <style> 대신 공용 시트에 둔다
   (SSOT: 부품 정의는 여기 한 곳). 열 개수는 달마다 달라 JS 가
   gridTemplateColumns 만 프로퍼티로 세팅한다. */
/* D115 자리 예약 — '—' 로 시작해 JS 가 '2026.09' 를 꽂는다. 폭을 안 잡아 두면
   값이 들어오는 순간 양옆 화살표가 벌어진다. 자릿수 고정폭 + 최소 폭. */
.month-label {
  margin: 0 12px; font-weight: 700;
  display: inline-block; min-width: 72px; text-align: center;
  font-variant-numeric: tabular-nums;
}
  /* (2026-08-24 정리) 구 간트 한 줄 뷰 클래스 6종 삭제 — 현재 템플릿은
     .gantt-note / .gantt-legend 두 개만 참조 (C7 월 뷰 전환 잔재). */
  .gantt-note { padding: 24px; text-align: center; color: var(--fg); opacity: var(--dim); }
  .gantt-legend { margin-top: 12px; font-size: var(--fs-xs); color: var(--fg); opacity: var(--dim); }


/* ── C7 (2026-08-20 밤) — 일정 달력 월 뷰: 주 그리드 + 공지 막대 ──
   v2 (2026-08-24 사용자 지시 "달력 2배, 메인에 차게") — 셀 치수는 아래
   CSS 변수가 SSOT. JS(announcements.html — 2026-08-26 공지와 한 화면으로 통합,
   구 calendar.html 삭제)는 lane "개수"만 세고 치수는 var() 참조.
   빈 주도 2 lane 을 예약해 월 전체 높이가 일정하게 메인을 채운다.
   2026-08-27 컴팩트 (사용자 "조금씩만 더 컴팩팅" — 공지 표까지 1440×900 한 화면):
   날짜 34→26 · lane 32→26 · 주 하단 여백 8→4 · 막대 line-height 1.8→1.5.
   6주 달 기준 156px 절감. 표·범례 쪽은 아래 "announcements compact" 섹션. */
#mcalCard { --mcal-date-h: 26px; --mcal-lane-h: 26px; }
.mcal-head {
  display: grid; grid-template-columns: repeat(7, 1fr);
  padding: 6px 0 5px; border-bottom: 1px solid var(--border);
}
.mcal-head span {
  text-align: center; font-size: var(--fs-sm); font-weight: 700;
  letter-spacing: var(--ls-body); opacity: var(--dim);
}
.mcal-week {
  display: grid; grid-template-columns: repeat(7, 1fr);
  border-bottom: 1px solid var(--border);
  position: relative; padding-bottom: 4px;
}
.mcal-day {
  font-size: var(--fs-sm); padding: 5px 10px 1px; z-index: 1;
  font-feature-settings: 'tnum' 1;
}
.mcal-day.is-other { opacity: var(--dim); }
.mcal-day.is-today { color: var(--primary); font-weight: 800; }
.mcal-daybg { border-left: 1px solid var(--border); }
.mcal-daybg:first-of-type { border-left: none; }
.mcal-bar {
  z-index: 1; margin: 1px 4px; padding: 2px 8px;
  border-radius: var(--r-sm);
  background: var(--primary); color: var(--bg);
  font-size: var(--fs-xs); line-height: 1.5;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  cursor: pointer; letter-spacing: var(--ls-body);
}
.mcal-bar.is-secondary { background: var(--fg); }

/* ── table group header row — D278-c (2026-09-26) 사용처 0 으로 삭제 (노쇼 정책이 .notify-group 으로 옮김) ── */

/* ── announcements compact (2026-08-27) ──
   공지 · 일정 한 화면 — 1440×900 에서 스크롤 없이 (사용자 "조금씩만 더 컴팩팅").
   달력 셀 치수는 위 #mcalCard 블록에서 함께 줄였고 여기는 표·범례·빈 안내.
   달력+표를 세로 flex 로 묶어 표가 "남는 높이"만큼만 차지하고 카드 안에서 스크롤
   (머리글 고정). 컨테이너 한도 = 100vh − .main 상하 32×2 − 헤더 44 − 헤더 아래 24.
   달력 lane 이 늘어 커져도 표가 그만큼 줄어든다 — 바닥은 머리글+1행(76px). */
.ann-stack { display: flex; flex-direction: column; gap: 16px; max-height: calc(100vh - 132px); }
#mcalCard { flex: none; }
#annTableCard { flex: 0 1 auto; min-height: 76px; overflow: auto; }
#annTableCard thead th { position: sticky; top: 0; z-index: 1; padding: 8px 16px; }
#annTableCard tbody td { padding: 10px 16px; }
#mcalCard .gantt-legend { margin-top: 4px; padding: 0 12px 4px; }
#mcalCard .gantt-note { padding: 16px; }

/* ── gym profile (2026-08-27) ── */
/* 세로 나열 폼(.form-stack)에서 입력칸 → 다음 라벨 사이 숨. 모달 폼의
   `.modal-body .form-label { margin-top: var(--sp-3) }` 과 같은 12px —
   체육관 프로필이 라벨이 앞 입력칸에 붙어 형제 화면보다 빽빽해 보이던 것. */
.form-stack .form-input + .form-label { margin-top: var(--sp-3); }

/* ── notifications lock (2026-08-27 P1) ── */
/* 마스터('자동 쪽지 알림')가 꺼졌을 때 항목 목록에 붙는 잠금.
   컨테이너 통째로 `.is-off`(= pointer-events:none + opacity)를 씌우던 것이
   D61 로 같은 컨테이너에 들어온 '문구 수정' 버튼까지 잠갔다. 알림을 꺼 둔 채
   문구만 다듬는 것이 자연스러운 순서라 문구 편집은 항상 살아 있어야 한다.
   → 잠금 대상을 토글로만 좁힌 전용 클래스. 다른 화면이 쓰는 `.is-off`
   (settings_achievements 의 꺼진 표 행 = opacity 만)와 `.setting-list.is-off`
   의 뜻은 그대로 둔다. */
.setting-list.is-toggle-locked .toggle,
.setting-list.is-toggle-locked .seg {
  pointer-events: none;
  opacity: 0.45;
}

/* 한 칸 선택지 (D278 · 2026-09-26 알림 설정) — 스위치 여러 개를 한 줄 선택으로 합칠 때.
   [끔|쪽지함|폰 알림] 처럼 칸이 붙어 있고 고른 칸만 채운다. 상태 = 버튼의 .is-on 한 단어.
   폭은 칸당 고정이라 줄마다 오른쪽 끝이 세로로 맞는다. */
.seg { display: inline-flex; flex: none; border: 1px solid var(--border); border-radius: var(--r-sm); overflow: hidden; }
.seg-btn {
  width: 64px; height: 32px; padding: 0; border: none; border-left: 1px solid var(--border);
  background: var(--bg); color: var(--fg); font: inherit; font-size: var(--fs-xs);
  letter-spacing: var(--ls-body); cursor: pointer;
}
.seg-btn:first-child { border-left: none; }
.seg-btn:hover { background: var(--border); }
.seg-btn.is-on { background: var(--fg); color: var(--bg); font-weight: 700; }
.seg-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
/* 그 항목에 없는 선택지 자리 — 칸은 세워 두고 비운다 (줄마다 같은 뜻이 같은 세로줄에 선다). */
.seg-btn.is-empty { display: inline-flex; align-items: center; justify-content: center; cursor: default; background: var(--bg); color: var(--border); }
.seg-btn.is-empty:hover { background: var(--bg); }
/* 알림 설정 항목 줄 — 아코디언 머리 한 줄에 [이름 · 시점] … [선택지] [화살표].
   이름은 줄이지 않고 시점만 남는 폭에서 줄임표 (전문은 title 과 펼친 칸). */
.notify-item .acc-head { align-items: center; padding: 10px 0; }
.notify-item .acc-info.is-inline .acc-title { flex: none; overflow: visible; }
.notify-item .acc-info.is-inline .acc-sum { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.notify-item .acc-chevron { margin-top: 0; }
.notify-group + .notify-group { margin-top: var(--sp-5); }
.notify-group-head { display: flex; align-items: baseline; gap: var(--sp-3); padding-bottom: 8px; border-bottom: 1px solid var(--fg); }
.notify-group-head .section-label { margin-bottom: 0; }
/* 선택지 범례 (D278-b) — 서버 mode_columns 의 이름·뜻. 세 줄, 이름 칸 폭 고정으로 뜻이 한 세로줄에 선다. */
.notify-legend { margin: 12px 0 20px; padding: 10px 14px; border: 1px solid var(--border); border-radius: var(--r-sm); }
.notify-legend > div { display: flex; gap: 12px; font-size: var(--fs-xs); line-height: 1.8; letter-spacing: var(--ls-body); }
.notify-legend dt { flex: none; width: 56px; font-weight: 700; }
.notify-legend dd { margin: 0; color: var(--fg); }
.notify-item .acc-info.is-inline .tag { flex: none; align-self: center; }
/* 노쇼 정책 줄 (D278-c) — [언제 | 조치 | 면제 | 삭제] 폭 고정 격자라 줄마다 칸이 세로로 맞는다.
   칸 이름 머리 줄은 두지 않는다 — 선택지 이름(1회 차감·회원권당 1회 면제)이 스스로 말한다. */
.policy-row { display: grid; grid-template-columns: 1fr 120px 180px 40px; align-items: center; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--border); }
.policy-row .form-input { width: 100%; }
.policy-when { display: flex; align-items: center; gap: 8px; font-size: var(--fs-sm); letter-spacing: var(--ls-body); }
.policy-when .form-input { width: 120px; }
.policy-add { margin-top: 10px; }
.policy-now { margin-top: var(--sp-5); padding: 12px 14px; border: 1px solid var(--border); border-radius: var(--r-sm); }
.policy-now-head { display: flex; align-items: baseline; gap: var(--sp-3); }
.policy-now-head .section-label { margin-bottom: 0; }
.policy-now ul { margin: 8px 0 0; padding-left: 18px; font-size: var(--fs-xs); line-height: 1.8; letter-spacing: var(--ls-body); }

/* ══ 페이지 <style> 흡수 (2026-09-05 · D118) ═══════════════════════════════
   아래 정의들은 종전에 각 템플릿의 `<style>` 블록 안에 갇혀 있었다. 갇혀 있는
   동안 규격이 지나가도 이 부품들만 빠졌다 — 실제로 D115 컨트롤 높이 통일이
   통계 화면의 날짜칸(29px)·단축 링크(24px)를 못 잡았고, 2026-08-19 에 지운
   다크 잔재(color-scheme:dark · filter:invert)가 그 블록 안에 한 벌 더 살아
   있었다. 부품 정의는 이 파일 한 곳이다 (§3 코드·클래스 SSOT).
   남은 `<style>` 은 랜딩·로그인 둘뿐이고 이유는 SSOT.md §20 에 적었다.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── 날짜 입력 (구 _layout.html · stats.html) ────────────────────────────
   달력 아이콘은 클릭 자리라 커서·여백을 준다. 전 화면 공통이므로 여기 한 곳. */
input[type=date], input[type=month] { cursor: pointer; }
input[type=date]::-webkit-calendar-picker-indicator,
input[type=month]::-webkit-calendar-picker-indicator {
  cursor: pointer; opacity: 0.7; padding: 2px 4px; margin-left: 4px;
}
input[type=date]::-webkit-calendar-picker-indicator:hover,
input[type=month]::-webkit-calendar-picker-indicator:hover { opacity: 1; }
/* 날짜 범위 칸 — 테두리·높이는 `.form-input` 정본을 그대로 쓰고 폭만 잡는다.
   (구 `.stat-date-input` 은 자기 테두리를 따로 그려 높이가 29px 로 남았다) */
.form-input.form-date { min-width: 130px; }
/* 링크로 서는 칩 — `.btn` 은 이미 밑줄을 끄지만 `.chip` 은 안 끄고 있었다.
   (구 `.quick-range` 는 그래서 자기만의 테두리·글꼴을 또 그렸다 — 높이 24px) */
a.chip { text-decoration: none; }

/* ── 표 안 뱃지 크기 ─────────────────────────────────────────────────────
   표 안에서는 뱃지·등급·D-day 가 표 글자 크기(--fs-sm = "표 · 버튼")를 따른다.
   기본 뱃지는 --fs-xs 라 한 행 안에서 이름·날짜와 1px 어긋나 튀어 보였다.
   2026-06-11 사용자 지시로 회원 목록 <style> 안에서만 고쳐져 있던 것을 D118 에
   전 표로 올렸다 — 한 페이지에만 있는 규칙은 다른 표를 계속 어긋난 채로 둔다. */
td .level, td .dday-badge, td .badge {
  font-size: var(--fs-sm); font-weight: 700;
  letter-spacing: var(--ls-body); line-height: 1.4;
}

/* ── D-day 표시 ──────────────────────────────────────────────────────────
   알약이 아니라 글자다 (2026-05-26 사용자 지시) — 뜻은 색 하나로 읽는다.
   D118 이전에는 **정의가 두 벌이고 톤 이름도 갈려** 있었다:
     members.html    `.dday-safe` / `.dday-urgent` / `.dday-expired`
     dashboard.html  `.dday-far`  / `.dday-3`      / `.dday-0`
   같은 사실을 두 이름으로 부르면 한쪽만 고쳐진다 (§0-B). 뱃지(§8.4)와 같은
   상태 어휘 하나로 모은다 — 기본 · `.is-warn` 임박 · `.is-danger` 오늘·만료. */
.dday-badge {
  display: inline; font-size: var(--fs-sm); font-weight: 700;
  letter-spacing: var(--ls-body); white-space: nowrap;
}
.dday-badge.is-warn    { color: var(--warning); }
.dday-badge.is-danger  { color: var(--primary); }
.dday-badge.is-expired { color: var(--primary); font-style: italic; }

/* ── 정렬 가능한 표 머리 (구 members.html) ───────────────────────────────
   엔진은 공용 `app.js makeSortable` 인데 **모양만** 회원 목록 <style> 안에
   있었다 — 다른 표가 같은 엔진을 붙이면 화살표가 안 보이는 상태였다. */
th.sortable { user-select: none; cursor: pointer; }
th.sortable:hover { background: var(--border); }
th.sort-asc, th.sort-desc { color: var(--primary); }
th.sort-asc .sort-arrow, th.sort-desc .sort-arrow { opacity: 1; font-weight: 700; }
.sort-arrow { font-size: var(--fs-xs); margin-left: 3px; opacity: 0.35; }

/* ── 회원 명단 표 (D213 · 2026-09-19 사용자 지시 7건) ─────────────────────
   "회원화면 레이아웃 쉬프트" · "헤더행 중앙정렬" · "D-day는 표시하라는게 아니고
   저렇게 뒷 배경색만" · "상태칸은 평소에는 숨김이 디폴트".

   시프트의 원인은 **칸 폭이 내용으로 정해지던 것**이었다. 검색·필터·정렬로 행이
   줄면 남은 행의 가장 긴 글자가 바뀌고, 그때마다 열네 칸이 통째로 좌우로 밀렸다
   (실측 107px). `table-layout: fixed` 로 폭을 못박아 0px 으로 만든다.

   **폭은 브라우저에서 잰 값이다** (사용자 "또 칸이 쓸데없이 지금 크잖아. 노트북같은
   작은화면에서 볼때도 괜찮게"). 표를 내용 최소폭으로 쪼그라뜨려 칸마다 진짜 필요한
   글자 폭을 읽고, 거기에 여백만 더했다. 짐작으로 준 구 비율(%)·넉넉한 px 은 폐기.
   합이 화면보다 작으면 남는 자리는 폭에 비례해 고르게 붙는다. */
.members-table-scroll { overflow-x: auto; }
.members-table { table-layout: fixed; width: 100%; min-width: 977px; }

/* 머리행 중앙정렬. 한 칸 안에서 줄을 바꾸지 않는다 — 종전엔 '성별' 이 '성/별' 로
   세로로 쪼개지고 정렬 화살표가 아래 줄로 떨어졌다. */
.members-table thead th { text-align: center; white-space: nowrap; position: relative; }

/* D213-d (2026-09-19 사용자 "레벨칸은 헤더와 내용이 중앙정렬이 안맞고") —
   **정렬 화살표가 머리 글자를 왼쪽으로 밀고 있었다.** 글자와 화살표를 합친 덩어리가
   가운데에 놓이니 글자만 보면 늘 6~7px 왼쪽이었다(좁은 칸일수록 눈에 띈다 — 실측).
   화살표를 글 흐름에서 빼 칸 오른쪽에 띄우면 머리 글자가 칸 한가운데에 선다.
   평소엔 감추고 **가리키거나 그 칸으로 정렬 중일 때만** 보인다 — 자리를 안 차지하니
   나타나고 사라져도 글자가 움직이지 않는다. */
.members-table thead th .sort-arrow {
  position: absolute; right: 2px; top: 50%; transform: translateY(-50%);
  margin: 0; opacity: 0; font-size: 10px;
}
.members-table thead th.sortable:hover .sort-arrow,
.members-table thead th.sort-asc .sort-arrow,
.members-table thead th.sort-desc .sort-arrow { opacity: 1; }

/* **머리와 몸통의 정렬은 칸마다 같다.** 머리만 가운데로 돌렸더니 왼쪽에 붙은 몸통
   글자와 어긋나 칸마다 삐뚤빼뚤해 보였다 (사용자 지적). 짧은 값 칸은 몸통도 가운데로
   맞추고(템플릿 `ta-center`), 글이 들어가는 세 칸과 숫자 한 칸만 머리를 되돌린다. */
.members-table th[data-col="name"],
.members-table th[data-col="safety"],
.members-table th[data-col="plan"] { text-align: left; }
.members-table th[data-col="points"] { text-align: right; }
/* D213-d — 관리 칸은 **다시 가운데**다 (사용자 "헤더행에서는 관리는 아예 좌측에 찌그러져있잖아").
   오른쪽으로 몰아 두니 머리 '관리' 는 오른쪽 끝, 몸통 '수정 삭제' 는 칸을 가득 채워 축이
   어긋나 보였다. '승인' 이 줄을 밀던 문제는 **정렬이 아니라 보조 줄**이 막는다(D213-c) —
   주 줄은 어느 회원이든 '수정 삭제' 둘뿐이라 가운데로 둬도 움직이지 않는다. */
.members-table tbody td.actions { text-align: center; }
.members-table tbody td.actions .row-sub { opacity: 1; }
.members-table tbody td.actions a + a { margin-left: 8px; }

/* 칸이 열넷이라 좌우 여백은 그것만으로 폭을 먹는다 — 이 표에서만 8px 로 좁힌다. */
.members-table th, .members-table td { padding-left: 8px; padding-right: 8px; }

/* ── 행 높이는 **한 값** 이다 (D213-b · 2026-09-19 사용자 "잔여3회/10회 이게 들어가니까
   또 행높이가 엉망이 된다 레이아웃시프트 철저히 고려해서 작업") ──────────────

   원인은 칸 안의 보조 줄 개수였다 — 이름 칸에 셋(식별 해시·로그인 아이디·늦은 취소),
   회원권 칸에 셋(잔여 횟수·해지 예정·겹침)까지 붙어 행마다 47·58·69px 로 갈렸다.
   보조 줄을 칸마다 **하나로 모으고, 비어 있어도 자리를 잡아 둔다** — 그래서 어떤 회원이
   어떤 사연을 달고 있든 모든 행이 같은 높이다 (프로젝트 '레이아웃 안정성 = 공간 예약').
   겸해서 여백을 줄여(사용자 승인 '한 화면에 더') 종전 47px 보다 낮은 45px 로 선다. */
.members-table {
  /* 자료 줄 한 칸의 높이 = 위아래 여백 + 주 줄 + 사이 + 보조 줄. **한 곳에서 파생한다** —
     빈 줄(pager-filler)도 이 값으로 서야 표에 걸리는 예약 높이가 자연 높이와 같아진다. */
  --row-pad: 6px; --row-main: 17px; --row-gap: 2px; --row-sub: 14px;
  --row-h: calc(var(--row-pad) * 2 + var(--row-main) + var(--row-gap) + var(--row-sub));
}
.members-table tbody td {
  padding-top: var(--row-pad); padding-bottom: var(--row-pad);
  line-height: var(--row-main);
}
.members-table thead th { padding-top: 8px; padding-bottom: 8px; }
/* 자료 줄 맨 윗줄에 값을 맞춘다 — 가운데로 두면 한 줄짜리 칸의 글자가 두 줄짜리 칸의
   어느 줄에도 안 붙어 떠 보인다. 굵기는 이름만(템플릿 <b>), 회원권은 보통 글자다. */
.members-table tbody td { vertical-align: top; }
/* 두 줄 칸의 윗줄 — 높이는 --row-main 이다. td 가 이미 같은 값을 주므로 화면은 그대로이고,
   이 줄은 마크업이 부르는 이름(.row-main)을 규격집에 올려 둔다 (D250 · lint §7.5 유령 클래스). */
.members-table tbody td .row-main { line-height: var(--row-main); }
.members-table tbody td .row-sub {
  display: block; height: var(--row-sub); line-height: var(--row-sub);
  margin-top: var(--row-gap);
  font-size: 11px; opacity: var(--dim);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* 빈 줄도 자료 줄과 **같은 높이**로 선다. 종전엔 한 줄짜리라 낮았고, 그래서 공용 페이저가
   표에 걸어 두는 예약 높이(D189)가 남아 **보이는 자료 줄이 대신 늘어났다** — 검색으로 두 줄만
   남기면 46 → 57px 로 부풀었다(실측). 빈 줄이 제 몫을 가져가면 자료 줄은 언제나 그대로다. */
.members-table tbody tr.pager-filler > td {
  /* +1px 은 아래 구분선 몫 — 자료 줄은 선까지 합해 그만큼 서고, 이 칸은 box-sizing 이
     border-box 라 height 안에 선이 들어간다. 이 1px 을 빼먹으면 줄마다 1px 이 남아
     걸러진 화면에서 자료 줄이 대신 늘어난다 (실측 46 → 48.75). */
  height: calc(var(--row-h) + 1px); padding-top: 0; padding-bottom: 0;
}
/* 칸 안의 배지·숫자가 제 줄높이를 들고 오면 그 행만 높아진다 — 표 줄높이를 따르게 한다. */
.members-table tbody td .level,
.members-table tbody td .dday-badge,
.members-table tbody td .badge,
.members-table tbody td .tag { line-height: inherit; }

/* ── 줄 배경 넷의 순서 (뒤가 이긴다) ──────────────────────────────────────
   줄무늬 → 만료 회색조 → D-day 색 → 마우스 올림. 겹칠 때 무엇이 남아야 하는지가
   그대로 순서다: 만료는 줄무늬를 덮고, 곧 끝나는 회원권은 만료보다 급하며,
   지금 가리키는 줄은 언제나 맨 위다. */
.members-table tbody tr.is-stripe > td { background: color-mix(in srgb, var(--fg) 3%, var(--bg)); }
.members-table tbody tr.is-expired > td {
  background: color-mix(in srgb, var(--fg) 7%, var(--bg));
  color: color-mix(in srgb, var(--fg) 52%, var(--bg));
}
/* 지난 회원권 줄에서는 D-day 도 붉게 쓰지 않는다 — 급한 것은 앞으로 남은 쪽이다. */
.members-table tbody tr.is-expired .dday-badge { color: inherit; }

/* 폭 = **실측 최소폭(px)을 비율로 옮긴 값** + 표 전체에 그 합을 min-width 로.
   두 가지를 한꺼번에 푼다.
     · min-width 977px — 1280 노트북 본문(978px)에도 들어가는 바닥. 더 좁아지면 칸을
       뭉개는 대신 카드 안에서 가로로 흐른다. 이 폭에서 비율은 실측 최소치 그대로다.
     · 비율 — 화면이 넓어지면 열셋이 **고르게** 늘어난다. px 로만 못박았더니 1920 화면에서
       합 1060px 에 표는 1618px 이라 오른쪽에 558px 이 빈 채로 남았다(실측).
   이름 칸은 '이름 미입력' 한 줄이면 된다 — 실제 이름은 다섯 자 안쪽이고(사용자 지적),
   이름이 없는 회원의 식별 해시는 옆이 아니라 아래 줄에 둔다.
   D213-d — 다시 쟀다. 화살표를 글 흐름에서 빼면서 좁은 머리 칸이 더 줄었고, 이름 칸도
   94 → 76 으로 내렸다. 거기서 나온 자리를 **부상 이력**으로 보냈다(76 → 91).
   락커·포인트는 실제 값('입구-09' · '1,200 P')에 맞춰 넓혔다.
   기준 px: 이름 76 · 성별 42 · 생년 64 · 전화 112 · 레벨 42 · 부상 91 · 회원권 88 ·
   시작 90 · 종료 89 · D-day 62 · 락커 64 · 포인트 64 · 관리 93 (= 977).
   D-day 62 는 정렬 중 화살표가 'D-DAY' 글자를 안 건드리는 값이고, 관리 93 은 가입 신청
   줄의 보조 줄('승인 거절')이 안 잘리는 값이다 — 둘 다 1~2px 모자라 실측으로 늘렸다. */
.members-table th[data-col="name"]   { width: 7.78%; }   /* 이름 다섯 자 · '이름 미입력' */
.members-table th[data-col="gender"] { width: 4.30%; }
.members-table th[data-col="birth"]  { width: 6.55%; }   /* 910829 */
.members-table th[data-col="phone"]  { width: 11.46%; }  /* 010-4444-5555 */
.members-table th[data-col="level"]  { width: 4.30%; }
.members-table th[data-col="safety"] { width: 9.31%; }
.members-table th[data-col="plan"]   { width: 9.01%; }  /* 이름 + '잔여 N회' 보조 줄 */
.members-table th[data-col="start"]  { width: 9.21%; }   /* 2026-08-23 */
.members-table th[data-col="end"]    { width: 9.11%; }
.members-table th[data-col="dday"]   { width: 6.35%; }   /* D-104 */
.members-table th[data-col="locker"] { width: 6.55%; }   /* A-12 */
.members-table th[data-col="points"] { width: 6.55%; }   /* 105 P */
.members-table th[data-col="actions"] { width: 9.52%; }  /* 수정 삭제 */
/* 평소엔 숨은 칸이라 비율 몫을 주지 않는다 — 켜면 그만큼 표가 넓어지고 카드가 흐른다. */
.members-table th[data-col="status"] { width: 86px; }

/* 폭을 못박으면 긴 값이 칸을 밀지 못하고 넘친다 — 한 줄로 자르고 전문은 title 로.
   이름 칸은 보조 줄(로그인 아이디·늦은 취소)이 있어 자르지 않는다. */
.members-table td.col-safety { white-space: nowrap; overflow: hidden;
  text-overflow: ellipsis; }

/* D-day 줄 배경 — 10일부터 연분홍(near) · 5일 아래 빨강(soon).
   어느 단계인지는 서버가 정한다(api/_metrics.dday_tone) — 여기에 숫자는 없다.
   색은 토큰에서 섞어 만든다 (§2-B — hex 직접 금지). 글자는 그대로 읽힌다. */
/* 선택자 무게를 줄무늬·만료와 **똑같이** 맞춘다(`tbody` 포함) — 그래야 뒤에 적힌 이 규칙이
   이긴다. 종전엔 줄무늬가 한 단계 무거워 D-day 색을 덮었다 (실측: 분홍이 안 나왔다). */
.members-table tbody tr.is-dday-near > td { background: color-mix(in srgb, var(--primary) 8%, var(--bg)); }
.members-table tbody tr.is-dday-soon > td { background: color-mix(in srgb, var(--primary) 15%, var(--bg)); }
/* D213-c — 농도를 15%·8% 로 내리니(사용자 "지금 너무 짙다 … 한 15%씩만") 붉은 글자가
   다시 읽힌다(대비 5.2:1). 임박(5일 이하)은 숫자도 제 붉은색으로 돌려놓고, 예고(10일부터)는
   배경만으로 말한다 — 두 단계가 배경 농도와 글자색 둘로 갈린다. */
.members-table tbody tr.is-dday-near .dday-badge { color: var(--fg); }

/* 마우스가 가리키는 줄이 맨 위 — 줄무늬·만료·D-day 어느 색 위에서도 같은 강조. */
.members-table tbody tr:hover > td { background: var(--border); }
.members-table tbody tr.is-dday-near:hover > td { background: color-mix(in srgb, var(--primary) 13%, var(--bg)); }
.members-table tbody tr.is-dday-soon:hover > td { background: color-mix(in srgb, var(--primary) 21%, var(--bg)); }

/* 상태 칸 여닫이 — 기본이 숨김이다 (켜는 체크박스는 필터 줄에 있다). */
.members-table.hide-status .col-status { display: none; }

/* ── 홈 목록 (구 dashboard.html) ─────────────────────────────────────────
   예약·출석 · 수업 일정 두 카드의 줄 부품 (만료 임박 카드는 D195 에서 걷어냈다 — 케어 필요 화면).
   (구 `.more-link` · `.class-card` 는 여기로 옮겨 오자마자 §7.8 이 사문으로
   잡아 지웠다 — 홈이 아코디언으로 바뀌며 죽었는데 페이지 <style> 안이라
   아무도 몰랐다.) */
.member-row {
  display: flex; align-items: center; gap: 12px;
  padding: 9px 0; border-bottom: 1px solid var(--border);
  cursor: pointer; transition: background 0.1s;
}
.member-row:last-child { border-bottom: none; }
.member-row:hover { background: var(--border); margin: 0 -24px; padding: 9px 24px; }
.member-avatar-sm {
  width: 32px; height: 32px; border-radius: 50%;
  background: var(--border); border: 1px solid var(--border);
  display: flex; align-items: center; justify-content: center;
  font-size: var(--fs-xs); font-weight: 700; color: var(--fg);
  flex-shrink: 0; overflow: hidden;
}
.member-name { font-size: var(--fs-body); font-weight: 700; color: var(--fg); flex: 1; }
.member-class { font-size: var(--fs-xs); color: var(--fg); flex: 2; }
/* (구 `.badge-attended`(초록) · `.badge-reserved`(바탕 채움) — D118 삭제.
   §8.4 는 뱃지가 셋뿐이고 "초록 뱃지는 규격에 없다" 고 못 박아 두었는데,
   페이지 <style> 안이라 그 압축이 지나갈 때 통째로 빠져 있었다.
   지금은 둘 다 정본 `.badge` 이고 뜻은 글자('출석'/'예약')가 말한다.) */
/* (구 `.class-strip` 수업 색 띠 — D177 에서 뜻을 잃고 D195 에서 홈 줄이 한 줄이 되며 삭제.) */
.class-badge { font-size: var(--fs-xs); font-weight: 700; padding: 3px 8px; border-radius: var(--r-sm); background: var(--border); color: var(--fg); white-space: nowrap; }
/* 예약 명단 — 수업 줄 아래 한 줄 (2026-08-28 사용자 지시) */
.roster-name { color: var(--fg); text-decoration: none; }
.roster-name:hover { color: var(--primary); text-decoration: underline; }
.class-roster { font-size: var(--fs-xs); letter-spacing: var(--ls-body); color: var(--fg); opacity: var(--dim); margin-top: 3px; word-break: keep-all; }
/* 아코디언 공용 컴포넌트 (2026-09-13) — 접힌 채로 열리고 머리를 누르면 펼친다. 상태 = .is-open 한 단어.
   머리 = [.acc-info(.acc-title + .acc-sum)] … [.acc-chevron]. 토글은 _layout.html 의 위임 리스너 한 곳.
   dashboard 의 수업 아코디언(구 .class-group)이 첫 인스턴스 — 설정 페이지(예약·업적)도 이것을 쓴다. */
.acc { border-bottom: 1px solid var(--border); }
.acc:last-child { border-bottom: none; }
.acc-head { display: flex; align-items: flex-start; gap: 14px; padding: 12px 0; cursor: pointer; user-select: none; }
.acc-head:hover .acc-title { color: var(--primary); }
.acc-info { flex: 1; }
.acc-title { font-size: var(--fs-body); font-weight: 700; color: var(--fg); letter-spacing: var(--ls-body); }
.acc-sum { font-size: var(--fs-xs); color: var(--fg); opacity: var(--dim); margin-top: 3px; letter-spacing: var(--ls-body); }
.acc-chevron { font-size: 15px; color: var(--fg); flex-shrink: 0; margin-top: 3px; transition: transform 0.18s; }
.acc.is-open .acc-chevron { transform: rotate(180deg); }
.acc-body { display: none; padding: 0 0 10px 18px; }
.acc.is-open .acc-body { display: block; }
.acc-body .class-roster { padding: 0 12px 6px 0; margin-top: 0; }
/* 머리 한 줄 상태 (D195 · 2026-09-16 홈 '한 화면에') — 시각 · 이름 · 코치를 한 줄에. 시각 칸은 폭을
   맞춰 이름이 세로로 정렬되고, 긴 이름은 줄임표. 줄 높이는 두 줄(≈64)에서 한 줄(≈48)로. */
.acc-head:has(> .acc-info.is-inline) { align-items: center; }
.acc-info.is-inline { display: flex; align-items: baseline; gap: 10px; min-width: 0; flex: 1; }
.acc-info.is-inline .acc-sum { margin-top: 0; white-space: nowrap; }
.acc-info.is-inline .acc-sum:first-child { flex: 0 0 96px; }
.acc-info.is-inline .acc-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* (구 `.expiry-row` 만료 임박 줄 — D195 에서 홈 카드를 걷어내며 삭제. 목록은 케어 필요 화면.) */

/* ── 통계 차트 (구 stats.html) ───────────────────────────────────────────
   위 "공통 차트 컴포넌트" 절과 한 몸이다 — 그쪽은 KPI·도넛, 여기는 막대·격자. */
.chart-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; }
.donut-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
/* 사용자 요청 2026-05-26 — 막대 차트 세로 2배(110→220) + 가장 큰 값이 천장에 닿게.
   막대가 100% 까지 도달하고 값 라벨은 막대 위쪽 안에 띄운다. */
.bar-chart { display: flex; gap: 6px; align-items: flex-end; height: 220px; padding-top: 22px; }
.bar-col { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 6px; height: 100%; justify-content: flex-end; }
.bar-fill { width: 100%; max-width: 44px; background: var(--primary); border-radius: var(--r-sm) var(--r-sm) 0 0; min-height: 3px; position: relative; transition: opacity .15s; }
.bar-fill:hover { opacity: 0.85; }
/* 0 값도 3px 짜리 막대로 서 있게 — 아예 없으면 "빠진 달" 처럼 읽힌다.
   (구 `.zero` · `.current` — 맨몸 상태어라 §7.3 위반이었다. D118 에 is- 접두) */
.bar-fill.is-zero { background: var(--border); }
.bar-fill .bar-val { position: absolute; top: -18px; left: 50%; transform: translateX(-50%); font-size: var(--fs-xs); font-weight: 700; color: var(--fg); white-space: nowrap; }
.bar-label { font-size: var(--fs-xs); color: var(--fg); font-weight: 700; }
.bar-label.is-current { color: var(--primary); font-weight: 800; }

/* ── 수업 관리 주간 달력 (구 classes.html) ──────────────────────────────
   월간 달력(`.mcal-*`)과 다른 화면이다 — 그쪽은 공지 기간 막대, 여기는
   주간 격자 + 수업 칩. 이름 접두로 갈라 둔다. */
.slot-template-hint { font-size: var(--fs-xs); margin: -8px 0 12px; }
.week-nav { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
.calendar-outer { overflow-x: auto; overflow-y: hidden; }
.calendar-grid {
  display: grid;
  /* 왼쪽 열 — 시각 + 프로그램 이름이 들어가므로 52 → 116 (2026-08-28). */
  grid-template-columns: 116px repeat(7, 1fr);
  grid-auto-rows: 38px;
  /* R5 (2026-08-21 사용자 보고) — 행 경계를 gap 이 아닌 border 로 그린다.
     세로줄만 column-gap 으로 유지하고, 가로줄은 정시=실선 / :30=없음. */
  row-gap: 0; column-gap: 1px;
  background: var(--border);
  min-width: 640px;
  border: 1px solid var(--border);
}
.cal-time, .cal-cell { border-top: 1px solid var(--border); }
.cal-header {
  background: var(--bg);
  display: flex; align-items: center; justify-content: center;
  font-size: 11px; color: var(--fg); font-weight: 700; letter-spacing: var(--ls-body);
}
/* 왼쪽 열 — 시간 눈금 대신 그 시각의 프로그램 이름 (2026-08-28 사용자 지시).
   수업이 있는 줄만 "06:30 Awake" 로 적고, 없는 줄은 비운다. */
.cal-time {
  background: var(--bg);
  display: flex; align-items: center; justify-content: flex-end;
  padding-right: 6px;
  font-size: 10px; color: var(--fg); opacity: var(--dim);
  white-space: nowrap; overflow: hidden;
}
.cal-time.has-class { opacity: 1; font-weight: 700; }
/* 시각은 절대 자르지 않는다 — 넘치는 건 프로그램 이름 쪽을 말줄임한다.
   (2026-08-28 실화면 검증: flex-end + overflow hidden 이라 긴 이름이
   왼쪽으로 밀려 "06:00" 앞부분까지 잘려 나갔다. 전체 문구는 title 툴팁.) */
.cal-time .ct-time { flex: 0 0 auto; }
.cal-time .ct-name {
  margin-left: 4px; font-weight: 700;
  flex: 0 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cal-cell {
  background: var(--bg);
  position: relative;
  cursor: pointer;
  transition: background 0.15s;
  display: flex; flex-direction: column; gap: 2px; padding: 2px;
}
/* C3·R5 — :30 행 위 경계는 지운다 (한 시간 = 2행이 한 덩어리로 보이게).
   구현 주의: border-top-style 만 주면 기본 폭 medium(3px)+글자색이 붙어
   굵은 검정 점선이 됐다 (2026-08-21 사용자 보고). */
.cal-cell.is-half, .cal-time.is-half { border-top: none; }
/* 2026-08-26 — 수업 앞뒤 1시간만 보이고, 그 사이 빈 시간은 접힌 행 하나로.
   위·아래 옅은 점선 경계, 누르면 그 주 전체 시간을 펼친다. 접힌 행 바로 아래
   정시 행의 실선 위 경계는 지운다 (점선+실선 이중선 방지). */
.cal-gap {
  background: var(--bg); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  font-size: 11px; color: var(--fg);
  border-top: 1px dashed var(--border); border-bottom: 1px dashed var(--border);
}
.cal-gap span { opacity: var(--dim); }
.cal-gap:hover span, .cal-gap:focus-visible span { opacity: 1; }
.cal-time.is-after-gap, .cal-cell.is-after-gap { border-top: none; }
.cal-chip-lane {
  display: flex; gap: 2px; margin: 1px 2px;
  position: relative; z-index: 1; pointer-events: none; /* 틈 클릭은 아래 셀(등록)로 */
  align-items: flex-start;
}
.cal-chip-lane .cal-class-chip { pointer-events: auto; }
.cal-class-chip {
  flex: 1 1 0; min-height: 0;
  background: var(--primary);
  border-radius: var(--r-sm);
  padding: 2px 6px;
  font-size: 11px; color: var(--bg);
  overflow: hidden;
  cursor: pointer;
  display: flex; flex-direction: column; justify-content: center;
  gap: 2px;
}
.cal-class-chip .chip-title { font-weight: 700; line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cal-class-chip .chip-count { font-size: 10px; line-height: 1.2; opacity: 0.85; }
/* 2026-08-29 (앱 D87) — 취소된 수업은 달력에서 취소된 것으로 보인다. 종전엔 상태를 안 봐서
   취소 뒤 목록을 다시 받아도 빨간 칩 그대로였다("취소했는데 계속 떠 있다"). 폰의 '취소됨'
   흐림 배지와 같은 뜻: 지우지 않고(그 시각에 수업이 있었다는 사실) 흐리게 + 취소선. */
.cal-class-chip.is-cancelled { background: var(--bg); color: var(--fg); opacity: var(--dim); border: 1px dashed var(--border); }
.cal-class-chip.is-cancelled .chip-title { text-decoration: line-through; }
/* 구 `.detail-row`(.k/.v) 는 D145(2026-09-14) 에 지웠다 — 상세 모달의 7줄 표(시간·소요·룸·종류·
   예약·대기·상태)를 아래 `.detail-meta` 한 줄로 접었고, 명단 행은 `.roster-row` 가 맡는다.
   룸·종류는 어떤 UI 도 값을 넣지 않던 고아 표시라 줄 자체가 없어졌다 (DB 컬럼은 휴면). */
/* ── 수업 상세 메타 한 줄 (D145) ── 시간 · 소요 · 예약 · 대기 · 상태.
   부가 정보라 dim 톤, 상태만 제 색·굵게. 항목 사이 구분자는 CSS 가 찍는다 (마크업은 값만). */
.detail-meta {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 0;
  font-size: var(--fs-sm); letter-spacing: var(--ls-body); font-variant-numeric: tabular-nums;
}
.detail-meta > span { opacity: var(--dim); }
.detail-meta > span + span::before { content: "·"; margin: 0 6px; color: var(--fg); }
.detail-meta > .detail-status { opacity: 1; font-weight: 700; }
/* 상태 색 — 이름 셋은 classes.html 의 `STATUS_TONE` 표에 그대로 적혀 있다.
   조립하지 않으므로 여기 이름과 저기 이름이 grep 으로 맞물린다 (§0-B). */
.detail-status.is-open { color: var(--success); }
.detail-status.is-cancelled { color: var(--primary); }
.detail-status.is-completed { color: var(--fg); }
/* ── 수업 내용 블록 — 상세 모달의 주인공 (D145) ──
   머리 = 제목(굵게) + 부제(작게·dim). 본문 pre 는 본문 크기·1.6 줄간격·min-height 140 —
   '불러오는 중' → 내용으로 바뀔 때 높이가 안 튄다 (§17). */
.wod-head {
  display: flex; align-items: baseline; gap: var(--sp-2); margin-bottom: var(--sp-1);
  font-size: var(--fs-sm); font-weight: 700; letter-spacing: var(--ls-body);
}
.wod-head .wod-sub { font-size: var(--fs-xs); font-weight: 400; opacity: var(--dim); }
.wod-block pre {
  white-space: pre-wrap; font-family: inherit; font-size: var(--fs-body);
  color: var(--fg); margin: 0; background: var(--bg);
  border: 1px solid var(--border); border-radius: var(--r-md);
  padding: var(--sp-3); line-height: 1.6; letter-spacing: var(--ls-body);
  min-height: 140px; box-sizing: border-box;
}
/* 비어 있음 — 점선 테두리 + 은은한 깜빡임. 칸 자체가 [수정] 로 가는 버튼이다
   (classes.html _setDayContent 가 role=button·tabindex 를 단다). */
.wod-block pre.is-empty {
  border-style: dashed; cursor: pointer;
  animation: hy-attention 1.4s ease-in-out infinite;
}
.wod-block pre.is-empty:hover, .wod-block pre.is-empty:focus-visible { border-color: var(--primary); }
/* 주의 끌기 — 빈 내용일 때 푸터 [수정] 버튼에 단다. 단일 속성(opacity) 키프레임,
   box-shadow·색 변화 없음 (design-block). 움직임을 줄인 환경에서는 끈다. */
.is-attention { animation: hy-attention 1.4s ease-in-out infinite; }
@keyframes hy-attention { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
@media (prefers-reduced-motion: reduce) {
  .wod-block pre.is-empty, .is-attention { animation: none; }
}
/* ── 예약자 명단 행 (D145) — 32px 한 줄: 이름(굵게) + 전화(작게·dim) | 출석·노쇼 또는 상태 글자.
   행 사이 1px 선. 고아 회원(탈퇴)은 유틸 op-55 로 흐리게 (종전과 같다). */
.roster-row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
  height: 32px; border-top: 1px solid var(--border);
  font-size: var(--fs-sm); letter-spacing: var(--ls-body);
}
.roster-row:first-child { border-top: 0; }
.roster-who { font-weight: 700; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.roster-who .roster-phone {
  font-size: var(--fs-xs); font-weight: 400; opacity: var(--dim);
  margin-left: var(--sp-2); font-variant-numeric: tabular-nums;
}
.roster-act { flex-shrink: 0; display: flex; align-items: center; }

/* ── 명단 출석·노쇼 표시 (구 classes.html 인라인) ───────────────────────
   한 줄 안에서 켜고 끄는 두 칸. 종전에는 JS 가 `style="font:inherit; cursor:…"`
   문자열로 **부품 하나를 통째로 인라인**으로 그리고 있었다 — 인라인 래칫은 1건으로
   세지만 실제로는 버튼 모양 전체가 규격 밖에 있었다 (토스트가 세 벌로 갈렸던 경로,
   §7.7 과 같은 병). 톤은 상태 클래스가 `--mark-tone` 하나만 바꾼다 — 색 리터럴 0.
   `.btn` 을 못 쓰는 자리다: 표 줄 안에 들어가고, 둘 중 하나만 켜지며, 빨간 버튼은
   화면에 하나뿐이어야 한다 (§8.3·§8.6). */
.roster-mark {
  font: inherit; cursor: pointer;
  border: 1px solid var(--mark-tone);
  background: transparent; color: var(--mark-tone);
  border-radius: var(--r-sm); padding: 3px 9px; margin-left: 6px;
  letter-spacing: var(--ls-body);
}
.roster-mark.is-attend { --mark-tone: var(--success); }
.roster-mark.is-noshow { --mark-tone: var(--primary); }
.roster-mark.is-on { background: var(--mark-tone); color: var(--bg); }

/* ── 수업 종류 시간표 (구 class_templates.html) ─────────────────────────
   규칙 1건 = 한 줄. 왼쪽 요일 스트립(월~일 7칸 고정폭, 해당 요일만 채움) +
   오른쪽 시각. 여러 규칙은 세로로, 시각 오름차순. 셀 안에 조작 요소 없음. */
.wk-row { display: flex; align-items: center; gap: 10px; white-space: nowrap; }
.wk-row + .wk-row { margin-top: 4px; }
.wk-strip { display: inline-flex; gap: 2px; }
.wk-strip span {
  width: 18px; height: 18px; box-sizing: border-box;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 10px; font-weight: 700; line-height: 1;
  border: 1px solid var(--border); border-radius: var(--r-sm);
  color: var(--fg); opacity: 0.35;
}
.wk-strip span.is-on { background: var(--fg); border-color: var(--fg); color: var(--bg); opacity: 1; }
.wk-time { font-size: var(--fs-xs); font-weight: 700; font-variant-numeric: tabular-nums; }
.wk-none { font-size: var(--fs-xs); opacity: var(--dim); }
/* 규칙 조회 '실패' 는 '시간표 없음'(정상 0건)과 다른 사실이다 — 흐리게 두면
   같은 것으로 읽힌다 (2026-08-27 P1). 또렷하게, 경고색으로. */
.wk-fail { font-size: var(--fs-xs); font-weight: 700; color: var(--primary); }
/* 수정 모달의 시간표 편집 행 — 요일 토글은 tab-chip 재사용 (글자 폭 따라 들쭉이지 않게 고정폭) */
.sched-edit { display: flex; flex-direction: column; gap: 6px; }
.sched-edit-row { display: flex; align-items: center; gap: 8px; }
.sched-edit-row .sched-days { display: flex; gap: 4px; }
.sched-edit-row .tab-chip { padding: 6px 0; min-width: 32px; text-align: center; }
/* 시각 input — 고정폭 112px 이면 한국어 로케일 표기("오전 06:00" + 시계 아이콘)가
   안쪽 여백(.form-row input 12px×2)을 빼면 86px 에 들어가야 해서 분 자리가
   "06:0" 으로 잘렸다 (2026-08-27 사용자 보고). 브라우저가 로케일 서식에 맞춰
   재는 고유 폭을 쓰고, 행 안에서 줄어들지 않게 한다. */
.sched-edit-row .form-input { width: auto; min-width: 120px; flex: 0 0 auto; padding: 10px; }
.sched-edit-row .link-btn { font-size: var(--fs-h3); line-height: 1; padding: 4px 6px; }
/* 수업 색 고르기 — 스와치 자체가 데이터(사장이 고르는 색)라 배경은 인라인이다.
   고른 것은 테두리로 표시한다. (구 `.selected` → `.is-selected`, §7.3) */
.t-swatch {
  width: 30px; height: 30px; border-radius: 50%; cursor: pointer;
  border: 3px solid transparent; transition: transform 0.12s;
  outline: 2px solid transparent; outline-offset: 2px;
}
.t-swatch:hover { transform: scale(1.12); }
/* (구 `box-shadow: 0 0 0 3px rgba(255,255,255,0.3)` — D118 삭제. 흰 후광이라
   라이트 배경(#FAFAFA)에서는 보이지 않는 다크 시절 잔재였다. 표시는 outline.) */
.t-swatch.is-selected { outline: 2px solid var(--fg); transform: scale(1.12); }

/* ══ AUTO-GENERATED UTILITIES — scripts/inline_style_sweep.py (수동 편집 금지) ══ */
.ai-center.ai-center.ai-center{align-items:center}
.ai-flex-end.ai-flex-end.ai-flex-end{align-items:flex-end}
.ai-flex-start.ai-flex-start.ai-flex-start{align-items:flex-start}
.as-center.as-center.as-center{align-self:center}
.bd-1-solid-border.bd-1-solid-border.bd-1-solid-border{border:1px solid var(--border)}
.bd-1-solid-primary.bd-1-solid-primary.bd-1-solid-primary{border:1px solid var(--primary)}
.bd-1-solid-rgba-238-43-43-0-2.bd-1-solid-rgba-238-43-43-0-2.bd-1-solid-rgba-238-43-43-0-2{border:1px solid rgba(238,43,43,0.2)}
.bd-1-solid-rgba-245-158-11-0-35.bd-1-solid-rgba-245-158-11-0-35.bd-1-solid-rgba-245-158-11-0-35{border:1px solid rgba(245,158,11,0.35)}
.bd-none.bd-none.bd-none{border:none}
.bdb-1-solid-border.bdb-1-solid-border.bdb-1-solid-border{border-bottom:1px solid var(--border)}
.bdc-warning.bdc-warning.bdc-warning{border-color:var(--warning)}
.bdcl-collapse.bdcl-collapse.bdcl-collapse{border-collapse:collapse}
.bdl-4-solid-primary.bdl-4-solid-primary.bdl-4-solid-primary{border-left:4px solid var(--primary)}
.bdr-1-solid-border.bdr-1-solid-border.bdr-1-solid-border{border-right:1px solid var(--border)}
.bdt-1-dashed-border.bdt-1-dashed-border.bdt-1-dashed-border{border-top:1px dashed var(--border)}
.bdt-1-solid-border.bdt-1-solid-border.bdt-1-solid-border{border-top:1px solid var(--border)}
.bg-bg.bg-bg.bg-bg{background:var(--bg)}
.bg-border.bg-border.bg-border{background:var(--border)}
.bg-none.bg-none.bg-none{background:none}
.bg-primary.bg-primary.bg-primary{background:var(--primary)}
.bg-rgba-0-0-0-0-45.bg-rgba-0-0-0-0-45.bg-rgba-0-0-0-0-45{background:rgba(0,0,0,0.45)}
.bg-rgba-238-43-43-0-05.bg-rgba-238-43-43-0-05.bg-rgba-238-43-43-0-05{background:rgba(238,43,43,0.05)}
.bg-rgba-245-158-11-0-06.bg-rgba-245-158-11-0-06.bg-rgba-245-158-11-0-06{background:rgba(245,158,11,0.06)}
.bg-transparent.bg-transparent.bg-transparent{background:transparent}
.bottom-24.bottom-24.bottom-24{bottom:24px}
.br-4.br-4.br-4{border-radius:4px}
.br-8.br-8.br-8{border-radius:8px}
.br-r-lg.br-r-lg.br-r-lg{border-radius:var(--r-lg)}
.br-r-md.br-r-md.br-r-md{border-radius:var(--r-md)}
.br-r-sm.br-r-sm.br-r-sm{border-radius:var(--r-sm)}
.clip.clip.clip{overflow:hidden}
.cur-crosshair.cur-crosshair.cur-crosshair{cursor:crosshair}
.cur-help.cur-help.cur-help{cursor:help}
.cur-pointer.cur-pointer.cur-pointer{cursor:pointer}
.d-block.d-block.d-block{display:block}
.d-contents.d-contents.d-contents{display:contents}
.d-flex.d-flex.d-flex{display:flex}
.d-grid.d-grid.d-grid{display:grid}
.d-inline-block.d-inline-block.d-inline-block{display:inline-block}
.fd-column.fd-column.fd-column{flex-direction:column}
.ff-inherit.ff-inherit.ff-inherit{font-family:inherit}
.flex-0-0-130.flex-0-0-130.flex-0-0-130{flex:0 0 130px}
.flex-0-0-140.flex-0-0-140.flex-0-0-140{flex:0 0 140px}
.flex-1.flex-1.flex-1{flex:1}
.flex-none.flex-none.flex-none{flex:none}
.fs-10.fs-10.fs-10{font-size:10px}
.fs-11.fs-11.fs-11{font-size:11px}
.fs-12.fs-12.fs-12{font-size:12px}
.fs-13.fs-13.fs-13{font-size:13px}
.fs-14.fs-14.fs-14{font-size:14px}
.fs-15.fs-15.fs-15{font-size:15px}
.fs-16.fs-16.fs-16{font-size:16px}
.fs-18.fs-18.fs-18{font-size:18px}
.fs-20.fs-20.fs-20{font-size:20px}
.fs-28.fs-28.fs-28{font-size:28px}
.fs-32.fs-32.fs-32{font-size:32px}
.fs-xs.fs-xs.fs-xs{font-size:var(--fs-xs)}
.fsh-0.fsh-0.fsh-0{flex-shrink:0}
.fw-700.fw-700.fw-700{font-weight:700}
.fw-800.fw-800.fw-800{font-weight:800}
.fxw-wrap.fxw-wrap.fxw-wrap{flex-wrap:wrap}
.gap-0.gap-0.gap-0{gap:0}
.gap-10.gap-10.gap-10{gap:10px}
.gap-12.gap-12.gap-12{gap:12px}
.gap-14.gap-14.gap-14{gap:14px}
.gap-16.gap-16.gap-16{gap:16px}
.gap-20.gap-20.gap-20{gap:20px}
.gap-4.gap-4.gap-4{gap:4px}
.gap-6.gap-6.gap-6{gap:6px}
.gap-8.gap-8.gap-8{gap:8px}
.gtc-1fr-1fr.gtc-1fr-1fr.gtc-1fr-1fr{grid-template-columns:1fr 1fr}
.h-18.h-18.h-18{height:18px}
.h-220.h-220.h-220{height:220px}
.h-3.h-3.h-3{height:3px}
.h-8.h-8.h-8{height:8px}
.h-calc-100vh-210.h-calc-100vh-210.h-calc-100vh-210{height:calc(100vh - 210px)}
.is-hidden.is-hidden.is-hidden{display:none}
.italic.italic.italic{font-style:italic}
.jc-center.jc-center.jc-center{justify-content:center}
.jc-flex-end.jc-flex-end.jc-flex-end{justify-content:flex-end}
.jc-space-between.jc-space-between.jc-space-between{justify-content:space-between}
.lh-1-6.lh-1-6.lh-1-6{line-height:1.6}
.lh-1-7.lh-1-7.lh-1-7{line-height:1.7}
.lh-1-8.lh-1-8.lh-1-8{line-height:1.8}
.lh-2-1.lh-2-1.lh-2-1{line-height:2.1}
.lsp-ls-body.lsp-ls-body.lsp-ls-body{letter-spacing:var(--ls-body)}
.m-0.m-0.m-0{margin:0}
.m-0-auto.m-0-auto.m-0-auto{margin:0 auto}
.m-8-0-0.m-8-0-0.m-8-0-0{margin:8px 0 0}
.m-8-0-12.m-8-0-12.m-8-0-12{margin:-8px 0 12px}
.m-8-0-24.m-8-0-24.m-8-0-24{margin:8px 0 24px}
.maxh-320.maxh-320.maxh-320{max-height:320px}
.maxh-90vh.maxh-90vh.maxh-90vh{max-height:90vh}
.maxw-280.maxw-280.maxw-280{max-width:280px}
.maxw-300.maxw-300.maxw-300{max-width:300px}
.maxw-320.maxw-320.maxw-320{max-width:320px}
.maxw-400.maxw-400.maxw-400{max-width:400px}
.maxw-420.maxw-420.maxw-420{max-width:420px}
.maxw-440.maxw-440.maxw-440{max-width:440px}
.maxw-480.maxw-480.maxw-480{max-width:480px}
.maxw-520.maxw-520.maxw-520{max-width:520px}
.maxw-560.maxw-560.maxw-560{max-width:560px}
.maxw-640.maxw-640.maxw-640{max-width:640px}
.maxw-70pct.maxw-70pct.maxw-70pct{max-width:70%}
.maxw-760.maxw-760.maxw-760{max-width:760px}
.maxw-920.maxw-920.maxw-920{max-width:920px;width:94vw}
.mb-10.mb-10.mb-10{margin-bottom:10px}
.mb-12.mb-12.mb-12{margin-bottom:12px}
.mb-14.mb-14.mb-14{margin-bottom:14px}
.mb-16.mb-16.mb-16{margin-bottom:16px}
.mb-20.mb-20.mb-20{margin-bottom:20px}
.mb-24.mb-24.mb-24{margin-bottom:24px}
.mb-4.mb-4.mb-4{margin-bottom:4px}
.mb-8.mb-8.mb-8{margin-bottom:8px}
.minh-200.minh-200.minh-200{min-height:200px}
.minh-36.minh-36.minh-36{min-height:36px}
.minh-400.minh-400.minh-400{min-height:400px}
.minh-420.minh-420.minh-420{min-height:420px}
.minw-0.minw-0.minw-0{min-width:0}
.minw-240.minw-240.minw-240{min-width:240px}
.ml-4.ml-4.ml-4{margin-left:4px}
.ml-8.ml-8.ml-8{margin-left:8px}
.ml-auto.ml-auto.ml-auto{margin-left:auto}
.mr-auto.mr-auto.mr-auto{margin-right:auto}
.mt-10.mt-10.mt-10{margin-top:10px}
.mt-12.mt-12.mt-12{margin-top:12px}
.mt-14.mt-14.mt-14{margin-top:14px}
.mt-16.mt-16.mt-16{margin-top:16px}
.mt-2.mt-2.mt-2{margin-top:2px}
.mt-4.mt-4.mt-4{margin-top:4px}
.mt-6.mt-6.mt-6{margin-top:6px}
.mt-8.mt-8.mt-8{margin-top:8px}
.nowrap.nowrap.nowrap{white-space:nowrap}
.op-55.op-55.op-55{opacity:.55}
.op-dim.op-dim.op-dim{opacity:var(--dim)}
.ovy-auto.ovy-auto.ovy-auto{overflow-y:auto}
.p-0.p-0.p-0{padding:0}
.p-0-24-12.p-0-24-12.p-0-24-12{padding:0 24px 12px}
.p-0-4.p-0-4.p-0-4{padding:0 4px}
.p-10-12.p-10-12.p-10-12{padding:10px 12px}
.p-12.p-12.p-12{padding:12px}
.p-12-16.p-12-16.p-12-16{padding:12px 16px}
.p-12-16-0.p-12-16-0.p-12-16-0{padding:12px 16px 0}
.p-12-24.p-12-24.p-12-24{padding:12px 24px}
.p-14-20.p-14-20.p-14-20{padding:14px 20px}
.p-16.p-16.p-16{padding:16px}
.p-16-20.p-16-20.p-16-20{padding:16px 20px}
.p-16-24.p-16-24.p-16-24{padding:16px 24px}
.p-2-10.p-2-10.p-2-10{padding:2px 10px}
.p-24.p-24.p-24{padding:24px}
.p-24-24.p-24-24.p-24-24{padding:24px 24px}
.p-32.p-32.p-32{padding:32px}
.p-32-24.p-32-24.p-32-24{padding:32px 24px}
.p-4-8.p-4-8.p-4-8{padding:4px 8px}
.p-40.p-40.p-40{padding:40px}
.p-8.p-8.p-8{padding:8px}
.p-8-12.p-8-12.p-8-12{padding:8px 12px}
.p-8-8.p-8-8.p-8-8{padding:8px 8px}
.pev-none.pev-none.pev-none{pointer-events:none}
.pl-24.pl-24.pl-24{padding-left:24px}
.pos-fixed.pos-fixed.pos-fixed{position:fixed}
.pr-40.pr-40.pr-40{padding-right:40px}
.pt-8.pt-8.pt-8{padding-top:8px}
.right-24.right-24.right-24{right:24px}
.rsz-none.rsz-none.rsz-none{resize:none}
.rsz-vertical.rsz-vertical.rsz-vertical{resize:vertical}
.shadow-lift.shadow-lift.shadow-lift{box-shadow:var(--shadow-lift)}
.ta-center.ta-center.ta-center{text-align:center}
.ta-right.ta-right.ta-right{text-align:right}
.tdec-none.tdec-none.tdec-none{text-decoration:none}
.tnum.tnum.tnum{font-feature-settings:'tnum' 1}
.tov-ellipsis.tov-ellipsis.tov-ellipsis{text-overflow:ellipsis}
.ttf-uppercase.ttf-uppercase.ttf-uppercase{text-transform:uppercase}
.tx-bg.tx-bg.tx-bg{color:var(--bg)}
.tx-fg.tx-fg.tx-fg{color:var(--fg)}
.tx-inherit.tx-inherit.tx-inherit{color:inherit}
.tx-primary.tx-primary.tx-primary{color:var(--primary)}
.tx-success.tx-success.tx-success{color:var(--success)}
.tx-warning.tx-warning.tx-warning{color:var(--warning)}
.u-font-variant-numeric-tabular-nums.u-font-variant-numeric-tabular-nums.u-font-variant-numeric-tabular-nums{font-variant-numeric:tabular-nums}
.u-grid-column-1-1.u-grid-column-1-1.u-grid-column-1-1{grid-column:1/-1}
.u-inset-0.u-inset-0.u-inset-0{inset:0}
.u-touch-action-none.u-touch-action-none.u-touch-action-none{touch-action:none}
.w-110.w-110.w-110{width:110px}
.w-120.w-120.w-120{width:120px}
.w-18.w-18.w-18{width:18px}
.w-280.w-280.w-280{width:280px}
.w-320.w-320.w-320{width:320px}
.w-48.w-48.w-48{width:48px}
.w-52.w-52.w-52{width:52px}
.w-56.w-56.w-56{width:56px}
.w-64.w-64.w-64{width:64px}
.w-72.w-72.w-72{width:72px}
.w-80.w-80.w-80{width:80px}
.w-88.w-88.w-88{width:88px}
.w-90.w-90.w-90{width:90px}
.w-96.w-96.w-96{width:96px}
.w-full.w-full.w-full{width:100%}
.ws-pre-wrap.ws-pre-wrap.ws-pre-wrap{white-space:pre-wrap}
.z-10001.z-10001.z-10001{z-index:10001}
.z-9999.z-9999.z-9999{z-index:9999}
/* ══ END AUTO-GENERATED UTILITIES ══ */


/* 페이지 상단 한 줄 알림 — 불러오기 실패·화면이 낡음 등. 화면마다 따로 만들지 말 것
   (2026-08-28 수업관리에만 있던 지역 규칙을 여기로 승격 — 클래스 SSOT).

   D115 자리 예약 — 종전에는 `is-hidden`(display:none) 으로 시작해서, 배너가 뜨는
   순간 페이지 전체가 약 45px 아래로 밀렸다. 이제 **자리는 항상 있고 내용만 바뀐다**:
   숨김은 `.is-hidden` 대신 `.page-banner` 자신이 높이를 갖고 `visibility` 로만
   감춘다 (§17 — 조건부 블록 금지).

   2026-09-05 D118 — 이 자리에 `.page-banner-slot`(감싸는 그릇)도 같이 만들어 뒀는데
   **마크업 어디에도 안 붙어 있었다.** 자리는 배너 자신의 height 가 이미 잡고 있어
   그릇이 없어도 안 밀리지만, 그릇에만 있던 아래 여백은 빠져 있었다 — 그 한 줄을
   배너로 옮기고 그릇은 지운다 (§7.8 사문 부품 게이트가 잡아냈다). */
.page-banner {
  height: var(--slot-banner);
  margin-bottom: var(--sp-2);
  box-sizing: border-box;
  display: flex; align-items: center;
  padding: 0 12px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  font-size: 13px;
  color: var(--primary);
}
.page-banner.is-empty { visibility: hidden; }
.page-banner a { color: inherit; }

/* ── 그날 운동 편집기 (static/program_editor.js — D88 1단계, 2026-08-29) ──
   수업 수정 모달·수업 내용 게시 모달 공용 — 마크업은 JS 가 만들고 규칙은 여기 한 곳.
   무게 칸은 동작이 무게를 안 받을 때도 자리를 지킨다 (visibility) — 동작을
   바꿔도 줄 높이·칸 위치가 밀리지 않는다 (레이아웃 안정성 = 공간 예약). */
/* D152 (2026-09-14) — 편집기는 테두리 없는 세로 묶음이고, 보이는 단위는 **파트 카드**(.pe-part)다.
   구 `.pe` 바깥 테두리와 파트 사이 점선은 지웠다 — 카드가 테두리를 갖자 액자 속 액자가 됐다.
   모달 안에서는 `.modal-body .form-input` 의 아래 여백(12)이 편집기의 칸마다 붙어, 구 머리줄이
   padding-bottom 으로 그 여백을 상쇄하고 있었다 — 여기서 0 으로 되돌린다 (정렬은 flex/grid 가 맡는다).
   지운 정의: `.pe-part + .pe-part`(점선) · `.pe-part-label`(글자 라벨 → `.pe-part-badge`) ·
   `.pe-part-btns`(`.pe-btn` 상자 셋 → `.pe-part-tools` 글자 셋) · `.pe-field-rounds`(위 라벨 → 앞 글자) ·
   `.pe-rows.is-empty::before`('동작 없음 —' 안내 — 아래 '+ 동작' 이 말한다) · `.pe-foot`(→ `.pe-add-row`). */
.pe { display: flex; flex-direction: column; gap: var(--sp-2); }
.pe .form-input { margin-bottom: 0; }
/* 편집기 다음 라벨(수업 메모)은 **다른 구획**이다 — 선으로 끊고 섹션 간격(24)을 준다.
   D211 코치 보고: "A파트 하는데 수업메모가 밑에 있으니까 B파트 메모인지 헷갈린다." 여백만으로는
   마지막 파트 카드와 이어져 보였다. 선 위 = 그날 운동(파트들), 선 아래 = 수업 전체. */
.pe + .form-label {
  margin-top: var(--sp-5); padding-top: var(--sp-4);
  border-top: 1px solid var(--border);
}
.pe-parts { display: flex; flex-direction: column; gap: var(--sp-2); }
/* 파트 카드 — 머리(배지 · 종류 · 도구) → 메타 한 줄(시간 · 라운드 · 타임캡) → 안내 → 동작 줄 → 바닥(+ 동작).
   머리와 메타를 두 줄로 나눈 이유: 640 모달(카드 안폭 574)에 한 줄로 서려면 약 620 이 필요해
   칸 폭 64 를 지키면 반드시 접힌다 — 접히는 자리를 브라우저에 맡기느니 줄을 정한다. */
/* 카드가 **덩어리 하나**로 보이도록 옅은 바탕을 깐다 (D211) — 안의 입력칸은 흰색(--bg)이라
   경계가 저절로 선다. 파트에 속한 것(동작 줄·파트 메모)은 바탕 위, 수업 전체에 붙는 것(수업
   메모)은 바탕 밖 — 소속이 색으로 보인다. */
.pe-part {
  display: flex; flex-direction: column; gap: var(--sp-2);
  border: 1px solid var(--border); border-radius: var(--r-md); padding: var(--sp-3);
  background: var(--border);
}
.pe-part .form-input { background: var(--bg); }
.pe-part-head { display: flex; align-items: center; gap: var(--sp-2); }
/* 파트 배지 — 서버 라벨(program-meta parts.labels · 'A 파트') 글자 그대로. 검정 바탕·흰 글자·높이 28,
   원도 pill 도 아니다(r-sm). 글자를 잘라 'A' 만 쓰지 않는다 — 라벨의 꼴은 서버가 정한다 (6-b). */
.pe-part-badge {
  display: inline-flex; align-items: center; height: 28px; padding: 0 var(--sp-2);
  background: var(--fg); color: var(--bg); border-radius: var(--r-sm);
  font-size: var(--fs-h3); font-weight: 800; letter-spacing: var(--ls-title); white-space: nowrap;
}
.pe-part-head .pe-type { width: 128px; }
/* 파트 도구 ↑ ↓ ✕ — 글자 버튼 셋, 오른쪽 끝. 손대기 전엔 흐리고 hover 에 또렷해진다 (밑줄 없음 — 기호다) */
.pe-part-tools { margin-left: auto; display: inline-flex; gap: var(--sp-1); }
.pe-part-tools .link-btn {
  width: 24px; height: 24px; display: inline-flex; align-items: center; justify-content: center;
  opacity: var(--dim);
}
.pe-part-tools .link-btn:hover { opacity: 1; text-decoration: none; }
.pe-part-tools .link-btn:disabled { opacity: 0.25; cursor: default; color: var(--fg); }
/* 메타 한 줄 — 라벨은 칸 **앞** 글자(위 라벨 아님), 칸 폭 64, 단위 '분' 은 칸 뒤 */
.pe-part-meta { display: flex; align-items: center; gap: var(--sp-3); }
.pe-meta {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  font-size: var(--fs-xs); color: var(--fg); letter-spacing: var(--ls-body); white-space: nowrap;
}
.pe-meta-text { opacity: var(--dim); }
.pe-meta .form-input { width: 64px; padding: 0 var(--sp-2); }
.pe-part-foot { display: flex; gap: var(--sp-4); align-items: center; }
/* '+ 동작' 이 주 동작(굵게), '사전에 없는 동작' 은 보조(작게·흐림) */
.pe-part-foot .pe-add { font-weight: 700; }
.pe-part-foot .pe-new-link { font-size: var(--fs-xs); opacity: var(--dim); }
.pe-part-foot .pe-new-link:hover { opacity: 1; }
/* D211 — '글로 넣기' 는 기본 입력이 아니라 꺼내 쓰는 도구다 (기본 입력 = 위의 동작 줄). */
.pe-field { display: block; min-width: 0; }
.pe-field-text {
  display: block; font-size: var(--fs-xs); color: var(--fg); opacity: var(--dim);
  margin-bottom: var(--sp-1); letter-spacing: var(--ls-body);
}
/* 메타 줄 아래 안내 한 줄 (D122 §5) — AMRAP 은 정해진 라운드가 없다는 고지 · '지우기'.
   비어 있어도 자리를 잡아 둔다 (문구가 떠도 아래가 안 밀린다 — 레이아웃 안정성). */
.pe-part-note {
  min-height: 16px; line-height: 16px; font-size: var(--fs-xs); color: var(--fg);
  letter-spacing: var(--ls-body);
}
/* D210 — 안내는 이제 '옛 라운드 값이 남은 AMRAP' 에만 뜬다(불러올 때 정해지고, 지우기는 코치 손길).
   빈 안내 줄이 종류와 글 상자 사이에 16px 빈틈을 만들어 비었을 때는 접는다. */
.pe-part-note:empty { display: none; }
/* 흐림은 **문구에만** — '지우기' 까지 흐리면 누를 것으로 안 읽힌다 */
.pe-part-note-text { opacity: var(--dim); }
.pe-part-note .link-btn { font-size: var(--fs-xs); text-decoration: underline; }
/* D210 — 종류를 고르기 전 파트는 머리(배지·종류·도구)만 보인다. 종류 고르기가 첫 단계다.
   고르면 그 종류가 여는 머리 칸(서버 head_fields)·글 상자·줄·바닥이 나온다 — 코치 손길로 펼쳐지는 것이라
   아래가 밀리는 것은 펼침이다. 칸 하나하나의 숨김은 .is-hidden (program_editor.js syncPartType). */
.pe-part.is-untyped .pe-part-note,
.pe-part.is-untyped .pe-text,
.pe-part.is-untyped .pe-rows,
.pe-part.is-untyped .pe-part-foot { display: none; }
/* D215 (2026-09-19) — 파트 메모·수업 메모 칸 폐기. 코치가 적는 자리는 글 상자 하나다
   (사용자 "TEXT 장치로 그냥 적으면 되는데"). 지운 정의: 파트 메모 그릇·라벨·칸 셋 —
   D210 에서 만들고 D211 에서 배지를 붙였던 그 칸이다. */
/* 종류가 라운드를 안 정하면 칸은 **자리를 지킨 채** 잠긴다 (무게 칸 .has-no-load 와 같은 관례) */
.pe-part.has-no-rounds .pe-rounds { opacity: var(--dim); cursor: not-allowed; }
/* 동작 줄 묶음 — 줄이 없으면 그릇째 접는다 (빈 안내 글 없음 — 아래 '+ 동작' 이 말한다) */
.pe-rows { display: flex; flex-direction: column; gap: var(--sp-2); }
.pe-rows.is-empty { display: none; }
/* 동작 줄 (D214) — 검색 · '회원이 적을 것' · 버튼 (3열). 목표·무게·단위 칸은 사라졌다:
   횟수도 무게도 코치가 **글에 적고**, 줄이 말하는 것은 '어느 동작' 과 '회원이 무엇을 적나' 둘뿐이다.
   구 7열(D152)은 이름 칸을 174px 까지 눌렀는데, 칸이 둘로 줄어 이름이 넉넉해졌다. */
.pe-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto auto;
  gap: var(--sp-3); align-items: center;
}
.pe-row .form-input { min-width: 0; width: 100%; }
/* 동작 고르기 — 검색 상자 + 결과 목록 (연관도순은 서버 ?q=). 목록은 줄 아래로 띄운다 — 자리를 밀지 않는다. */
.pe-pick { position: relative; display: block; min-width: 0; }
.pe-results {
  /* 결과 목록은 **칸보다 넓다** — 7열 줄에서 이름 칸이 140px 까지 좁아져 'Bar Muscle-up · 바 머슬업'
     이 네 줄로 접혀 읽히지 않았다 (D152 실측). 떠 있는 상자라 넓혀도 줄 자리를 밀지 않는다. */
  position: absolute; top: calc(100% + 2px); left: 0; right: auto; z-index: 30;
  min-width: 320px; max-width: 420px;
  max-height: 224px; overflow-y: auto;
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--r-sm);
  box-shadow: var(--shadow-lift);
}
.pe-result {
  display: flex; justify-content: space-between; align-items: center; gap: var(--sp-2);
  white-space: nowrap;
  width: 100%; padding: 6px 10px; border: 0; background: transparent; color: var(--fg);
  font-family: inherit; font-size: var(--fs-sm); text-align: left; cursor: pointer;
  letter-spacing: var(--ls-body);
}
.pe-result:hover, .pe-result.is-active { background: var(--border); }
.pe-result.is-current .pe-result-name { font-weight: 700; }
.pe-result-cat { font-size: var(--fs-xs); opacity: var(--dim); white-space: nowrap; }
.pe-result-none {
  padding: 8px 10px; font-size: var(--fs-xs); color: var(--fg); opacity: var(--dim);
  letter-spacing: var(--ls-body);
}
/* D210 (2026-09-17) — 동작 줄 글로 넣기. 파트 안내 줄 아래 · 동작 줄 위. 글 상자 + 바닥(적용 · 다음부터 기억 ·
   결과 한 줄). 결과 한 줄은 비어 있어도 자리를 잡는다 (문구가 떠도 아래 줄이 안 밀린다 — 레이아웃 안정성).
   서버가 끄면(program-meta text_entry.enabled=false) 칸째 숨는다(is-hidden). */
/* 수업 내용 (D214) — 파트 카드 **맨 위**의 글 상자 하나. 여기 적은 글이 그대로 회원 본문이다.
   구 '줄 칸'(D210 — 한 줄에 동작 하나, 번호 칸)은 폐기: 코치가 Buy-in·Odd/Even·Tabata 를 줄 규칙에
   맞춰 적어야 했고, 그 구조를 시스템이 다시 읽어야 했다. 이제 규칙이 없다 — 적은 대로가 수업이다. */
.pe-text { display: flex; flex-direction: column; gap: var(--sp-1); }
.pe-text-label {
  display: flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-xs); color: var(--fg); opacity: var(--dim); letter-spacing: var(--ls-body);
}
.pe-text-label .tag { opacity: 1; }
.pe-text-box {
  margin-bottom: 0; min-height: 168px; line-height: 1.6; letter-spacing: var(--ls-body);
}
/* [적용] 과 결과 문구는 한 줄. 문구가 비어도 높이를 잡아 둔다 — 뜰 때 아래가 밀리지 않는다. */
.pe-text-foot { display: flex; align-items: center; gap: var(--sp-3); min-height: 32px; }
.pe-text-foot .pe-text-apply { white-space: nowrap; }
/* 찾기 결과 문구 — [적용] 바로 옆 (무엇을 눌러 나온 말인지 붙어 읽힌다). */
.pe-text-msg {
  min-width: 0; min-height: 16px; line-height: 16px;
  font-size: var(--fs-xs); color: var(--fg); letter-spacing: var(--ls-body);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* 정할 줄 — 글자는 있는데 동작을 안 고른 줄(저장을 막는다). 칸 테두리만 강조색 — 줄 자리는 그대로. */
.pe-row.is-unresolved .pe-search { border-color: var(--primary); }
/* 결과 목록 끝 '+ … 새 동작으로 추가' — 동작 목록과 선으로 가른다 */
.pe-result-add { border-top: 1px solid var(--border); font-weight: 700; }
/* 회원이 적을 것 (D214) — 동작마다 코치가 고른다. '없음' 도 값 하나다 (동작은 남고 칸만 없다) —
   ✕(동작 빼기)와 헷갈리지 않게 취소선·흐림 같은 '지움' 표시를 쓰지 않는다 (사용자 지적). */
.pe-record-wrap {
  display: inline-flex; align-items: center; gap: var(--sp-2); white-space: nowrap; cursor: pointer;
}
.pe-record-text { font-size: var(--fs-xs); color: var(--fg); opacity: var(--dim); letter-spacing: var(--ls-body); }
/* 이름은 **첫 줄에만** — 줄마다 되풀이하면 목록이 글자로 덮인다. 자리는 그대로 비워 두어
   (visibility) 아래 줄의 드롭다운이 첫 줄과 같은 x 에 선다 (레이아웃 안정성 = 공간 예약). */
.pe-row:not(:first-child) .pe-record-text { visibility: hidden; }
.pe-record { width: 88px; padding: 0 var(--sp-2); }
.pe-row-btns { display: inline-flex; gap: 2px; }
.pe-btn {
  width: 28px; height: 28px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--bg); color: var(--fg);
  border: 1px solid var(--border); border-radius: var(--r-sm);
  font-size: var(--fs-sm); font-family: inherit; line-height: 1; cursor: pointer;
}
.pe-btn:hover { background: var(--border); }
.pe-btn:disabled { opacity: 0.35; cursor: default; background: var(--bg); }
/* 사전 로드 중·실패 한 줄 — 자리를 미리 잡아 둔다 (나타나도 아래가 안 밀린다) */
.pe-status { min-height: 18px; font-size: var(--fs-xs); color: var(--fg); letter-spacing: var(--ls-body); }
.pe-status .link-btn { font-size: var(--fs-xs); color: var(--primary); }
/* '+ 파트 추가' — 전폭 점선 상자, 상자 전체가 누르는 자리(높이 40). 파트가 최대면 버튼이 숨고 상자도 0 이 된다 */
.pe-add-row { display: flex; }
.pe-add-row .link-btn {
  flex: 1 1 auto; height: 40px; border: 1px dashed var(--border); border-radius: var(--r-md);
  font-weight: 700;
}
.pe-add-row .link-btn:hover { color: var(--fg); border-color: var(--fg); text-decoration: none; }
/* D110 — '그날 운동' 라벨 줄 오른쪽의 보관함 피커 (수업 등록·수정·게시 모달 공용).
   D152 — 섹션 머리다: 위 여백 12 는 앞 칸의 모달 여백 12 와 합쳐 섹션 간격 24 가 된다.
   안의 라벨은 모달 라벨 여백(margin-top 12)을 받지 않는다 — 한 줄에 피커와 나란히 선다. */
.pe-pick-row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); margin-top: var(--sp-3); margin-bottom: var(--sp-2); }
.pe-pick-row .form-label { margin: 0; }
.pe-pick { min-width: 0; text-align: right; }
.pe-pick-select { width: auto; min-width: 200px; font-size: var(--fs-xs); }
.pe-pick .note { font-size: var(--fs-xs); }
/* 사전에 없는 동작 — 인라인 폼. 편집기 안에서는 점선 상자(바깥 테두리가 없어져 위 경계선만으로는 안 갈린다),
   동작 사전 탭에서는 실선 상자(.pe-new-panel) */
.pe-new { border: 1px dashed var(--border); border-radius: var(--r-md); padding: var(--sp-3); }
.pe-new-panel { border: 1px solid var(--border); border-radius: var(--r-md); padding: var(--sp-3); margin-bottom: var(--sp-4); }
.pe-new-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-2); }
.pe-check { display: flex; align-items: center; gap: var(--sp-1); font-size: var(--fs-sm); grid-column: 1 / -1; }
.pe-new-foot { display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-2); }
.pe-new-error { font-size: var(--fs-xs); color: var(--primary); min-height: 16px; letter-spacing: var(--ls-body); }
/* (구 `.pe-lib-card` · `.pe-lib-tags` — 동작 사전 탭의 카드 그리드. 2026-09-14 D146 삭제:
   사용자 "난잡하게 전부 그리드로 표시" → 분류마다 아코디언 안의 표(공용 table)로 바뀌며 사용처 0.) */
/* D146 (2026-09-14) — 동작 사전 탭. 분류별 아코디언 몸통은 표라 왼쪽 들여쓰기(18px)를 없앤다 —
   표 머리 바탕이 아코디언 제목과 같은 선에서 시작해야 한 덩이로 읽힌다. 상태 클래스 하나. */
.acc-body.is-flush { padding-left: 0; }
/* D153 (2026-09-14) — 동작 사전 표. 사용자 "표 화면의 디자인·내용 구성이 보기 어렵고 복잡해 보임".
   열 다섯 → 셋(동작 · 기록 · 조작). 행마다 반복되던 '—'·'기본' 을 없애고 예외(코치 추가)만 꼬리표로 남겼다.
   .is-dense = 표 상태 하나 — 행 36 (칸 6px 12px). 머리 줄(<thead>)이 없어 아코디언 머리가 표 머리 노릇을 한다.
   바로 아래 인라인 편집 행 규칙(.pe-edit-row td)이 같은 특이도로 뒤에 와서 그 행의 여백은 폼 것이 이긴다. */
.is-dense td { padding: 6px 12px; height: 36px; box-sizing: border-box; }
/* 열 폭 고정 — <colgroup> 의 col 폭이 그대로 선다. 자동 배치에서는 col 폭이 힌트일 뿐이라 조작 칸이 채워지는
   순간 기록 열이 150px 옆으로 뛰었다 (D153 실측). 표(table)는 이미 width:100% 다. */
.is-fixed { table-layout: fixed; max-width: 720px; }
/* 읽는 폭 720 — 칸이 셋뿐인 표를 화면 폭(1450)까지 늘리면 '회' 가 이름에서 1000px 넘게 떨어져
   두 값이 한 줄로 안 읽힌다 (2026-09-14 본체 실측). 폭을 줄이는 것이 열을 늘리는 것보다 싸다. */
.cell-dim { opacity: var(--dim); }   /* 보조 칸(기록) — 값은 --dim 토큰 하나 (§13) */
/* 검색 중인 분류 — 몸통을 펼쳐 보인다. 열림 기억 클래스는 건드리지 않아 검색을 비우면 종전 상태로 돌아온다. */
.acc.is-filtered .acc-body { display: block; }
.acc.is-filtered .acc-chevron { transform: rotate(180deg); }
/* 표 안 인라인 편집 행 — 행 하나를 통째로 차지하는 .pe-new-panel. 행 강조(hover 바탕)는 폼에는 안 준다. */
.pe-edit-row td { padding: var(--sp-2) var(--sp-3); }
.pe-edit-row:hover { background: transparent; }
.pe-edit-row .pe-new-panel { margin-bottom: 0; }
/* D142 (2026-09-13) — 동작 사전 탭 '분류' 영역. 한 줄 = 이름 · 꼬리표(코치 추가/숨김 — 예외만, D153) · 사용 동작 수 · 동작 링크.
   편집·삭제(옮길 분류 고르기) 폼은 그 줄 아래 .pe-new-panel (동작 편집 폼과 같은 상자).
   D146 — 그릇은 아코디언(.acc) 하나. 아래 선은 그릇이 긋고(.acc 는 마지막 자식이라 선이 없다) 머리 여백은 .acc-head 것. */
.pe-cat { border-bottom: 1px solid var(--border); margin-bottom: var(--sp-4); }
.pe-cat-list { display: flex; flex-direction: column; gap: var(--sp-1); }
.pe-cat-row { display: flex; align-items: center; gap: var(--sp-2); min-height: 32px; font-size: var(--fs-sm); }
.pe-cat-label { font-weight: 700; letter-spacing: var(--ls-body); }
.pe-cat-count { font-size: var(--fs-xs); letter-spacing: var(--ls-body); }
.pe-cat-actions { margin-left: auto; display: flex; gap: var(--sp-2); }
.pe-cat-actions .link-btn { font-size: 11px; }
.pe-cat-row + .pe-new-panel { margin-top: var(--sp-1); }

/* 2026-08-30 — 자리는 지키고 보이지만 않게 (규칙 빌더 동작 줄). JS 의 style.visibility 대체. */
.is-invisible.is-invisible{visibility:hidden}

/* F2 (2026-08-30) — 픽토그램 배지(static/pictogram.js). 종전엔 render() 가 display·position 을
   inline 으로 박아 업적 설정 페이지에 inline display 요소가 96개 남았다. 크기만 inline. */
.pg-badge{display:inline-block;position:relative;vertical-align:middle}
.pg-badge > .pg-ico{position:absolute;left:0;top:0;display:flex;align-items:center;justify-content:center}

/* D168 (2026-09-14) — 브라우저가 만나는 실패 한 장 (templates/error.html).
   로그인 전에도 뜨므로 사이드바 없이 한가운데. 값은 전부 토큰 — 여기서 색을
   새로 만들지 않는다. */
.err-page { min-height: 100vh; display: flex; align-items: center; justify-content: center;
            padding: var(--sp-6) var(--sp-4); background: var(--bg); }
.err-box { max-width: 460px; text-align: center; }
.err-code { font-size: 56px; font-weight: 800; letter-spacing: -0.03em;
            color: var(--border); line-height: 1; margin-bottom: var(--sp-3); }
.err-lead { margin-top: var(--sp-3); font-size: var(--fs-sm); letter-spacing: var(--ls-body); }
.err-hint { margin-top: var(--sp-2); font-size: var(--fs-xs); opacity: var(--dim);
            letter-spacing: var(--ls-body); }
.err-actions { margin-top: var(--sp-5); display: flex; gap: var(--sp-3);
               align-items: center; justify-content: center; }

/* 목록 칸 예약 (D189 · 2026-09-15 사용자 "칸을 딱 잡아놔") — 페이지를 넘겨도 표가
   접히지 않게 남는 줄을 빈 줄로 채운다. 줄 높이는 보통 줄과 같고, 글자·테두리는 없다.
   높이 값을 여기 적지 않는 이유 = 보통 줄과 **같은 규칙**(td padding)으로 서야 한다. */
.pager-filler td { border-color: transparent; }
.pager-filler td::after { content: "\00a0"; }

/* 로딩 줄 — **보통 줄과 같은 높이**로 선다 (D189). 종전처럼 한 칸에 선을 여러 개 그리면
   불러온 뒤 표가 통째로 튄다(수업 종류 실측 244 → 494). 글자 한 줄(\00a0)로 높이를 만들고
   막대는 그 위에 얹는다 — 줄 수가 같으니 높이도 같다. */
.pager-skeleton td { position: relative; }
.pager-skeleton td::before { content: "\00a0"; }
/* 줄 밑에 작은 글자(.cell-sub)가 붙는 표 — 로딩 줄도 두 줄 높이로 선다. */
.pager-skeleton.is-2line td::before { content: "\00a0\a\00a0"; white-space: pre-line; }
.pager-skeleton .skeleton-line {
  position: absolute; left: 16px; right: 16px; top: 50%;
  transform: translateY(-50%); margin: 0; width: auto;
}

/* <div> 목록의 빈 줄 (D194 · 2026-09-16 홈) — 줄 모양은 호출부가 <template> 로 주고(진짜 줄과
   같은 부품이라 높이가 같다), 여기서는 셋만 정한다: 누를 수 없고 · 선은 투명하며 · 안은 비어
   보인다. 안의 막대(.skeleton)는 그릇이 .is-loading 일 때만 보인다 — **로딩 줄 = 빈 줄**. */
.pager-filler { pointer-events: none; }
div.pager-filler { position: relative; border-color: transparent; }
div.pager-filler > * { visibility: hidden; }
.is-loading > div.pager-filler > .skeleton { visibility: visible; }
/* 한 줄 문구(빈 상태·실패)도 **같은 줄 모양**으로 선다 — 부품은 비우고 글만 보인다.
   막대와 글은 같은 자리(왼쪽·세로 가운데)라 로딩 → 문구로 바뀌어도 줄이 안 움직인다. */
div.row-msg { position: relative; pointer-events: none; }
div.row-msg > * { visibility: hidden; }
div.row-msg > .row-msg-text { visibility: visible; font-size: var(--fs-xs); opacity: var(--dim); }
div.pager-filler > .skeleton-line, div.row-msg > .row-msg-text {
  position: absolute; left: 0; top: 50%; transform: translateY(-50%); margin: 0;
}

/* JS 가 채우는 칩 줄 — 채워지기 전에도 자리를 잡아 둔다 (팝인 방지). */
.filterbar.is-reserved { min-height: 40px; }

/* 계약서 발급 — 나갈 문서를 그 자리에서 보여 준다 (D236 · 2026-09-20).
   상자 높이는 고정이라 스켈레톤 → 문서로 바뀌어도 아래 버튼이 밀리지 않는다. */
.contract-preview-box {
  height: 380px;
  overflow: auto;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 12px;
  background: var(--bg);
}
.contract-preview-frame { width: 100%; height: 100%; border: 0; display: block; }
