/* ══════════════════════════════════════════════════════════════════════════
   A-SEARCH — mobile-first 스타일시트 (DC-46 / S1)
   레이어 순서를 지킨다. 새 규칙은 해당 레이어 안에 넣는다.
     1. 토큰            (:root — 색·간격·반경·레이어·레이아웃)
     2. 리셋 · 베이스   (타이포·폼 기본·오버플로우 안전망)
     3. 앱 셸           (header · hmenu · tabbar · main · footer)
     4. 공용 컴포넌트   (card · filters · table · notice · badge · pill · pager)
     5. 화면별          (pricegap · item · category · admin/access)
     6. 유틸 · 뷰포트별 조정
   ⚠️ 색 리터럴(hex)은 레이어 1 안에서만 쓴다. 그 밖에서는 var(--…) 로만.
      회귀 방지: tests/test_smoke.py::test_css_has_no_hex_outside_tokens
   ══════════════════════════════════════════════════════════════════════════ */

/* ── 1. 토큰 ─────────────────────────────────────────────────────────────
   테마 = OS 설정을 따르는 자동 전환 (DC-49 가 DC-47/U-2 '라이트 고정'을 뒤집었다).
   기본값은 라이트다 — 매장 주간 조도 기준이라는 U-2 의 근거는 그대로 유효하고,
   어두운 데서 보는 사람만 `prefers-color-scheme: dark` 로 갈라져 나간다.
   라이트 배경은 순백보다 한 단 낮춰(#f2f4f7) 흰 카드가 떠 보이게 하고 눈부심을 줄인다.
   모든 글자/배경 조합은 WCAG AA(4.5:1)를 양쪽 테마에서 실제로 계산해 맞췄다 —
   최소 글자가 12px 이라 '큰 글씨 3:1' 완화는 어디에도 적용하지 않는다.
   ⚠️ 색은 이 레이어에서만 정의한다. 레이어 2~6 은 테마를 모른다(한 줄도 안 바뀐다).
   자동만으로는 부족하다 — 이 앱을 보는 사람의 OS 가 라이트인데 화면만 어둡게
   보고 싶을 수 있다(실제 요청이 그랬다). 그래서 `[data-theme]` 수동 고정을
   한 벌 더 두고 헤더 버튼으로 자동 → 라이트 → 다크를 돈다. 선택은 localStorage
   에만 남는다 — 서버는 테마를 모르고, 읽기 전용 원칙(불변원칙 2)도 그대로다. */
:root {
  color-scheme: light;

  /* 표면 · 경계 */
  --bg: #f2f4f7;              /* 페이지 바닥 */
  --surface: #ffffff;         /* 카드·패널·입력 */
  --surface-2: #e9edf3;       /* 칩·행 hover·코드 */
  --border: #dde1e8;          /* 장식용 구분선 (표 줄·카드 테두리) */
  --border-strong: #7b8494;   /* 폼 컨트롤 외곽선 — 표면 대비 3.2:1 이상 */

  /* 글자 */
  --text: #171a21;            /* 본문      vs bg 15.8:1 */
  --text-muted: #59616f;      /* 보조 라벨 vs bg  5.7:1 */
  --text-dim: #626a78;        /* 최소 강조 vs bg  4.9:1 */
  --text-disabled: #9aa2b0;   /* 비활성(WCAG 대비 요건 면제) */

  /* 강조 */
  --accent: #1558c0;          /* 링크·기본 버튼 vs bg 6.0:1 */
  --accent-hover: #0f4498;
  --on-accent: #ffffff;       /* accent 위 글자 6.6:1 */

  /* 상태 — 글자/배경/테두리를 한 벌로 (배지·알림·칩 공용, 전부 6:1 이상) */
  --pos: #12633a;   --pos-bg: #e4f2ea;   --pos-bg-hover: #d5eadf;  --pos-border: #b7dcc7;
  --neg: #a3241c;   --neg-bg: #fdeceb;   --neg-bg-hover: #fbdedb;  --neg-border: #f0c3bf;
  --warn: #7a5300;  --warn-bg: #fdf2e2;  --warn-bg-hover: #f9e7cb; --warn-border: #e8cf9c;
  --info: #12508f;  --info-bg: #e7f0fd;  --info-bg-hover: #d8e6fb; --info-border: #bcd3f5;
  --admin: #57369f; --admin-bg: #f0eafb; --admin-bg-hover: #e6dcf8; --admin-border: #d6c7f0;

  /* 계열 색 — 경영 요약 꺾은선의 4계열 (DC-57). 상태색(pos/neg/warn)을 쓰지 않는 이유:
     매장 하나가 빨강이면 그 매장이 '나쁘다'로 읽힌다. 계열은 값이 아니라 이름표다.
     합계는 색이 아니라 **무게**로 구분한다(진한 회색 + 굵은 선) — 셋 위에 얹히는 줄이라
     네 번째 색을 더 주면 색만 네 개가 다투고 어느 것도 눈에 안 든다.
     넷 다 흰 표면 대비 4.5:1 이상(합계 10.1 · 파랑 6.6 · 보라 7.1 · 청록 6.0). */
  --sr-t: #374151;            /* 3지점 합계 */
  --sr-n: #1558c0;            /* NORTH */
  --sr-s: #7c3aa0;            /* SOUTH */
  --sr-d: #0b6e7a;            /* DT */
  --sr-grid: #e4e8ee;         /* 눈금선 — 선보다 확실히 흐려야 배경으로 물러난다 */

  /* 그림자 */
  --shadow-1: 0 10px 30px rgba(23, 26, 33, .16);
  --fade-edge: rgba(23, 26, 33, .20);   /* 가로 스크롤 줄의 '더 있음' 그림자 (S3) */

  /* 간격 — 4px 스케일 */
  --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px;
  --sp-5: 24px; --sp-6: 32px; --sp-7: 48px;

  /* 반경 */
  --r-sm: 6px; --r-md: 10px; --r-lg: 14px; --r-pill: 999px;

  /* 레이어 */
  --z-sticky-th: 50; --z-header: 100; --z-tabbar: 100; --z-sheet: 200;

  /* 타이포 — 최소 12px (P6). 10~11px 은 매대 앞 조도에서 안 읽힌다. */
  --fs-xs: 12px; --fs-sm: 13px; --fs-md: 14px;
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans KR", sans-serif;

  /* 레이아웃 */
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-t: env(safe-area-inset-top, 0px);
  --header-h: calc(56px + var(--safe-t)); /* sticky th 오프셋은 오직 이 변수만 본다 (P3) */
  --tabbar-h: 56px;
  --tap: 44px;                            /* 최소 탭 타깃 (HIG 44 / Material 48) */
  --colw: 1100px;                         /* 본문 열 폭 — 화면이 넓어지면 아래에서 키운다 */
}

/* 다크 — 라이트와 '같은 이름, 다른 값'만 둔다. 간격·반경·타이포·레이아웃 토큰은
   테마와 무관하므로 여기서 다시 쓰지 않는다(어긋날 여지를 남기지 않는다).
   순흑(#000)을 안 쓰는 이유: OLED 대비가 너무 세서 흰 글자가 번져 보이고,
   표의 얇은 구분선이 아예 안 보인다. 바닥을 #14171c 로 들어 올려 층을 만든다.
   실측(WCAG AA): 본문 14.89:1 · 보조 8.03:1 · 최소강조 6.90:1 · 링크 7.85:1 ·
   배지 글자 7.5~8.0:1(라이트의 6.2~7.3:1 보다 높다) · 폼 외곽선 4.81:1. */
/* ① 자동 — OS 가 다크면 따라간다. 단, 사용자가 라이트를 '고정' 했으면 비켜 준다. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;

    /* 표면 · 경계 */
    --bg: #14171c;
    --surface: #1b1f26;
    --surface-2: #262b34;
    --border: #333a45;
    --border-strong: #828b9a;   /* 표면 대비 4.8:1 */

    /* 글자 */
    --text: #e7eaef;            /* 본문      vs bg 14.9:1 */
    --text-muted: #a6aebb;      /* 보조 라벨 vs bg  8.0:1 */
    --text-dim: #99a1af;        /* 최소 강조 vs bg  6.9:1 */
    --text-disabled: #6b7280;   /* 비활성(WCAG 대비 요건 면제) */

    /* 강조 */
    --accent: #7fabff;          /* 링크 vs bg 7.9:1 */
    --accent-hover: #a3c4ff;
    --on-accent: #0b1020;       /* accent 위 글자 8.3:1 */

    /* 상태 — 라이트가 '연한 배경 + 진한 글자' 였다면 다크는 그 반대다.
       배경은 각 색상의 어두운 끝, 글자는 밝은 끝으로 뒤집는다. */
    --pos: #5fd39b;   --pos-bg: #11301f;   --pos-bg-hover: #173d29;  --pos-border: #245c3d;
    --neg: #ff938c;   --neg-bg: #3a1614;   --neg-bg-hover: #4a1c19;  --neg-border: #6e2b26;
    --warn: #e8b25a;  --warn-bg: #33260f;  --warn-bg-hover: #413015; --warn-border: #5e4718;
    --info: #8ab6f5;  --info-bg: #132339;  --info-bg-hover: #1a2f4a; --info-border: #2a4570;
    --admin: #c3a9f2; --admin-bg: #241a3d; --admin-bg-hover: #2e2250; --admin-border: #463473;

    /* 계열 색 — 라이트와 같은 순서로 밝게 뒤집는다(표면 대비 7.4~10.8:1). */
    --sr-t: #cbd2dc;
    --sr-n: #7fb0ff;
    --sr-s: #d3a5ea;
    --sr-d: #64c8d6;
    --sr-grid: #2b313b;

    /* 그림자 — 어두운 바닥 위에서는 라이트의 반투명 남색이 안 보인다. 더 세게. */
    --shadow-1: 0 10px 30px rgba(0, 0, 0, .55);
    --fade-edge: rgba(0, 0, 0, .55);   /* 가로 스크롤 줄의 '더 있음' 그림자 (S3) */
  }
}
/* ② 수동 — 버튼으로 고른 다크는 OS 가 라이트여도 이긴다.
   ⚠️ 위 블록과 내용이 같아야 한다. 회귀 방지: test_dark_theme_blocks_match */
