/* [W9-M20B] Ops console styles — self-contained, no external fonts/CDN (§0). Desktop-first with a
   fluid grid. A11y: 44px touch targets, visible focus, sr-only. */

/* ═══ 디자인 토큰 ═══════════════════════════════════════════════════════════════
   [UI 2026-07-26] 종전에는 :root가 세 군데(5행·25행·188행)로 흩어져 있었고, 표·헤더·금액색은
   토큰을 거치지 않고 raw hex로 규칙 안에 박혀 있었다. 값이 규칙 안에 살면 테마를 바꿀 방법이
   "규칙을 덮어쓰기"밖에 없어지고, 그 덮어쓰기는 **순서에 의존**해 조용히 죽는다(파일 끝 라이트
   블록 사고의 기전이 정확히 그것이었다). ⟹ 표면·간격·밀도를 전부 토큰으로 올리고, 라이트 테마는
   가능한 한 **값만 갈아끼운다**(순서 무의존).
   ★ 팔레트는 새로 발명하지 않았다: --accent(버건디)는 브랜드 정체성(01_비전_브랜드_정체성 §2 ·
     소비자 웹뷰 --accent #a8324a)과 같은 계열이고 기존 값 그대로다. 아래는 정리이지 개편이 아니다. */
:root {
  /* 표면·잉크 (기존 이름·값 무변) */
  --bg: #0b0e14; --panel: #131824; --panel2: #1a2030; --line: #232c40;
  --ink: #e7ecf5; --muted: #93a0b8; --accent: #a92348;
  --accent-soft: rgba(169, 35, 72, .16); --gold: #e1b654;
  --up: #35c07a; --down: #ef5f6b; --flat: #7a869c;
  --cell-immature: #2a3040;
  --s-today: #4f7cff; --s-prev-day: #f0a94c; --s-prev-week: #9b8cff;
  --s-d: #35c07a; --s-e: #e06fae; --s-f: #46c2d8;

  /* 레이아웃 상수 — 두 거처로 갈리면 하나는 죽는다(구 `top: 55px`·`top: 98px` 사고). */
  --appbar-h: 56px;
  --sidebar-w: 188px;

  /* 간격 스케일 — 화면이 임의 px을 쓰지 않게 한다. */
  --sp-1: 4px; --sp-2: 6px; --sp-3: 8px; --sp-4: 10px; --sp-5: 14px; --sp-6: 18px; --sp-7: 24px;

  /* 모서리·그림자 */
  --r-sm: 8px; --r-md: 10px; --r-lg: 12px; --r-pill: 999px;
  --shadow-1: 0 1px 2px rgba(0,0,0,.28);
  --shadow-2: 0 8px 30px rgba(0,0,0,.35);
  --shadow-sticky: 6px 0 8px -8px rgba(0,0,0,.55);

  /* 타이포 스케일 — 12/13/14가 산발적으로 박혀 있던 것을 한 축으로 모은다. */
  --fs-cap: 11px; --fs-sm: 12px; --fs-md: 13px; --fs-base: 14px; --fs-lg: 16px; --fs-xl: 22px;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* 표 밀도 — 행 높이·구분선·얼룩말·호버가 **하나의 스케일**을 본다.
     --row-pad-y 하나만 바꾸면 표 전체 밀도가 함께 움직인다. */
  --row-pad-y: 8px;
  --row-pad-x: 10px;
  --th-bg: #181d29;
  --th-ink: #bcc5d5;
  --row-zebra: rgba(255,255,255,.022);
  --row-hover: rgba(255,255,255,.075);
  --tint-mmp: rgba(79,124,255,.07);
  --tint-tool: rgba(53,192,122,.07);
  --money-ink: #f0cc78;
  --col-sorted: rgba(225,182,84,.045);   /* 정렬 중인 열의 아주 옅은 세로 띠 */
  --null-ink: #6d7789;      /* "값 없음(—)"은 0이 아니다 — 숫자보다 약하게, 그러나 읽히게 */
  --btn-line: var(--line);

  /* ── [OWNER 2026-07-26] 포인트 경제 관측판 토큰 ────────────────────────────────
     ★ 색 계열을 **줄인다.** 종전 화면은 빨강·주황·초록·파랑·보라 5계열을 동시에 썼고
       그 중 무엇이 주인공인지 화면이 말하지 않았다. 여기서는 **두 색만** 데이터를 칠한다:
         --pe-1(적립) · --pe-2(소진).  나머지(가입·활동·매출)는 «맥락»이므로 무채색이다.
       그 색 배분 자체가 제품의 주장이다 — 포인트가 주어이고 나머지는 그 문장의 배경이다.
     ★ 두 색은 눈대중이 아니라 **검산해서** 골랐다(색각 이상 분리·표면 대비 3:1·명도대·채도).
       다크(#3987e5/#d95926 on #131824)·라이트(#2a78d6/#eb6834 on #fff) 전 항목 통과.
     ★ 액센트(버건디)는 **선택 상태 전용**이다. 데이터에 액센트를 쓰면 «지금 고른 것»과
       «중요한 값»이 같은 색이 되어 둘 다 신호이기를 그만둔다. */
  --pe-1: #3987e5;          /* 포인트 적립 */
  --pe-2: #d95926;          /* 포인트 소진 */
  --pe-context: #8b97ad;    /* 맥락 계열(가입·활동·매출) — 무채색 */
  --pe-surface: #10141e;    /* 요약 스트립 바닥 — 카드보다 한 단 낮은 면 */
  --pe-rule: #1e2637;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--ink);
  font: var(--fs-base)/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans KR", sans-serif;
  -webkit-font-smoothing: antialiased; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

/* appbar + game selector */
/* [UI 2026-07-26] 헤더 높이를 고정한다. 종전 `flex-wrap: wrap`은 게임 선택 버튼이 늘어나면
   헤더가 2줄이 되어 높이가 변했고, 그러면 아래 .tabbar의 `top: 55px`(고정값)과 어긋나
   스크롤 중 탭바가 헤더를 파고들거나 틈이 벌어졌다. OWNER 실사용 지적:
   "영역이 와따가따 안좋아 창이 길어졌다 좁아졌다 스트레스 많다".
   ⟹ 헤더는 한 줄로 고정하고, 게임 선택이 넘치면 그 안에서만 가로 스크롤한다.
      높이를 변수로 승격해 .tabbar가 같은 값을 참조하게 한다(두 거처로 갈리면 하나는 죽는다).
      ★ --appbar-h 선언은 파일 상단 토큰 블록으로 이관했다(:root 3거처 통합). */
.appbar { display: flex; align-items: center; gap: 16px; padding: 10px 18px; background: var(--panel);
  border-bottom: 1px solid var(--line); position: sticky; top: 0; z-index: 5;
  flex-wrap: nowrap; height: var(--appbar-h); box-sizing: border-box; }
.appbar-brand { font-weight: 700; letter-spacing: .3px; }
.gamesel { display: flex; gap: 6px; margin-left: auto; }
.gbtn { min-height: 34px; padding: 4px 14px; border-radius: 8px; border: 1px solid var(--line);
  background: var(--panel2); color: var(--muted); cursor: pointer; }
.gbtn[aria-pressed="true"] { background: var(--accent); color: #fff; border-color: var(--accent); }
.gbtn:disabled { opacity: .4; cursor: not-allowed; }

/* tabs */
/* [UI 2026-07-26 · OWNER 지시] 메뉴 = 좌측 고정 사이드바.
   헤더 아래를 가로 2단으로 나누고, 사이드바는 sticky로 붙여 본문만 스크롤한다.
   top은 헤더 높이 **변수**를 참조한다 — 하드코딩 55px은 헤더가 바뀌면 조용히 어긋난다. */
.shell-body { display: flex; align-items: flex-start; gap: 0; }
.tabbar {
  display: flex; flex-direction: column; gap: 2px; padding: 10px 8px;
  background: var(--panel); border-right: 1px solid var(--line);
  position: sticky; top: var(--appbar-h); z-index: 4;
  width: var(--sidebar-w); flex: 0 0 var(--sidebar-w); box-sizing: border-box;
  height: calc(100vh - var(--appbar-h)); overflow-y: auto; scrollbar-width: thin;
}
.tabbar .tab { width: 100%; justify-content: flex-start; }
/* 게임 선택이 많아져도 헤더를 2줄로 만들지 않는다 — 그 안에서만 가로 스크롤. */
.gamesel { overflow-x: auto; flex: 0 1 auto; scrollbar-width: thin; }
.gamesel .gbtn { flex: 0 0 auto; }
/* 본문: 남은 폭 전부. min-width:0 이 없으면 넓은 표가 flex 아이템을 밀어 가로가 튄다. */
.stage { flex: 1 1 auto; min-width: 0; min-height: calc(100vh - var(--appbar-h)); }
/* 좁은 화면(모바일)에서는 사이드바를 상단 가로 스크롤 바로 되돌린다 — R-4의 모바일 요구. */
@media (max-width: 860px) {
  .shell-body { display: block; }
  .tabbar { flex-direction: row; width: auto; flex: none; height: auto;
    border-right: 0; border-bottom: 1px solid var(--line); overflow-x: auto; overflow-y: hidden; }
  .tabbar .tab { width: auto; }
  .stage { min-height: calc(100vh - var(--appbar-h) - 58px); }
}
.tab { display: flex; align-items: center; gap: var(--sp-2); min-height: 44px; padding: 6px 16px; border: 0;
  background: transparent; color: var(--muted); cursor: pointer; border-radius: var(--r-sm);
  white-space: nowrap; font: inherit; font-size: var(--fs-md);
  border-left: 3px solid transparent; }
.tab:hover { color: var(--ink); background: var(--row-hover); }
/* 활성 탭은 색만이 아니라 좌측 강조선·굵기로도 드러난다(색각 이상·저대비 화면 대비). */
.tab.active { color: var(--ink); background: var(--panel2); border-left-color: var(--accent); font-weight: 600; }
/* [UI 2026-07-26] 탭 아이콘은 이제 **한 계열의 인라인 SVG**다(assets/icons.mjs). 종전에는 기하 글리프·
   OS 컬러 이모지·느낌표가 섞여 있었고, 이모지는 플랫폼이 칠하므로 탭 색을 절대 따라오지 못했다
   — 그래서 «흐린 탭»과 «활성 탭»의 아이콘이 같은 색으로 남았다. currentColor 하나로 수렴시킨다. */
.tab-ic { display: flex; width: 18px; height: 18px; flex: 0 0 18px; }
.ic { width: 18px; height: 18px; display: block;
  fill: none; stroke: currentColor; stroke-width: 1.4;
  stroke-linecap: round; stroke-linejoin: round; }
.tab .ic { opacity: .78; }
.tab:hover .ic, .tab.active .ic { opacity: 1; }
/* 사이드바가 가로 바로 눕는 폭(≤860px)에서는 강조선도 아래로 눕혀야 «활성»이 보인다.
   ★ 선택자를 `.tabbar .tab`(0,2,0)·`.tabbar .tab.active`(0,3,0)로 **일부러 올렸다**: 미디어 쿼리는
     특이도를 주지 않으므로, `.tab`(0,1,0)로 쓰면 바로 위 `.tab.active`(0,2,0)의 border-left-color가
     살아남아 세로 강조선이 유령처럼 남는다. 순서가 아니라 특이도로 이기게 못박는다. */
@media (max-width: 860px) {
  .tabbar .tab { border-left: 0; border-bottom: 3px solid transparent; }
  .tabbar .tab.active { border-bottom-color: var(--accent); border-left-color: transparent; }
}

/* stage */
.stage { padding: var(--sp-6); max-width: 1680px; margin: 0 auto; width: 100%; }
.stage:focus { outline: none; }
.screen { display: flex; flex-direction: column; gap: var(--sp-5); }
/* 화면 안의 구획 사이는 본문 줄간보다 넉넉히 — 종전엔 표·폼·각주가 같은 간격으로 붙어
   «어디까지가 한 덩어리인가»가 보이지 않았다. */
.screen > .ops-details + .ops-details { margin-top: calc(var(--sp-7) - var(--sp-5)); }

/* [UI 2026-07-26] 위계. 종전 섹션 제목은 본문과 같은 14px에 굵기만 달라, 화면을 열었을 때
   «어디가 구획의 시작인가»가 보이지 않았다. 요약이 상세보다 앞에 오려면 그 경계가 보여야 한다. */
.sec-title { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--sp-4);
  font-weight: 700; font-size: var(--fs-lg); letter-spacing: -.01em;
  margin-top: var(--sp-5); padding-bottom: var(--sp-2); border-bottom: 1px solid var(--line); }
.sec-sub { font-weight: 400; font-size: var(--fs-sm); color: var(--muted); letter-spacing: 0; }
.footnote { color: var(--muted); font-size: var(--fs-sm); margin: var(--sp-1) 0 0; line-height: 1.6; }

.card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg);
  padding: var(--sp-5); box-shadow: var(--shadow-1); }