:root[data-theme="dark"] {
    color-scheme: dark;

    /* 표면 · 경계 */
    --bg: #14171c;
    --surface: #1b1f26;
    --surface-2: #262b34;
    --border: #333a45;
    --border-strong: #828b9a;   /* 표면 대비 4.8:1 */

    /* 글자 */
    --text: #e7eaef;            /* 본문      vs bg 14.9:1 */
    --text-muted: #a6aebb;      /* 보조 라벨 vs bg  8.0:1 */
    --text-dim: #99a1af;        /* 최소 강조 vs bg  6.9:1 */
    --text-disabled: #6b7280;   /* 비활성(WCAG 대비 요건 면제) */

    /* 강조 */
    --accent: #7fabff;          /* 링크 vs bg 7.9:1 */
    --accent-hover: #a3c4ff;
    --on-accent: #0b1020;       /* accent 위 글자 8.3:1 */

    /* 상태 — 라이트가 '연한 배경 + 진한 글자' 였다면 다크는 그 반대다.
       배경은 각 색상의 어두운 끝, 글자는 밝은 끝으로 뒤집는다. */
    --pos: #5fd39b;   --pos-bg: #11301f;   --pos-bg-hover: #173d29;  --pos-border: #245c3d;
    --neg: #ff938c;   --neg-bg: #3a1614;   --neg-bg-hover: #4a1c19;  --neg-border: #6e2b26;
    --warn: #e8b25a;  --warn-bg: #33260f;  --warn-bg-hover: #413015; --warn-border: #5e4718;
    --info: #8ab6f5;  --info-bg: #132339;  --info-bg-hover: #1a2f4a; --info-border: #2a4570;
    --admin: #c3a9f2; --admin-bg: #241a3d; --admin-bg-hover: #2e2250; --admin-border: #463473;

    /* 계열 색 — 라이트와 같은 순서로 밝게 뒤집는다(표면 대비 7.4~10.8:1). */
    --sr-t: #cbd2dc;
    --sr-n: #7fb0ff;
    --sr-s: #d3a5ea;
    --sr-d: #64c8d6;
    --sr-grid: #2b313b;

    /* 그림자 — 어두운 바닥 위에서는 라이트의 반투명 남색이 안 보인다. 더 세게. */
    --shadow-1: 0 10px 30px rgba(0, 0, 0, .55);
    --fade-edge: rgba(0, 0, 0, .55);   /* 가로 스크롤 줄의 '더 있음' 그림자 (S3) */
}
@media (min-width: 900px) {
  :root {
    --header-h: calc(49px + var(--safe-t)); /* 데스크톱 헤더는 기존 높이 유지 */
    --tabbar-h: 0px;                        /* 하단 탭바 없음 → 본문 하단 여백도 0 */
  }
}
/* 본문을 1100px 로 묶어 두면 24"+ 모니터에서 양옆이 통째로 비는데, 정작 표는
   그 1100px 안에서 넘쳐 페이지가 가로로 밀린다(가장 넓은 `/item` 검색 결과가 1,438px).
   이 앱의 본문은 산문이 아니라 표라서 가독 폭(65~75자) 상한을 지킬 이유가 없다.
   1240px 부터는 단계가 아니라 화면을 따라 늘린다 — 단계식이면 1,439px 처럼 어중간한
   폭에서 양옆만 120px 씩 비고 표는 그대로 넘치는 구간이 생긴다. 96px 은 좌우 여백 몫
   이고(스크롤바를 포함한 100vw 오차까지 흡수한다), 상한 1680px 은 그 위로 더 넓혀도
   표가 이미 다 들어가서 글줄만 길어지기 때문이다. */
@media (min-width: 1240px) { :root { --colw: min(1680px, calc(100vw - 96px)); } }

/* ── 2. 리셋 · 베이스 ──────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
/* 모바일 가로 오버플로우 안전망. clip 은 스크롤 박스를 만들지 않아 sticky 가 살아 있다.
   표는 `.tablewrap` 이 받아내므로 여기서 잘리는 것은 예상 못 한 오버플로뿐이다.
   ≥900px 에서는 풀어 준다 — 아래 `.tablewrap` 규칙 참조. */
html, body { overflow-x: clip; }
body {
  margin: 0; background: var(--bg); color: var(--text);
  font: 16px/1.55 var(--font);   /* 모바일 16px — iOS 입력 확대 방지의 기준선 */
  padding-bottom: calc(var(--tabbar-h) + var(--safe-b));  /* 하단 탭바 자리 */
}
@media (min-width: 900px) { body { font-size: 15px; } }
img, svg { max-width: 100%; }
img { height: auto; }
table { max-width: 100%; }

/* P4 — iOS Safari 는 뷰포트 폭이 아니라 실제 font-size 를 본다.
   16px 미만 입력에 포커스하면 페이지를 확대해 버리고 되돌아오지 않는다.
   ⚠️ 어떤 미디어쿼리에서도 이 값을 16px 아래로 내리지 말 것. */
input, select, textarea, button { font: inherit; font-size: 16px; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

h1 { font-size: 22px; margin: var(--sp-1) 0 var(--sp-2); }
h2 { font-size: 17px; margin: var(--sp-5) 0 var(--sp-2); }
a { color: var(--accent); }
.muted { color: var(--text-muted); }
.small { font-size: var(--fs-xs); }

/* ── 3. 앱 셸 ──────────────────────────────────────────────────────────── */
header {
  display: flex; align-items: center; gap: var(--sp-3);
  height: var(--header-h); padding: var(--safe-t) var(--sp-4) 0;
  background: var(--surface); border-bottom: 1px solid var(--border);
  position: sticky; top: 0; z-index: var(--z-header);
}
.brand { font-weight: 700; letter-spacing: .5px; color: var(--text); text-decoration: none; white-space: nowrap; }

/* 헤더 nav 는 데스크톱 전용 — 모바일 주 내비게이션은 하단 탭바가 맡는다 (DC-47 / U-1) */
header > nav { display: none; }
header > nav a { color: var(--text-muted); text-decoration: none; padding: var(--sp-1) 2px; }
header > nav a:hover { color: var(--text); }

main { max-width: min(var(--colw), 100%); margin: 0 auto; padding: var(--sp-4) var(--sp-4) var(--sp-6); }
footer { max-width: min(var(--colw), 100%); margin: 0 auto; padding: var(--sp-4); color: var(--text-muted); font-size: var(--fs-xs); }
/* `/pricegap` 은 열이 10개라 한 단계 더 넓게 쓴다 (DC-37).
   ≥1200px 로 한정한다 — 그보다 좁으면 `100%` 가 이겨서 어차피 효과가 없고(즉 지금까지도
   무해했다), 조건을 적어 두는 편이 '넓은 화면 전용 규칙'이라는 의도를 남긴다. */
@media (min-width: 1200px) {
  main.wide { max-width: min(calc(var(--colw) + 300px), 100%); }
}

/* 헤더 오버플로 메뉴 — <details> 라 JS 없이 열린다 (모바일 전용).
   데스크톱에서는 통째로 숨기고 같은 항목이 헤더에 직접 놓인다. */
.hmenu { position: relative; margin-left: auto; }
.hmenu > summary {
  list-style: none; cursor: pointer;
  width: var(--tap); height: var(--tap); margin-right: calc(var(--sp-3) * -1);
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--r-md); color: var(--text-muted); font-size: 20px; line-height: 1;
}
.hmenu > summary::-webkit-details-marker { display: none; }
.hmenu[open] > summary { background: var(--surface-2); color: var(--text); }
.hmenu-panel {
  position: absolute; right: 0; top: calc(100% + var(--sp-2)); z-index: var(--z-sheet);
  display: flex; flex-direction: column; align-items: stretch; gap: var(--sp-2);
  min-width: 210px; padding: var(--sp-3);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-md); box-shadow: var(--shadow-1);
}
.hmenu-panel > * { justify-content: flex-start; }

/* 하단 고정 탭바 (DC-47 / U-1) — 엄지 도달 범위. 모바일에서만 존재한다. */
.tabbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-tabbar);
  display: flex; background: var(--surface); border-top: 1px solid var(--border);
  padding-bottom: var(--safe-b);
}
.tabbar a {
  flex: 1 1 0; min-width: 0; height: var(--tabbar-h);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
  padding: var(--sp-1) 2px; text-align: center; text-decoration: none;
  color: var(--text-muted); font-size: var(--fs-xs); line-height: 1.15;
}
.tabbar a svg { width: 22px; height: 22px; flex: 0 0 auto; }
.tabbar a[aria-current="page"] { color: var(--accent); font-weight: 600; }

/* ── 4. 공용 컴포넌트 ──────────────────────────────────────────────────── */
.badge { font-size: var(--fs-xs); color: var(--pos); background: var(--pos-bg); border: 1px solid var(--pos-border); padding: 3px 9px; border-radius: var(--r-pill); white-space: nowrap; }

.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--sp-3); margin-top: var(--sp-4); }
/* 홈 카드 — **카드 전체가 링크**다. 제목 글자만 겨냥하게 두면 폰에서 실패한다.
   min-height 88px 은 --tap(44px) 의 두 배 — 한 손 엄지로 목표를 못 맞출 수 없는 크기다. */
.card {
  display: flex; align-items: flex-start; gap: var(--sp-3); min-height: 88px;
  padding: var(--sp-4); background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg); text-decoration: none; color: var(--text);
}
.card:hover { border-color: var(--accent); }
.card h2 { margin: 0 0 var(--sp-2); font-size: 16px; }
.card p { margin: 0; color: var(--text-muted); font-size: var(--fs-sm); }
.card .ct { min-width: 0; }
/* 아이콘은 하단 탭바와 같은 그림이다(_macros.navicon) — 그래서 색도 탭바처럼 흐리게 두고
   본문 글자와 경쟁시키지 않는다. 카드에 마우스를 올리면 테두리와 함께 살아난다. */
.card .ci { flex: none; display: inline-flex; color: var(--text-dim); }
.card .ci svg { width: 26px; height: 26px; }
.card:hover .ci { color: var(--accent); }

.filters { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: end; margin: var(--sp-3) 0 var(--sp-4); padding: var(--sp-3); background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md); }
.filters label { display: flex; flex-direction: column; gap: var(--sp-1); font-size: var(--fs-xs); color: var(--text-muted); }
.filters select, .filters input { background: var(--surface); color: var(--text); border: 1px solid var(--border-strong); border-radius: var(--r-sm); padding: var(--sp-2) var(--sp-3); min-width: 140px; max-width: 100%; }
/* iOS Safari 는 type=search 를 네이티브 알약으로 그리면서 높이·패딩을 무시한다 —
   그대로 두면 44px 탭 타깃이 안 나온다. 지우개(x) 버튼은 그대로 남는다. */
.filters input[type="search"] { -webkit-appearance: none; appearance: none; }
.filters button { background: var(--accent); color: var(--on-accent); border: 0; border-radius: var(--r-sm); padding: 9px 18px; cursor: pointer; font-weight: 600; }
.filters button:hover { background: var(--accent-hover); }

/* ── 조회 중 (P10, S5) ─────────────────────────────────────────────────────
   제출하고 서버가 돌아오기까지 화면은 **옛 결과 그대로**다. 표도 그대로, 버튼도 그대로라
   눌린 티가 안 나고, 그러면 사람이 한 번 더 누른다 — 2년 롤업 쿼리를 두 번 돌리는 값을
   치른다(허브 부하, 하드 인바리언트 1). 그래서 버튼을 잠그고 '조회 중…' 을 띄운다.
   ⚠️ 이 상태는 **JS 가 만든다**. JS 가 없으면 클래스가 안 붙고 버튼은 평소대로 동작한다
   — 기능이 아니라 피드백이라 없어도 조회는 된다(진행적 향상, U-4).
   회귀 방지: `test_loading_feedback_is_progressive`. */
button.busy { cursor: progress; background: var(--accent-hover); }
button.busy::before {
  content: ""; display: inline-block; width: 0.85em; height: 0.85em;
  margin-right: var(--sp-2); vertical-align: -0.08em; border-radius: 50%;
  /* 테두리는 currentColor 기준 — 버튼 글자색이 무엇이든 따라간다(테마 무관). */
  border: 2px solid currentColor; border-top-color: transparent;
  animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* 움직임을 줄여 달라고 한 사람에게는 돌리지 않는다 — 글자 '조회 중…' 만으로도 뜻은 전달된다. */
@media (prefers-reduced-motion: reduce) { button.busy::before { animation: none; } }
.filters label.chk { flex-direction: row; align-items: center; gap: var(--sp-2); padding-bottom: 9px; }
.filters label.chk input { min-width: 0; }
.filters .clear { font-size: var(--fs-xs); color: var(--text-muted); text-decoration: none; padding-bottom: var(--sp-3); }
.filters .clear:hover { color: var(--text); text-decoration: underline; }

/* ── 필터 접기 · 조건 요약 (S3) ────────────────────────────────────────────
   `.filterbox` 는 **토글 스위치일 뿐**이고 진짜 패널은 바로 다음 형제인 `.filters` 다
   (왜 <details> 안에 안 넣는지는 _macros.html 의 filterbox() 주석 — 요약하면 닫힌
   details 의 자식은 섀도 슬롯이 숨기고, 그걸 데스크톱에서 되돌리는 `::details-content`
   가 안 먹는 브라우저에서는 필터가 통째로 사라지기 때문이다).
   데스크톱은 스위치가 없는 셈 치므로 재설계 전 레이아웃 그대로다. */
.filterbox { display: none; }
/* 본문 안내 접기 (S5) — 같은 구조·같은 이유(_macros.html hintbox 주석).
   데스크톱에는 스위치가 없으므로 설명문이 재설계 전처럼 그냥 문단으로 보인다. */
.hintbox { display: none; }

/* mode_select 가 셀렉트와 칩을 함께 내보내면서 라벨을 감싸는 칸이 하나 더 생겼다.
   `.filters label` 이 하던 세로 배치를 그대로 물려받아야 데스크톱이 안 흔들린다. */
.filters .fld { display: flex; flex-direction: column; }
/* AND/OR 칩은 '모바일 + JS' 에서만 뜬다. JS 가 없으면 셀렉트가 유일한 컨트롤이고
   칩 안의 라디오는 disabled 라 제출에 끼지 않는다(mode= 가 두 번 실리는 사고 방지). */
.modechips { display: none; }

table { width: 100%; border-collapse: collapse; margin-top: var(--sp-2); font-size: var(--fs-md); }
th, td { text-align: left; padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--border); white-space: nowrap; }
th { color: var(--text-muted); font-weight: 600; background: var(--bg); position: sticky; top: var(--header-h); z-index: var(--z-sticky-th); }
tbody tr:hover { background: var(--surface-2); }
.r { text-align: right; font-variant-numeric: tabular-nums; }
th.sorted { color: var(--text); }   /* 현재 정렬 기준 컬럼 */
.empty { color: var(--text-muted); padding: var(--sp-5) 0; }

/* 기법 A — 열 우선순위 접기 (S2). 좁을수록 뒤쪽 열부터 접는다.
   ⚠️ `display:none` 이지 열 삭제가 아니다. 데스크톱 폭에서는 재설계 전과 열 구성이
   똑같아야 한다(회귀 방지: test_no_column_disappears). 접힌 값은 행의 상세 링크로
   전부 볼 수 있으므로, 상세 링크가 없는 표에는 이 기법을 쓰지 않는다(→ 기법 B).
   `table-cell` 로 되돌리는 이유: 기법 B 가 td 를 block 으로 만들기 때문에
   `display:revert` 로는 두 기법이 한 표에서 겹칠 때 결과가 갈린다. */
.p2, .p3, .p4 { display: none; }
@media (min-width: 600px)  { .p2 { display: table-cell; } }
@media (min-width: 900px)  { .p3 { display: table-cell; } }
@media (min-width: 1200px) { .p4 { display: table-cell; } }

/* 품목명만 줄바꿈 예외. 금액·수량·날짜는 쪼개지면 오히려 읽기 나쁘다(전역 nowrap 유지).
   min-width 를 주지 않으면 좁은 화면에서 한 글자씩 세로로 서는 열이 된다. */
.cell-name { white-space: normal; word-break: break-word; min-width: 8em; }

/* 기법 B — 카드 변환 (S2). *행 자체가 결론인* 표, 즉 상세로 갈 링크가 없어
   접으면 정보가 그냥 사라지는 표에 쓴다. 폰 세로(≤599px)에서만 전개한다 —
   600px 부터는 이 표들이 대개 5~9열이라 래퍼 스크롤로 충분하다.
   ⚠️ `.cards` 가 아니라 `.rowcards` 다. `.cards` 는 이미 첫 화면의 링크 카드 격자다. */
@media (max-width: 599px) {
  .rowcards, .rowcards tbody, .rowcards td { display: block; }
  .rowcards thead { display: none; }          /* 라벨은 각 td 가 스스로 달고 나온다 */
  .rowcards { border-collapse: separate; border-spacing: 0; }
  /* tr 을 flex 세로 열로 두는 이유는 `order` 를 쓰기 위해서다 — 카드 제목이 될 열이
     표에서는 세 번째 칸일 수 있는데(예: /discounts 의 '할인'), 데스크톱 열 순서를
     바꾸지 않고 카드에서만 맨 위로 올린다. */
  /* ⚠️ `flex-direction: column` 이 아니라 **줄바꿈하는 가로 flex** 다(S4). 칸은 기본이
     `flex: 1 1 100%` 라 결과는 세로 한 줄씩으로 같지만, `.bit` 몇 개를 한 줄에 나란히
     놓을 수 있다 — /pricegap 의 세금 3종처럼 짧은 값이 줄을 통째로 먹지 않게. */
  .rowcards tr {
    display: flex; flex-wrap: wrap; align-items: baseline;
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md);
    padding: var(--sp-2) var(--sp-3); margin-bottom: var(--sp-3);
  }
  .rowcards tr:hover { background: var(--surface); }   /* 행 hover 는 표일 때만 의미가 있다 */
  /* ⚠️ 셀을 flex 컨테이너로 만들면 안 된다. 셀 안의 `<strong>`·`<span class="muted">`
     같은 인라인 조각이 *각각* flex 항목이 돼 한 줄에 나란히 늘어서고(줄바꿈도 안 된다),
     `justify-content: space-between` 이 그것들을 셀 폭 끝까지 벌려 놓는다.
     정의목록처럼 — 라벨은 inline-block 으로 폭만 잡아 두고 값은 그냥 인라인으로 흐르게 둔다. */
  .rowcards td {
    display: block; flex: 1 1 100%; min-width: 0;
    padding: var(--sp-1) 0; border-bottom: 0;
    white-space: normal; text-align: left;
  }
  /* 라벨이 없는 칸(순번 등)은 값만 남긴다 — 빈 ::before 가 자리를 먹지 않게. */
  .rowcards td[data-label]::before {
    content: attr(data-label);
    /* min-width 라서 긴 라벨('최근 90일 판매')도 잘리지 않고 값을 오른쪽으로 밀 뿐이다. */
    display: inline-block; min-width: 7em; margin-right: var(--sp-2);
    color: var(--text-muted); font-size: var(--fs-xs);
  }
  /* 값은 라벨 오른쪽에 붙는다 — 표에서의 우측 정렬은 카드에서 의미가 없다. */
  .rowcards td.r { text-align: left; }
  /* 카드 제목 — 행에서 가장 먼저 읽어야 하는 값. 라벨을 떼고 굵게 올린다. */
  .rowcards td.cell-title { order: -1; font-weight: 600; font-size: var(--fs-md); padding-bottom: var(--sp-2); }
  .rowcards td.cell-title::before { content: none; }
  /* 카드에서 강조할 값 하나(할인제공액·금액 등). 라벨은 남기고 값만 키운다. */
  .rowcards td.cell-key { font-size: 16px; font-weight: 600; color: var(--text); }
  /* 접기 클래스는 카드에서 무효다 — 카드는 접는 대신 펼치는 표현이다. */
  .rowcards .p2, .rowcards .p3, .rowcards .p4 { display: block; }

  /* 순번 — 카드 맨 위, 제목보다 앞. 7em 라벨 자리를 먹지 않게 '#' 만 붙인다. */
  .rowcards td.cell-rank { order: -2; flex: 1 1 100%; padding-bottom: 0;
                           font-size: var(--fs-xs); color: var(--text-muted); }
  .rowcards td.cell-rank[data-label]::before { min-width: 0; margin-right: 2px; }

  /* 짧은 값 여러 개를 한 줄에 — 라벨을 값 위로 올려 가로 폭을 라벨이 안 먹게 한다. */
  .rowcards td.bit { flex: 1 1 30%; padding-top: var(--sp-2); }
  .rowcards td.bit[data-label]::before { display: block; min-width: 0; margin: 0 0 2px; }

  /* `.tri` 는 nowrap 3칸이라 카드 폭(360px 에서 ~245px)을 넘겨 페이지를 가로로 민다.
     마크업은 회귀 테스트가 고정하고 있으므로(trin 출력) **카드 안에서만** 세로로 세운다.
     라벨(12px)만으로는 값이 안 읽히니 값 글꼴은 오히려 키운다. */
  .rowcards .tri { flex-direction: column; align-items: stretch; gap: 1px; padding: 3px 7px; }
  .rowcards .tri .br { justify-content: space-between; gap: var(--sp-3); }
  .rowcards .tri .br em { font-size: var(--fs-md); }

  /* /pricegap 카드 — 표에서 폭을 잡아 두던 규칙을 카드에서는 푼다. */
  .rowcards.gap td.rank { width: auto; }
  .rowcards.gap td.cat { max-width: none; overflow: visible; }
  .rowcards.gap td.item .sub { color: var(--text-muted); font-weight: 400; }
  .rowcards.gap td.qty .sub { display: inline; margin-left: var(--sp-2); }
}

/* 좁은 화면에서 표가 넘칠 때만 가로 스크롤 박스. ⚠️ 스크롤 컨테이너가 되는 순간
   sticky 헤더의 기준이 페이지가 아니라 이 박스로 바뀌어, 전역 th{top:var(--header-h)}
   (= 페이지 헤더 높이) 가 헤더를 박스 안쪽으로 그만큼 밀어내 첫 행을 덮는다 → 여기선 0 으로. */
.tablewrap { overflow-x: auto; }
.tablewrap th { top: 0; }
/* ⚠️ 래퍼는 모바일에서만 스크롤 박스로 둔다. `overflow-x:auto` 인 박스는 세로로도
   스크롤 컨테이너가 되는데(overflow-y:visible 이 auto 로 계산된다) 높이가 내용과
   같아 세로로는 안 움직이므로, 그 안의 `th{position:sticky}` 가 통째로 죽는다.
   데스크톱은 재설계 전 그대로 — 표가 열 폭을 넘으면 페이지가 가로로 스크롤되고
   sticky th 는 페이지(헤더) 기준으로 붙는다. */
@media (min-width: 900px) {
  html, body { overflow-x: visible; }
  .tablewrap { overflow-x: visible; }
  .tablewrap th { top: var(--header-h); }
  /* 예외 — `/pricegap` 은 10열이라 데스크톱에서도 페이지째 밀리는 폭이다.
     DC-37 이 래퍼를 붙인 이유가 그것이라 그 동작을 그대로 지킨다. */
  main.wide .tablewrap { overflow-x: auto; }
  main.wide .tablewrap th { top: 0; }
}
/* 표가 통째로 들어가는 폭이면 `/pricegap` 도 래퍼를 풀어 페이지 sticky 를 살린다 */
@media (min-width: 1760px) {
  main.wide .tablewrap { overflow-x: visible; }
  main.wide .tablewrap th { top: var(--header-h); }
}

/* 데이터 기준(캐시 갱신 시각) — 평소엔 눈에 띄지 않게, 오래되면 경고색 */
.stamp { font-size: var(--fs-xs); color: var(--text-muted); border: 1px solid var(--border); background: var(--surface); padding: 5px 11px; border-radius: var(--r-pill); text-decoration: none; white-space: nowrap; }
.stamp:hover { color: var(--text); border-color: var(--accent); }
.stamp.stale { color: var(--neg); border-color: var(--neg-border); background: var(--neg-bg); }
td.warn, .warn { color: var(--neg); }
.warn.small { font-size: var(--fs-xs); }

.refreshbtn { font-size: var(--fs-xs); color: var(--warn); border: 1px solid var(--warn-border); background: var(--warn-bg); padding: 5px 11px; border-radius: var(--r-pill); text-decoration: none; white-space: nowrap; }
.refreshbtn:hover { background: var(--warn-bg-hover); }
.dlbtn { font-size: var(--fs-xs); color: var(--pos); border: 1px solid var(--pos-border); background: var(--pos-bg); padding: 5px 11px; border-radius: var(--r-pill); text-decoration: none; white-space: nowrap; margin-left: var(--sp-3); vertical-align: middle; }
.dlbtn:hover { background: var(--pos-bg-hover); }
.adminbtn { font-size: var(--fs-xs); color: var(--admin); border: 1px solid var(--admin-border); background: var(--admin-bg); padding: 5px 11px; border-radius: var(--r-pill); text-decoration: none; white-space: nowrap; }

/* 테마 전환 버튼 (DC-49). 기본은 숨김 — JS 가 없으면 자동 전환만 동작하고
   눌러도 아무 일 없는 죽은 컨트롤을 보여 주지 않는다. 머리말 스크립트가
   <html data-js> 를 세우면 그때 나타난다. */