.chart-card { overflow-x: auto; }

/* tiles */
.tile-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(168px, 1fr)); gap: var(--sp-4); }
.tile { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg);
  padding: var(--sp-5) var(--sp-5) var(--sp-4); box-shadow: var(--shadow-1); }
.tile-lb { color: var(--muted); font-size: var(--fs-sm); font-weight: 600; letter-spacing: .01em; }
/* 숫자는 tabular-nums로 — 타일이 나란히 서면 자릿수가 세로로 맞아야 비교가 된다. */
.tile-val { font-size: var(--fs-xl); font-weight: 700; margin-top: var(--sp-2);
  font-variant-numeric: tabular-nums; letter-spacing: -.02em; line-height: 1.2; }
/* 값이 없는 타일은 «0처럼» 보이면 안 된다 — 굵기까지 낮춰 숫자와 다른 물건임을 알린다. */
.tile-val.null { color: var(--null-ink); font-weight: 500; }
.tile-deltas { display: flex; gap: var(--sp-3); margin-top: var(--sp-3); flex-wrap: wrap; }
.delta { display: inline-flex; align-items: center; gap: var(--sp-1); font-size: var(--fs-cap);
  color: var(--muted); font-variant-numeric: tabular-nums; }
.delta-lb { color: var(--muted); opacity: .8; }
.delta.up { color: var(--up); } .delta.down { color: var(--down); } .delta.flat { color: var(--flat); }

.bench { color: var(--muted); font-size: 12px; display: flex; gap: 6px; align-items: center;
  border-left: 3px solid var(--s-prev-week); padding-left: 8px; }
.bench-ic { color: var(--s-prev-week); }

/* charts */
/* [2026-07-26] `width: 100%` 제거. viewBox는 데이터 개수로 만들어지는데 폭을 무조건 컨테이너에
   맞추니 배율이 깨졌다(실측·컨테이너 1000px): 90일 스택막대 3304×252 → 970×**74**(0.29배,
   눈금 글자 2.9px) / 30코호트 히트맵 272×806 → 970×**2874**(3.57배). 세 프리셋 모두 비정상이었다.
   ⟹ 고유 크기를 유지하고 넘치면 .chart-card가 가로 스크롤한다(그 규칙은 이미 있었는데
      width:100%가 "넘치는 대신 줄어들게" 만들어 무력했다). */
.chart { max-width: 100%; height: auto; min-width: 520px; }
.chart-card > .chart { max-width: none; }
.chart .axis { stroke: var(--line); stroke-width: 1; }
.chart .tick { fill: var(--muted); font-size: 10px; }
.chart .rowlabel { font-size: 10px; }
.chart .line { stroke-width: 2; fill: none; }
.line.s-today { stroke: var(--s-today); }
.line.s-prev-day { stroke: var(--s-prev-day); }
.line.s-prev-week { stroke: var(--s-prev-week); }
.hm-cell.immature { stroke: var(--line); }
.hm-val { fill: #0b0e14; font-size: 11px; font-weight: 700; }
.legend { display: flex; gap: 14px; margin-top: 8px; font-size: 12px; color: var(--muted); }
.lg::before { content: "■ "; }
.lg.s-today::before { color: var(--s-today); } .lg.s-prev-day::before { color: var(--s-prev-day); }
.lg.s-prev-week::before { color: var(--s-prev-week); }
.lg.hm-observed::before { color: hsl(150 55% 55%); } .lg.hm-immature::before { color: var(--cell-immature); }

/* gates + pending */
.gate { text-align: center; padding: 30px 18px; color: var(--muted); }
.gate-ic { font-size: 26px; } .gate-title { font-weight: 700; color: var(--ink); margin-top: 8px; }
.gate-body { margin: 6px auto 0; max-width: 460px; } .gate-code { font-size: 11px; opacity: .7; }
.fatal { min-height: 70dvh; display: flex; align-items: center; justify-content: center; }
.lg-btn { margin-top: 14px; }
.pending { display: flex; gap: 8px; align-items: center; background: var(--panel2);
  border: 1px dashed var(--line); border-radius: 10px; padding: 10px 14px; color: var(--muted); }

/* badges + forms */
.badge { display: inline-block; padding: 2px 8px; border-radius: var(--r-pill);
  font-size: var(--fs-cap); font-weight: 600; margin: 2px; white-space: nowrap;
  background: var(--panel2); border: 1px solid var(--line); color: var(--muted); }
.badge.neutral { color: var(--muted); }
.badge.ok { color: var(--up); border-color: rgba(53,192,122,.42); background: rgba(53,192,122,.08); }
.badge.warn { color: var(--gold); border-color: rgba(225,182,84,.45); background: rgba(225,182,84,.08); }
.badge.danger { color: var(--down); border-color: rgba(239,95,107,.45); background: rgba(239,95,107,.08); }
.form-row { display: flex; gap: 8px; flex-wrap: wrap; margin: 8px 0; }
.form-actions { display: flex; gap: 8px; }
/* [W9-M20D] 속성 선택자 조합(0,2,0)이 .form-actions(0,1,0)를 이긴다 — UA의 [hidden]{display:none}은
   클래스가 display를 지정하는 순간 눌린다(2단계 확인 행이 항상 열려 있던 실결함 · e2e ops-boards가 적발). */
.form-actions[hidden], .confirm-row[hidden] { display: none; }
.inp { min-height: 40px; padding: 6px 10px; border-radius: var(--r-sm); border: 1px solid var(--btn-line);
  background: var(--panel2); color: var(--ink); min-width: 160px;
  font: inherit; font-size: var(--fs-md); }
.inp:hover:not(:disabled) { border-color: var(--muted); }
.inp::placeholder { color: var(--muted); opacity: .75; }
.inp:disabled, .inp[readonly] { opacity: .55; cursor: not-allowed; }
.btn { min-height: 40px; padding: 6px 18px; border-radius: var(--r-sm); border: 1px solid var(--btn-line);
  background: var(--panel2); color: var(--ink); cursor: pointer;
  font: inherit; font-size: var(--fs-md); font-weight: 600;
  transition: background-color .12s, border-color .12s; }
.btn:hover:not(:disabled) { border-color: var(--muted); }
.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn.primary:hover:not(:disabled) { filter: brightness(1.12); border-color: var(--accent); }
.btn.danger { border-color: var(--down); color: var(--down); }
.btn.compact { min-height: 30px; padding: 3px 9px; font-size: var(--fs-sm); }
.btn:disabled { opacity: .42; cursor: not-allowed; }
/* 포커스 링은 버건디 강조색 위에서도 보여야 한다 — 흰 테두리를 한 겹 깔아 어느 배경에서나 대비 확보. */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px;
  box-shadow: 0 0 0 4px var(--accent-soft); }
.btn.primary:focus-visible { outline-color: var(--ink); }

/* operators / moderation */
.op-list { display: flex; flex-direction: column; gap: 6px; }
.op-row { display: grid; grid-template-columns: 1fr auto 2fr; gap: 10px; align-items: center;
  padding: 8px 4px; border-bottom: 1px solid var(--line); }
.op-id { font-family: ui-monospace, monospace; font-size: 12px; }
.mod-result { margin-top: 10px; min-height: 22px; }
.mod-result .err { color: var(--down); } .mod-result .ok { display: flex; gap: 8px; align-items: center; }
.toggle-row { display: flex; gap: 10px; align-items: center; }
.switch { width: 40px; height: 22px; }