.themebtn { display: none; align-items: center; gap: 5px; font-size: var(--fs-xs); color: var(--text-muted);
  border: 1px solid var(--border); background: var(--surface); padding: 5px 11px; border-radius: var(--r-pill);
  white-space: nowrap; cursor: pointer; font-family: inherit; }
.themebtn:hover { color: var(--text); border-color: var(--border-strong); }
[data-js] .themebtn { display: inline-flex; }
.hmenu-panel .themebtn { justify-content: center; }
.adminbtn:hover { background: var(--admin-bg-hover); }

/* 안내 상자. 안에 <code>db/14_price_gap.sql</code> 같은 공백 없는 긴 토막이 들어가는데,
   폰 폭에서는 그것 하나가 상자를 밀어내 페이지가 통째로 가로로 스크롤됐다(S4 §6).
   `anywhere` 라야 공백이 없는 낱말도 쪼갠다 — `break-word` 는 상자 폭 계산에서 빠진다. */
.notice { background: var(--info-bg); border: 1px solid var(--info-border); color: var(--info); padding: var(--sp-3); border-radius: var(--r-sm); margin: var(--sp-3) 0; overflow-wrap: anywhere; line-height: 1.6; }
.notice.bad { background: var(--neg-bg); border-color: var(--neg-border); color: var(--neg); }
/* 좋은 소식 상자. 1회용 초기 비번처럼 **성공했다는 사실 자체가 정보**인 경우에만 쓴다. */
.notice.ok { background: var(--pos-bg); border-color: var(--pos-border); color: var(--pos); }
/* 주의 상자 — 확정은 아니지만 사람이 대조해야 하는 일(리콜 '확인 필요', DC-65). */
.notice.warn { background: var(--warn-bg); border-color: var(--warn-border); color: var(--warn); }
/* 줄바꿈 규칙은 `.notice` 안에만 있었는데, <code> 는 본문 문단에도 들어간다 — /admin/access
   의 '저장 위치 <code>/var/lib/a-search/access.db</code>' 가 360px 에서 화면 밖으로
   68px 밀려 나가 경로 끝이 잘려 보였다(S5 실측). 상자를 가리지 말고 code 자체에 건다. */
code { background: var(--surface-2); border: 1px solid var(--border); padding: 1px 5px; border-radius: var(--r-sm); font-size: var(--fs-xs); overflow-wrap: anywhere; }

/* 기간 빠른 선택 (DC-35) — /sales · /discounts · /category 공용 */
.presets { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; margin: -6px 0 var(--sp-3); }
.presets .lbl { font-size: var(--fs-xs); color: var(--text-muted); margin-right: 2px; }
.presets a { font-size: var(--fs-xs); color: var(--text-muted); border: 1px solid var(--border); background: var(--surface); padding: var(--sp-1) var(--sp-3); border-radius: var(--r-pill); text-decoration: none; white-space: nowrap; }
.presets a:hover { color: var(--text); border-color: var(--border-strong); }
.presets a.on { color: var(--info); border-color: var(--info-border); background: var(--info-bg); }

/* 페이저 — 표 위·아래 같은 것을 둔다(500건이면 아래까지 내려가야 넘길 수 있으니). */
.pager { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
         gap: var(--sp-2); margin: var(--sp-3) 0; font-size: var(--fs-xs); color: var(--text-muted); }
.pager .range strong { color: var(--text); font-variant-numeric: tabular-nums; }
.pager .pgs { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1); }
.pager .pgs a, .pager .pgs .cur, .pager .pgs .off, .pager .pgs .tot {
  min-width: 28px; text-align: center; padding: var(--sp-1) 9px; border-radius: var(--r-sm);
  border: 1px solid var(--border); text-decoration: none; font-variant-numeric: tabular-nums;
}
.pager .pgs a { color: var(--text-muted); background: var(--surface); }
.pager .pgs a:hover { color: var(--text); border-color: var(--border-strong); }
.pager .pgs .cur { color: var(--info); background: var(--info-bg); border-color: var(--info-border); font-weight: 600; }
.pager .pgs .off { color: var(--text-disabled); border-color: transparent; }
.pager .pgs .gap { color: var(--text-disabled); border: 0; padding: 0 2px; }
/* '3 / 10' 의 **10**. 눌 수 없다는 점에서 .off 와 같아 보이지만 성격이 다르다 — 이건
   비활성 버튼이 아니라 '전체 몇 쪽인가' 라는 정보다. 비활성색(대비 2.3:1)으로 두면
   페이지가 몇 개인지가 사실상 안 읽힌다(S5 실측). 보조 라벨색으로 올린다. */
.pager .pgs .tot { color: var(--text-muted); border-color: transparent; }
/* 모바일 페이저의 '3 / 27'. 데스크톱에서는 번호 목록이 그 자리를 대신한다. */
.pager .pgs .pos { display: none; }

/* ── 5. 화면별 ─────────────────────────────────────────────────────────── */
/* 지점별 가격 차이 (DC-37) */
.lead { margin: 2px 0 var(--sp-3); }
.lead .tri { margin: 0 2px; }

/* 차이 유형 = 1차 분류라 탭으로. 필터 패널이 탭에 물려 한 덩어리로 보인다. */
.tabwrap { margin: var(--sp-3) 0 var(--sp-4); }
.tabs { display: flex; gap: 2px; overflow-x: auto; border-bottom: 1px solid var(--border); }
.tabs .tab {
  display: flex; align-items: center; gap: var(--sp-2); padding: 9px 15px; white-space: nowrap;
  color: var(--text-muted); text-decoration: none; font-size: var(--fs-sm);
  border: 1px solid transparent; border-bottom: 0; border-radius: 9px 9px 0 0; margin-bottom: -1px;
}
.tabs .tab:hover { color: var(--text); background: var(--surface-2); }
.tabs .tab.on { color: var(--text); font-weight: 600; background: var(--surface); border-color: var(--border); }
.tabs .tab .n { font-size: var(--fs-xs); color: var(--text-dim); font-variant-numeric: tabular-nums; }
.tabs .tab.on .n { color: var(--info); background: var(--info-bg); border: 1px solid var(--info-border); padding: 0 7px; border-radius: var(--r-pill); }
.filters.attached { margin: 0; border-top: 0; border-radius: 0 var(--r-md) var(--r-md) var(--r-md); }
.hint { color: var(--text-dim); font-weight: 400; }

/* 표기 규칙: 같은 값은 접고(uni), 갈리는 값만 지점 라벨과 함께 펼친다(tri).
   펼친 3칸은 **하나의 알약** 안에 넣는다 — 칸마다 테두리를 두면 표가 250px 넘게 불어난다. */
.uni { color: var(--text-dim); font-variant-numeric: tabular-nums; }
.tri {
  display: inline-flex; gap: 7px; white-space: nowrap; padding: 1px 7px;
  background: var(--surface); border: 1px solid var(--border); border-radius: 7px;
}
.tri .br { display: inline-flex; align-items: baseline; gap: 2px; font-size: var(--fs-xs); color: var(--text-dim); }
.tri .br em { font-style: normal; font-size: var(--fs-sm); color: var(--text); font-variant-numeric: tabular-nums; }
.tri .br.hi em { color: var(--neg); font-weight: 600; }
.tri .br.off { opacity: .45; }

/* 상세 화면의 '정규가격' 한 줄 (DC-42) — 표 위에 접힌 값 하나로 먼저 보여준다.
   .uni/.tri 는 표 안에서 쓰던 것보다 여기서는 조금 크게 읽혀야 한다. */
.pxline { display: flex; align-items: center; gap: var(--sp-2); margin: var(--sp-3) 0; flex-wrap: wrap; }
.pxline b { font-size: var(--fs-sm); color: var(--text-muted); font-weight: 600; }
.pxline .uni { font-size: 17px; color: var(--text); font-weight: 600; }

/* ── 시간대·객단가 /hours (DC-63) ──────────────────────────────────────────
   합계 행·가장 붐비는 시간만 살짝 띄운다. 매출 칸의 막대는 서버가 계산한 **폭(--w)만**
   인라인으로 받는다 — 색은 토큰이다(/exec 와 같은 규칙, 다크 테마가 따라온다). */
tr.total td { font-weight: 600; border-top: 1px solid var(--border-strong); }
tr.peak td.cell-title { color: var(--info); }
.hbarcell { position: relative; min-width: 9em; }
.hbar {
  position: absolute; right: var(--sp-2); bottom: 3px; height: 3px; width: calc(var(--w, 0%) * .9);
  background: var(--info); opacity: .55; border-radius: var(--r-pill); pointer-events: none;
}
@media (max-width: 599px) {
  /* 카드에서는 막대가 값 아래 한 줄로 선다 — 절대 위치가 카드 폭 계산을 흔들지 않게. */
  .rowcards td.hbarcell { flex: 1 1 100%; }
  .rowcards .hbar { position: static; display: block; order: 2; margin-top: var(--sp-1); width: var(--w, 0%); }
}

/* ── 상세 요약 스트립 (S4) ─────────────────────────────────────────────────
   `/item?itemnum=` 첫 화면에서 답부터 준다 — 정규가격·최근 실판매가·최근 판매일·
   취급 지점(+ 있을 때만 등록일). 아래 여섯 절을 다 펼치지 않아도 '지금 얼마'가 읽힌다.
   `.uni`/`.tri` 는 표 안보다 크게 — .pxline 과 같은 17px 급이다. */
.sumstrip {
  display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-5); align-items: center;
  margin: var(--sp-3) 0; padding: var(--sp-3) var(--sp-4);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md);
}
.sumrow { display: flex; align-items: baseline; gap: var(--sp-2); min-width: 0; }
.sumrow > b { font-size: var(--fs-xs); color: var(--text-muted); font-weight: 600; white-space: nowrap; }
.sumrow .uni, .sumrow .sv { font-size: 17px; color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }

/* 취급 지점 — '없다' 와 '있는데 중지' 는 매대 앞에서 완전히 다른 답이라 둘 다 남긴다. */
/* DC-60 — '그 실판매가가 세일가였나' 라벨. 값 바로 옆에 붙는 작은 알약이라
   .brtag 와 같은 모양을 쓰되 색만 다르다. 경고가 아니라 조건 표시다. */
.salemk {
  display: inline-block; margin-left: var(--sp-1); padding: 1px 6px;
  border: 1px solid var(--warn-border); border-radius: var(--r-pill);
  background: var(--warn-bg); color: var(--warn);
  font-size: var(--fs-xs); font-weight: 700; white-space: nowrap; vertical-align: middle;
}
.salemk.reg { border-color: var(--border); background: var(--surface-2);
              color: var(--text-muted); font-weight: 600; }

/* DC-64 — 리콜 공지와 연결된 품목(높음 판정). .salemk 와 같은 알약이되 **경고색**이다:
   조건 표시가 아니라 '이 품목을 확인하라' 는 신호다. 정부 공지로 가는 링크라 밑줄을 뺀다. */
.recallmk {
  display: inline-block; margin-left: var(--sp-1); padding: 1px 6px;
  border: 1px solid var(--neg-border); border-radius: var(--r-pill);
  background: var(--neg-bg); color: var(--neg);
  font-size: var(--fs-xs); font-weight: 700; white-space: nowrap; vertical-align: middle;
  text-decoration: none;
}
a.recallmk:hover { background: var(--neg-bg-hover); }
/* 리콜 등급 알약 — 높음(경고) · 중간(주의) · 참고(무채색) */
.lvl { display: inline-block; padding: 1px 7px; border-radius: var(--r-pill); border: 1px solid;
       font-size: var(--fs-xs); font-weight: 700; white-space: nowrap; }
.lvl.l3 { color: var(--neg); background: var(--neg-bg); border-color: var(--neg-border); }
.lvl.l2 { color: var(--warn); background: var(--warn-bg); border-color: var(--warn-border); }
.lvl.l1 { color: var(--text-muted); background: var(--surface-2); border-color: var(--border); font-weight: 600; }