/* boot spinner */
.boot { flex: 1; display: flex; align-items: center; justify-content: center; min-height: 60dvh; }
.boot-spin { width: 34px; height: 34px; border-radius: 50%; border: 3px solid var(--line);
  border-top-color: var(--accent); animation: spin .8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .boot-spin { animation: none; } }

/* light theme */


/* [W9-M23C] CS active surface — ticket rows + thread + response controls */
/* [W18-2차 ①] Inbox status board. Chips wrap: four counts plus a total must never push the last one
   off a narrow panel, and the last one is "종료됨" — the number an operator checks to know the queue
   is actually draining. `.active` is carried by aria-pressed too, so the selected state survives for
   anyone who cannot see the tint. */
/* Selected state reuses the console's existing chip idiom (.mx-chip[aria-pressed]) rather than
   inventing a second one — two "selected" looks in one console is how one of them stops reading as
   selected. The state is driven off aria-pressed, so the accessible name and the paint cannot drift. */
.screen.cs .cs-tally{display:flex;flex-wrap:wrap;gap:var(--sp-2);margin-top:6px}
.screen.cs .cs-tally-chip{display:inline-flex;align-items:baseline;gap:7px;min-height:30px;padding:3px 11px;
  border:1px solid var(--btn-line);border-radius:var(--r-pill);background:var(--panel2);color:var(--ink);
  cursor:pointer;font:inherit;font-size:var(--fs-sm)}
.screen.cs .cs-tally-chip:hover{border-color:var(--accent)}
.screen.cs .cs-tally-chip[aria-pressed="true"]{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:700}
.screen.cs .cs-tally-label{color:var(--muted)}
.screen.cs .cs-tally-chip[aria-pressed="true"] .cs-tally-label{color:#fff}
.screen.cs .cs-tally-count{font-variant-numeric:tabular-nums;font-weight:700}
.screen.cs .cs-list{display:flex;flex-direction:column;gap:8px;margin:10px 0}
.screen.cs .cs-row{display:block;width:100%;text-align:left;background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:10px 12px;color:var(--ink);cursor:pointer}
.screen.cs .cs-row-head{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.screen.cs .cs-row-cat{font-size:12px;color:var(--muted)}
/* [W18-⑧] The meta row gained two cells (server + nickname). Without wrapping, a long uid plus a
   long nickname pushes the last cell out of a narrow panel and the identity is the thing that
   disappears — which is exactly what this change was made to fix. */
.screen.cs .cs-row-meta{display:flex;gap:12px;font-size:12px;color:var(--muted);margin-top:5px;flex-wrap:wrap}
/* The nickname is what an operator actually reads to know WHO this is, so it carries the ink colour
   while the derived server stays muted — the server is inferred from the uid, the name is stored. */
.screen.cs .cs-row-nick{color:var(--ink);opacity:.85}
/* Age is the first thing a triaging operator reads, so it leads the meta row and is tabular so the
   rows line up as a scannable column rather than ragged text. */
.screen.cs .cs-row-time{font-variant-numeric:tabular-nums;color:var(--ink);opacity:.75}
.screen.cs .cs-row-sum{margin-top:5px;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.screen.cs .cs-thread{display:flex;flex-direction:column;gap:10px;margin:12px 0}
.screen.cs .cs-msg{border-radius:10px;padding:9px 11px;border:1px solid var(--line)}
.screen.cs .cs-msg-user{background:rgba(79,124,255,.08)}
.screen.cs .cs-msg-operator{background:rgba(247,198,90,.08)}
.screen.cs .cs-msg-who{display:flex;gap:8px;align-items:baseline;font-size:12px;font-weight:700;color:var(--muted);margin-bottom:3px;flex-wrap:wrap}
.screen.cs .cs-msg-time{font-weight:400;font-variant-numeric:tabular-nums;opacity:.8}
.screen.cs .cs-msg-body{font-size:13.5px;line-height:1.55;white-space:pre-wrap}
.screen.cs .cs-attachment-grid{display:grid;grid-template-columns:repeat(3,minmax(0,150px));gap:8px;margin-top:9px}
.screen.cs .cs-attachment{aspect-ratio:1;border-radius:8px;overflow:hidden;border:1px solid var(--line);background:var(--panel)}
.screen.cs .cs-attachment img{display:block;width:100%;height:100%;object-fit:cover}
.screen.cs .cs-attachment.unavailable{display:flex;align-items:center;justify-content:center;padding:8px;color:var(--muted);font-size:12px;text-align:center}
.screen.cs .cs-reply{width:100%;min-height:64px}

/* [W9-M20C] metric-breakdown surface — mode/period controls, extra series colours, stacked bars
   (계열색 --s-d/--s-e/--s-f 선언은 파일 상단 토큰 블록으로 이관 — :root 3거처 통합) */
.controls { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; padding: 4px 0; }
.ctl-group { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.ctl-lb { color: var(--muted); font-size: 12px; }
.ctl-btn { min-height: 34px; padding: 4px 12px; border-radius: 8px; border: 1px solid var(--line);
  background: var(--panel2); color: var(--ink); cursor: pointer; font-size: 13px; }
.ctl-btn[aria-checked="true"], .ctl-btn[aria-pressed="true"] {
  background: var(--accent); color: #fff; border-color: var(--accent); }
.ctl-note { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; width: 100%; }
.ctl-hint { color: var(--muted); font-size: 12px; }
.notes { display: flex; flex-direction: column; gap: 4px; margin-bottom: 8px; align-items: flex-start; }

/* [2026-07-26] 세그먼트 컨트롤 — 원천 토글(MMP/TOOL/둘 다)처럼 배타 선택에 쓴다.
   선택 상태를 색만으로 구분하지 않는다: 배경·테두리·글자 굵기가 함께 바뀐다(색각 이상 대비). */
.seg { display: inline-flex; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
.seg-btn { min-height: 30px; padding: 3px 14px; border: 0; background: transparent;
  color: var(--muted); cursor: pointer; font: inherit; font-size: 13px; }
.seg-btn + .seg-btn { border-left: 1px solid var(--line); }
.seg-btn[aria-pressed="true"] { background: var(--panel2); color: var(--ink); font-weight: 600; }
.seg-btn:hover:not([aria-pressed="true"]) { color: var(--ink); }
.tile-sub { color: var(--muted); font-size: 11px; margin-top: 4px; }
.line.s-d { stroke: var(--s-d); } .line.s-e { stroke: var(--s-e); } .line.s-f { stroke: var(--s-f); }
.lg.s-d::before { color: var(--s-d); } .lg.s-e::before { color: var(--s-e); } .lg.s-f::before { color: var(--s-f); }
.chart .bar { stroke: none; }
.bar.p-new { fill: var(--s-today); } .bar.p-ret { fill: var(--s-d); } .bar.p-res { fill: var(--s-prev-day); }
.lg.p-new::before { color: var(--s-today); } .lg.p-ret::before { color: var(--s-d); }
.lg.p-res::before { color: var(--s-prev-day); }

/* [Q-42] Table-first KPI and administrator operating surfaces.
   [UI 2026-07-26] 모든 치수·색을 토큰으로 올렸다. 종전에는 헤더 배경/글자·금액색이 raw hex라
   라이트 테마를 «규칙 덮어쓰기»로만 고칠 수 있었고, 그 덮어쓰기가 순서에 지면 조용히 죽었다. */
.data-table-wrap {
  width: 100%;
  /* 가로 스크롤은 **이 컨테이너 안에서만** 일어난다 — 20열 표가 본문을 가로로 흔들면 안 된다. */
  overflow: auto;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--panel);
  max-height: min(68dvh, 760px);
}
.data-table {
  width: max-content;
  min-width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  font-size: var(--fs-sm);
}
.data-table th,
.data-table td {
  padding: var(--row-pad-y) var(--row-pad-x);
  border-right: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
  text-align: left;
  vertical-align: middle;
}
.data-table th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--th-bg);
  color: var(--th-ink);
  font-weight: 700;
  /* 헤더가 스크롤로 본문 위에 겹칠 때 «떠 있음»이 보이게 — 종전엔 경계가 1px 선뿐이었다. */
  box-shadow: inset 0 -1px 0 var(--line);
}
.data-table tr[tabindex] { cursor: pointer; }
.data-table td.num,
.data-table th.num { text-align: right; font-variant-numeric: tabular-nums; }
.data-table td.money { color: var(--money-ink); }
.data-table td.mono, .data-table th.mono { font-family: var(--mono); }
.data-table .sticky-col {
  position: sticky;
  left: 0;
  z-index: 1;
  /* ★[3R 중② 2026-07-26] 장문이어야 한다 — 이 규칙은 td에도 얹히므로(`td.sticky-col`), 단축
     `background:`는 background-image를 재설정해 파일 말미의 정렬 틴트 레이어와 같은 채널을 건드린다.
     (현행 특이도에서는 (0,2,3) 틴트 규칙이 이겨 증상이 없지만, 채널 규율은 순서·특이도에 기대지 않는다
     — :586 「셀 배경 규칙은 전부 장문」이 이 파일 자신의 규칙이고, 이 줄이 그 유일한 위반이었다.) */
  background-color: var(--panel);
  box-shadow: var(--shadow-sticky);
}
.data-table th.sticky-col { z-index: 3; background: var(--th-bg); }

/* ★[OWNER 2026-07-29 · 게임팀 요청] 처리 열 오른쪽 고정 — `.sticky-col`의 거울상.
   게시물 관리 표는 12열이고 마지막 열이 «처리»(숨김/복구·삭제·제재)다. 앞의 열들은 전부 **읽는**
   것이고 처리 열만 **하는** 것인데, 하필 그 하나만 가로 스크롤 없이는 절대 화면에 없었다.
   ★ 그림자 방향이 반대다: `--shadow-sticky`(6px 0 …)는 왼쪽 고정 열의 **오른쪽** 경계를 그린다.
     오른쪽 고정 열은 **왼쪽** 경계가 필요하므로 x를 뒤집는다(새 토큰 도입 없이 부호만).
   ★ 배경은 반드시 장문(`background-color`)이다 — 단축은 background-image를 재설정해 파일 말미의
     정렬 틴트 레이어를 조용히 지운다(:367·:646 판례). 아래 특이도 사슬도 `.sticky-col`과 동형으로
     깐다: (0,2,0)만으로는 얼룩말 (0,2,3)에 져서 짝수 행의 고정 열이 반투명해지고, 그 밑으로
     가로 스크롤한 셀이 비쳐 지나간다(경④와 같은 결함의 거울상). */
.data-table .sticky-col-right {
  position: sticky;
  right: 0;
  z-index: 1;
  background-color: var(--panel);
  box-shadow: -6px 0 8px -8px rgba(0, 0, 0, .55);
}
.data-table th.sticky-col-right { z-index: 3; background-color: var(--th-bg); }

/* 본문 프리뷰 — 폭 상한 + 말줄임. 표는 `white-space: nowrap`이라 상한이 곧 말줄임의 조건이다.
   프리뷰는 «행을 구별하는» 열이지 «읽는» 열이 아니다(전문은 댓글 패널·소비자 화면이 정본). */
.data-table td.cell-preview { max-width: 34ch; overflow: hidden; text-overflow: ellipsis; }

/* 축약 ID 칩 — 표에서는 8자로 구별하고, 전체 값은 title(마우스)과 클릭 복사(키보드·터치)로 남는다. */
.id-chip {
  font-family: var(--mono);
  font-size: var(--fs-sm);
  color: var(--ink);
  background: transparent;
  border: 1px dashed var(--line);
  border-radius: var(--r-sm);
  padding: 2px 7px;
  cursor: pointer;
}
.id-chip:hover { border-style: solid; border-color: var(--accent); background: var(--panel2); }
.id-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ── 헤더 클릭 정렬 ──────────────────────────────────────────────────────────
   ★ 표식은 **의사요소(::after)** 로만 낸다. th의 textContent가 라벨 그대로 유지돼야
     e2e(`th.first()).toHaveText(ko.kpi.table.date)`)가 살아 있다. 텍스트로 화살표를 붙이면
     그 파수꾼이 죽는다. 상태의 정본은 aria-sort(스크린리더가 읽는 그 속성)다. */
.data-table th.sortable { padding: 0; }
.th-sort {
  display: flex; align-items: center; gap: var(--sp-1);
  width: 100%; min-height: 34px;
  padding: var(--row-pad-y) var(--row-pad-x);
  border: 0; background: transparent; color: inherit;
  font: inherit; font-weight: 700; text-align: inherit;
  cursor: pointer;
}
.data-table th.num .th-sort { justify-content: flex-end; }
.th-sort:hover { color: var(--ink); background: var(--row-hover); }
.th-sort::after {
  content: "⇅"; font-size: var(--fs-cap); opacity: .35; flex: 0 0 auto;
}
.data-table th[aria-sort="ascending"] .th-sort::after { content: "▲"; opacity: 1; color: var(--gold); }
.data-table th[aria-sort="descending"] .th-sort::after { content: "▼"; opacity: 1; color: var(--gold); }
/* 정렬 중인 열은 색만이 아니라 **형태**로도 드러난다(색각 이상·흑백 인쇄 대비): 위쪽 강조선.
   ★ box-shadow가 아니라 **테두리**로 낸다. inset box-shadow로 그리면 같은 속성을 쓰는
     `.data-table .sticky-col`(0,2,0 · 우측 경계 그림자)과 한 속성을 두고 다투고, 특이도가 더 높은
     이 규칙이 이겨서 **고정 열이 정렬되는 순간 우측 경계 그림자가 사라진다**(두 신호가 한 속성에
     얹히면 하나는 죽는다). 투명 3px를 모든 th에 미리 깔아 두므로 색만 켜지고 높이는 안 움직인다. */
.data-table th { border-top: 3px solid transparent; }
.data-table th[aria-sort="ascending"],
.data-table th[aria-sort="descending"] { border-top-color: var(--gold); }
/* ★[경④ 2026-07-26] 정렬 열의 **세로 띠**는 여기 없다 — 「표 시인성」블록 말미에서 background-image
   레이어로 깔린다(2R). 배경 **색**으로 그리면 얼룩말·원천 틴트·고정 열과 한 속성을 두고 다투고,
   그 싸움은 조용하다. 색이 아니라 레이어여야 어떤 결합 클래스에서도 살아남는다. */

/* ── 행 상태 표식 — 색만이 아니라 좌측 형태로도 구분 ───────────────────────────
   투명 3px를 **모든** 첫 셀에 미리 깔아 둔다: 표식이 붙는 행만 두꺼워지면 열이 어긋난다. */
.data-table tbody td:first-child { border-left: 3px solid transparent; }
.data-table tbody tr.row-alert td:first-child { border-left-color: var(--down); }
.data-table tbody tr.row-watch td:first-child { border-left-color: var(--gold); }

.table-empty {
  border: 1px dashed var(--line);
  color: var(--muted);
  padding: 28px;
  border-radius: var(--r-md);
  text-align: center;
}
.mono { font-family: var(--mono); }
.muted { color: var(--muted); font-size: var(--fs-sm); }

.range-controls,
.search-controls,
.page-controls,
.inline-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: 8px;
}
.control-label {
  display: flex;
  flex-direction: column;
  gap: 4px;
  color: var(--muted);
  font-size: 12px;
}
.search-wide, .inp.wide { min-width: min(360px, 100%); flex: 1; }
.page-controls { justify-content: flex-end; align-items: center; margin-top: 9px; }
/* 커서 페이지 목록이 «왜 열 클릭 정렬이 없는가»를 말하는 자리. 왼쪽 끝으로 밀어 버튼과 분리한다. */
.page-note { margin-right: auto; color: var(--muted); font-size: var(--fs-sm); }

/* ═══ KPI 요약 스트립 — 요약이 상세보다 앞에 온다 ══════════════════════════════
   [2026-07-26] 원표(20열) 위에 «판단 가능한 층»을 둔다. 강조 배경을 쓰지 않는다:
   이 층은 원표와 **같은 데이터**이지 더 확실한 데이터가 아니다(실시간 잠정 스트립과 다르다). */
.kpi-summary { display: flex; flex-direction: column; gap: var(--sp-4); }
.kpi-summary-head { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-3); }
.kpi-summary-head .footnote { margin: 0; }

.live-monitor {
  border: 1px solid var(--line);
  border-left: 3px solid var(--gold);
  border-radius: 10px;
  padding: 10px;
  background: linear-gradient(90deg, rgba(225,182,84,.055), transparent 38%);
}
.live-monitor .data-table-wrap { max-height: 260px; margin-top: 8px; }
.live-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.live-note { color: var(--gold); font-size: 11px; }

.ops-details {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 0 13px 13px;
}
.ops-details > summary {
  cursor: pointer;
  min-height: 44px;
  display: flex;
  align-items: center;
  font-weight: 700;
}
.admin-form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 9px;
  align-items: end;
}
.admin-form-grid .span-2 { grid-column: span 2; }
.textarea { min-height: 96px; resize: vertical; }
.code-input { min-height: 180px; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; }