/* DC-61 — 묶음 세일 표기 `$1.50 (2 for $2.99)` 의 괄호 부분. 개당가가 주값이고 괄호는
   그 근거라 한 단계 물러나되, 매대에서 그대로 읽는 문장이라 흐려지면 안 된다
   (--text-muted 는 bg 대비 5.7:1 — AA 통과). 줄바꿈만 막는다. */
.bnd { font-size: var(--fs-xs); font-weight: 600; color: var(--text-muted); white-space: nowrap; }

.brtags { display: inline-flex; gap: 5px; }
.brtag {
  font-size: var(--fs-xs); font-weight: 600; color: var(--pos); background: var(--pos-bg);
  border: 1px solid var(--pos-border); border-radius: 5px; padding: 0 6px;
}
.brtag.off { color: var(--text-dim); background: var(--surface-2); border-color: var(--border); text-decoration: line-through; }

/* ── 상세 아코디언 (S4) ────────────────────────────────────────────────────
   `.sect` 는 **제목 겸 스위치**일 뿐이고 본문은 바로 다음 형제인 `.sectbody` 다
   (닫힌 <details> 의 자식은 섀도 슬롯이 숨기고 되돌릴 방법이 없다 — DC-51).
   여기(=데스크톱 기본)에서는 삼각형만 지운 **그냥 <h3> 한 줄**이고, 본문은 형제라
   항상 보인다 → 재설계 전 화면과 같다. 알약·▾·접기는 전부 아래 ≤899px 블록에만 있다.
   회귀 방지: test_s4_accordion_is_mobile_only · test_item_sections_are_siblings. */
.sect > summary { list-style: none; }
.sect > summary::-webkit-details-marker { display: none; }   /* 구형 Safari 삼각형 */

table.gap td, table.gap th { vertical-align: top; }
table.gap .rank { width: 32px; font-variant-numeric: tabular-nums; }
table.gap .item .nm { font-weight: 600; }
table.gap .item .sz {
  font-size: var(--fs-xs); color: var(--info); background: var(--info-bg); border: 1px solid var(--info-border);
  border-radius: 5px; padding: 0 5px; margin-left: 5px; vertical-align: 1px;
}
table.gap .item .sub, table.gap .qty .sub { display: block; margin-top: 1px; }
table.gap .cat { max-width: 150px; overflow: hidden; text-overflow: ellipsis; }
table.gap .gapcol { color: var(--warn); font-variant-numeric: tabular-nums; font-weight: 600; }
table.gap .gapcol .pct { display: block; font-size: var(--fs-xs); color: var(--text-muted); font-weight: 400; }
table.gap .qty .tot { font-variant-numeric: tabular-nums; }
table.gap .flag { font-size: var(--fs-xs); color: var(--warn); border: 1px solid var(--warn-border); background: var(--warn-bg);
                  border-radius: 5px; padding: 0 5px; white-space: nowrap; }

.foot { margin-top: var(--sp-3); line-height: 1.7; }

/* ── 접속·행동 감시 /admin/access (DC-41) ── */
.cards.stats { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
.stat { padding: var(--sp-3) var(--sp-4); background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-lg); }
.stat b { display: block; font-size: 24px; font-variant-numeric: tabular-nums; line-height: 1.2; }
.stat span { font-size: var(--fs-xs); color: var(--text-muted); }
.stat.bad b { color: var(--neg); }
/* IP 옆 라벨 — 아는 자리인지 한눈에. 라벨이 없으면 = 바깥에서 들어온 것 */
.iplab { font-size: var(--fs-xs); color: var(--pos); background: var(--pos-bg); padding: 1px 6px; border-radius: var(--r-pill); margin-left: var(--sp-1); }
/* 조회 조건 한 칸. '검색어'만 밝게 띄운다 — 이 화면에서 제일 먼저 보고 싶은 값이다 */
.act { white-space: normal; max-width: 620px; }
.qbit { font-size: var(--fs-xs); color: var(--text-dim); border: 1px solid var(--border); border-radius: var(--r-sm); padding: 1px 6px; margin-right: 3px; white-space: nowrap; display: inline-block; }
.qbit em { font-style: normal; color: var(--text); margin-left: 5px; }
.qbit.hi { border-color: var(--info-border); background: var(--info-bg); color: var(--info); }
.qbit.hi em { color: var(--info); font-weight: 600; }
.meth { font-size: var(--fs-xs); color: var(--warn); border: 1px solid var(--warn-border); border-radius: 5px; padding: 0 5px; margin-right: 5px; }
.srcn { font-size: var(--fs-xs); color: var(--text-dim); border: 1px dashed var(--border-strong); border-radius: 5px; padding: 0 5px; }
em.q, a.q { font-style: normal; color: var(--accent); font-weight: 600; text-decoration: none; }
.logout { margin-top: var(--sp-6); }
.logout button { background: transparent; color: var(--text-muted); border: 1px solid var(--border-strong); border-radius: var(--r-sm); padding: 7px var(--sp-3); cursor: pointer; font-size: var(--fs-xs); }
.logout button:hover { color: var(--text); border-color: var(--text-muted); }

/* ── 로그인 /login + 헤더 계정 표시 (DC-55) ─────────────────────────────────
   색은 전부 토큰이다 — 다크 테마(DC-49)가 그대로 따라온다. */
.loginbox { max-width: 380px; margin: var(--sp-6) auto 0; }
.loginbox h1 { margin-bottom: var(--sp-2); }
.loginbox form { display: flex; flex-direction: column; gap: var(--sp-3); margin: var(--sp-5) 0 var(--sp-4); }
.loginbox label { display: flex; flex-direction: column; gap: 6px; font-size: var(--fs-sm); color: var(--text-muted); }
/* 16px 미만이면 iOS 사파리가 포커스할 때 화면을 확대한다(UI 규약). */
.loginbox input { font-size: 16px; min-height: var(--tap); padding: 10px var(--sp-3);
  border: 1px solid var(--border-strong); border-radius: var(--r-sm);
  background: var(--surface); color: var(--text); }
.loginbox button { min-height: var(--tap); font-size: var(--fs-md); cursor: pointer;
  border: 1px solid var(--accent); border-radius: var(--r-sm);
  background: var(--accent); color: var(--on-accent); }
.loginbox button[disabled] { opacity: .55; cursor: not-allowed; }

.mebox { display: inline-flex; align-items: center; gap: var(--sp-2); }
.mebox .me { font-size: var(--fs-xs); color: var(--text-muted); white-space: nowrap;
  max-width: 140px; overflow: hidden; text-overflow: ellipsis; }
.mebox button { font-size: var(--fs-xs); color: var(--text-muted); cursor: pointer;
  background: transparent; border: 1px solid var(--border-strong);
  border-radius: var(--r-pill); padding: 5px 11px; white-space: nowrap; }
.mebox button:hover { color: var(--text); border-color: var(--text-muted); }

/* ── 계정 관리 /admin/users + 본인 비밀번호 /password (DC-56) ───────────────
   계정 하나당 폼이 다섯이다. 전부 펼쳐 두면 계정 세 개만 있어도 화면이 벽이 되고,
   "지금 누구를 만지고 있는지" 가 사라진다 → 계정마다 `<details>` 스위치를 둔다.
   ⚠️ `<details>` 는 **스위치일 뿐**이고 본문(.ubody)은 바로 다음 형제다(DC-51).
      닫힌 details 의 자식은 섀도 슬롯이라 데스크톱에서 되살릴 방법이 없다.
   filterbox·sect 와 달리 이 스위치는 **모든 폭에서** 산다 — 데스크톱이라고 계정 20개의
   폼 100개를 한꺼번에 펼쳐 놓을 이유가 없다. */
.ubox { margin: var(--sp-3) 0 0; }
.ubox > summary {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2);
  min-height: var(--tap); padding: var(--sp-2) var(--sp-3); cursor: pointer;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md);
  list-style: none;
}
.ubox > summary::-webkit-details-marker { display: none; }   /* 구형 Safari 삼각형 */
/* 삼각형만으로는 '눌러서 여는 것'인지 모른다 — filterbox 와 같은 동사를 쓴다. */
.ubox > summary::after { content: "펼치기 ▾"; margin-left: auto; font-size: var(--fs-xs); color: var(--accent); font-weight: 600; }
.ubox[open] > summary::after { content: "접기 ▴"; }
.ubox > summary:hover { border-color: var(--border-strong); }
.ubox[open] > summary { border-bottom: 0; border-radius: var(--r-md) var(--r-md) 0 0; }
.ubox:not([open]) + .ubody { display: none; }
.ubody {
  margin: 0 0 var(--sp-4); padding: var(--sp-3);
  background: var(--surface); border: 1px solid var(--border); border-top: 0;
  border-radius: 0 0 var(--r-md) var(--r-md);
}
.ubody > p:first-child { margin-top: 0; }

.uname { font-weight: 600; overflow-wrap: anywhere; }
.ulabel { font-size: var(--fs-sm); color: var(--text-muted); overflow-wrap: anywhere; }
/* 상태 배지 — 색만으로 구분하지 않는다(글자가 곧 뜻이다). */
.ubadge { font-size: var(--fs-xs); border-radius: var(--r-pill); padding: 1px 8px; white-space: nowrap;
  color: var(--text-muted); background: var(--surface-2); border: 1px solid var(--border); }
.ubadge.adm { color: var(--admin); background: var(--admin-bg); border-color: var(--admin-border); }
.ubadge.off { color: var(--neg); background: var(--neg-bg); border-color: var(--neg-border); }
.ubadge.warn { color: var(--warn); background: var(--warn-bg); border-color: var(--warn-border); }
.ubadge.me { color: var(--pos); background: var(--pos-bg); border-color: var(--pos-border); }

/* 동작 폼들. 폼 하나 = 하는 일 하나다. 여럿이 한 줄에 섞이면 무엇을 누르는지 헷갈린다
   → 폼마다 자기 줄을 갖고 구분선으로 나눈다. */
.uacts { display: flex; flex-direction: column; }
.uact { display: flex; flex-wrap: wrap; align-items: end; gap: var(--sp-2) var(--sp-3);
  padding: var(--sp-3) 0; border-top: 1px solid var(--border); }
.uacts > .uact:first-child { border-top: 0; padding-top: var(--sp-2); }
.uacts > .uact:last-child { padding-bottom: 0; }
.uact label { display: flex; flex-direction: column; gap: 5px; flex: 1 1 220px; min-width: 0;
  font-size: var(--fs-sm); color: var(--text-muted); }
/* 16px 미만이면 iOS 사파리가 포커스할 때 화면을 확대한다(UI 규약). */
.uact input[type="text"] { font-size: 16px; min-height: var(--tap); padding: 8px var(--sp-3);
  border: 1px solid var(--border-strong); border-radius: var(--r-sm);
  background: var(--bg); color: var(--text); width: 100%; }
.uact button { min-height: var(--tap); padding: 0 var(--sp-4); cursor: pointer; white-space: nowrap;
  font-size: var(--fs-sm); color: var(--text); background: var(--surface-2);
  border: 1px solid var(--border-strong); border-radius: var(--r-sm); }
.uact button:hover { border-color: var(--text-muted); }
/* 되돌릴 수 없는 동작(삭제)은 다른 색으로. 눌러도 바로 지워지지는 않고 확인 화면을 거친다. */
.uact button.danger, .filters button.danger { color: var(--neg); background: var(--neg-bg); border-color: var(--neg-border); }
.uact button.danger:hover, .filters button.danger:hover { background: var(--neg-bg-hover); }
/* 서버 쪽 안전장치(마지막 관리자·자기 자신)를 화면에도 그대로 비춘다 — 누를 수 있는데
   눌러 보면 거절당하는 버튼은 사람을 두 번 헛수고시킨다(app/users.py 의 _guard_*). */
.uact button[disabled] { opacity: .5; cursor: not-allowed; }
.uact button[disabled]:hover { border-color: var(--border-strong); }

/* 1회용 초기 비밀번호. **이 응답에서 한 번만** 보이고 어디에도 남지 않으므로
   (리다이렉트로 넘기면 쿼리스트링이 접속 로그에 남는다 — DC-41) 옮겨 적기 좋게 크게. */