.detail-panel {
  border: 1px solid var(--gold);
  background: rgba(225,182,84,.035);
  border-radius: 10px;
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.detail-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.detail-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 8px;
}
.detail-field {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 8px;
  border: 1px solid var(--line);
  border-radius: 8px;
}
.detail-label { color: var(--muted); font-size: 11px; }
.detail-value { white-space: pre-wrap; overflow-wrap: anywhere; }
.sanction-summary { display: flex; flex-direction: column; gap: 6px; }
.mini-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

.shop-summary {
  display: flex;
  gap: 18px;
  padding: 9px 12px;
  border-block: 1px solid var(--line);
  color: var(--gold);
}
.valve-panel {
  border: 1px solid rgba(239,95,107,.4);
  border-radius: 10px;
  overflow: hidden;
}
.valve-panel .pending { border: 0; border-radius: 0; border-bottom: 1px solid var(--line); }
.valve-row {
  display: grid;
  grid-template-columns: minmax(220px, 1fr) auto auto;
  gap: 10px;
  align-items: center;
  padding: 10px 12px;
  border-bottom: 1px solid var(--line);
}
.valve-row:last-child { border-bottom: 0; }

@media (max-width: 720px) {
  .stage { padding: 10px; }
  .appbar { padding: 8px 10px; gap: 8px; }
  .gamesel { margin-left: auto; overflow-x: auto; }
  /* [2026-07-26] `top: 98px` 하드코딩 제거. 헤더 높이를 --appbar-h로 승격해놓고 이 자리만
     옛 상수가 남아 있었다 — 700px 폭에서 헤더 하단 56px vs 탭바 상단 98px로 **42px 틈**이
     벌어져 본문이 그 사이로 흘러 지나갔다(UI 감사 실측). 변수 하나만 보게 한다.
     `.gamesel { width: 100% }`도 함께 되돌린다: 헤더를 nowrap 고정으로 바꿨으므로
     폭 100%는 브랜드를 밀어낸다. */
  .tabbar { padding-inline: 6px; }
  .tab { padding-inline: 11px; }
  .sec-title { align-items: flex-start; flex-direction: column; gap: 2px; }
  .range-controls > *,
  .search-controls > * { flex: 1 1 140px; }
  .admin-form-grid { grid-template-columns: 1fr; }
  .admin-form-grid .span-2 { grid-column: span 1; }
  .valve-row { grid-template-columns: 1fr auto; }
  .valve-row .btn { grid-column: 1 / -1; }
  .data-table-wrap { max-height: 62dvh; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   [2026-07-26 UI 감사 수리] JS가 참조하는데 CSS가 받지 못한 클래스들.
   한 기전에서 나온 결함군이다 — 빌더가 `class:`를 썼는데 스타일이 따라오지 않았다.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ① 토스트 — 콘솔의 **모든 쓰기 피드백**이 여기 걸려 있었다. 규칙이 없어
      position:static·배경 없음으로 화면 밖(하단 843px)에 맨 텍스트로 렌더됐다.
      밸브 저장·제재 등록·상품 활성화·KPI 리비전 저장이 전부 무반응으로 보였다.
      형제 파일 frontend/webview/assets/app.css에는 존재한다 — 이식 누락이었다. */
.toast { position: fixed; left: 50%; bottom: 28px; transform: translateX(-50%) translateY(8px);
  z-index: 60; background: var(--panel2); border: 1px solid var(--line); color: var(--ink);
  font-size: 13px; font-weight: 600; border-radius: 12px; padding: 10px 16px; max-width: 86%;
  text-align: center; line-height: 1.5; opacity: 0; pointer-events: none;
  transition: opacity .2s, transform .2s; box-shadow: var(--shadow-2); }
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
@media (prefers-reduced-motion: reduce) { .toast { transition: none; } }

/* ② badge()의 **기본값**이 'honest'인데 규칙이 없어 neutral(회색)과 픽셀 단위로 같았다.
      결제 상태 3종·CORRECTED vs MISSING·데이터 정합성 경보가 전부 구분 불가였다. */
.badge.honest { color: var(--ink); border-color: var(--line); background: var(--panel2); }

/* ③ .pending.warn — 콘솔 최고위험 경고 2건("실서비스 밸브를 열지 마세요",
      "제재 집행 밸브는 닫혀 있습니다")이 ⏳ "준비 중" 플레이스홀더와 동일하게 보였다.
      색만이 아니라 테두리 실선·좌측 강조선으로 형태를 함께 바꾼다(색각 이상 대비). */
.pending.warn { color: var(--gold); border-style: solid; border-color: rgba(225,182,84,.45);
  background: rgba(225,182,84,.07); border-left: 3px solid var(--gold); }

/* ④ 게시판·공지 화면이 아예 미스타일이었다 — .bd-row가 display:block이라 3줄로 적층됐다.
      형제 규칙 .op-row(grid)와 동형으로 맞춘다. */
.bd-list { display: flex; flex-direction: column; gap: 6px; }
.bd-row { display: grid; grid-template-columns: minmax(90px, auto) 1fr auto; gap: 10px;
  align-items: center; padding: 9px 12px; background: var(--panel2);
  border: 1px solid var(--line); border-radius: 10px; }
.bd-slug { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--muted); }
.bd-title { font-weight: 600; }
.bd-untitled { color: var(--muted); font-style: italic; }
.bd-id { font-size: 12px; color: var(--muted); }

/* ⑤ 오류/성공 색이 `.mod-result` 안으로만 스코프돼 있어, 게시판·공지의 권한 거절이
      평범한 본문색으로 나왔다("거절됨"과 "생성했습니다"가 색으로 구분 안 됨).
      전역으로 승격한다 — 결과 표시는 화면마다 다를 이유가 없다. */
.err { color: var(--down); }
.ok  { color: var(--up); }

/* ⑥ .note — "삭제는 되돌릴 수 없습니다"가 맨 본문 텍스트였다. */
.note { font-size: 12px; color: var(--muted); line-height: 1.55; }

/* ⑦ CS 시스템 메시지가 배경 틴트 없이 렌더됐다(user/operator만 규칙 존재). */
.screen.cs .cs-msg-system { background: var(--panel2); border-style: dashed; }

/* ═══ 표 시인성 — OWNER "표는 명확하게" ═══════════════════════════════════════
   실측: 셀 구분선 1.27:1 · 호버 하이라이트 1.04:1 · 원천 틴트 1.04:1.
   20열 표를 두 축으로 스크롤하는데 행을 눈으로 따라갈 단서가 없었다.
   ★[2026-07-26 UI] 값 자체는 그대로 두고 **토큰 경유**로만 바꿨다(라이트 대비 사고의 수리를
     되돌리지 않는다). 종전에는 여기 알파값이 raw로 박혀 있어 라이트 블록이 규칙을 통째로
     다시 써야 했고, 그 규칙이 뒤에 오는 규칙에 지면 조용히 죽었다. 이제 라이트는 값만 갈아끼운다. */
/* ★ 장문(`background-color`)이어야 한다 — 단축 `background:`는 background-image를 재설정해
   블록 말미의 정렬 틴트 레이어를 조용히 지운다(2R 경④). 셀 배경 규칙은 전부 장문으로 통일한다. */
.data-table tbody tr:nth-child(even) td { background-color: var(--row-zebra); }

/* ★★[외부 검수 SOL/MAX 경④ · 2026-07-26] **열 신호가 얼룩말에 잘려 있었다.**
   실측: `.data-table td.sorted-col`·`.data-table td.source-mmp/-tool` 은 특이도 (0,2,1)인데
   바로 위 얼룩말 `.data-table tbody tr:nth-child(even) td` 는 (0,2,3)이다 — 순서와 무관하게
   얼룩말이 이긴다 ⟹ **짝수 행에서 세로 띠가 끊긴다.** 원천 틴트는 이중 원천 설계의 핵심인데
   한 줄 걸러 사라지면 신호가 아니라 잡음이 된다.
   ★ 미디어 쿼리는 특이도를 올려주지 않는다 — 순서가 전부다(누적 판례). 그래서 값이 아니라
     **특이도와 자리**를 고친다: `tbody tr`을 명시해 얼룩말과 같은 (0,2,3)로 맞추고 그 **뒤**에 둔다.
   ★ 호버보다는 **앞**에 둔다(종전 순위 보존): 호버는 «지금 가리키는 행»이라 원천 틴트를 덮는 것이 맞고,
     그것이 수리 전 동작이기도 하다(옛 규칙은 (0,2,1)이라 호버 (0,2,3)에 이미 지고 있었다). */
.data-table tbody tr td.source-mmp  { background-color: var(--tint-mmp); }
.data-table tbody tr td.source-tool { background-color: var(--tint-tool); }

.data-table tbody tr:hover td { background-color: var(--row-hover); }
/* 고정 일자 열이 호버 시 반투명해져 숫자가 비쳐 지나갔다 — 특이도를 올려 되찾는다.
   ★★[경④와 같은 축 — 함께 적발] `.data-table .sticky-col` 은 (0,2,0)이라 **얼룩말에도 졌다**:
     짝수 행에서 고정 열이 반투명 얼룩말 배경이 되어 가로 스크롤한 셀이 그 밑으로 비쳐 지나간다.
     호버 팔만 고치고 얼룩말 팔을 놓친 것이다(같은 결함의 나머지 절반). td 팔을 (0,2,3)으로 올린다 —
     th 팔은 `.data-table .sticky-col` 이 그대로 맡는다(헤더는 얼룩말 정의역 밖이라 (0,2,0)으로 충분). */
.data-table tbody tr:hover td.sticky-col,
.data-table .sticky-col,
.data-table tbody tr td.sticky-col { background-color: var(--panel); }
.data-table tbody tr:hover td.sticky-col { background-color: var(--panel2); }
/* [OWNER 2026-07-29] 오른쪽 고정 열도 같은 사슬을 갖는다 — 위 판례를 새 열에 다시 적용하지 않으면
   짝수 행에서 처리 버튼 뒤로 셀이 비쳐 지나간다(같은 결함의 거울상). */
.data-table tbody tr:hover td.sticky-col-right,
.data-table .sticky-col-right,
.data-table tbody tr td.sticky-col-right { background-color: var(--panel); }
.data-table tbody tr:hover td.sticky-col-right { background-color: var(--panel2); }

/* ★★[외부 검수 SOL/MAX 2R 경④ · 2026-07-26] **결합 클래스에서 정렬 틴트가 다시 죽어 있었다.**
   셀 클래스는 data-table.mjs에서 합성된다(`sticky-col mono sorted-col` · `num source-mmp sorted-col`).
   1R 수리는 `.data-table tbody tr td.sorted-col`(0,2,3)을 얼룩말 뒤에 뒀을 뿐이라, **같은 특이도의
   뒤쪽 규칙**(source-*·sticky-col)이 그대로 이겼다 ⟹ 날짜 열을 정렬하면·BOTH 모드에서 MMP/TOOL 열을
   정렬하면 정렬 틴트가 **한 픽셀도 안 보였다**(Playwright 실측).
   ⟹ 규칙 하나를 더 얹어 이기는 것은 O(n²) 땜질이다(열 클래스가 하나 늘 때마다 조합이 다시 샌다).
     이 파일이 이미 적어둔 판례가 정답을 말한다 — 「두 신호가 한 속성에 얹히면 하나는 죽는다」(:367).
     ⟹ **다른 속성으로 옮긴다**: 행·열 정체성은 background-**color**가 정하고, 정렬 강조는
       background-**image** 레이어로 그 위에 깔린다. 둘은 다투지 않으므로 어떤 조합에서도 함께 산다.
   ★ 그래서 위 셀 규칙들이 전부 `background:` 단축이 아니라 `background-color:` 장문이다 —
     단축은 background-image를 **재설정**해 이 레이어를 조용히 지운다. 새 규칙을 추가할 때도 장문을 써라.
   ★ 부수 효과(의도 · 고지): 호버 중에도 정렬 틴트가 살아남는다. 호버는 «지금 가리키는 행»이고
     정렬은 «지금 보고 있는 열»이라 서로 지울 이유가 없다. 고정 열은 색이 불투명한 --panel이므로
     그 위에 반투명 틴트가 얹혀도 밑이 비치지 않는다(실측 확인). */
.data-table tbody tr td.sorted-col {
  background-image: linear-gradient(var(--col-sorted), var(--col-sorted));
}
/* 숫자 열은 자릿수를 세로로 맞춘다 — 금액 비교의 기본. */
.data-table td.num, .data-table th.num { font-variant-numeric: tabular-nums; }
/* 데이터 부재(—)와 관측된 0을 눈으로 구분한다. 0은 값이고 —는 값이 없는 것이다(§0 · J-D1). */
.data-table td.is-null { color: var(--null-ink); }

/* ═══ 상태 3형상 — 로딩 · 빈 상태 · 오류 ══════════════════════════════════════
   OWNER "창이 길어졌다 좁아졌다": spinner()가 부트용 .boot(min-height:60dvh)를 재사용해,
   검색 한 번에 표가 432px 빈 상자로 교체됐다가 돌아왔다. 인플레이스 로딩은 높이를 먹지 않는다.
   세 상태가 같은 상자 모양(점선 테두리 · 중앙 정렬 · 같은 높이대)을 쓰게 해서, 화면이 바뀔 때
   레이아웃이 튀지 않고 «지금 무슨 상태인가»가 한눈에 구별되게 한다. */
.loading { display: flex; align-items: center; justify-content: center; padding: 28px 0; }
.state-box {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--sp-2); text-align: center;
  border: 1px dashed var(--line); border-radius: var(--r-md);
  padding: 28px var(--sp-6); color: var(--muted);
}
.state-ic { font-size: 20px; opacity: .8; }
.state-title { color: var(--ink); font-weight: 600; }
.state-body { font-size: var(--fs-sm); max-width: 460px; }