.onetime { font-size: var(--fs-md); font-weight: 700; letter-spacing: .06em; padding: 3px 9px;
  color: var(--text); background: var(--bg); border-color: var(--border-strong); }

/* ── 경영 요약 /exec (DC-54 · 꺾은선 전환 DC-57) ────────────────────────────
   차트 라이브러리가 없다(DC-46 ②). 좌표는 전부 **서버가 계산해서** 폴리라인 points 와
   인라인 `--x`/`--y`/`--y2` 로 넣는다 → JS 를 꺼도 그래프·체크박스·축 전환이 산다(U-4).
   ⚠️ 마크업에 들어가는 인라인 값은 **길이뿐**이다. 색은 한 톨도 넣지 않는다 —
   다크 테마(DC-49)가 따라오려면 색은 전부 여기서 var(--…) 로 와야 한다. */

/* 큰 숫자 한 덩이. 화면에서 24px 넘는 글자는 여기와 지점 카드 둘뿐이다. */
.exhero { margin-top: var(--sp-4); padding: var(--sp-4); background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-lg); }
.exhero p { margin: 0 0 var(--sp-2); }
.exhero p:last-child { margin-bottom: 0; }
.ex-lbl { font-size: var(--fs-sm); color: var(--text-muted); }
.ex-num { font-size: 34px; font-weight: 700; line-height: 1.15; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.ex-sub, .ex-sub2 { font-size: var(--fs-sm); }
/* ⛔ 접지 않는다 — 이 화면 숫자를 회계 매출로 읽는 것이 여기서 가장 비싼 사고다. */
.ex-warn { font-size: var(--fs-xs); color: var(--warn); background: var(--warn-bg); border: 1px solid var(--warn-border); border-radius: var(--r-sm); padding: var(--sp-2) var(--sp-3); }

/* 증감률. 값보다 **부호**가 먼저 읽혀야 해서 색과 굵기를 같이 준다. */
.delta { font-weight: 700; white-space: nowrap; font-variant-numeric: tabular-nums; }
.delta.up { color: var(--pos); }
.delta.down { color: var(--neg); }
/* 반올림해서 0.0% 인 값. 초록으로 칠하면 '늘었다'로 읽힌다 — 글자와 색을 맞춘다. */
.delta.flat { color: var(--text-muted); }

.exh2 { font-size: 16px; margin: var(--sp-5) 0 var(--sp-2); }

/* ── 월별 추이 — 점 찍힌 꺾은선 (DC-57) ──────────────────────────────────
   막대 두 벌(합계 + 지점 3행)을 선 한 장으로 합쳤다. 막대는 달마다 칸을 나눠 가져서
   25칸이면 한 칸이 손톱만 해지고, 0 에서 시작해야 정직하므로 ±8% 짜리 월 변화가
   안 보인다. 선은 칸을 안 나누고 기울기로 말한다.

   ⚠️ **JS 가 한 줄도 없다.** 계열 선택도, 세로축 전환도 전부 CSS 다:
     · 체크박스 4개가 그림 **앞**에 있고, `#id:not(:checked) ~ .exline …` 이 뒤쪽을 고른다.
       순서가 뒤집히면 선택이 통째로 죽는다 — `~` 는 뒤쪽 형제만 본다.
     · 합계는 지점의 약 3배라 한 축에 얹으면 지점 세 줄이 아래 45% 안에 눌린다. 그래서
       서버가 좌표를 **두 벌**(`.a` = 합계 포함 축 / `.b` = 지점만의 축) 그려 두고,
       합계 체크박스가 어느 쪽을 보일지 고른다. 점은 `--y` / `--y2` 둘을 다 들고 있다.
   인라인으로 들어가는 값은 여전히 **길이뿐**이다(`--x`·`--y`·`--y2`). 색은 한 톨도 없다. */
.exwrap { position: relative; margin-top: var(--sp-2); }
/* 체크박스 자체는 안 보이지만 **포커스는 살아 있어야** 한다 — display:none 이면
   키보드로 계열을 못 고른다. 라벨이 대신 눌리므로 마우스는 통과시킨다. */
.exchk { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }

.expick { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-bottom: var(--sp-2); }
.expick .pk { display: inline-flex; align-items: center; gap: var(--sp-2); min-height: var(--tap);
  padding: 0 var(--sp-3); border: 1px solid var(--border); border-radius: var(--r-pill);
  background: var(--surface); font-size: var(--fs-sm); font-weight: 600; cursor: pointer;
  -webkit-user-select: none; user-select: none; }
.expick .pk:hover { border-color: var(--border-strong); }
/* 범례 표식 = 선 한 토막 + 그 위의 점. 그래프에서 찾을 모양을 그대로 축소해 둔다. */
.mk { position: relative; display: inline-block; width: 20px; height: 10px; flex: none; }
.mk::before { content: ""; position: absolute; left: 0; top: 50%; width: 100%; height: 3px;
  margin-top: -1.5px; border-radius: 2px; background: currentColor; }
.mk::after { content: ""; position: absolute; left: 50%; top: 50%; width: 9px; height: 9px;
  margin: -4.5px 0 0 -4.5px; border-radius: 50%; background: currentColor; }

.exline { position: relative; display: grid; grid-template-columns: 58px 1fr;
  grid-template-rows: 250px auto; padding: var(--sp-4) var(--sp-3) var(--sp-2);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-lg); }
.exyax { grid-area: 1 / 1; position: relative; }
.exyax .tk { position: absolute; right: var(--sp-2); top: var(--y); transform: translateY(-50%);
  font-size: var(--fs-xs); color: var(--text-muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
.explot { grid-area: 1 / 2; position: relative; --dotsz: 8px; }
/* 선만 SVG 다. 상자에 맞춰 늘리므로(`preserveAspectRatio="none"`) 굵기는 non-scaling-stroke
   가 지켜 준다. 원·글자는 여기 넣으면 찌그러지므로 HTML 로 얹는다. */
.exsvg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.exsvg .gl { stroke: var(--sr-grid); stroke-width: 1; vector-effect: non-scaling-stroke; }
.exsvg .ln { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round;
  stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.dot { position: absolute; left: var(--x); top: var(--y);
  width: var(--dotsz); height: var(--dotsz);
  margin: calc(var(--dotsz) / -2) 0 0 calc(var(--dotsz) / -2);
  border-radius: 50%; background: currentColor; }
/* 진행 중인 달 — 속을 비우고 **선을 잇지 않는다**(좌표를 서버가 안 준다).
   선에서는 기울기가 곧 메시지라, 이틀치로 내려긋는 순간 '매출 급락' 이 된다. */
.dot.part { background: var(--surface); border: 2px solid currentColor; }
.exx { grid-area: 2 / 2; position: relative; height: 34px; }
.exx .xl { position: absolute; left: var(--x); top: var(--sp-1); transform: translateX(-50%);
  font-size: var(--fs-xs); color: var(--text-muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
.exx .xl.start { transform: none; }
.exx .xl.end { transform: translateX(-100%); }
.exx .xl b { display: block; font-weight: 600; color: var(--text-dim); }

/* 계열 색. 선은 `currentColor` 를 물고 있어서 `<g>` 에 색만 주면 따라온다.
   합계는 색이 아니라 **무게**로 구분한다(진한 회색 + 굵은 선) — 네 번째 색을 더 주면
   색만 넷이 다투고 어느 것도 눈에 안 든다. */
.pk.s-t, .exsvg g.s-t, .dot.s-t { color: var(--sr-t); }
.pk.s-n, .exsvg g.s-n, .dot.s-n, .exrow.s-n .mk { color: var(--sr-n); }
.pk.s-s, .exsvg g.s-s, .dot.s-s, .exrow.s-s .mk { color: var(--sr-s); }
.pk.s-d, .exsvg g.s-d, .dot.s-d, .exrow.s-d .mk { color: var(--sr-d); }
.exsvg g.s-t .ln { stroke-width: 3; }

/* 꺼진 계열 — 그림에서 빼고, 범례는 흐리게 + 취소선. 흐리게만 두면 '색이 옅은 계열'
   처럼 보인다. 글자는 --text-muted 까지만 내린다(AA 5.7:1) — 안 읽히면 못 되돌린다. */
#ex-s-t:not(:checked) ~ .exline .s-t,
#ex-s-n:not(:checked) ~ .exline .s-n,
#ex-s-s:not(:checked) ~ .exline .s-s,
#ex-s-d:not(:checked) ~ .exline .s-d { display: none; }
#ex-s-t:not(:checked) ~ .expick .pk.s-t,
#ex-s-n:not(:checked) ~ .expick .pk.s-n,
#ex-s-s:not(:checked) ~ .expick .pk.s-s,
#ex-s-d:not(:checked) ~ .expick .pk.s-d {
  color: var(--text-muted); text-decoration: line-through; background: var(--surface-2); }
/* 키보드로 옮겨 다닐 때 어디 있는지 보여야 한다 — 체크박스가 안 보이므로 라벨에 그린다. */
#ex-s-t:focus-visible ~ .expick .pk.s-t,
#ex-s-n:focus-visible ~ .expick .pk.s-n,
#ex-s-s:focus-visible ~ .expick .pk.s-s,
#ex-s-d:focus-visible ~ .expick .pk.s-d { outline: 2px solid var(--accent); outline-offset: 2px; }

/* 세로축 전환. 기본은 `.a`(합계 포함). 합계를 끄면 `.b`(지점만의 축)로 갈아탄다 —
   합계 계열에는 `.b` 좌표를 아예 안 그리므로 여기서 다시 숨길 일이 없다. */
.exline .ln.b, .exline .tk.b { display: none; }
#ex-s-t:not(:checked) ~ .exline .ln.a,
#ex-s-t:not(:checked) ~ .exline .tk.a { display: none; }
#ex-s-t:not(:checked) ~ .exline .ln.b { display: inline; }
#ex-s-t:not(:checked) ~ .exline .tk.b { display: block; }
#ex-s-t:not(:checked) ~ .exline .dot { top: var(--y2); }

.exlegend { margin-top: var(--sp-2); line-height: 1.7; }
.exlegend .swatch { display: inline-block; width: 10px; height: 10px; margin-right: var(--sp-1);
  vertical-align: -1px; border-radius: 50%; background: var(--surface); border: 2px solid var(--text-dim); }

/* 매장별 월 매출 — 추이는 위 한 장이 다 그린다. 여기는 **지난달 숫자판**이다.
   미니 막대를 다시 두지 않는 이유: 같은 그림이 두 장이면 눈이 어느 쪽을 믿을지 모르고,
   두 축이 어긋나는 순간 한쪽은 거짓말이 된다. 표식만 남겨 위 선과 색을 잇는다. */
.exrows { display: grid; gap: var(--sp-2); margin-top: var(--sp-2); }
.exrow { padding: var(--sp-3) var(--sp-4); background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-lg); }
.er-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-1) var(--sp-2); margin: 0; }
.er-name { flex: 1 1 auto; font-size: var(--fs-sm); color: var(--text-muted); }
.er-name .mk { vertical-align: -1px; margin-right: var(--sp-1); }
.er-num { font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; }
.er-sub { font-size: var(--fs-xs); }

/* 숫자 표 — 합계 열만 굵게, 조각 달은 행째로 흐리게. */
.extable td.exnet { font-weight: 700; }
.extable tbody tr.expart td { color: var(--text-dim); }

/* /recalls (DC-65) — 공지 1건 = 카드 1장, 등급별 묶음. 표였을 땐 판정 근거(UPC 나열)가
   1280px 에서 1733px 로 넘쳤고 좁은 '우리 품목' 칸에서 바코드가 숫자 중간에 끊겼다(운영 실측).
   왼쪽 띠 색이 우리 쪽 판정(높음·중간·참고), 알약이 정부 위험 등급이다 — 둘은 다른 축이다. */
.rc-alert p { margin: 0 0 var(--sp-1); }
.rc-alert ul { margin: var(--sp-1) 0; padding-left: 1.2em; }
.rc-group { margin-top: var(--sp-5); }
.rc-gh { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); margin: 0; font-size: var(--fs-md); }
.rc-gh .n { color: var(--text-muted); font-size: var(--fs-sm); font-weight: 600; }
.rc-gnote { margin: 2px 0 var(--sp-2); }
.rc-list { display: grid; gap: var(--sp-3); }
@media (min-width: 900px) { .rc-list { grid-template-columns: repeat(auto-fill, minmax(26rem, 1fr)); align-items: start; } }
.rc { min-width: 0; padding: var(--sp-3); background: var(--surface); border: 1px solid var(--border);
      border-left: 4px solid var(--border-strong); border-radius: var(--r-md); overflow-wrap: anywhere; }
.rc.l3 { border-left-color: var(--neg); }
.rc.l2 { border-left-color: var(--warn); }
.rc p { margin: 0 0 var(--sp-1); }
.rc-what { font-size: var(--fs-md); }
.rc-grade { display: inline-block; margin-left: var(--sp-1); padding: 1px 7px; border: 1px solid; border-radius: var(--r-pill);
            font-size: var(--fs-xs); font-weight: 700; white-space: nowrap; vertical-align: middle; }
.rc-grade.g1 { color: var(--neg); background: var(--neg-bg); border-color: var(--neg-border); }
.rc-grade.g2 { color: var(--warn); background: var(--warn-bg); border-color: var(--warn-border); }
.rc-grade.g3 { color: var(--text-muted); background: var(--surface-2); border-color: var(--border); }
.rc-title { font-size: var(--fs-sm); }
.rc-box { margin: var(--sp-2) 0; padding: var(--sp-2) var(--sp-3); background: var(--surface-2); border-radius: var(--r-sm); }
.rc-lbl { font-size: var(--fs-sm); font-weight: 700; }
.rc-items { margin: 0; padding: 0; list-style: none; }
.rc-items li { display: flex; flex-wrap: wrap; align-items: center; gap: 2px var(--sp-2);
               padding: var(--sp-1) 0; border-top: 1px solid var(--border); }
.rc-items li:first-child { border-top: 0; }
/* 품목 이름 전체가 누르는 자리 — 폰에서 손가락 하나 높이(44px). */
.rc-items a { display: flex; flex-direction: column; justify-content: center; flex: 1 1 100%;
              min-height: var(--tap); text-decoration: none; }
.rc-items .nm { font-weight: 700; text-decoration: underline; text-underline-offset: 2px; }
.rc-items .en { color: var(--text-muted); font-size: var(--fs-xs); }
.rc-items .how { color: var(--text-muted); font-size: var(--fs-xs); }
/* 바코드는 한 덩어리 — 숫자 중간에서 끊기면 매대 대조가 안 된다. */
code.upc { white-space: nowrap; overflow-wrap: normal; font-size: var(--fs-sm); }
.rc-upcs { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1); }
.rc .rc-find { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); margin-top: var(--sp-2); }
.rc-find a { display: inline-flex; align-items: center; min-height: var(--tap); padding: 0 var(--sp-3);
             border: 1px solid var(--border-strong); border-radius: var(--r-pill); background: var(--surface); text-decoration: none; }
.rc .rc-why { margin-top: var(--sp-2); }
/* 상세 화면 경고 상자 — .notice.bad 안의 목록 들여쓰기만 줄인다. 접지 않는다(.notice 규칙). */
.recallbox p { margin: 0 0 var(--sp-1); }
.recallbox ul { margin: var(--sp-1) 0; padding-left: 1.2em; }
.recallbox a { color: inherit; }

/* ── 6. 유틸 · 뷰포트별 조정 ───────────────────────────────────────────── */

/* 모바일(≤899px) — 헤더 축약 + 탭 타깃 44px (P5) */
@media (max-width: 899px) {
  .brand { margin-right: auto; }
  /* 알약 버튼 숨김·데이터 기준 축약은 1199px 까지 적용된다 — 아래 별도 블록 참조. */

  /* 컨트롤 성격의 요소만 44px 로. 본문 인라인 링크까지 키우면 글이 성겨진다. */
  header > nav a, .stamp, .dlbtn, .refreshbtn, .adminbtn, .themebtn, .presets a, .tabs .tab,
  .filters button, .logout button, .mebox button, .pager .pgs a, .pager .pgs .cur, .pager .pgs .off {
    min-height: var(--tap); display: inline-flex; align-items: center; justify-content: center;
  }
  .dlbtn { margin-left: 0; }

  /* 필터 입력이 360px 에서 화면을 밀어내지 않게. 접기·요약 칩은 S3. */
  .filters { gap: var(--sp-2) var(--sp-3); }
  .filters label { flex: 1 1 140px; min-width: 0; }
  /* 체크박스는 제외 — width:100% 를 먹으면 상자가 라벨 폭만큼 늘어나 가운데에 뜬다. */
  .filters select, .filters input:not([type="checkbox"]) { width: 100%; min-width: 0; }
  .filters label.chk, .filters button { flex: 1 1 100%; }
  /* 입력창도 탭 타깃이다 — 기본 높이가 select 39px · input 43px 라 미달이었다.
     체크박스는 상자 자체를 키우지 않고 label 을 44px 행으로 만들어 누를 곳을 넓힌다
     (label 클릭이 곧 토글이므로 기능은 같고 네이티브 컨트롤 모양은 유지된다). */
  .filters select, .filters input:not([type="checkbox"]) { min-height: var(--tap); }
  .filters label.chk { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); min-height: var(--tap); }
  .filters label.chk input { flex: none; }
  /* 체크박스 라벨의 부연 설명은 flex 아이템이라 같은 줄에서 눌려 두 칸으로 쪼개진다.
     (`/category` 의 '신규 등록(신상)만 보기 — 판매 기간 대신…') 통째로 다음 줄로 내린다. */
  .filters label.chk > .small { flex: 1 1 100%; }
  /* 헤더 로고도 링크다(→ '/'). 글자 높이 25px 뿐이라 누르기 어렵다. */
  .brand { display: flex; align-items: center; min-height: var(--tap); }
  /* 필터 안의 링크는 본문이 아니라 컨트롤이다 — /category 의 '전체 선택'·'해제'가 14px 였다. */
  .filters a { display: inline-flex; align-items: center; min-height: var(--tap); }
  /* 계정 관리(DC-56) — 동작 버튼은 폰에서 한 줄을 통째로 쓴다. 삭제가 다른 버튼 옆에
     나란히 붙으면 오탭 한 번이 곧 삭제 확인 화면이다. */
  .uact label, .uact button { flex: 1 1 100%; }
}

/* ── S3 필터·검색·페이저 — 모바일 전용(≤899px) ────────────────────────────
   여기 있는 것은 **한 줄도 빠짐없이 좁은 화면 전용**이다. 900px 로 넓히면 전부 꺼지고
   재설계 전 필터·프리셋·페이저가 그대로 돌아온다(회귀 방지: test_desktop_filters_untouched). */
@media (max-width: 899px) {
  /* ① 필터 접기 — 결과의 첫 행이 첫 화면에 들어오게 (P7).
     summary 자체가 44px 탭 타깃이고, 접혀 있어도 지금 조건이 칩으로 읽힌다. */
  .filterbox { display: block; margin: var(--sp-3) 0 0; }
  .filterbox > summary {
    display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1) var(--sp-2);
    min-height: var(--tap); padding: var(--sp-2) var(--sp-3); cursor: pointer;
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md);
    color: var(--text-muted); font-size: var(--fs-sm); list-style: none;
  }
  .filterbox > summary::-webkit-details-marker { display: none; }   /* 구형 Safari 삼각형 */
  /* 삼각형만으로는 '눌러서 여는 것'인지 모른다 — 동사를 준다. */
  .filterbox > summary::after { content: "펼치기 ▾"; margin-left: auto; color: var(--accent); font-weight: 600; }
  .filterbox[open] > summary::after { content: "접기 ▴"; }
  .filterbox > summary .fb-t { color: var(--text); font-weight: 600; }
  .filterbox > summary .fb-chips { display: flex; flex-wrap: wrap; gap: var(--sp-1); min-width: 0; }
  .fb-chip { background: var(--surface-2); border-radius: var(--r-pill);
             padding: 2px var(--sp-2); font-size: var(--fs-xs); white-space: nowrap; }
  /* 접혀 있으면 패널을 감춘다. 열려 있으면 summary 와 한 덩어리로 붙여 하나의 상자로 보인다. */
  .filterbox:not([open]) + .filters { display: none; }
  .filterbox[open] > summary { border-bottom: 0; border-radius: var(--r-md) var(--r-md) 0 0; }
  .filterbox[open] + .filters { margin-top: 0; border-radius: 0 0 var(--r-md) var(--r-md); }
  /* /pricegap 은 탭 바로 아래에 물려 있다 — 위쪽 모서리는 탭이 맡는다. */
  .tabwrap > .filterbox { margin-top: 0; }
  .tabwrap > .filterbox > summary { border-radius: 0 0 var(--r-md) var(--r-md); }
  .tabwrap > .filterbox[open] > summary { border-radius: 0; }

  /* ② 조회 버튼을 패널 바닥에 붙인다 — 필터가 길어도 늘 손 닿는 곳에 있다.
     탭바 위로 띄우지 않으면 둘이 겹쳐서 어느 쪽도 제대로 안 눌린다. */
  .filters button[type="submit"] {
    position: sticky; bottom: calc(var(--tabbar-h) + var(--safe-b) + var(--sp-2));
    z-index: 1; box-shadow: var(--shadow-1);
  }
  /* ③ 값을 바꿨는데 아직 조회를 안 누른 상태. 자동 제출을 끈 대가를 눈에 보이게 갚는다
     (라벨은 base.html 의 필터 스크립트가 '조건 변경됨 · 조회' 로 바꾼다). */
  .filters button.dirty { outline: 2px solid var(--warn); outline-offset: 2px; }

  /* ④ 기간 프리셋 — 줄바꿈해서 3줄이 되느니 가로로 한 줄. 세로 공간을 안 먹는다.
     끝단 그라디언트: 커버(local)는 내용과 함께 비켜나고 그림자(scroll)는 상자에 붙어
     있으므로, **더 스크롤할 게 남았을 때만** 그림자가 드러난다(Lea Verou). */
  .presets {
    flex-wrap: nowrap; overflow-x: auto; overscroll-behavior-x: contain;
    scroll-snap-type: x proximity; -webkit-overflow-scrolling: touch;
    scrollbar-width: none; padding-bottom: var(--sp-1);
    background:
      linear-gradient(to right, var(--bg), transparent) 0 0 / 20px 100% no-repeat local,
      linear-gradient(to left,  var(--bg), transparent) 100% 0 / 20px 100% no-repeat local,
      radial-gradient(farthest-side at 0 50%,    var(--fade-edge), transparent) 0 0 / 12px 100% no-repeat,
      radial-gradient(farthest-side at 100% 50%, var(--fade-edge), transparent) 100% 0 / 12px 100% no-repeat;
  }
  .presets::-webkit-scrollbar { display: none; }
  .presets .lbl, .presets a { flex: none; scroll-snap-align: start; }
  /* 지금 고른 기간이 가장자리에 반쯤 잘려 있으면 '어디에 있는지'를 못 읽는다. */
  .presets a.on { scroll-margin-inline: var(--sp-5); }

  /* ⑤ AND/OR — 셀렉트를 여는 대신 한 탭에 바꾼다. JS 가 붙었을 때만 맞바꾼다
     (JS 가 없으면 셀렉트가 그대로 보이고 칩은 계속 숨어 있다). */
  .filters .fld { flex: 1 1 140px; min-width: 0; }
  /* `.filters label` 의 `flex: 1 1 140px` 가 세로 방향 basis 로 새면 라벨이 140px 높이가 된다. */
  .filters .fld > label { flex: none; }
  [data-js] .filters .modeswitch { flex: 1 1 100%; }
  [data-js] .filters .modeswitch > label { display: none; }
  [data-js] .filters .modechips { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
  .modechips .cl { flex: 1 1 100%; color: var(--text-muted); font-size: var(--fs-xs); }
  .chipradio { position: relative; cursor: pointer; }
  /* 라디오 상자는 접근성 트리에는 남기고 화면에서만 치운다 (display:none 이면 포커스를 못 받는다).
     `.filters input` 계열 규칙이 140px 짜리 상자로 만들어 버리므로 같은 무게로 눌러야 한다. */
  .filters .chipradio input { position: absolute; width: 1px; height: 1px;
                              min-width: 0; min-height: 0; padding: 0; border: 0; opacity: 0; }
  .chipradio > span { display: inline-flex; align-items: center; justify-content: center;
                      min-height: var(--tap); padding: 0 var(--sp-4); white-space: nowrap;
                      border: 1px solid var(--border-strong); border-radius: var(--r-pill);
                      background: var(--surface); color: var(--text); }
  .filters .chipradio input:checked + span { color: var(--info); background: var(--info-bg);
                                             border-color: var(--info); font-weight: 600; }
  .filters .chipradio input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px; }

  /* ⑥ 페이저 — 번호 나열 대신 '‹ 이전 · 3 / 27 · 다음 ›'. 큰 타깃 둘이면 충분하다.
     번호 링크는 지우는 게 아니라 접는다 — 데스크톱에서는 그대로 나온다. */
  .pager .range { flex: 1 1 100%; }
  .pager .pgs { flex-wrap: nowrap; width: 100%; gap: var(--sp-2); }
  .pager .pgs .num { display: none; }
  .pager .pgs .pos { display: inline-flex; align-items: center; justify-content: center;
                     flex: 1 1 auto; min-height: var(--tap);
                     color: var(--text-muted); font-variant-numeric: tabular-nums; }
  .pager .pgs .pos b { color: var(--text); }
  .pager .pgs .nav { flex: 0 0 auto; min-width: 88px; }
}