/* ═══ 조회 조건 표시 — "없습니다"가 왜 없는지 말하게 한다 ══════════════════════
   [2026-07-26] CS 화면에서 도입한 패턴(무엇을 물었는지 + 무엇이 돌아왔는지)을 공용으로 올린다.
   "0건"만 있으면 게임 스코프 탓인지·필터 탓인지·진짜 0건인지 구별할 수 없다(실제로 프로덕션
   왕복 한 번을 그 구별에 썼다). 조건은 **요청이 실제로 실어 보낸 값**이지 화면의 의도가 아니다. */
.scope-line {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-4);
  padding: var(--sp-2) var(--sp-4); margin-bottom: var(--sp-3);
  border: 1px solid var(--line); border-left: 3px solid var(--accent);
  border-radius: var(--r-sm); background: var(--panel2);
  font-size: var(--fs-sm); color: var(--muted);
}
.scope-item { display: inline-flex; align-items: center; gap: var(--sp-1); }
.scope-k { opacity: .8; }
.scope-v { color: var(--ink); font-weight: 600; }
.scope-count { margin-left: auto; color: var(--ink); font-weight: 700; font-variant-numeric: tabular-nums; }

/* ═══ 배지 — 색만이 아니라 형태로도 구분(색각 이상 + 흑백 인쇄) ═════════════════
   [2026-07-26] 종전 배지 4종의 유일한 차이는 색상이었다. 적록색각 이상에서 ok(초록)와
   danger(빨강)는 같은 톤으로 보이고, 흑백 인쇄에서는 넷이 전부 회색이 된다. 제재 상태·확정
   상태·정합성 경보가 그 배지에 걸려 있다 ⟹ 선행 글리프로 **모양**을 붙인다.
   ★ ::before(의사요소)이므로 textContent는 변하지 않는다 — 문구를 무는 e2e를 깨지 않는다. */