/* ── S4 상세 아코디언 · 요약 스트립 — 모바일 전용(≤899px) ────────────────────
   S3 블록과 같은 규칙이다: 여기 있는 것은 **한 줄도 빠짐없이 좁은 화면 전용**이고,
   900px 로 넓히면 전부 꺼져 여섯 절이 재설계 전처럼 전부 펼쳐진 채로 돌아온다. */
@media (max-width: 899px) {
  /* 요약 스트립을 키/값 카드로. 값은 오른쪽 끝에 붙어 세로로 눈이 한 줄에 떨어진다. */
  .sumstrip { gap: 0; padding: var(--sp-2) var(--sp-3); }
  .sumrow {
    flex: 1 1 100%; flex-wrap: wrap; align-items: center; justify-content: space-between;
    gap: var(--sp-1) var(--sp-2); padding: var(--sp-2) 0; min-height: 34px;
  }
  .sumrow + .sumrow { border-top: 1px dashed var(--border); }

  /* 절 제목이 곧 스위치다. 44px 알약이라 손가락으로 누를 곳이 분명하고,
     삼각형만으로는 '눌러서 여는 것'인지 모르므로 filterbox 와 같은 동사를 준다. */
  .sect { margin: var(--sp-4) 0 0; }
  .sect > summary {
    display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1) var(--sp-2);
    min-height: var(--tap); padding: var(--sp-2) var(--sp-3); cursor: pointer;
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md);
  }
  /* flex 컨테이너 안에서는 h3 의 기본 위·아래 여백이 접히지 않아 알약이 두 배로 부푼다. */
  .sect > summary > h3 { margin: 0; font-size: 15px; font-weight: 600; }
  .sect > summary::after {
    content: "펼치기 ▾"; margin-left: auto; color: var(--accent);
    font-weight: 600; font-size: var(--fs-xs);
  }
  .sect[open] > summary::after { content: "접기 ▴"; }
  /* 본문은 형제라 여기서만 접힌다. 열렸을 때 위아래로 숨 쉴 자리를 준다 —
     카드(.rowcards tr)가 이미 테두리를 갖고 있어 상자를 한 겹 더 두르지 않는다. */
  .sect:not([open]) + .sectbody { display: none; }
  .sect[open] + .sectbody { padding: var(--sp-2) 0 var(--sp-3); }
  /* 절 안의 첫 <h3>·표는 제목 알약 바로 아래라 위 여백이 두 번 붙는다. */
  .sect + .sectbody > .tablewrap > table { margin-top: 0; }

  /* ── 공용 컴포넌트 마감 (S4 §6) ── */
  /* '⤓ 엑셀'은 <h1> 안의 인라인 알약이라 제목 끝에 매달려 두 줄로 찢어졌다.
     블록으로 바꿔 제목 아래 **자기 줄**을 준다 — 44px 타깃도 그때 온전해진다. */
  h1 > .dlbtn { display: flex; width: fit-content; margin-top: var(--sp-2); }

  /* 라벨성 배지들. 카드 안에서 1px 세로 패딩이면 글자가 테두리에 붙어 읽힌다. */
  .iplab, .meth, .srcn, .qbit { padding-top: 2px; padding-bottom: 2px; line-height: 1.5; }

  /* 실행(비조회) 버튼은 탭바 위에 띄우지 않는다 — 오탭이 곧 운영 POS 부하다.
     암호 칸에서 한 칸 떼고, 대신 확실히 큰 타깃으로. S4 §5 / admin.html 주석 참조. */
  .filters.danger button[type="submit"] {
    position: static; box-shadow: none;
    margin-top: var(--sp-4); min-height: 52px;
  }
}

/* ── S5 본문 안내 접기 — 모바일 전용(≤899px) ───────────────────────────────
   (로딩 피드백 `button.busy` 는 폭과 무관해서 레이어 4 에 있다 — 데스크톱 쿼리도 느리다.)
   S3·S4 블록과 같은 규칙: 여기 있는 것은 전부 좁은 화면 전용이고, 900px 로 넓히면
   설명문이 그냥 문단으로 돌아온다(회귀 방지: test_intro_copy_folds_on_mobile_only). */
@media (max-width: 899px) {
  /* 스위치는 필터 요약(.filterbox)과 **같은 알약**이다 — 한 화면에 두 종류의
     '펼치기' 가 다르게 생기면 그것부터 읽어야 한다. 규칙을 나눠 쓰지 않고 묶는다. */
  .hintbox { display: block; margin: var(--sp-3) 0 0; }
  .hintbox > summary {
    display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1) var(--sp-2);
    min-height: var(--tap); padding: var(--sp-2) var(--sp-3); cursor: pointer;
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md);
    color: var(--text-muted); font-size: var(--fs-sm); list-style: none;
  }
  .hintbox > summary::-webkit-details-marker { display: none; }   /* 구형 Safari 삼각형 */
  .hintbox > summary::after { content: "펼치기 ▾"; margin-left: auto; color: var(--accent); font-weight: 600; }
  .hintbox[open] > summary::after { content: "접기 ▴"; }
  .hintbox > summary .fb-t { color: var(--text); font-weight: 600; }
  /* 본문은 형제라 여기서만 접힌다 (닫힌 details 의 자식은 되살릴 수 없다 — DC-51). */
  .hintbox:not([open]) + .hintbody { display: none; }
  .hintbox[open] > summary { border-bottom: 0; border-radius: var(--r-md) var(--r-md) 0 0; }
  .hintbox[open] + .hintbody {
    padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border); border-top: 0;
    border-radius: 0 0 var(--r-md) var(--r-md);
  }
  .hintbody > p:first-child { margin-top: 0; }
  .hintbody > p:last-child { margin-bottom: 0; }
}

/* 폰 세로(≤599px) — 표 헤더 sticky 해제.
   좁은 화면에서는 고정 헤더가 세로 공간을 먹는 손해가 값 비교의 이득보다 크다. */
@media (max-width: 599px) {
  th { position: static; }
  main { padding-left: var(--sp-3); padding-right: var(--sp-3); }
  .cards { grid-template-columns: 1fr; }
  .cards.stats { grid-template-columns: repeat(2, 1fr); }
}

/* 헤더에 알약 버튼(읽기 전용·테마·캐시 갱신·접속 감시)까지 펼치려면 1200px 은 있어야 한다.
   그 아래에서는 ⋯ 오버플로 메뉴에 그대로 둔다 — 헤더에는 로고·nav·데이터 기준만 남는다.
   ⚠️ 900px 에 다 펼치려던 것이 S1 의 실수였다. 실측(1400px, 전 버튼 노출): 로고 84 + nav 420
   + 배지 64 + 데이터기준 233 + 테마 60 + 갱신 68 + 감시 68 + 간격 108 + 여백 40 = 1,145px.
   900px 에 애초에 안 들어간다(245px 초과). 넘친 자리에서
   nav 가 `flex:1` 로 찌그러지며 한글 링크가 글자 단위로 쪼개져(min-content ≈ 0) nav 높이가
   147px 이 되고 49px 헤더 밖으로 삐져나갔다. 실측은 DC-49 참조. */
@media (max-width: 1199px) {
  /* 자식 선택자여야 한다 — 오버플로 메뉴 안의 같은 항목까지 숨으면 안 된다. */
  header > .badge, header > .refreshbtn, header > .adminbtn,
  header > .mebox { display: none; }   /* 오버플로 메뉴 안으로 들어간다 */
  /* .themebtn 은 [data-js] 로 켜지므로(0,2,0) 같은 무게로 눌러야 한다. */
  [data-js] header > .themebtn { display: none; }
  .stamp .lbl { display: none; }   /* '데이터 기준' 라벨은 title 에만 남는다 */
  .stamp { overflow: hidden; text-overflow: ellipsis; }
  /* 단, '오래됨' 경고만은 절대 안 잘린다. 헤더가 붐빌 때 하필 이것이 줄어들면
     빨간 알약에 '⚠' 도 날짜도 반만 남아 무슨 경고인지 못 읽는다 (S4 §6). */
  .stamp.stale { flex: none; overflow: visible; }
}

/* 데스크톱(≥900px) — 재설계 전 헤더/본문 레이아웃을 그대로 복원한다.
   S1 은 데스크톱 UX 를 바꾸는 세션이 아니다. */
@media (min-width: 900px) {
  header { gap: 18px; padding: var(--safe-t) 20px 0; }
  /* `flex: 1` 을 주면 안 된다 — 남는 공간을 먹는 건 좋지만 모자랄 때 내용보다
     좁아지는 것까지 허용된다. 여백은 auto 마진으로 밀어 내고 링크는 절대 안 쪼갠다. */
  header > nav { display: flex; gap: 14px; margin-right: auto; }
  header > nav a { white-space: nowrap; }
  .hmenu { margin-left: 0; }       /* 남는 공간은 위 nav 가 이미 밀어 냈다 */
  .tabbar { display: none; }
  main { padding: var(--sp-5) 20px var(--sp-6); }   /* 하단 60px 은 푸터 위 죽은 공간이었다 */
  footer { padding: 20px; }
}

@media (min-width: 1200px) {
  .hmenu { display: none; }        /* 이제 알약이 헤더에 직접 놓인다 */
}

/* ── 경영 요약 — 모바일 전용(≤899px) (DC-54 → DC-57) ───────────────────────
   막대 시절에는 25칸이 폰에서 12px 밑으로 내려가 **먼 달을 접어야** 했다. 선은 칸을
   나누지 않으므로 25개월이 그대로 들어간다 — 접는 규칙이 통째로 사라졌다.
   좁아지는 만큼 점과 눈금 자리만 줄인다. */
@media (max-width: 899px) {
  .ex-num { font-size: 28px; }
  .exline { grid-template-columns: 50px 1fr; grid-template-rows: 190px auto; padding: var(--sp-3) var(--sp-2) var(--sp-1); }
  .explot { --dotsz: 6px; }
  .dot.part { border-width: 1.5px; }
  .expick .pk { flex: 1 1 auto; justify-content: center; }
}