.badge.ok::before     { content: "✓ "; font-weight: 700; }
.badge.warn::before   { content: "▲ "; font-size: .85em; }
.badge.danger::before { content: "● "; font-size: .85em; }

/* ═══════════════════════════════════════════════════════════════════════════
   [OWNER 2026-07-26] 포인트 경제 관측판(「대시보드」).
   ★ 위계를 **네 단**으로 만든다. 종전 화면의 결함은 «전부 같은 1px 테두리 상자»여서
     요약·차트·표가 같은 무게로 보였다는 것이다(위계 0). 여기서는 무게가 다르다:
       ① 요약 밴드  = 다른 바닥면·모서리 없음·전폭 (한 덩어리로 읽힌다)
       ② 구획       = 상자가 아니다. 윗 구분선 + 제목만 — 차트가 배경 위에 떠 있다
       ③ 차트 타일  = 옅은 카드(그리드가 그리드로 읽히게)
       ④ 표         = 기존 표 규칙 그대로
     같은 테두리를 한 겹 더 두르는 대신 «면·간격·크기»로 나눈다.
   ═══════════════════════════════════════════════════════════════════════════ */
.screen.point-economy { gap: var(--sp-6); }
.point-economy .pe-honest { color: var(--muted); font-size: var(--fs-sm); line-height: 1.6;
  margin: 0; padding-left: var(--sp-4); border-left: 2px solid var(--line); }

/* 조회 컨트롤 — 액센트를 쓰지 않는다(액센트 = 선택 상태 전용). */
.pe-controls { display: flex; flex-wrap: wrap; align-items: end; gap: var(--sp-3); }

/* 구역 제목 — ①규모/③포인트 경제. 화면이 «세 덩어리»라는 것을 글자로도 말한다. */
.pe-rank { margin: var(--sp-6) 0 0; font-size: var(--fs-sm); font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--muted); }

/* ① 규모 밴드 — 카드가 아니라 «면»이다. 전폭으로 깔려 한 덩어리로 읽힌다. */
.pe-scale { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  background: var(--pe-surface); border-block: 1px solid var(--pe-rule);
  margin-inline: calc(-1 * var(--sp-6)); padding-inline: var(--sp-6); }
.pe-scale-cell { padding: var(--sp-5) var(--sp-5) var(--sp-5) 0; border-right: 1px solid var(--pe-rule); }
.pe-scale-cell:last-child { border-right: 0; }
.pe-scale-cell + .pe-scale-cell { padding-left: var(--sp-5); }
.pe-scale-lb { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  color: var(--muted); font-size: var(--fs-sm); font-weight: 600; }
.pe-scale-val { margin-top: var(--sp-2); font-size: 28px; font-weight: 700; letter-spacing: -.025em;
  font-variant-numeric: tabular-nums; line-height: 1.15; }
.pe-scale-val.null { color: var(--null-ink); font-weight: 500; font-size: 22px; }
.pe-scale-sub { margin-top: var(--sp-2); color: var(--muted); font-size: var(--fs-cap); line-height: 1.5; }
.pe-scale-note { margin-top: var(--sp-2); color: var(--muted); font-size: var(--fs-cap);
  line-height: 1.5; max-width: 34ch; }

/* ② 지금 — 당일 한 줄 + 당월/전월 대조. 대조는 «한 행에 나란히»가 전부다. */
.pe-today { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-5);
  padding: var(--sp-3) var(--sp-4); border: 1px solid var(--line); border-left: 3px solid var(--pe-1);
  border-radius: var(--r-sm); background: var(--panel); }
.pe-today-lb { color: var(--muted); font-size: var(--fs-sm); font-weight: 600; }
.pe-today-v { display: inline-flex; align-items: baseline; gap: var(--sp-2);
  font-variant-numeric: tabular-nums; }
.pe-today-k { color: var(--muted); font-size: var(--fs-sm); }
.pe-cmp { display: flex; flex-direction: column; }
.pe-cmp-row { display: grid; grid-template-columns: minmax(120px, 1.2fr) repeat(3, minmax(110px, 1fr));
  gap: var(--sp-4); padding: var(--sp-3) 0; border-bottom: 1px solid var(--line);
  font-variant-numeric: tabular-nums; align-items: baseline; }
.pe-cmp-row:last-child { border-bottom: 0; }
.pe-cmp-row.head { color: var(--muted); font-size: var(--fs-sm); font-weight: 600; }
.pe-cmp-row.head > span:not(:first-child) { text-align: right; }
.pe-cmp-k { color: var(--muted); }
.pe-cmp-cur { text-align: right; font-weight: 700; font-size: var(--fs-lg); }
.pe-cmp-prev { text-align: right; color: var(--muted); }
.pe-cmp-d { text-align: right; }
.pe-change { font-variant-numeric: tabular-nums; font-weight: 600; }
.pe-change.up { color: var(--up); } .pe-change.down { color: var(--down); }
.pe-change.flat { color: var(--flat); }
.pe-change.null { color: var(--null-ink); font-weight: 500; font-size: var(--fs-sm); }

/* ★[OWNER 2026-08-13] 인당 지표 행 — 라벨 칸이 «이름 / 분모 / (왜 비었는가)» 세 줄이다.
   분모를 hover 로 밀면 아무도 읽지 않는다: 이 패널은 전부 몫이고, 세 몫의 분모가 서로 다른
   사람 집합이라 그 한 줄이 없으면 세 수가 같은 «유저»라는 낱말로 뭉개진다.
   ★선택자를 이 패널로 좁힌다 — 위 「지금」 블록의 같은 클래스는 한 줄짜리 라벨이고, 거기까지
   flex 로 바꾸면 이 열차가 손대지 않기로 한 화면이 함께 움직인다.
   ★[검수 경-1] 이 주석은 한때 `*/` 를 **두 번** 달고 있었다. 첫 `*/` 에서 주석이 닫히면서 남은
   두 줄이 선택자로 파싱됐고, 그 바람에 바로 아래 규칙(.pe-percapita .pe-cmp-k)이 통째로 그
   불량 선택자에 삼켜져 **적용되지 않았다** — 라벨 세 줄이 한 줄로 뭉쳐 있었다는 뜻이다.
   CSS 는 불량 선택자를 조용히 버린다: 화면이 «조금 이상한» 상태로 초록이었다. */
.pe-percapita .pe-cmp-k { display: flex; flex-direction: column; gap: 2px; }
.pe-percapita .pe-cmp-denom { color: var(--null-ink); font-size: var(--fs-sm); font-weight: 500; }
.pe-percapita .pe-cmp-why { color: var(--null-ink); font-size: var(--fs-sm); font-style: italic; }

/* ③ 포인트 경제 머리 — 적립·소진·워프 세 숫자 */
.pe-head-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--sp-5); }
.pe-head-cell { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg);
  padding: var(--sp-5); box-shadow: var(--shadow-1); }
.pe-head-lb { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  color: var(--muted); font-size: var(--fs-sm); font-weight: 600; }
.pe-head-val { margin-top: var(--sp-2); font-size: 26px; font-weight: 700; letter-spacing: -.025em;
  font-variant-numeric: tabular-nums; }
.pe-head-d { margin-top: var(--sp-2); }
.pe-three { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: var(--sp-6); }
.pe-third { display: flex; flex-direction: column; gap: var(--sp-3); min-width: 0; }

/* ── 실시간 지표 카드 그리드 ────────────────────────────────────────────────
   [OWNER 2026-07-27] 화면 코드(rt-card·rt-ghost·rt-val…)는 완성돼 있었으나 이 스타일이
   app.css에 **한 번도 들어오지 않아**, 스타일 없는 div가 세로로 쌓여 텍스트 리스트로 보였다
   (PC에서 「저게 뭐야」로 관측). 전광판형 카드 그리드로 복원한다 —
   폰 2열(원 스펙 2×6), 넓은 화면은 자동 채움. 새 토큰을 만들지 않고 기존 --sp-*/--r-*/--up/--down. */
.rt-head { display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--sp-4); flex-wrap: wrap; margin-bottom: var(--sp-5); }
.rt-refresh { flex: none; }

.rt-grid { display: grid; gap: var(--sp-4);
  grid-template-columns: repeat(auto-fill, minmax(164px, 1fr)); margin-bottom: var(--sp-6); }

.rt-card { position: relative; overflow: hidden; background: var(--panel);
  border: 1px solid var(--line); border-radius: var(--r-lg); padding: var(--sp-5);
  box-shadow: var(--shadow-1); min-height: 104px; }
/* 전일 동시간 값 — 숫자 뒤에 아주 옅게. 읽는 값이 아니라 «비교 배경»이다(JS에서 aria-hidden). */
.rt-ghost { position: absolute; right: var(--sp-4); bottom: var(--sp-1);
  font-size: 42px; font-weight: 800; line-height: 1; color: var(--ink);
  opacity: .05; font-variant-numeric: tabular-nums; letter-spacing: -.03em;
  pointer-events: none; user-select: none; }

.rt-card-in { position: relative; display: flex; flex-direction: column; gap: var(--sp-2); }
.rt-lb { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  color: var(--muted); font-size: var(--fs-sm); font-weight: 600; letter-spacing: .01em; }
/* 숫자는 tabular-nums — 카드가 나란히 서면 자릿수가 세로로 맞아야 한 눈에 비교된다. */
.rt-val { font-size: 26px; font-weight: 700; line-height: 1.12; letter-spacing: -.02em;
  font-variant-numeric: tabular-nums; }
/* 값 없는 카드는 «0처럼» 보이면 안 된다 — 굵기·색을 낮춰 숫자와 다른 물건임을 알린다. */
.rt-val.null { color: var(--null-ink); font-weight: 500; font-size: var(--fs-xl); }
.rt-denom { color: var(--muted); font-size: var(--fs-cap); margin-top: -2px; }

/* 등락선 — 방향·크기·전일값을 한 줄에. 화살표는 색, %는 옅은 알약, 전일값은 muted. */
.rt-delta { display: inline-flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-cap); font-variant-numeric: tabular-nums; flex-wrap: wrap; margin-top: var(--sp-1); }
.rt-arrow { font-size: 9px; line-height: 1; }
.rt-pct { display: inline-flex; align-items: center; padding: 1px 7px; border-radius: var(--r-pill);
  font-weight: 700; }
.rt-delta.up .rt-arrow, .rt-delta.up .rt-pct { color: var(--up); }
.rt-delta.up .rt-pct { background: rgba(53,192,122,.10); }
.rt-delta.down .rt-arrow, .rt-delta.down .rt-pct { color: var(--down); }
.rt-delta.down .rt-pct { background: rgba(239,95,107,.10); }
.rt-delta.flat .rt-arrow, .rt-delta.flat .rt-pct { color: var(--flat); }
.rt-delta.flat .rt-pct { background: var(--panel2); }
.rt-prev { color: var(--muted); }
/* 비교 불가 — 알약 없이 옅은 한 줄(카드 높이를 늘리지 않는다). */
.rt-delta.null { color: var(--muted); opacity: .85; display: block; }
.rt-note { color: var(--muted); font-size: var(--fs-cap); line-height: 1.4; margin-top: var(--sp-1); }

/* 실시간 — 요약 두 칸은 규모 밴드와 같은 형태를 재사용한다(새 형태를 발명하지 않는다). */
.rt-scale { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.chart.combo { min-width: 640px; }
.chart.combo .mk.bar-rev { fill: var(--pe-1); }
.chart.combo .mk.line-ccu { stroke: var(--pe-2); stroke-width: 2; fill: none; }
.chart.combo circle.mk.line-ccu { fill: var(--pe-2); stroke: none; }
.chart.combo .axis.y-bar { stroke: var(--pe-1); opacity: .6; }
.chart.combo .axis.y-line { stroke: var(--pe-2); opacity: .6; }
.chart.combo .tick.t-bar { fill: var(--pe-1); }
.chart.combo .tick.t-line { fill: var(--pe-2); }
.pe-lg.rt-rev::before { background: var(--pe-1); }
.pe-lg.rt-ccu::before { background: var(--pe-2); height: 3px; border-radius: 2px; }

/* [W13-KPIFIX] 동접 서버별 분해 — 합계 아래 한 줄씩. 낡은 서버는 «회색으로 흐린 숫자»가 아니라
   숫자 자체를 「—」로 바꾼다: 마지막으로 들은 값은 «지금»이 아니므로 그것을 지금의 자리에 두면
   합계가 낮은 이유를 오히려 감춘다. 언제 마지막이었는지는 그 옆에 적는다. */
.rt-srv-list { margin-top: var(--sp-2); border-top: 1px dashed var(--line); padding-top: var(--sp-1); }
.rt-srv { display: flex; align-items: baseline; gap: var(--sp-2); font-size: var(--fs-cap);
  color: var(--muted); font-variant-numeric: tabular-nums; }
.rt-srv-id { min-width: 9ch; }
.rt-srv-v { font-weight: 600; color: var(--ink); }
.rt-srv.stale .rt-srv-v { color: var(--muted); font-weight: 400; }
.rt-srv-at { opacity: .8; }
/* 미수신 경고는 카드 안에서 유일하게 색을 쓰는 줄이다 — 나머지 줄과 같은 회색이면 못 읽는다. */
.rt-srv.missing { color: var(--down); display: block; line-height: 1.4; margin-top: var(--sp-1); }
.pe-lg.rt-srv-lg::before { height: 3px; border-radius: 2px; }
.pe-lg.rt-srv-lg.s0::before { background: var(--s-today); }
.pe-lg.rt-srv-lg.s1::before { background: var(--s-prev-day); }
.pe-lg.rt-srv-lg.s2::before { background: var(--s-prev-week); }
.pe-lg.rt-srv-lg.s3::before { background: var(--s-d); }
.pe-lg.rt-srv-lg.s4::before { background: var(--s-e); }
.pe-lg.rt-srv-lg.s5::before { background: var(--s-f); }
.rt-srv-chart .chart.multiline { min-width: 640px; }

/* [W13-KPIFIX] 운영진 원장 — 증감의 부호. 색은 **두 번째** 부호다: 부호 문자(+/−)가 항상 함께
   인쇄되므로 색각 이상·흑백 인쇄·강제 색상 모드에서도 지급과 사용이 갈린다. */
.staff-ledger .sl-up { color: var(--up); }
.staff-ledger .sl-down { color: var(--down); }
/* [W13 review R1] 설정 결함 경고 — 스크롤로 지나칠 수 없게 한 줄을 차지한다(색만으로 구별하지 않고 배지 동반). */
.sl-warn { display: flex; align-items: baseline; gap: var(--sp-2); margin: var(--sp-2) 0;
  padding: var(--sp-2); border-left: 3px solid var(--down); color: var(--ink); font-size: var(--fs-sm); }

/* 코호트 매트릭스 — 엑셀식 틀 고정은 기존 .data-table 규칙이 이미 준다:
   `th { position:sticky; top:0 }`(헤더행) × `.sticky-col { position:sticky; left:0 }`(A열) ×
   `th.sticky-col`(좌상단 모서리 — 두 고정이 겹치는 자리, z-index 3). 새 구조를 만들지 않고
   그 구조를 **승계**한다. 스크롤은 .data-table-wrap 안에서만 일어난다(가로·세로 둘 다). */
.mx-cell { display: inline-block; min-width: 4.5ch; padding: 1px 6px; border-radius: var(--r-sm);
  font-variant-numeric: tabular-nums; }
/* 관측값 — 색은 **두 번째** 부호다. 숫자가 항상 함께 있어 색각 이상·흑백에서도 읽힌다. */
.mx-cell.obs { background: color-mix(in srgb, var(--pe-1) var(--cell-heat, 0%), transparent);
  font-weight: 600; }
.mx-cell.obs.money { background: none; color: var(--money-ink); }
/* 미도래 = 그 날이 아직 안 왔다(정상) · 결측 = 그 날이 지났는데 값이 없다(추적 대상). 다른 사실이다. */
.mx-cell.pending { color: var(--null-ink); opacity: .7; }
.mx-cell.missing { color: var(--down); opacity: .85; }
/* [W13-KPIFIX] 진행 중(부분일) — 값은 **그대로 인쇄된다**. 흐리게 하거나 색을 빼지 않는다:
   이 값은 거짓이 아니라 «아직 자라는 중»이고, 흐리게 하면 결측·미도래와 같은 부류로 읽힌다.
   구별은 ① 점선 밑줄 ② 「◷」 글리프 두 겹이다 — 색만으로 구별하면 색각 이상·흑백 인쇄에서 사라진다. */
.mx-cell.partial { border-bottom: 1px dashed var(--muted); }
.mx-partial { margin-left: 2px; font-size: .75em; color: var(--muted); line-height: 1; }
.mx-legend .pe-lg.mx-obs::before { background: var(--pe-1); }
.mx-legend .pe-lg.mx-pending::before { background: transparent; border: 1px dashed var(--line); }
.mx-legend .pe-lg.mx-missing::before { background: var(--down); opacity: .7; }
.mx-legend .pe-lg.mx-partial-lg::before { background: transparent; border-bottom: 2px dashed var(--muted);
  border-radius: 0; height: 6px; }
/* D열 선택 UI — 180칸을 그리지 않고 «고른 칸만» 그린다는 사실이 화면에 보여야 한다. */
.mx-picker { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
.mx-picker .ctl-lb { color: var(--muted); font-size: var(--fs-sm); }
.mx-chip { min-height: 30px; padding: 3px 11px; border-radius: var(--r-pill);
  border: 1px solid var(--btn-line); background: var(--panel2); color: var(--muted);
  cursor: pointer; font: inherit; font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }
.mx-chip[aria-pressed="true"] { background: var(--accent); border-color: var(--accent);
  color: #fff; font-weight: 700; }
.mx-chip:disabled { opacity: .35; cursor: not-allowed; }

/* ② 구획 — 상자가 아니다 */

.pe-panel { display: flex; flex-direction: column; gap: var(--sp-4);
  padding-top: var(--sp-5); border-top: 1px solid var(--line); }
.pe-panel-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--sp-3); }
.pe-panel-title { margin: 0; font-size: var(--fs-base); font-weight: 700; letter-spacing: -.01em; }
.pe-panel-sub { color: var(--muted); font-size: var(--fs-sm); }
.pe-two { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: var(--sp-7); }
.pe-half { display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0; }

/* 전월 대비 */
.pe-mom-grid { display: flex; flex-direction: column; }
.pe-mom-row { display: grid; grid-template-columns: minmax(72px, 1fr) repeat(3, minmax(96px, 1fr));
  gap: var(--sp-4); padding: var(--sp-3) 0; border-bottom: 1px solid var(--line);
  font-variant-numeric: tabular-nums; }
.pe-mom-row.head { color: var(--muted); font-size: var(--fs-sm); font-weight: 600; }
.pe-mom-row:last-child { border-bottom: 0; }
.pe-mom-k { color: var(--muted); }
.pe-mom-cur { font-weight: 700; text-align: right; }
.pe-mom-prev, .pe-mom-d { text-align: right; }
.pe-mom-d.up { color: var(--up); } .pe-mom-d.down { color: var(--down); }
.pe-mom-d.null, .pe-mom-prev { color: var(--null-ink); }

/* ③ 차트 타일 — 각 계열은 **자기 눈금**을 갖는다(하나의 y축에 겹치지 않는다) */
.pe-series-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(212px, 1fr));
  gap: var(--sp-4); }
.pe-series { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-md);
  padding: var(--sp-4); display: flex; flex-direction: column; gap: var(--sp-2); min-width: 0; }
.pe-series-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3); }
.pe-series-lb { color: var(--muted); font-size: var(--fs-sm); font-weight: 600; }
.pe-series-val { font-size: var(--fs-lg); font-weight: 700; font-variant-numeric: tabular-nums;
  letter-spacing: -.02em; }
.pe-series-foot { color: var(--muted); font-size: var(--fs-cap); font-variant-numeric: tabular-nums; }
.chart.micro { min-width: 0; width: 100%; height: auto; }
.chart.micro .axis { stroke: var(--line); }
.chart.micro .mk { fill: var(--pe-context); stroke: none; }
.pe-series.pe-accrual .mk { fill: var(--pe-1); }
.pe-series.pe-burn .mk { fill: var(--pe-2); }
/* 관측된 0 = 바닥의 얇은 눈금. 아무 것도 안 그린 «관측 불가»와 눈으로 구별된다. */
.chart.micro .mk.zero { fill: var(--muted); opacity: .6; }
.chart.micro .mk.line { fill: none; stroke: currentColor; stroke-width: 2; }
.pe-axis { display: flex; justify-content: space-between; color: var(--muted);
  font-size: var(--fs-cap); font-variant-numeric: tabular-nums; }
.pe-legend { display: flex; flex-wrap: wrap; gap: var(--sp-5); color: var(--muted); font-size: var(--fs-cap); }
.pe-lg { display: inline-flex; align-items: center; gap: var(--sp-2); }
.pe-lg::before { content: ""; display: inline-block; width: 10px; height: 10px; border-radius: 2px; }
.pe-lg.observed::before { background: var(--pe-1); }
.pe-lg.zero::before { height: 2px; border-radius: 1px; background: var(--muted); }
.pe-lg.absent::before { background: transparent; border: 1px dashed var(--line); }

/* 크기 막대 — 한 색·길이만으로 크기를 말한다(무지개 금지) */
.pe-bars { display: flex; flex-direction: column; }
.pe-bar-row { display: grid; grid-template-columns: minmax(104px, auto) 1fr minmax(88px, auto);
  gap: var(--sp-4); align-items: center; padding: var(--sp-2) 0; }
.pe-bar-lb { color: var(--muted); font-size: var(--fs-sm); font-family: var(--mono);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pe-bar-track { height: 8px; background: var(--panel2); border-radius: var(--r-pill); overflow: hidden; }
.pe-bar-fill { display: block; height: 100%; background: var(--pe-1); border-radius: var(--r-pill); }
.pe-bar-fill.burn { background: var(--pe-2); }
.pe-bar-val { text-align: right; font-variant-numeric: tabular-nums; font-size: var(--fs-md); font-weight: 600; }

/* 표 — 본문 발췌는 한 줄 고정 대신 접힌다(다른 열은 그대로 nowrap) */
.point-economy .data-table td.pe-cell-game { color: var(--muted); }
.point-economy .data-table td.pe-cell-body { white-space: normal; max-width: 46ch; line-height: 1.5; }

/* TOP 표의 유저 칸 — 닉네임이 읽는 값, 식별자는 옮겨 적는 값. 위계를 분명히 둔다:
   같은 크기로 나란히 두면 둘 다 눈에 걸려 어느 쪽이 이름인지 매번 판단해야 한다. */
.top-user { display: flex; flex-direction: column; gap: 1px; }
.top-user-name { font-weight: 600; }
.top-user-id { font-size: var(--fs-xs); color: var(--muted); font-variant-numeric: tabular-nums; }

@media (max-width: 720px) {
  .pe-strip { margin-inline: calc(-1 * var(--sp-4)); padding-inline: var(--sp-4);
    grid-template-columns: 1fr; }
  .pe-stat { border-right: 0; border-bottom: 1px solid var(--pe-rule); padding-left: 0; }
  .pe-stat + .pe-stat { padding-left: 0; }
  .pe-stat:last-child { border-bottom: 0; }
}

/* ═══ 라이트 테마 오버라이드 — **반드시 파일 맨 끝** ═══════════════════════════
   [외부 검수 적발 2026-07-26] 이 블록이 파일 중간(166행)에 있어서, 그 뒤에 추가된 표 규칙들이
   같은 특이도로 **덮어버렸다.** 라이트 모드에서 금액 셀이 의도한 어두운 금색 대신 다크용 밝은
   금색(#f0cc78, 흰 배경 대비 1.54:1)으로 나오고, 얼룩말·호버도 흰색 알파가 적용돼 밝은 배경에서
   구분이 사라졌다. 미디어 쿼리는 특이도를 올려주지 않는다 — 순서가 전부다.

   ★★ [UI 2026-07-26 — 재발 방지 구조 수리] 위 사고의 **기전**은 "블록이 앞에 있었다"가 아니라
      "라이트 테마를 규칙 덮어쓰기로 표현했다"이다. 규칙끼리는 순서로 싸우고, 그 싸움은 조용하다.
      ⟹ 이 블록을 **거의 전부 변수 재정의로 전환**했다. 값만 갈아끼우면 뒤에 어떤 규칙이 추가돼도
      그 규칙 역시 같은 변수를 읽으므로 라이트가 지지 않는다(순서 무의존).
      실측 등가 확인 — 종전 규칙과 **같은 값**을 같은 자리에 넣었다:
        thead th 배경 var(--panel2) / 글자: 종전 var(--muted)(#5b6577 on #eef1f7 = 5.12:1)
          → --th-ink #39435a(8.74:1)로 **강화**만 했다(약화 아님 · 헤더는 표의 뼈대다).
        td.money #8a6d1f(4.95:1 on #fff) · 얼룩말 rgba(0,0,0,.022) · 호버 rgba(0,0,0,.055)
        source-mmp rgba(79,124,255,.10) · source-tool rgba(53,192,122,.12)  ← 전부 종전 그대로.
   ★ 그래도 이 블록은 끝에 둔다(남은 규칙 2건이 있다). 뒤에 규칙을 추가하지 마라. */
/* [OWNER 2026-07-27] 운영툴은 **고정 네이비**다 — OS 라이트 설정을 더 이상 따르지 않는다.
   종전에는 `@media (prefers-color-scheme: light)`라 관리자 기기가 라이트 모드이면 흰 바탕이
   이겼고(OWNER가 「네이비로 바꿨다 하지 않았나」로 관측), 브랜드 색이 기기마다 갈렸다.
   아래 라이트 팔레트는 **버리지 않고** 명시 opt-in(`:root[data-theme='light']`) 뒤로 옮긴다 —
   지금은 아무도 data-theme을 세우지 않으므로 네이비가 항상 기본이고, 훗날 토글을 붙이면
   실측해 둔 대비값(3:1·색각 분리)을 다시 도출하지 않고 그대로 되살릴 수 있다. */
:root[data-theme='light'] {
    /* 표면·잉크 */
    --bg: #f4f6fb; --panel: #fff; --panel2: #eef1f7; --line: #d9dfec; --ink: #1a2030;
    --muted: #5b6577; --cell-immature: #e2e6ef;
    /* 표 토큰 — 종전에는 이 값들이 «규칙 덮어쓰기»였다. */
    --th-bg: #eef1f7;
    --th-ink: #39435a;
    --row-zebra: rgba(0,0,0,.022);
    --row-hover: rgba(0,0,0,.055);
    --tint-mmp: rgba(79,124,255,.10);
    --tint-tool: rgba(53,192,122,.12);
    --money-ink: #8a6d1f;
    --col-sorted: rgba(180,140,30,.07);
    --null-ink: #8b93a4;
    /* 그림자는 밝은 바탕에서 그대로 쓰면 탁해진다 — 알파를 낮춘다. */
    --shadow-1: 0 1px 2px rgba(16,24,40,.08);
    --shadow-2: 0 8px 30px rgba(16,24,40,.18);
    --shadow-sticky: 6px 0 8px -8px rgba(16,24,40,.22);
    /* 보조 버튼이 밝은 배경에서 테두리가 사라져 «클릭할 수 있는 것»으로 안 보였다. */
    --btn-line: #c3cde2;
    /* [OWNER 2026-07-26] 포인트 계열 라이트 스텝 — 다크와 같은 두 색을 밝은 표면에 맞춰 다시 고른 값이다
       (자동 반전이 아니라 선택). 흰 표면 대비 3:1 이상·색각 이상 분리 통과를 실측했다. */
    --pe-1: #2a78d6;
    --pe-2: #eb6834;
    --pe-context: #5f6b80;
    --pe-surface: #eef1f7;
    --pe-rule: #dbe1ee;
}
/* [2026-07-26] `.hm-val { fill:#fff }` 제거 — 오버라이드가 거꾸로였다. 히트맵 셀 색은
   heatColor()가 `hsl(150 55% L%)`로 **테마와 무관하게 항상 연녹색**을 낸다. 그 위에 흰 글씨를
   얹으니 대비가 rate 0에서 1.13:1, rate 1.0에서도 3.39:1로 전 구간 실패했다 — 기본값(어두운
   fill)이 옳다. 라이트 팔레트는 이제 규칙 오버라이드 0건이고 **값만** 바꾼다(opt-in 시). */
