/* ===================================================================
   디자인 기준 공통 스타일
   기준 문서: 구현/TravelTraderDMC/UI-디자인-기준.md
   레퍼런스 구현: templates/trade/quoteDetail.html

   ※ 점진 적용 구조
     - 이 파일은 head.html 에서 전역 로드되지만, 모든 규칙이 `.ds` 로 스코프돼 있어
       `.ds` 를 붙이지 않은 화면에는 아무 영향이 없다.
     - 화면을 이 기준으로 전환하려면 그 화면의 `#kt_content` 에 `ds` 클래스를 추가한다.
         <div class="content d-flex flex-column flex-column-fluid ds" id="kt_content">
     - 전환 후에는 반드시 getComputedStyle 로 최종값을 확인한다.
       (Metronic/common.css 의 !important 에 막혀 조용히 무시되는 경우가 많다)

   ※ root font-size 는 **16px** 이다(2026-08-13 사용자 결정 — 아래 0-0 절).
     .9rem=14.4px / .95rem=15.2px / 1rem=16px / 1.15rem=18.4px
   🕘 예전에는 13px 이었다. 옛 주석·문서의 「13px 기준」 환산값은 더 이상 맞지 않는다.
   =================================================================== */

/* ===================================================================
   0-0) 글자 크기 — **root font-size 는 여기서만 정한다** (2026-08-13 사용자 결정)

   > *"업계 특성상 나이가 좀 있으신 분들이 이용하시는 경우가 많은데 잘 보이지 않는다"*

   🔥 **Metronic 번들이 `html{font-size:13px!important}` 로 root 를 못박고 있다.**
      이 파일이 번들보다 **나중에** 로드되므로(head.html 참고) 같은 특이도 + `!important`
      로 이긴다. ⚠️ **이 파일을 번들보다 위로 옮기면 글자 크기가 통째로 되돌아간다.**

   🔑 이 저장소의 크기·여백·배지·버튼은 **거의 전부 `rem`** 이라 root 하나로 함께 커진다.
      → **개별 화면에서 `font-size` 를 px 로 박지 말 것.** 박는 순간 그 자리만 안 커진다.

   🔑 **사용자별 3단계 토글**(헤더 ▸ 사용자 메뉴). 서버가 `fontScale` 쿠키를 읽어
      `<html data-font-scale>` 에 찍고 여기서 크기를 정한다 — 다크모드와 같은 구조다.
        normal(기본) 16px · large 18px · xlarge 20px
        ⚠️ **모바일(≤767.98px)은 12 / 13 / 14px** 이다 — 아래 블록의 실제 값이다.
           🕘 이 자리에 「각 1px 작게」라고 적혀 있었으나 **사실이 아니었다**(4·5·6px 작다).
              2026-09-01 에 실제 값으로 고쳤다 — 되돌리지 말 것.
   ⚠️ 값을 더 올릴 때는 **열이 많은 표의 카드 전환 임계값(6-4 절)도 함께** 올려야 한다.
      안 그러면 넓은 화면에서도 표가 카드로 바뀐다.
   =================================================================== */
html                          { font-size: 16px !important; }
html[data-font-scale="large"]  { font-size: 18px !important; }
html[data-font-scale="xlarge"] { font-size: 20px !important; }
@media (max-width: 767.98px) {
    html                          { font-size: 12px !important; }
    html[data-font-scale="large"]  { font-size: 13px !important; }
    html[data-font-scale="xlarge"] { font-size: 14px !important; }
}

/* 📱 **폰 브라우저의 글자 자동 확대는 번들이 이미 막아 두었다 — 여기서 다시 걸지 말 것** (2026-08-18).

   🔬 실측(계산된 값으로 확인): `style.bundle.css` 가 **`body` 와 `html` 둘 다**에
      `text-size-adjust: 100%` 를 걸고 있다 — 리부트의 `body {…}` 와, 그 아래
      `html { font-family: sans-serif; text-size-adjust: 100% }` 두 규칙이다.
      우리 규칙을 CSSOM 에서 지워도 `html` 의 계산값이 **100% 그대로**이고,
      **번들의 `html` 규칙까지** 지워야 비로소 **`auto`** 로 떨어진다 → 우리 두 줄은 **완전한 중복**이었다.
      🕘 2026-08-17 에 「모바일 글자가 크다」의 처방으로 넣었는데 실기기에서 **화면이 전혀 바뀌지 않았다.**
         그때는 원인을 못 밝히고 남겨 뒀는데, 이유가 이것이다.
   ⚠️ **`text-size-adjust` 는 상속되지 않는다** — `html` 과 `body` 는 따로 봐야 한다(위 실험에서
      번들 `html` 규칙만 지웠을 때 `html`=auto / `body`=100% 로 **갈렸다**).
      그래서 「번들은 `body` 에만 걸었을 테니 `html` 은 우리가」라는 추측이 그럴듯해 보이지만 **틀렸다.**
   🔥 같은 가설로 이 규칙을 다시 넣지 말 것. 글자 크기는 위 3단계 토글(root)이 정한다. */

/* 🔥 **`body` 도 함께 눌러야 한다** (2026-08-13 — 이걸 빠뜨려 **절반만** 커졌다).
   Metronic 번들의 선택자는 `body, html { font-size: 13px !important }` **둘 다**이다.
   위에서 `html` 만 덮었더니 **`body` 는 13px 인 채로 남았고**, 크기 클래스가 없는 텍스트
   (=데이터 값 · 표 셀 · 미리보기 본문)는 body 를 상속하므로 **전혀 커지지 않았다.**
   🔬 실측: 상품 상세의 라벨(`.form-label`, 1.05rem)은 16.8px 인데 바로 아래 **값은 13px**
      이었다 — 라벨이 값보다 큰 뒤집힌 화면이다. 사용자가 세 번에 걸쳐 지적한
      「데이터 폰트가 너무 작다」가 전부 이것이다.
   ⚠️ **`1rem` 이어야 한다.** px 로 적으면 위 3단계 토글을 따라가지 않는다.
   ⚠️ 이 한 줄이 세 앱 전 화면의 본문 크기를 13px→root 로 올린다 — 되돌리면 다시 절반만 커진다. */
body { font-size: 1rem !important; }

/* 🔤 **한글은 낱말 중간에서 줄을 바꾸지 않는다 — 문서 전체 한 줄** (2026-09-17 여행사 1회차 ① 처방)
   🔬 CSS 기본 `word-break: normal` 은 **한글을 글자 단위로 끊는다** — 1회차에서 공지 요약「일|부」·
      문의 답변「걸|릴까요」·계약 항목명「(2박 3|일)」·알림 창「승|인 대기」 같은 줄이 **16개 시나리오**에서
      나왔다. 원인은 화면이 아니라 **기본값 하나**라서 화면마다 `keep-all` 을 덧바르면 다음 화면에서 또 난다.
   ✅ `keep-all` 이면 **띄어쓰기에서만** 줄이 바뀐다(영문은 원래 그렇다 — 영문에는 아무 변화가 없다).
      짝인 `overflow-wrap: break-word` 는 **공백 없는 긴 덩어리(URL·계좌·이메일)가 한 줄보다 길 때만** 자른다.
   🔥 **짝을 `anywhere` 로 바꾸지 말 것** — `anywhere` 는 min-content 를 한 글자로 내려 **영문 표 머리·버튼이
      좁은 칸에서 `D e p a r t u r e` 로 분해된다**(관리자 `Trade.css` 첨부 일정표 절에 같은 금지가 있다).
      `break-word` 는 min-content 를 건드리지 않는다.
      📌 **문서 표 글 칸(`keep-all`+`anywhere`)과 `word-break: break-word` 를 쓰던 자리(= normal+anywhere)는
         자기 규칙을 그대로 쓴다** — 그 자리들은 `keep-all` + `anywhere` 로 옮겨 min-content 를 종전과 같게 뒀다.
   🔬 실측(적용 전 → 후, 세 앱 25화면 × 다섯 폭, 가로 스크롤·상자 넘침·한글 낱말 중간 끊김을 셈):
      여행사 공지 목록 360 **11 → 0** · 계약 상세 360 **24 → 0**(영) · 관리자 계약 상세 360 **79 → 0** ·
      **가로 스크롤과 상자 넘침은 25화면 전부 전과 같다**(늘어난 곳 0).
   ✨ `text-wrap-style: pretty` — 문단 끝줄에 **한 낱말만 남는 것**(「주세요.」「used.」)을 브라우저가 앞줄에서
      한 낱말을 끌어와 막는다. 줄 수·상자 폭은 바꾸지 않는다(끝 몇 줄의 끊는 자리만 고른다).
      ⚠️ 지원하지 않는 브라우저는 이 선언만 무시한다(깨지지 않는다).
   🔥 **`text-wrap: pretty`(단축형)로 적지 말 것** — 단축형은 `text-wrap-mode` 까지 `wrap` 으로 다시 써서
      `white-space: nowrap` 을 **상속으로** 받던 자식이 풀릴 수 있다. */
body {
    word-break: keep-all;
    overflow-wrap: break-word;
    text-wrap-style: pretty;
}
/* 번들이 `word-break: normal` / `break-word` 를 **직접** 박아 body 상속이 닿지 않는 자리 —
   말풍선(`.tooltip`·`.popover`) · 알림창 본문(`.swal2-html-container`) · 유틸리티 `.text-break`.
   🔑 `.text-break` 는 번들이 `!important` 라 같은 무게로 덮는다. min-content 는 종전(`anywhere`)과 같다. */
.tooltip, .popover { word-break: keep-all; }
.swal2-popup .swal2-html-container { word-break: keep-all; overflow-wrap: anywhere; }
.text-break { word-break: keep-all !important; overflow-wrap: anywhere !important; }

/* 🔤 **`.ds-break-anywhere`** — 「낱말이 아닌 문자열」을 그리는 **표 칸**에만 주는 예외
   (2026-09-24 · 관리자 「미참조 첨부」(`/setting/attach/orphan`) 파일명 열에서 드러났다)

   🔬 **증상**: 구분자(`-`·`_`·공백)가 하나도 없는 **80자 한글 파일명** 한 건이 표를 옆으로 늘렸다.
      실측(감싼 요소 1200px): 표 **1902px** · 가로 넘침 **702px** · 파일명 열 **1176px**.
      그 한 건만 빼면 표 1200px · 넘침 0 · 열 336px 이고, **행 높이는 60px 그대로**였다
      (= 줄바꿈이 **아예 일어나지 않았다**). 992px 에서는 넘침 910px · 360·768px 은 카드형이라 0 이다.
   🔑 **원인은 min-content 다.** 위 0-0 절의 짝 `keep-all` + `break-word` 에서 **`break-word` 는
      min-content 계산에 반영되지 않는다** → `table-layout: auto` 인 표는 「안 끊기는 가장 긴 덩어리」
      만큼 **열을 늘려** 버리고, 그래서 칸이 좁아질 일이 없어 `break-word` 는 **끝내 발동하지 않는다.**
   ✅ **`anywhere` 는 min-content 에 반영되는 유일한 값**이라 열이 먼저 좁아지고 그제서야 칸 안에서
      줄이 바뀐다. `keep-all` 과 짝이라 한국어는 낱말째 넘기고 **그래도 안 들어갈 때만** 글자에서 끊는다.
      📌 같은 처방의 선례가 이미 둘 있다 — `menuLevel/Customer.css` 의 `.cs-doc-body table td`
         (2026-09-20 E2-21) · 문서 표 글 칸(`screen-tokens.md` 「문서를 담는 판」 절).
   🔴 **전역(`body`)의 짝을 `anywhere` 로 바꾸지 말 것** — 바로 위 0-0 절에 이유가 있다(영문 표 머리·
      버튼이 좁은 칸에서 `D e p a r t u r e` 로 분해된다). 그래서 **칸 단위 예외 클래스**로 둔다.
   🎯 **붙일 자리는 「사용자가 올린 파일명」처럼 길이를 통제할 수 없고 낱말이 아닌 문자열**이다.
      🔥 **항목명·제목처럼 사람이 읽는 글에는 붙이지 말 것** — 그 칸까지 min-content 가 한 글자가 되어
         `table-layout: auto` 의 열 폭 배분이 통째로 달라진다(넓은 화면에서 글 칸만 눌린다). */
.ds-break-anywhere {
    word-break: keep-all;
    overflow-wrap: anywhere;
}

/* ===================================================================
   0-0-1) ♿ **본문 바로가기** — 키보드로 사이드바를 지나지 않고 본문에 닿는 길
          (2026-09-18 여행사 3회차 I1-66)

   🔬 **왜** — 탭 초점 순서가 DOM 순서(사이드바 → 헤더 → 본문)이고 초점 대상이 **33개**라,
      키보드만 쓰는 사람은 화면마다 사이드바를 통째로 지나야 본문에 닿았다.
      🔑 **고칠 것은 순서가 아니라 「건너뛸 길이 없다」는 것**이다 — 순서를 바꾸면 사이드바가
      본문 뒤로 밀려 이번에는 메뉴에 닿기 어려워진다(문제를 옮기는 것일 뿐이다).

   🔴 **`display:none`·`visibility:hidden` 으로 숨기지 말 것** — 그러면 초점을 받을 수 없어
      링크가 아예 없는 것과 같고 낭독기도 못 읽는다. 이 저장소가 이미 쓰는
      **`.visually-hidden` 방식(절대 위치 + `clip`)** 으로 **화면에서만** 걷고 `:focus` 에서 되살린다.
   🔑 **마크업은 `layout/defaultLayout.html` 한 곳**이다(`body` 의 첫 자식이어야 첫 탭 대상이 된다).
      그 레이아웃 하나가 **사이드바와 헤더를 가진 화면 전부**를 덮는다(실측 2026-09-18:
      여행사 34 · 랜드사 47 · 관리자 55). `asideLayout`·`fullWidthLayout` 은 로그인·가입·설치 화면이라
      **사이드바 자체가 없어 건너뛸 것이 없다** — 거기에는 넣지 않는다.
   🎨 **바탕을 `--ds-primary` 로 칠하지 않는다** — 그 위에 올릴 「흰 글자」 토큰이 이 저장소에 없고
      0절이 hex·흰색 리터럴을 금지한다. 판 색 위 파란 글자 + 파란 테두리면 두 테마 모두 대비가 선다.
   =================================================================== */
.ds-skip-link {
    position: absolute;
    top: 0;
    left: 0;
    z-index: 1080;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}
.ds-skip-link:focus {
    width: auto;
    height: auto;
    padding: var(--ds-sp-2) var(--ds-sp-4);
    margin: var(--ds-sp-2);
    overflow: visible;
    clip: auto;
    background-color: var(--ds-surface);
    color: var(--ds-primary);
    border: 2px solid var(--ds-primary);
    border-radius: .475rem;
    font-size: var(--ds-font-body);
    font-weight: 600;
    text-decoration: none;
}
/* 🔑 건너뛴 **자리**에는 초점 테두리를 그리지 않는다 — 목표가 본문 영역 전체라 큰 네모가 화면을 가른다.
   🔥 그렇다고 마크업의 `tabindex="-1"` 을 빼지 말 것 — 그것이 없으면 초점이 실제로 옮겨지지 않는다. */
#kt_content_container:focus,
#kt_content:focus { outline: none; }

/* ===================================================================
   0-1) 본문 글꼴 — **Noto Sans KR** (2026-08-17 사용자 결정 — 시안 N6)

   🔑 실제 본문 글꼴은 `common.css` 맨 앞의 `body, button, input, select, textarea`
      규칙이 !important 로 정한다. 여기서는 그 규칙이 닿지 않는 두 곳만 맡는다.
        ① sans-serif 토큰 — **라이트 번들은 이미 Noto Sans KR 이지만 다크 번들은 아직 Poppins 다.**
           그 토큰을 쓰는 자리(`.tooltip`·`.popover`)가 다크에서만 다른 글꼴이 되는 것을 막는다.
        ② `html` 요소 — 다크 번들이 `html, body` 에 Poppins 를 박아 두었는데
           common.css 는 body 만 덮는다.
   🔥 **벤더 번들(style.bundle.css·style.dark.bundle.css)를 고쳐서 해결하지 말 것** — 갱신하면 덮인다.
   🔥 이 파일은 번들보다 **나중에** 로드돼야 한다(head.html) — 위로 옮기면 토큰 교정이 다시 진다.
   📌 글꼴 파일은 head.html 의 구글 폰트 링크가 받는다.
      자체 호스팅하지 않은 이유(글리프 전량이 앱당 3.36MB)도 그 주석에 적었다.
   =================================================================== */
:root { --bs-font-sans-serif: 'Noto Sans KR', sans-serif; }
html   { font-family: var(--bs-font-sans-serif) !important; }

/* ===================================================================
   0) 색 토큰 — **색은 여기서만 정한다** (2026-07-21 확정)

   규칙 3줄. 이것만 지키면 다크모드가 자동으로 따라온다.
     ① CSS 에 hex 를 쓰지 않는다.            ❌ color: #181c32;
     ② var() 에 **폴백을 쓰지 않는다.**      ❌ var(--bs-gray-900, #181c32);
     ③ 역할 토큰만 쓴다.                     ✅ color: var(--ds-text-strong);

   ⚠️ **`--kt-*` 는 존재하지 않는 이름이다.** Metronic 8.2 는 `--bs-*` 를 쓴다.
      우리 CSS 250곳(design-system 60 · Trade 132 · TravelProduct 58)이 `--kt-*` 를 참조하고
      있는데 **전부 폴백값으로 굳어 있었다** — 라이트 값이 다크모드에도 그대로 나와
      카드 제목이 통째로 안 보였다(대비 1.02). 폴백이 있어서 "잘 되는 것처럼" 보였고
      그래서 아무도 몰랐다. → **폴백 금지가 핵심이다.** 이름이 틀리면 즉시 색이 사라져 발각된다.

   ※ 다크모드 판별자(`data-theme` 등)는 **필요 없다.** 서버가 `theme` 쿠키로
     `style.dark.bundle.css` 로 갈아끼우고, 그 번들이 `--bs-gray-*` 를 **반전된 값으로**
     재정의한다(`gray-900`: #181C32 → #FFFFFF). 우리 토큰이 그걸 참조하면 자동 전환된다.

   ※ 등급 상한 — **400·500 은 텍스트에 쓰지 않는다.** 카드 배경 대비 실측(WCAG):
       계조   라이트(#FFF)  다크(#1E1E2D)
        400      2.03          1.83     ← 텍스트 금지
        500      2.45          2.33     ← 텍스트 금지 (예전 라벨 색. **라이트에서도 미달이었다**)
        600      3.79          3.24     ← 보조만. 작은 글씨(fs-8)에는 쓰지 말 것
        700      6.01          5.34     ← 본문 가능
        800      9.91         10.47
        900     16.79         16.42
     계산 근거: 구현/TravelTraderDMC/UI-디자인-기준.md
   =================================================================== */
/* ⚠️ 토큰은 `.ds` 가 아니라 **`:root`** 에 둔다.
   변수 정의는 그 자체로 아무 스타일도 바꾸지 않으므로 `.ds` 스코프 원칙과 충돌하지 않는다.
   `.ds` 에 두면 **`.ds` 바깥(헤더·브레드크럼·모달)에서 `var(--ds-*)` 가 해석되지 않아**
   그 자리만 색이 사라진다. 실제로 아래 `.text-muted` 교정이 그 바깥에도 걸린다. */
:root {
    /* 글자 — 라벨은 500 에서 **700 으로 승격**했다(위 표 참고) */
    --ds-text-strong: var(--bs-gray-900);   /* 카드 제목 · 금액 머리글 */
    --ds-text:        var(--bs-gray-800);   /* 본문 값 */
    --ds-text-label:  var(--bs-gray-700);   /* 항목명 · 표 헤더 */
    --ds-text-faint:  var(--bs-gray-600);   /* 단위 · 경과일 등 보조. 본문 금지 */
    /* 면 */
    --ds-longtext-lines: 4;   /* 📜 긴 자유 텍스트 상자가 접혀 있을 때 보이는 줄 수 (22절 `.ds-longtext-box`).
                                 🔑 **두 규격(`.ds-longtext-box` · `.ds-field-v.ds-longtext`)이 이 값 하나를 본다** —
                                    한쪽만 고쳐 갈리는 일을 막으려고 토큰으로 뺐다(2026-09-22 사장님 지시).
                                 ⚠️ 단위 없는 **숫자**다(`calc(1.45em * var(…))` 의 곱수) — px 를 넣지 말 것. */
    --ds-line:        var(--bs-gray-300);   /* 구분선 · 테두리 */
    --ds-line-soft:   var(--bs-gray-200);   /* 더 옅은 선 (구 --kt-border-color) */
    --ds-line-strong: var(--bs-gray-400);   /* 진한 선. **글자에는 쓰지 말 것**(대비 2.0) */
    --ds-fill:        var(--bs-gray-100);   /* 옅은 배경 */
    /* 의미색 — 번들이 테마별로 정의한다 */
    --ds-primary:     var(--bs-primary);
    --ds-danger:      var(--bs-danger);
    --ds-success:     var(--bs-success);
    --ds-warning:     var(--bs-warning);
    /* 의미색 옅은 배경 (배지·배너 바탕) */
    --ds-primary-soft: var(--bs-light-primary);
    --ds-danger-soft:  var(--bs-light-danger);
    --ds-success-soft: var(--bs-light-success);
    --ds-warning-soft: var(--bs-light-warning);
    --ds-info-soft:    var(--bs-light-info);

    /* 카드 표면 — ⚠️ **이것만 `--bs-*` 로 못 받는다.**
       Metronic 은 카드 배경을 변수가 아니라 `.card{background-color:#fff}` 리터럴로 넣고,
       다크 번들이 같은 선택자를 `#1e1e2d` 로 덮어쓴다. 즉 담아 갈 변수가 아예 없다.
       → 여기서만 **테마 판별자**를 쓴다. `fragments/head.html` 이 이미
         `document.documentElement.setAttribute("data-theme", theme)` 를 하고 있다(새로 만들 필요 없음).
       ※ 글자·선 색은 `--bs-gray-*` 가 테마별로 반전되므로 판별자가 **필요 없다** — 표면만 예외다. */
    --ds-surface: #FFFFFF;

    /* 🌫 **뒤를 덮는 막(scrim)** — 모달·드로워·미리보기·확인창이 **이 토큰 하나**를 쓴다
       (2026-08-18 사용자 지적 — 폰 실기기 다크: *"모달이나 드로워가 뜰 때 바깥 암전 레이어가 없는 것처럼 보여"*).
       🔥 **막을 화면마다 따로 칠하지 말 것.** 정리 직전 실측(앱 하나 기준): 같은 `rgba(20,25,35,.4)` 가
          **드로워 · 미리보기(`.pv-scrim`) · 이력(`.hist-scrim`) · 날짜 오버레이 네 곳**에 각각 박혀
          있었고, 모달(`.modal-backdrop`)과 확인창(SweetAlert2)만 **벤더 기본**(검정 .3/.4)이라
          **여섯 벌**이었다 → 다크 대응이 한 곳에만 걸리고 나머지는 그대로 남는 일이 생긴다.
       🔴 **표면(`--ds-surface`)과 같은 이유로 여기서만 테마 판별자를 쓴다** — 담아 갈 `--bs-*` 가 없다.
          ⚠️ 그렇다고 **다른 색을 테마별로 가르지 말 것**(0 절 원칙은 그대로다).
       🔬 세기는 **막 뒤 배경의 흰 글자가 얼마나 물러나는가**로 정했다(다크에서 눈이 실제로 재는 것):
            .40(종전) → 판 대비 **5.76** — 판 안 본문(16.4)과 같은 급이라 **뒤가 안 죽는다**
            **.60(채택)** → **2.86** 으로 물러나되, 막 뒤 배경 대비 **3.48** 이라
                            **뒤가 무엇인지는 그대로 읽힌다**(사용자가 건 조건이다)
            .75 → 1.58 — 뒤를 알아볼 수 없다. **여기까지 올리지 말 것.** */
    --ds-scrim: rgba(20, 25, 35, .40);

    /* 🪟 떠 있는 판(모달·드로워)의 **테두리**. 라이트는 **없음**(그림자로 충분하다),
       다크에서만 선을 준다 — 🔑 **다크에서 그림자는 검정 위 검정이라 보이지 않는다.**
       🔥 **`--ds-scrim` 과 짝이다. 하나만 되돌리지 말 것** — 이유는 10절에 실측과 함께 적었다. */
    --ds-panel-edge: transparent;
}
html[data-theme="dark"] {
    --ds-surface:    #1E1E2D;
    --ds-scrim:      rgba(0, 0, 0, .60);
    /* gray-400 — 실측: 막 뒤 배경 대비 **2.23** / 판 대비 1.83. 한 단계 낮추면(gray-300) 1.60 이라
       폰 화면에서 경계로 안 읽힌다. 🔥 hex 를 직접 쓰지 말 것(0 절). */
    --ds-panel-edge: var(--ds-line-strong);
}

/* ===== Metronic 유틸리티 교정 — **양쪽 테마 모두 대비 미달이라 그대로 둘 수 없다** =====
   실측(카드 배경 대비):
     `.text-muted`     라이트 #A1A5B7 = 2.45  /  다크 #565674 = **2.33**
     `.text-gray-500`  동일            `.text-gray-400` 2.03 / 1.83
   전부 3:1 미만이다. 오늘 확정한 "400·500 은 글자에 쓰지 않는다" 규칙과 정면으로 어긋난다.
   → 이 세 유틸리티만 **한 단계 진한 토큰**으로 되돌린다(gray-600).

   ⚠️ `.ds` 로 스코프하지 않는다. 브레드크럼·푸터·모달 등 `.ds` 바깥에서도 쓰이고,
      거기서도 똑같이 안 보였다.
   ⚠️ 특이도는 Metronic 과 같은 (0,1,0) 이지만 **이 파일이 번들보다 나중에 로드**돼서 이긴다
      (`head.html` 순서). 순서가 바뀌면 조용히 무효가 되므로 옮기지 말 것.
   🕘 `Mypage.css` 가 이 문제를 **자기 화면에서만** 손으로 고치고 있었다(다크에서 `.text-muted`
      를 밝은 값으로 덮어씀). 그 국소 패치는 이 규칙으로 대체됐다. */
.text-muted,
.text-gray-500,
.text-gray-400 { color: var(--ds-text-faint) !important; }

/* ===================================================================
   0-2) 타이포 토큰 — **크기·굵기는 여기서만 정한다** (2026-07-21 확정)

   색 토큰과 **같은 역할 축**이다. "이 자리는 title" 하나로 색과 크기가 같이 정해진다.

   실사 근거: 템플릿 73개의 `fs-*`/`fw-*` 사용 빈도(다수결).
     · 카드 제목  fs-5 + fw-bolder  57/85 (67%)
     · 본문       클래스 없음(=1rem) 91%
     · 항목명     클래스 없음        69%   · 표 헤더 100%
     · 버튼·입력  크기 지정 0%       (기본 그대로가 이미 통일돼 있다)

   ※ root font-size **16px** 기준 환산 (Metronic 스케일)
     fs-4 1.25rem=20.0px · fs-5 1.15rem=18.4px · fs-6 1.075rem=17.2px
     기본 1rem=16px · fs-7 .95rem=15.2px · fs-8 .85rem=13.6px · fs-9 .75rem=12.0px
   ※ 굵기: `fw-normal`=400 · **`fw-bold`=500** · `fw-bolder`=600
     ⚠️ `fw-bold` 가 700 이 아니라 **500** 이다. 눈으로 맞추지 말고 이 값을 볼 것.

   🚫 **`fs-1`·`fs-2`·`fs-3` 금지** — 정의가 `calc(1.3rem + .6vw)` 라 **창 폭에 따라 크기가 변한다.**
      제목에 쓰면 리사이즈할 때마다 글자가 흔들리고 다른 카드와 안 맞는다.
      `h1`~`h3` 태그의 기본값도 같은 calc 이다 → **`h3.card-title` 에는 `fs-5` 를 반드시 함께** 줄 것.
   🚫 **`fs-9` 금지** — 본문 대비 너무 작다(16px root 에서도 12px). 배지에 16곳 쓰이고 있다(정리 대상).
   =================================================================== */
/* ⚠️ 색 토큰과 **같은 이유로 `:root`** 다 — `.ds` 에 두면 `.ds` 바깥(모달·드로어·헤더)에서
   `var(--ds-font-*)` 가 해석되지 않아 **글자가 조용히 본문 크기로 렌더된다.**
   실제로 `.ds-item-title` 을 만들자마자 이 함정을 밟았다(13px/400 으로 나왔다). */
:root {
    /* 🕘 한때 목록 카드 제목만 1.35rem(17.6px) 이었다. 2026-07-21 사용자 결정으로
       **카드 헤더 제목과 같은 15px 로 합쳤다** — 토큰이 하나 줄었다.
       (정산 대상 카드는 13.7px 이었어서 셋이 제각각이었다: 13.7 / 15 / 17.6) */
    --ds-font-lead:   1.25rem;   /* fs-4 · 결론 금액, 페이지 최상위 강조 */
    --ds-font-title:  1.15rem;   /* fs-5 · **카드 제목** (다수결) */
    --ds-font-strong: 1.075rem;  /* fs-6 · 카드 안 소제목, 중간 단계 금액 */
    --ds-font-body:   1rem;      /* 본문 값 · 항목명 · 표 · 버튼 · 입력 (다수결) */
    --ds-font-sub:    .85rem;    /* fs-8 · 보조. **꼭 필요할 때만** (아래 참고) */

    --ds-weight-title:  600;     /* fw-bolder */
    --ds-weight-strong: 500;     /* fw-bold — 700 이 아니다 */
    --ds-weight-body:   400;
    /* 항목명(라벨) — **값(400)과 한눈에 갈려야** 한다 (2026-08-15 사용자 지시:
       *"항목명은 데이터와 구분하기 위해서 볼드 처리해"*).
       ⚠️ `fw-bold` 는 500 이라 값과 거의 구별되지 않는다 — `fw-bolder`(600)와 같은 값을 쓴다.
       🔑 카드 제목과 같은 600 이지만 **크기와 색이 달라**(1rem / gray-700) 층이 섞이지 않는다.
       🔥 **이 토큰을 `.ds-field-v`(데이터)에 다시 쓰지 말 것** — 2026-08-17 까지 값과 항목명이
          **둘 다 이 토큰**을 써서 상세 화면이 통째로 굵었다(아래 「세 층」 절 참고). */
    --ds-weight-label:  600;

    /* ── 📐 상세 화면 **항목 그리드의 세 층** (2026-08-17 사용자 확정) ──────────────────
       > *"항목명만 bold 처리하고 데이터는 bold 처리하지 마. 항목명, 데이터, 보충 설명을
       >  **사이즈·굵기·컬러 모든 걸 동원해서** 확실하게 차이 나도록. 데이터가 항목명보다
       >  중요하지만 **데이터보다는 항목명이 먼저 눈에 들어와야** 해. 그래야 원하는 항목을 바로 찾지."*

       읽는 순서는 **항목명 → 데이터 → 설명**이다. 세 축을 서로 반대로 걸어 순서를 만든다:
         항목명  .875rem   / 600 / gray-700 + 자간 .03em   ← **굵고 위에 있어** 먼저 걸린다
         데이터  1.0625rem / 400 / gray-900 + 폭 고정 숫자 ← **크고 진해서** 그다음
         설명    .8rem     / 400 / gray-600               ← **흐려서** 물러난다

       🔥 **데이터 크기를 더 키우지 말 것** — 시안 대조에서 확인했다. 1.125rem 을 넘기면
          **크기가 굵기를 이겨** 데이터가 항목명보다 먼저 잡히고, 위 지시가 그대로 뒤집힌다.
       🔥 **데이터 굵기를 600 으로 되돌리지 말 것.** 그것이 2026-08-17 이전 상태였고,
          항목명과 **같은 굵기**라 화면이 온통 굵어 둘이 구분되지 않았다(이 작업의 발단이다).
       🔥 **설명 색을 더 낮추지 말 것**(gray-600 이 바닥이다) — 더 가려야 하면 **크기·여백**으로 가른다.
       ⚠️ **px 로 박지 말 것** — 글자 크기 3단계 토글(0-0 절)이 root 기준 `rem` 으로 동작한다.

       🔴 **지표 카드(16절 `.ds-kpi`)·통계·차트는 이 규칙의 대상이 아니다. 거기는 정반대다.**
          그쪽은 **값이 주인공**이라 값이 크고 굵고(1.25rem/700) 라벨이 작고 연하다(.95rem/gray-600).
          🔑 **왜 반대인가** — 이 세 층은 **「여러 항목 중 원하는 것을 찾는」** 화면의 규칙이다.
             항목이 10~20개 늘어선 상세 그리드에서는 **먼저 이름을 훑어 자리를 찾고** 그다음 값을 읽는다.
             지표 카드는 **항목이 3~5개로 이미 정해져 있고 물음도 하나**라, 찾을 것이 없고 **답(숫자)만**
             보면 된다 — 「총 회원 10」에서 「총 회원」이 먼저 걸리면 그 카드는 아무 일도 안 하는 것이다.
          🔥 **한쪽 기준으로 다른 쪽을 고치지 말 것.** 「여기 값이 굵네, 위반이다」 하며 지표 숫자를
             얇게 만들거나, 반대로 상세 값을 다시 굵히는 일이 이 저장소에서 반복됐다.
             ❓ 판정 기준은 **「이 화면에서 눈이 먼저 하는 일이 찾기인가, 읽기인가」** 하나다.
          📌 **목록 카드(9절 `.ds-list-card`)는 어느 쪽도 아니다** — 실측하고 그대로 뒀다.
             그 카드의 `.ds-lc-key`/`.ds-lc-val` 은 **한 줄에 라벨과 값이 나란히** 서고(위아래가 아니다)
             값이 **우측 정렬**이라 두 열이 이미 자리로 갈려 있다. 게다가 그 카드에서 먼저 읽히는 것은
             라벨도 값도 아니라 **카드 제목(`.ds-item-title`)**이다 — 목록은 「어느 건인가」를 먼저 고른다.
             🔥 **여기에 세 층을 적용하지 말 것**: 값을 1.0625rem 으로 키우면 카드 높이가 늘어 그리드
             줄이 어긋나고, 라벨 자간을 벌리면 한 줄 안에서 값 칸이 밀린다.
             표(`table`)도 같다 — **헤더가 항목명 자리**라 열이 이미 이름을 말하고 있다. */
    --ds-font-field-k:   .875rem;    /* 항목명 */
    --ds-font-field-v:   1.0625rem;  /* 데이터 — 🔥 더 키우지 말 것(위 참고) */
    --ds-font-field-sub: .8rem;      /* 설명 */
    --ds-track-field-k:  .03em;      /* 항목명 자간 — 이름을 「띠」로 만들어 값 덩어리와 갈라 준다 */
}
/* ===== 목록 데이터 카드의 **제목** — `.ds-item-title` (2026-07-21 사용자 지시로 명시) =====
   목록 9개(여행상품 3 · B2B 거래 4 · 이행 2)의 카드 제목은 원래 전부
   `class="fs-3 text-dark fw-bolder …"` 로 **같은 값을 각 템플릿이 따로 적고** 있었다.
   실측상 9개가 우연히 일치했을 뿐, **한 곳만 고치면 그날로 갈라진다.** → 클래스 하나로 못 박는다.

   ⚠️ **카드 헤더 제목(15px)보다 크다**(17.6px). 오타가 아니라 의도다 —
      목록에서는 페이지 제목이 아니라 **각 항목의 이름이 주인공**이다.
      그래서 `--ds-font-title`(카드 헤더용)과 **별도 토큰**을 둔다. 둘을 합치지 말 것.

   ⚠️ `fs-3` 를 그대로 쓰지 않는 이유: Metronic 의 `.fs-3` 는 1200px 미만에서
      `calc(1.26rem + .12vw)` 로 **창 폭에 따라 변한다.** 고정 `rem` 이면 root(13px→모바일 12px)만
      따라가므로 배지·버튼과 같은 방식으로 줄어든다.
   ⚠️ 색은 `text-dark`(Metronic) 대신 `--ds-text-strong` — 다크모드에서 자동으로 뒤집힌다. */
.ds-item-title {
    font-size: var(--ds-font-title);
    font-weight: var(--ds-weight-title);
    color: var(--ds-text-strong);
}

/* ===== 🏷 **항목명(라벨)은 `.ds-label` 한 곳이 정한다** (2026-08-15 사용자 지시) =====
   > *"승인 상태, 승인 처리, 제출 서류 모두 항목 명인데 폰트 컬러가 달라. 항목명의 폰트 컬러 하나로
   >  통일하고, 항목명은 데이터와 구분하기 위해서 볼드 처리해."*

   🔥 **`text-muted` 를 항목명에 쓰지 말 것.** 그 유틸리티는 이 파일이 `--ds-text-faint`(gray-600)로
      되돌려 놓는데, gray-600 은 0-1 절이 **「단위·경과일 같은 보조에만, 본문 금지」**로 못 박은 계조다.
      항목명은 **읽어야 하는 글자**라 `--ds-text-label`(gray-700) 이상이어야 한다.
      ⚠️ 같은 요소에 `text-muted` 를 **남겨 두지 말 것** — `!important` 라 조용히 이겨 다시 회색이 된다.
   🔑 **표의 라벨 칸·표 헤더·카드 안 절 제목이 전부 같은 역할**이다. 화면마다 손으로 칠하면
      한 화면 안에서도 색이 갈린다(업체 상세가 실제로 그랬다: 표 라벨 gray-600 / 절 제목 gray-800).
   📌 `<tr>`·`<td>`·`<th>`·`<span>` 어디에나 붙는다 — `th` 는 브라우저 기본 굵기가 따로 있어
      행에만 붙이면 굵기가 안 먹으므로 **자식 칸까지 함께** 잡는다.
   ⚠️ 좁은 화면(≤767px)의 `td.ds-card-title` 은 **더 센 규칙**이라 그대로 카드 제목으로 남는다
      (4-1 절). 그것이 의도다 — 카드에서는 그 칸이 라벨이 아니라 제목이다. */
.ds .ds-label,
.ds .ds-label > th,
.ds .ds-label > td {
    color: var(--ds-text-label);
    font-weight: var(--ds-weight-label);
}

/* ⚠️ **보조 텍스트는 크기를 낮추기 전에 색부터 낮춘다.**
   실사에서 보조 텍스트의 50%가 크기를 안 낮추고 색만 낮췄다(다수결) — 그게 맞다.
   크기까지 줄이면 13px→11px 이라 읽기 부담이 커진다. `--ds-text-faint` 로 충분하면 거기서 멈출 것.
   `--ds-font-sub` 는 한 줄에 밀어 넣어야 하는 단위·경과일 같은 곳에만 쓴다. */

/* ===================================================================
   0-3) 여백·높이 토큰 (2026-07-21 확정)

   실사 근거: 로그인 후 **25개 화면**을 1600px 로 렌더해 `getComputedStyle` 실측(다수결).
     · 카드 헤더 높이      48px            81%
     · 카드 헤더 좌우패딩  2.25rem(29.3px) 90%
     · 카드 본문 좌우패딩  2.25rem         93%   ← 헤더와 같아야 끝선이 맞는다
     · 카드 아래여백       0px             88%   ← 간격은 **부모의 gap** 이 준다
     · 입력창 높이         32px           `.ds` 안 **100%**
     · 배지 높이           22px            75%
     · 버튼 높이           28.9px          66%   ← 나머지 34%가 32/34.3/42.9 로 갈림

   ⚠️ **42.9px 입력창은 "다른 기준"이 아니라 `.ds` 바깥**이다(모달·헤더). 실측에서
      `.ds` 안 입력창은 예외 없이 32px 이었다. 모달을 `.ds` 로 덮으면 함께 맞는다.
   =================================================================== */
/* ═══════════════════════════════════════════════════════════════════════════════════════
   0-4) **간격 척도 — 여백은 이 여섯 중에서만 고른다** (2026-08-21 구현 #47 사용자 지시)

   > *"오와 열이 맞지 않아 제각각이고 요소들끼리 너무 다닥다닥 붙어있어서 보기 답답하거나 ...
   >  이런 화면 오류 때문에 고생해서 만든 사이트가 싸구려 같아 보이고"*
   > *"요소 추가하고 할때마다 크기 여백 배치 정렬 문제가 틀어지는것때문에 ... 추가하고 수정할때마다
   >  똑같은 수정을 반복해야 하니 일이 너무 더뎌져."*

   🔬 **왜 이 척도를 만들었나 — 실측이 근거다.** 세 앱 CSS 의 여백 선언을 세어 보니
      **값이 70종**이고 **토큰을 쓴 선언은 22곳뿐**이었다. 게다가 같은 값이 두 표기로 갈려 있다
      (`.5rem` 81곳 + `0.5rem` 35곳 · `.75rem` 66곳 + `0.75rem` 36곳).
      🔑 **규칙이 없어서가 아니라 「고를 값이 정해져 있지 않아서」** 새 요소를 넣을 때마다
         그 자리에 맞는 숫자를 즉석에서 정하게 되고, 그래서 이웃과 안 맞는다.

   🔴 **새로 쓰는 `padding`·`margin`·`gap` 은 이 여섯 중 하나를 쓴다. 생 숫자를 적지 마라.**
      값은 실제 사용 빈도 상위에서 뽑았다(1rem 106 · .5rem 116 · .75rem 102 · 1.5rem 48 · .25rem 37).
      🔑 **중간값이 필요하면 그 자리가 잘못된 것이다** — 이웃과 같은 단을 쓰면 끝선이 저절로 맞는다.

   ⚠️ **기존 값을 일괄로 바꾸지 마라.** 400KB × 3앱을 한 번에 치환하면 무엇이 깨졌는지 알 수 없다.
      **새로 만드는 것과 손대는 자리만** 이 척도로 가져온다 — 그것만으로 새 어긋남이 안 생긴다.
   📌 이미 있는 의미 토큰(`--ds-pad-x`·`--ds-gap` 등)은 **그대로 둔다.** 그것들은 「카드 좌우」처럼
      자리 이름이 붙은 값이라 척도보다 위 개념이다. 척도는 **그런 이름이 없는 자리**에 쓴다.
   ═══════════════════════════════════════════════════════════════════════════════════════ */
:root {
    --ds-sp-1: .25rem;   /*  4px  글자와 아이콘 사이 · 배지 안쪽 */
    --ds-sp-2: .5rem;    /*  8px  같은 묶음 안 요소 사이 */
    --ds-sp-3: .75rem;   /* 12px  버튼끼리 · 입력칸과 설명 사이 */
    --ds-sp-4: 1rem;     /* 16px  묶음과 묶음 사이 */
    --ds-sp-5: 1.5rem;   /* 24px  구역과 구역 사이 */
    --ds-sp-6: 2rem;     /* 32px  큰 구역 사이 */
}

/* ⚠️ 여백·높이 토큰도 `:root` 다(위와 같은 이유). */
:root {
    --ds-pad-x:        2.25rem;  /* 카드 헤더·본문 좌우. **둘이 달라지면 끝선이 어긋난다** */
    --ds-pad-y:        2.25rem;  /* 카드 본문 상하 (실측 14가지로 가장 심하게 갈렸다) */
    --ds-card-head-h:  48px;     /* 카드 헤더 높이 */
    --ds-gap:          1.5rem;   /* 카드 사이 간격 — 카드 자신의 margin 이 아니라 **부모 gap** */

    /* 목록 데이터 카드(9절 `.ds-list-card`) — 정산 대상 카드 실측값 */
    /* 카드와 카드 사이. `.75rem`(9.75px) 이었는데 카드가 서로 붙어 보인다는 지적으로
       `1.05rem`(13.65px) 로 넓혔다 (2026-07-22 사용자 요청 "조금만 늘려").
       🕘 **(2026-09-07 에 이 경고는 해당 없어졌다)** 이 값을 바꾸면 `.ds-list-grid` 의
          **열 계산식도 따라가야 한다** — 거기 `1.5rem` 은 gap 2개분을 하드코딩한 것이었고,
          *"숫자로 되돌리지 말 것: 어긋나면 3열 상한이 깨져 초고해상도에서 4열이 나온다"* 가 근거였다.
          🔑 **그 3열 상한 자체를 걷어냈다**(9절 `.ds-list-grid`) — 식에서 gap 이 빠져 이제 무관하다. */
    --ds-gap-card:     1.05rem;  /* 카드와 카드 사이 */
    /* 🔴 **목록 카드의 최소 너비** (2026-09-07 사장님 확정)
       > *"구간별로 요소별 카드 최소 너비를 설정하고 넘어가는 데이터는 줄바꿈"*
       > *"목록은 한 페이지 12개씩이므로 열 갯수는 무조건 12의 공약수"*
       🔬 **왜 30rem(480px) 인가** — 상품 목록 카드가 **줄바꿈 없이 필요한 폭이 482px** 인데
          바닥값이 `23.75rem`(380px)이라 열이 과하게 늘어 카드가 빡빡했다.
       📐 480px 이면 열 수가 전 구간에서 **12의 약수**로 떨어진다 —
          360·768 → **1열** · 992·1200 → **2열** · 1920 → **3열** · 3840 → **6열**.
       🔴 **`rem` 이다 — px 로 되돌리지 말 것** (글자 크기 3단계 토글을 따라가야 한다.
          근거는 9절 `.ds-list-grid` 주석의 2026-08-24 실측). */
    /* 🔴 **목록 카드 최소 너비 — 구간마다 다르다** (2026-09-07 사장님 확정: *"구간별로 나눠"*).
       값은 아래 「📐 구간별 카드 최소 너비」 절에서 미디어 쿼리로 덮는다. 여기 값은 폰(~767) 몫이다.
       🔑 폰에서는 격자가 `min(100%, …)` 로 잘려 **어차피 1열**이라 값이 무엇이든 같다. */
    --ds-lc-min:       30rem;    /* 폰(~767) — 실질 1열 */
    --ds-lc-pad-x:     1rem;     /* 카드 안 좌우 */
    --ds-lc-pad-y:     .65rem;   /* 머리·본문 상하 */
    --ds-lc-foot-y:    .6rem;    /* 발 상하 (머리보다 살짝 좁다 — 실측 그대로) */
    /* ⚠️ 데이터 행 사이. 한때 `.15rem`(1.95px) 이었다 — **표 카드 시절의 값**을 그대로 옮긴 것이라
       글자 높이 19.5px 사이에 2px 밖에 안 남아 "다닥다닥 붙어 보인다"는 지적을 받았다(2026-07-21).
       표일 땐 셀 자체 패딩이 있어 괜찮았지만 `div.card` 에는 그게 없다. */
    --ds-lc-row-gap:   .35rem;   /* ≈4.6px */

    /* 컨트롤 높이 — 🔥 **전부 rem 이다. px 를 박지 말 것.**
       2026-08-13 에 글자 크기를 사용자가 고를 수 있게 되면서(0-0 절) px 로 박힌 값은
       글자만 커지고 상자는 그대로라 **글자가 상자를 뚫는다.** */
    --ds-control-h:    2.4rem;   /* 입력창·셀렉트 (16px root 기준 38.4px)
                                    🕘 예전에는 `32px` 고정이었다 — 되돌리지 말 것. */
    --ds-badge-h:      1.7rem;   /* 배지. 배지 여백이 em 기반이라 root 를 따라 함께 준다. */
    --ds-btn-h:        2.2rem;   /* 단독 버튼 */
    /* 🔴 카드 **머리** 안 클릭 요소(버튼·드롭다운·토글)의 높이 — 하나로 통일한다 (2026-08-24 사장님 확정).
       > *"전체이력과 전체보기 통화 선택 등등 모든 헤더에 들어가는 버튼이나 클릭 가능한 요소들은
       >  세로 높이를 하나로 통일하는게 좋겠어. 물론 375·430 와 768~1920 로 나누어야지."*
       🔬 기준은 **견적 이력 머리**다(사장님: *"기준은 견적이력 카드"*). 머리는 48px 인데
          컨트롤이 44px(`--ds-btn-h`)이면 위아래 **2px** 밖에 안 남아 꽉 찬다.
          `2.05rem` 이면 root 20(아주 크게)에서 **41px** — 48px 안에 **3.5px** 씩 남는다.
       🔥 좁은 화면 바닥은 **36px** — *"375, 430도 큰화면 사이즈로 변경되면 안돼"*(사장님).
       ⚠️ 카드 **본문**의 입력칸은 이 값이 아니다(`--ds-control-h`) — 머리는 「누르는 것」이 서는 줄이다. */
    --ds-header-ctl-h: calc(1.945rem + 2px);
    /* 🔬 위 값의 출처는 **전체 이력 버튼**이다(사장님: *"기준은 견적이력 카드"*).
          그 버튼 = 줄높이 `1.5 × .95rem`(=1.425rem) + 세로 패딩 `.26rem × 2`(=.52rem) + 테두리 2px.
          실측 일치 — root 20 → **40.9px** · root 14 → **29.2px**.
       🔥 `max(36px, …)` 바닥을 다시 넣지 말 것 — 좁은 화면에서 36px 이 되어 전체 이력(29.2)과 어긋난다.
       🔑 짝이 되는 여백은 아래 `--ds-header-ctl-pad` 다. 둘은 **함께** 써야 줄 높이가 맞는다. */
    /* 🔴 카드 머리의 세로 패딩 — 전체 이력 줄(`.ds-hist-row`)과 같은 값이다.
          그 줄은 `min-height:48px` + 패딩 `.5rem` 이라 root 14 에서 48px(버튼이 가운데),
          root 20 에서 **60.9px**(= 40.9 + 10 + 10)이 된다. 견적 항목 머리도 이 규격을 그대로 쓴다. */
    --ds-header-ctl-pad: .5rem;
    /* 세로 스크롤바의 **실제 폭**. `common.js` 의 `measureScrollbar()` 가 부팅 때 재서 덮어쓴다.
       🔑 여기서 **기본값 0 을 선언**하는 이유 — `var(--x, 0px)` 폴백으로 두면 이름을 오타 내도
          조용히 살아난다(0 절 · `ds-audit` C2). 선언은 한 곳이면 된다. */
    --ds-scrollbar-w:  0px;
}

/* 🔑 **인접 규칙** — 옆에 붙은 요소는 짝의 높이를 따른다 (2026-07-21 사용자 지시)
   ```
   입력창 옆 버튼 → var(--ds-control-h)   (2.4rem)
   배지   옆 버튼 → var(--ds-badge-h)     (1.7rem)
   ```
   ⚠️ **px 를 박지 말 것 — 둘 다 rem 이다.** 박으면 글자 크기 토글(0-0 절)에서 어긋난다.
   ⚠️ 높이를 맞출 때 **입력창을 올리지 말 것** — `.ds input.form-control` 이 정한 기준이고,
      `.quote-workspace input.form-control`(0,2,1) 이 더 세서 조용히 무시된다.
   실측 위반 2건: `/stats/category` 의 버튼 34.3px vs 입력창 32px. */

/* -------------------------------------------------------------
   1) 페이지 여백
   common.css 의 `#kt_content{padding-bottom:.5rem!important}` 때문에
   상단 30px / 하단 6.5px 로 비대칭이 된다.
   둘 다 !important 라 specificity 로 이겨야 한다(ID 1,0,0 > class 0,1,0)
   → ID 를 함께 물어 (1,1,0) 로 만든다.
   ------------------------------------------------------------- */
#kt_content.ds { padding-bottom: 15px !important; }
@media (min-width: 992px) {
    #kt_content.ds { padding-bottom: 30px !important; }
}

/* -------------------------------------------------------------
   2) 카드 헤더 — 높이 48px 통일, 좌우 패딩은 card-body(2.25rem)와 동일
   !important 이유: common.css `.card .card-header.tpl-card-header{...!important}`
   ------------------------------------------------------------- */
/* 🔴 **세로 패딩은 0 이고 높이는 `min-height` 가 정한다** (2026-08-20 사용자 실주행 지적:
      *"「견적 항목」 카드 머리만 다른 카드보다 크다"* — 랜드사 앱에서 실측).
   🔬 실측(1440px, 랜드사 견적 상세): 그 머리 **58.8px** vs 옆 카드들 **48.0px**.
      갈라 재니 제목은 22.1px 로 문제가 없고 헤더 안 `span.select2-selection` 이 **41.8px** 이었다
      → 41.8 + 8 + 8 + 1(아래선) = 58.8. **`min-height:48px` 은 걸려 있는데 내용이 더 커서 무의미했다.**
   🔥 **규격 두 개가 세로 패딩 `.5rem` 아래서는 동시에 성립할 수 없다** — 48 − 16(8+8) − 1(테두리) = **31px**
      인데 컨트롤 규격은 그보다 크다. 그래서 컨트롤을 아무리 낮춰도 머리가 48px 을 넘는다.
      → **패딩을 0 으로 두고 `align-items:center` 에 맡기면** 컨트롤이 38.4px(`--ds-control-h`)이어도
      38.4 + 1 = 39.4 < 48 이라 **`min-height` 가 이겨 정확히 48px** 이 된다.
   🔑 **`height: 48px` 로 못 박지 말 것 — `min-height` 여야 한다.** 좁은 화면에서 이 머리는
      검색 폼을 만나면 **세로로 쌓이고**(아래 11절 `:has(form)` → `flex-direction: column`)
      제목·배지가 접히는 화면도 있다. 고정 높이면 그 내용이 **잘린다.**
      📌 이 앱은 목록 검색 헤더가 가장 많다 — 그래서 특히 `height` 로 못 박으면 안 된다.
   ⚠️ **머리를 키우는 방향(예: `min-height` 를 58.8 로)으로 풀지 말 것** — 규격 위반을 규격화하는 것이다.
   📌 좌우 `2.25rem` 은 그대로다 — `card-body` 와 끝선이 맞아야 한다(0-3 절). */
.ds .card > .card-header {
    min-height: 48px !important;
    padding: 0 2.25rem !important;
    align-items: center;
    gap: .75rem;
}
/* 🔴 **검색 폼이 든 머리는 세로 여백을 따로 받는다** (2026-08-21 구현 #47 실주행 · 사용자 지적)
   > *"목록 카드 헤더(검색영역)에 상하 여백이 너무 작지 않아?"*
   🔬 실측(여행사 상품 목록): 머리 **48px** · 세로 패딩 **0** · 내용 위·아래 **4.8px** 씩.
      검색 입력칸이 `--ds-control-h`(38.4px)라 48px 안에서는 그만큼밖에 안 남는다.
   🔑 **위 규칙이 틀린 것이 아니다** — 그것은 「제목 + 컨트롤 하나」가 든 머리를 48px 로 통일하려고
      만든 것이고 그 목적에는 맞다. **검색 폼이 든 머리는 성격이 다른 자리**라 예외를 둔다
      (좁은 화면에는 이미 `:has(form)` 예외가 있었다 — 그것을 **모든 폭으로** 넓힌 것이다).
   🔬 값을 넷 재고 화면으로 골랐다: `.5rem`→54.4 · `.75rem`→62.4 · **`1rem`→70.4** · `1.5rem`→86.4.
      🔑 **1rem 을 고른 이유**: 같은 화면의 「메시지」 카드 머리가 이미 **70px** 이라 새 값을 만들지 않는다.
   ⚠️ 특이도 (0,3,1) 로 좁은 화면 블록(11절, (0,3,0))도 이긴다 — **의도한 것이다.** 폰에서도 같은 여백이다.
   🔥 `min-height:48px` 을 여기서 풀지 말 것 — 내용이 그보다 낮은 폼(버튼 하나짜리)이 있다. */
.ds .card > .card-header:has(form) {
    padding-top: var(--ds-sp-4) !important;
    padding-bottom: var(--ds-sp-4) !important;
    /* 🔴 **검색 머리 안의 줄 간격은 `--ds-search-gap` 하나다** (2026-08-23 사용자 지시: *"전체 페이지에 검색창 내 여백을 통일해 줘."*). 🔬 그전에는 값이 셋으로 갈려 있었다 — 이 머리의 `gap`(`.75rem`) · 상태 드롭다운 줄(`.ds-filter-row` `.4rem`) · 검색 격자(`.input-group` `.4rem`). 375px 에서 **첫 줄 아래만 9px, 나머지 줄 사이는 4.8px** 이라 「첫 줄 아래 여백만 크다」로 보였다. 🔥 여기서 생 숫자로 되돌리지 말 것 — 세 자리가 같은 이름을 봐야 갈리지 않는다. 📌 이 앱의 검색 폼은 거르는 칸이 `.input-group` 밖 `form.d-flex.gap-3` 에 있어 **이미 같은 값(12px)** 이었다 — 그래서 이 앱은 눈에 띄게 바뀌는 곳이 없다(그것이 이 값을 고른 근거이기도 하다). */
    gap: var(--ds-search-gap);
}
/* 🔴 **검색 폼 자신의 줄 간격도 같은 토큰이다** (2026-08-23 사용자 결정 — ⑩).
   🔬 **이 앱이 바로 그 앱이다** — 이 앱의 검색 폼은 거르는 칸이 `.input-group` 밖에 서고
      **폼 자신이 `d-flex flex-wrap`** 이라 줄 간격을 폼의 `gap` 이 정한다. 그런데 그 값이 마크업의
      **Metronic 유틸리티 `gap-3`**(`!important`)이었다 → 오늘은 우연히 같은 12px 이지만
      **`--ds-search-gap` 을 바꾸면 이 앱만 안 따라온다.**
   🔑 **유틸리티를 마크업에서 걷고 이 규칙이 정하게 했다** — `!important` 로 이기려 들지 않는다
      (배치 규칙 ④: *"이길 규칙이 아니라 겹치지 않을 규칙을 쓴다"*). 걷어낸 화면은 **19곳**이다
      (목록 18 + 초기설정 자동승인). 🔥 새 검색 폼에 `gap-3` 을 다시 적지 말 것.
   🔑 랜드사·여행사 폼은 `display:block` 이라 이 규칙이 아무 일도 하지 않는다.
      🔥 여기에 `display:flex` 를 더하지 말 것: 더하는 순간 그쪽 `.input-group` 이 flex 항목이 되어
      **폭이 내용만큼으로 줄어** 검색 줄이 무너진다. */
.ds .card > .card-header form { gap: var(--ds-search-gap); }

/* ── 2-0-1) 🔴 **C9 — 선택 상자가 든 카드 머리도 위아래 여백을 받는다** (2026-08-24 사장님 확정)
   > *"통화선택 드롭다운 박스 세로로 너무 뚱뚱하다고 카드 헤더에 세로로 너무 꽉 차잖아"*
   > → 컨트롤만 낮춘 시안을 보시고 *"이게 맞춘거라고?"*

   🔬 실측: 컨트롤을 `--ds-btn-h` 로 낮춰도 **드롭다운이 머리의 92%** 였다(위 2 / 아래 3px).
      **원인은 컨트롤이 아니라 머리가 `min-height:48px` + 세로 패딩 0 으로 묶여 있는 것**이다.
   🔑 **바로 위 `:has(form)` 예외와 같은 물음이고 같은 값(`--ds-sp-4`)을 쓴다** — 새 값을 만들지 않는다.
      두 규칙을 합치지 않고 나눠 둔 이유는 **`:has()` 를 쉼표 목록 하나로 묶으면 하나라도 못 읽는
      브라우저에서 규칙 전체가 버려지기 때문**이다(이 파일이 이미 두 번 나눠 적은 이유와 같다).
   🔴 **판정은 「성격」이지 화면 이름이 아니다** — 다만 **버튼만 든 머리는 대상이 아니다.**
      🔬 세 앱 전수(2026-08-24): 컨트롤이 든 카드 머리 **121개** 중 **77개가 폼 없는 머리**인데
         그중 **70개는 `btn-sm`(약 30px) 하나뿐**이라 48px 안에 여유가 있다 — 고칠 결함이 없다.
         실제 선택 상자가 든 머리는 **48개**이고 그중 **41개가 검색 폼**(위 규칙이 이미 맡는다),
         남는 **7개**가 이 규칙의 자리다.
   ⚠️ **숨겨진 선택 상자는 세지 않는다**(`hidden` · `d-none`) — 관리자 견적/예약 상세의 언어 선택은
      언어가 하나면 `hidden` 이고, 랜드사 템플릿 등록의 `#selScheduleCategory` 는 `d-none` 이다.
      그것까지 세면 **보이지도 않는 컨트롤 때문에 머리만 부푼다.**
      🔑 `:has()` 는 살아 있는 판정이라 나중에 `hidden` 이 벗겨지면 그때 여백이 생긴다.
   🔥 **「카드 머리는 전부 48px」로 되돌리지 말 것** — 그 규격은 「제목 + 작은 버튼」이 든 머리의
      것이고, 선택 상자가 든 머리에서 그것을 지키면 **글자 크기 3단계를 키울 때마다 다시 낀다.** */
/* 🕘 **2026-08-24 같은 날 이 두 규칙을 걷어냈다 — 전제가 바뀌었다.**
   위 실측(「컨트롤을 낮춰도 92%」)은 컨트롤이 **44px(`--ds-btn-h`)** 일 때의 것이다.
   사장님이 *"전체 이력 버튼에 맞추자"* → *"견적 항목 카드 헤더 높이를 견적 이력과 똑같이"* 로 정하면서
   머리 컨트롤이 **`--ds-header-ctl-h`(36 / 41px)** 로 낮아졌고, 48px 머리 안에 위아래 **3.5px** 이 남는다.
   → 세로 패딩이 더는 필요 없다. 🔥 **되살리면 머리가 다시 82px 로 부푼다.**
   📌 근거·실측: `.claude/rules/screen-layout.md` C9·C10 */
/* 🔑 **그 머리 안의 컨트롤 높이는 `--ds-btn-h`(액션바 버튼과 같은 값)** 다.
   🔑 **가르는 기준은 「그 컨트롤이 어느 줄에 서 있는가」** 다 — 카드 **본문**의 입력칸·셀렉트는
      글자를 넣는 칸끼리 줄을 맞추므로 `--ds-control-h` 이고, 카드 **머리**는 「누르는 것」이 서는 줄이다.
   ⚠️ **검색 폼이 든 머리는 대상이 아니다**(`:not(:has(form))`) — 거기서 셀렉트의 짝은 액션바가 아니라
      **옆에 선 검색 입력칸**(`--ds-control-h`)이다. 🔥 `.ds-header-action` 을 이 값으로 바꾸지 말 것.
   ⚠️ `.select2-hidden-accessible` 은 Select2 가 원본 `<select>` 를 `height:1px !important` 로 접어
      숨긴 것이다 — 높이를 주면 **접힌 원본이 펴져 드롭다운이 둘로 보인다.** 반드시 뺄 것. */
.ds .card > .card-header:not(:has(form)) .form-select:not(.select2-hidden-accessible),
.ds .card > .card-header:not(:has(form)) select:not(.select2-hidden-accessible) {
    height: var(--ds-header-ctl-h) !important;
    min-height: 0 !important;
    padding-top: 0;
    padding-bottom: 0;
}
.ds .card > .card-header:not(:has(form)) .select2-container { height: var(--ds-header-ctl-h); }
.ds .card > .card-header:not(:has(form)) .select2-container .select2-selection--single {
    height: var(--ds-header-ctl-h) !important;
    min-height: 0 !important;
    display: flex;
    align-items: center;
}
/* 벤더가 `__rendered`(`line-height:28px`)와 `__arrow`(`height:26px`)에 값을 박아 두어,
   상자만 낮추면 **글자와 화살표가 상자 밖으로 뜬다** — 셋을 함께 잡아야 끝난다. */
.ds .card > .card-header:not(:has(form)) .select2-container .select2-selection--single .select2-selection__rendered { line-height: normal; }
.ds .card > .card-header:not(:has(form)) .select2-container .select2-selection--single .select2-selection__arrow { height: 100%; top: 0; }

/* ═══ 2-0-2) 🔴 **H2~H7 — 콘텐츠 영역의 카드 머리·좌우 여백을 한 벌로** (2026-08-24 사장님 확정) ═══
   🔑 **스코프가 `#kt_content` 인 이유** — 이 규칙은 「본문에 놓인 카드」의 것이다.
      머리의 알림 드롭다운·메시지 드로워는 `#kt_header` 쪽이라 **저절로 빠진다**(24절이 따로 쥔다).
   ⚠️ **특이도 (1,2,0)** 이라 위 2절(`.ds .card > .card-header`, (0,3,0))의 `!important` 선언까지 이긴다 —
      `!important` 끼리는 특이도가 가른다. 🔥 위 2절의 값을 고칠 때 **여기부터 볼 것**(안 그러면 안 바뀐다).
   📌 근거·실측은 `.claude/rules/screen-layout.md` 의 H2~H7 절. */

/* ── H2. 카드 머리 높이 — `min-height` + 세로 패딩 + `flex:none`
   🔑 **`height` 가 아니라 `min-height`** 다 — 검색줄이 두 줄이 되면 머리가 따라 커져야 한다(사장님 지적).
      🔥 `height` 로 못 박지 말 것: 좁은 화면의 검색 머리는 세로로 쌓여 **내용이 잘린다.**
   🔑 **`flex:none`** — 미리보기처럼 **세로 flex 카드**에서 카드가 눌릴 때 머리까지 함께 찌그러졌다
      (실측 60.9 → 48px). 머리는 줄어들 자리가 아니다.
   🔴 **`--ds-hdr-pad-y` 를 두는 이유** — 검색 폼이 든 머리(위 2절 `:has(form)`)는 2026-08-21 사장님
      지적(*"목록 카드 헤더(검색영역)에 상하 여백이 너무 작지 않아?"*)으로 `--ds-sp-4` 를 받는다.
      이 규칙이 특이도로 그것을 이겨 버리므로 값을 **변수로 받아** 그 예외가 계속 살아 있게 한다.
      🔥 여기에 `var(--ds-header-ctl-pad)` 를 직접 쓰지 말 것 — 그날로 목록 검색 머리가 8px 로 돌아간다. */
#kt_content .card > .card-header {
    --ds-hdr-pad-y: var(--ds-header-ctl-pad);
    /* 🔬 **`+ 1px` 은 머리의 아래 테두리 몫**이다 — `box-sizing: border-box` 라 테두리가 높이에 든다.
       빼면 **컨트롤이 없는 머리**(미리보기 판)만 60.9px 로 서고 옆 카드는 61.9px 이 되어 1px 어긋난다
       (🔬 1920·4K 에서 실측). 이 값이 곧 전체 이력 줄의 바닥값과도 같아진다. */
    min-height: calc(var(--ds-header-ctl-h) + var(--ds-hdr-pad-y) * 2 + 1px) !important;
    padding-top: var(--ds-hdr-pad-y) !important;
    padding-bottom: var(--ds-hdr-pad-y) !important;
    flex: none;
}
#kt_content .card > .card-header:has(form) { --ds-hdr-pad-y: var(--ds-sp-4); }

/* ── H3. 머리 안에서 **누르는 것**은 전부 같은 높이(`--ds-header-ctl-h` = 전체 이력 버튼)
   ⚠️ **닫기(`.pv-close`·`.hist-close`)를 빼지 말 것** — 그 둘이 터치 규격(44px)이라 머리를
      **59px** 로 밀어 올리고 있었다(실측). 🔑 **세로만 맞추고 `min-width`(44px)는 그대로 둔다** —
      손가락이 닿는 폭은 남기고 줄 높이만 맞추는 것이다(27절 「손가락이 닿는 칸」과의 절충).
   ⚠️ **검색 폼이 든 머리는 대상이 아니다**(`:not(:has(form))`) — 거기서 셀렉트·버튼의 짝은
      액션바가 아니라 **옆에 선 검색 입력칸**(`--ds-control-h`)이다(위 2절·C9 와 같은 판정).
   ⚠️ **`.btn-icon` 도 대상이 아니다** — 그쪽은 **정사각**이라 높이만 주면 폭이 안 따라와
      35×44 같은 찌그러진 상자가 되고(C5 실측), 좁은 화면 터치 목표와 정면으로 부딪친다.
   ⚠️ **`.select2-hidden-accessible` 은 반드시 뺄 것** — Select2 가 원본 `<select>` 를
      `height:1px !important` 로 접어 둔 것이라, 높이를 주면 **드롭다운이 둘로 보인다.** */
#kt_content .card > .card-header:not(:has(form)) .btn:not(.btn-icon),
#kt_content .card > .card-header:not(:has(form)) .form-select:not(.select2-hidden-accessible),
#kt_content .card > .card-header:not(:has(form)) select:not(.select2-hidden-accessible),
#kt_content .card > .card-header:not(:has(form)) .select2-container,
#kt_content .card > .card-header:not(:has(form)) .select2-selection--single,
#kt_content .card > .card-header .pv-close,
#kt_content .card > .card-header .hist-close {
    height: var(--ds-header-ctl-h) !important;
    min-height: 0 !important;
}
/* ── H3-1. 🔴 **높이를 못 박았으면 세로 여백도 함께 걷어야 한다** (2026-08-27 · 결함 E36 / A-E22)
   🔬 실측(세 앱 마이페이지 「Edit Profile」, `clipped()`): 상자는 위 H3 이 정한 **33.1px** 인데
      Metronic `.btn` 이 세로 패딩 **13px + 13px** 에 줄높이 **26.4px** 를 요구해 내용이 **52.4px** —
      글자가 상자보다 **6px 커서 잘렸다**(375px 에서는 25px 상자에 30px 내용). 전 폭에서 났다.
   🔑 **높이를 `--ds-btn-h` 로 올리는 것으로는 안 낫는다** — 2.2rem(35.2px)도 52.4px 보다 작다.
      원인은 「높이 값」이 아니라 **「높이를 고정한 채 패딩·줄높이를 그대로 둔 것」**이다.
   🔑 **줄높이를 상자 안쪽 높이에 맞춘다** — 그러면 글자가 상자 세로 한가운데에 그대로 앉는다.
   🔥 `display: inline-flex` 로 바꾸지 말 것 — flex 상자에서는 **공백만 든 텍스트 노드가 사라져**
      `<i>` 아이콘과 글자 사이를 공백으로 띄운 버튼들의 간격이 통째로 없어진다.
   ⚠️ **선택 상자·닫기 버튼에는 주지 않는다**(위 H3 목록에서 `.btn` 만 다시 잡는다) — `select` 의
      줄높이를 건드리면 브라우저마다 글자 세로 위치가 갈린다. */
#kt_content .card > .card-header:not(:has(form)) .btn:not(.btn-icon) {
    padding-top: 0 !important;
    padding-bottom: 0 !important;
    line-height: calc(var(--ds-header-ctl-h) - 2px) !important;
}

/* ── H5. 카드의 좌우 여백은 **한 값**이다 — `--ds-card-x` (17 / 30px)
   🔬 기준은 **견적서 미리보기 문서**(`.quote-invoice`)가 카드 끝에서 갖는 여백이다 —
      좁은 화면 **17px** / 넓은 화면 **30px**. 사장님이 그 화면을 보고 확정했다.
   🔴 **이 값이 저장소 표준을 바꾼 것이다** — 예전 표준은 `2.25rem`(root 20 에서 45px)이었다.
      `.claude/rules/screen-tokens.md` 의 그 줄도 함께 고쳤다. 🔥 45px 로 되돌리지 말 것.
   🕘 **경계는 430px 이었다** — 사장님 지시 *"375, 430은 375, 430끼리, 나머지는 나머지끼리"*.
      🕘 (2026-09-07 에 이 경계를 **767.98px 로 합쳤다** — 그전에는 *"767.98px 로 옮기지 말 것"* 이었다.
         그 문장을 근거로 430 으로 되돌리지 말 것: 아래 사장님 확정이 그것을 대신한다.)
   🔴 **2026-09-07 사장님 확정 — 구간 수를 줄이고 검증 폭을 구간 폭에 맞춘다.**
      > *"구간수를 줄이고 검증 화면 너비를 구간 너비로 설정"* · *"검증폭은 디바이스 종류별 최저 폭"*
      > *"레이아웃이 깨지는건 좁아서 깨지는거지 넓어서 깨지지는 않으니까"*
      🔑 우리 CSS 에만 있던 비표준 경계(430 · 575.98/576 · 899.98/900)를 **Metronic 기본 경계로 흡수**했다.
         여기는 **430 → 767.98** 이다 → **768 미만 폰 구간 전체가 17px** 이 된다(그것이 의도다).
      🔥 **새 경계를 만들지 말 것.** 경계가 늘면 재야 할 폭이 늘고, 그만큼 **안 재는 폭**이 생긴다.
   🔑 **px 인 이유** — 이 값은 「문서의 종이 여백」이라 글자 크기 3단계와 함께 커질 것이 아니다
      (27절의 「손가락이 닿는 칸」과 같은 성격의 예외다). rem 으로 두면 큰 글자에서 45px 로 돌아간다. */
:root { --ds-card-x: 30px; }
@media (max-width: 767.98px) { :root { --ds-card-x: 17px; } }

#kt_content .card > .card-header,
#kt_content .card > .card-footer {
    padding-left: var(--ds-card-x) !important;
    padding-right: var(--ds-card-x) !important;
}
/* 🔑 **본문에는 `!important` 를 쓰지 않는다** — 마크업의 `p-0`·`px-*` 유틸리티가 이겨야 하기 때문이다
      (세 앱에 `card-body p-0` 이 **20곳** 있고 전부 표를 카드 끝까지 붙이려고 일부러 준 것이다).
   🔬 Metronic 기본 `.card-body{padding:2.25rem}`(0,1,0)은 이 규칙(1,2,0)이 그냥 이긴다 —
      `!important` 가 필요 없다. 🔥 `!important` 를 붙이지 말 것: 붙이는 순간 `p-0` 카드가 전부 벌어진다. */
#kt_content .card > .card-body {
    padding-left: var(--ds-card-x);
    padding-right: var(--ds-card-x);
}
/* 🔴 **카드 본문의 위아래 여백은 한 값이다 — 마크업에 `pt-9` 를 붙이지 마라** (2026-09-07 사장님 확정:
      *"여백은 … 상하가 대칭 되어야 해"* · *"요소 안쪽 위아래, 좌우측 여백은 무조건 통일"*)
   🔬 실측(여행사 1440px): `card-body pt-9` 카드가 **위 36px / 아래 32px** — 벤더 `.pt-9`(2.25rem)가
      위만 덮어 4px 어긋나 있었다. 벤더 기본 `.card-body{padding:2rem 2.25rem}` 의 32px 이 정본이다.
   🔑 **CSS 로 덮지 않고 마크업에서 걷었다** — `.pt-9` 는 `!important` 라 여기서 이길 수 없고,
      이겨 봐야 「유틸리티는 붙어 있는데 안 먹는다」가 되어 다음 사람이 더 헷갈린다.
      🔬 2026-09-07 에 세 앱 **41곳**(랜드사 12 · 여행사 8 · 관리자 21)에서 `pt-9` 를 걷었다.
   ⚠️ **`pb-0` 은 남아 있다**(마이페이지 3곳) — 탭 밑줄이 카드 밑변에 붙어야 하는 **구조**라
      여백 불일치가 아니다. 🔥 그 셋의 `pb-0` 을 「대칭」을 이유로 지우지 말 것.
   🔥 새 목록 화면에 `card-body pt-9` 를 다시 쓰지 말 것 — 그러면 이 값이 또 갈린다. */
/* 미리보기 바디는 0 — 안쪽 문서(`.quote-invoice`)가 자기 여백을 갖는다(그 값이 이 절의 기준이다).
   📌 마크업이 이미 `p-0` 이라 대개 필요 없지만, `p-0` 이 빠진 미리보기가 생겨도 어긋나지 않게 못박는다. */
#kt_content .preview-col .card > .card-body { padding-left: 0 !important; padding-right: 0 !important; }

/* ── H6. 카드가 아닌 부품도 같은 좌우 여백
   ⚠️ **요약 바와 이력 줄은 `.card-header` 가 아니다** — 빠뜨리면 그 둘만 옛 값(14 / 45px)으로 남는다.
   ⚠️ `.ds-detail-summary > *` 로 **자식 전부**를 잡아야 한다 — 머리만 잡으면
      **요청사항(`.ds-ds-note`)·펼친 상세(`.ds-ds-detail`)가 45px 로 남는다**(사장님 지적). */
#kt_content .ds-detail-summary > *,
#kt_content .ds-hist-card > *,
#kt_content .ds-hist-row {
    padding-left: var(--ds-card-x) !important;
    padding-right: var(--ds-card-x) !important;
}


/* 🔴 **검색 머리의 「아직 안 고른 칸」은 넷 다 같은 회색이다** (2026-08-23 사용자 지적:
   *"상태 전체, 전체 통화 이 두 가지는 글자가 크고 흰색인데 나머지는 왜 작고 회색이야?
     한 가지로 맞춰야 할 것 같은데 회색 쪽으로 맞추도록 할까?"* → *"플레이스홀더가 아니라서 그런가?"*)
   🔬 실측(랜드사 견적 목록 1440px): **크기·굵기는 이미 같았다**(14.4px / 500). 다른 것은 색뿐이다 —
      선택 상자는 「선택된 값」이라 `color`(gray-900, 대비 라이트 **16.79** / 다크 **16.42**)를 쓰고,
      입력칸은 `::placeholder` 라 벤더가 gray-500(**2.45 / 2.33**)으로 따로 칠한다.
      **사용자가 짚은 진단 그대로다** — 크게 보인 것은 흰색이라 또렷해서다.
   🔑 **왜 회색으로 맞추나** — 선택 상자의 「전체」도 **아직 아무것도 안 고른 상태**다. 자리표시자와
      같은 무게여야 하고, **값을 고르면 그때 진해진다** → 「고른 것 ↔ 안 고른 것」이 색으로 갈린다.
   🔴 **다만 그 회색 자체를 올린다.** gray-500 을 그대로 쓰면 **읽기 어려운 상태를 네 칸으로 늘리는
      셈**이다. `--ds-text-label`(gray-700)이면 **6.01 / 5.34** 로 양쪽 테마 모두 AA(4.5)를 넘는다.
      🔥 `--ds-text-faint`(gray-600)로 내리지 말 것 — **3.79 / 3.24** 라 이미 미달인 계조다(0-2 절 표).
      🔥 다크용으로 값을 따로 가르지 않는다(0 절 원칙) — `--bs-gray-*` 가 테마별로 반전된다.
   🔍 **범위는 「폼이 든 카드 머리」다** — 이 파일이 이미 쓰는 판별 기준 그대로다
      (바로 위 2절: *"폼이 있으면 목록/필터 헤더다"*). 🔬 세 앱 전수 **44화면 · 선택 상자 91개**가
      대상이고, 입력 폼·모달의 `<select>`·`<input>` 은 카드 **본문**에 있어 걸리지 않는다.
   🔢 「안 고른 상태」의 판정은 **구조로만** 한다(값의 뜻을 CSS 가 알 수 없다). 규칙을 **둘로 나눠 적는다**:
        ① **빈 값 옵션이 선택돼 있다** — 「전체」 80곳이 여기 해당한다.
        ② **빈 값 옵션이 아예 없는데 첫 옵션이 선택돼 있다** — 모바일 필터 드롭다운 10곳은
           「전체」의 value 가 **URL**(`@{/trade/quote/list}`)이라 ①에 안 걸린다.
      🔥 **①을 빼고 「첫 옵션」만으로 판정하지 말 것** — 관리자 시스템 오류 화면의 `resolved` 는
         **첫 옵션이 「미해결」(실제로 거르는 값)**이고 「전체」가 셋째다. 첫 옵션만 보면 그 화면은
         **필터가 걸린 줄 모르게** 된다. ②의 「빈 값 옵션이 없을 때만」이 그것을 막는다.
      🔥 **두 규칙을 쉼표로 한 덩어리에 묶지 말 것** — 선택자 목록은 **하나라도 못 읽으면 규칙 전체가
         버려진다**(`:is`/`:where` 와 달리 관대하지 않다). 나눠 두면 한쪽만 빠진다.
      ⚠️ 알려진 예외 한 곳 — 관리자 계약 목록의 **정렬(`sortType`)** 은 기본값이 빈 값
         (「급한 것 먼저」)이라 ①에 걸려 회색이 된다. 「아직 안 골랐다」는 맞지만 **거르는 칸은 아니다** →
         거슬린다는 판단이 서면 그때 빼되, **이름으로 빼는 조건을 여기 늘리지 말 것**(다음 정렬 칸에서 또 갈린다).
   🔢 특이도: 벤더는 `.form-control::placeholder`(0,1,1) · `.form-select`(0,1,0) 이다.
      아래는 (0,3,3) / (0,5,3) / (0,6,4) 라 넉넉히 이긴다 — **`!important` 가 필요 없다**(배치 규칙 ④).
   ⚠️ **`opacity: 1` 을 함께 준다** — Firefox 의 `::placeholder` 기본 `opacity` 가 .54 라
      색만 올리면 **그만큼 다시 흐려진다.** */
.ds .card > .card-header:has(form) :is(input, textarea)::placeholder {
    color: var(--ds-text-label);
    opacity: 1;
}
/* ① 빈 값 옵션이 선택된 상태 */
.ds .card > .card-header:has(form) select:has(option[value=""]:checked) {
    color: var(--ds-text-label);
}
/* ② 빈 값 옵션이 없는 선택 상자에서 첫 옵션(=「전체」)이 선택된 상태 */
.ds .card > .card-header:has(form) select:not(:has(option[value=""])):has(option:first-child:checked) {
    color: var(--ds-text-label);
}
/* h3 기본 마진이 헤더를 밀어올린다 */
/* 🔴 **카드 제목은 줄어들 수 있어야 하고, 안의 배지는 접혀야 한다** (2026-09-04 A-5).
   🔬 실측(관리자 회원 상세 · 영문 · 375px): 「약관 동의」 카드 제목이 글자 53.5 + 구분선 +
      배지 147.2 + 배지 136.6 = **364.2px** 인데 카드 머리의 내용 폭은 그보다 좁아
      제목이 카드를 뚫고 나가 **문서 폭 393px(뷰포트 375)** — **페이지에 가로 스크롤**이 생겼다.
   🔑 원인은 **flex 항목의 `min-width` 초기값이 `auto`(= min-content)** 라 줄어들지 않는 것이다
      (같은 함정을 `.ds-grid > *` 와 `.input-group` 에서 이미 겪었다).
   🔑 **둘을 함께 줘야 고쳐진다** — `min-width: 0`(제목 자신이 줄어들 수 있게) +
      `flex-wrap: wrap`(안의 배지가 다음 줄로 접히게). `min-width` 만 주면 자식이 그대로 넘치고,
      `flex-wrap` 만 주면 제목이 여전히 안 줄어든다.
   📌 접힌 줄 사이 간격만 척도에서 준다(`row-gap`) — 가로 간격은 `.ds-title-sep` 이 이미 만든다.
      🔥 `gap` 으로 바꾸지 말 것(가로 간격이 두 벌이 된다). */
.ds .card > .card-header .card-title {
    margin: 0;
    min-width: 0;
    flex-wrap: wrap;
    row-gap: var(--ds-sp-1);
}
/* Metronic `.card .card-header .card-toolbar{margin:.5rem 0}` 가 헤더를 13px 키운다 */
.ds .card > .card-header .card-toolbar { margin-top: 0; margin-bottom: 0; }

/* ── 2-1) 카드 푸터 = **행위 버튼 자리** (2026-08-15 사용자 지적: *"카드 푸터에 버튼 위아래 여백이
   너무 크다"*) ─────────────────────────────────────────────────────────────────────
   Metronic 기본은 `.card .card-footer{padding:2rem 2.25rem}` 이라 **30px 짜리 `btn-sm` 위아래로
   32px 씩** 붙는다 — 버튼보다 여백이 크다.
   🔑 값은 이미 이 저장소가 두 곳에서 손으로 좁혀 쓰던 것과 **같은 .9rem** 이다
      (DMC/Agency `Term.css` 의 `.term-footer`, DMC `Trade.css` 의 `.settle-card-footer`).
      화면마다 다시 좁히지 말고 여기 한 줄을 쓸 것.
   🔑 **좌우는 2.25rem 그대로** — card-header·card-body 와 끝선이 맞아야 한다(0-3 절).
   ⚠️ **`!important` 를 붙이지 말 것.** 붙이면 `py-4`·`py-6 px-9` 같이 **일부러 다른 값을 준 푸터**
      (Admin 의 FAQ·공지·약관 폼, 마이페이지)까지 덮는다. 지금은 (0,3,0) 이라
      Metronic(0,2,0)만 이기고 유틸리티(!important)에는 진다 — 그것이 의도다.
   📌 `.ds` 밖은 손대지 않는다 — DMC·Agency 의 거래/상품 작업대(`.preview-actions`·
      `.quote-form-footer`)는 `#kt_content` 에 `ds` 가 없어 이 규칙이 닿지 않는다(실측 확인). */
/* 🔘 **처리 버튼은 오른쪽이다** (2026-08-17 사용자 지적: *"버튼 우측 정렬 해야지"*)
   🔑 이 저장소는 이미 두 자리에서 오른쪽으로 정해 두었다 — **접이식 요약 카드 푸터**
      (17절 `justify-content: flex-end`, 구현 #71)와 **목록 카드 발**(9절 `text-align: right`).
      그런데 **평범한 `.card-footer` 에는 기본이 없어** 유틸리티를 붙인 화면만 오른쪽이었다.
   🔬 실측(2026-08-17): 세 앱에서 정렬 유틸리티가 없는 `.card-footer` 는 **5곳**이고 전부
      승인·처리 버튼 자리인데(마진율·업체 상세·회원 상세 2곳·상품 검수) 전부 왼쪽에 서 있었다.
   🔥 **화면에 `d-flex justify-content-end` 를 붙여 고치지 말 것** — 그것이 지금까지의 방식이었고
      그래서 **붙인 화면만** 오른쪽이 됐다. 규격 한 곳에서 정한다.
   📌 `justify-content-between` 처럼 **일부러 다르게 잡은 화면은 그대로**다(유틸리티가 `!important` 라 이긴다).
   📌 `me-auto` 도 그대로 산다 — 왼쪽(일상 작업) / 오른쪽(심사 행위)을 **가르는** 장치다. */
.ds .card > .card-footer {
    padding: .9rem 2.25rem;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: .5rem;
}

/* 🔴 **작업대 카드의 푸터는 「버튼 줄」이 아니라 「쌓이는 블록」이다** (2026-08-28 · D-5 F13·A-E24 / D-6 E19·A-E25)

   🔬 **실측(여행사 견적 상세 `.quote-form-footer`)**: 바로 위 규칙이 푸터를 **가로 flex 한 줄**로
      만드는데, 이 푸터에 든 것은 버튼이 아니라 **세로로 쌓일 블록 넷**(총액 줄 · 부가세 안내 ·
      항목 그리드 · 숨은 칸)이었다. 그래서 넷이 **나란히 서서** 이렇게 눌렸다 —
        375px: 총액 줄 142 / 안내 129 / **항목 그리드 34px**
        1440px: 287 / 508 / **항목 그리드 134px**
      → 「견적 총액」 라벨이 **12.7px 칸에서 「견/적/총/액」 4줄**(F13),
        「Valid Until」 값이 **34px 칸에서 3줄**(A-E24), 부가세 안내가 금액 옆에서 접혔다(A-E25).
   🔑 **원인은 라벨이 좁은 것이 아니라 「푸터가 이것을 버튼 줄로 취급한」 것**이다 →
      🔥 **라벨에 `min-width` 를 박아 메우지 말 것.** 그러면 칸마다 값을 지어내게 되고
      다음에 블록이 하나 더 붙으면 그대로 재발한다.
   ✅ **블록을 한 줄에 하나씩 쌓는다** — `flex-wrap` + 자식의 `flex-basis: 100%`.
      🔑 `display:block` 으로 바꾸지 않은 이유: 바로 위 규칙의 `align-items`·`gap`·오른쪽 정렬을
         **그대로 물려받는 자리**(랜드사 견적 작성 푸터의 발송 버튼 줄)가 있기 때문이다.
   🔬 **닿는 자리는 두 곳뿐이다** — 세 앱 템플릿 전수 검색에서 `.quote-form-footer` 는
      여행사 견적 상세 · 랜드사 견적 상세 **2곳**(관리자 0곳). 랜드사는 자식이 **하나뿐**이라
      전후가 같다(1035px → 1035px). 🔥 **랜드사가 안 바뀐다고 이 규칙을 지우지 말 것.**
   ⚠️ 랜드사 `menuLevel/Trade.css` 에 같은 취지의 `.quote-workspace .quote-form-footer > div`
      (`flex:1 1 auto; min-width:0`)가 있다 — 이 규칙이 (0,4,0) 으로 더 세다. **두 곳을 다 볼 것.** */
.ds .card > .card-footer.quote-form-footer { flex-wrap: wrap; }
.ds .card > .card-footer.quote-form-footer > * { flex: 1 1 100%; min-width: 0; }

/* 카드 헤더 우측 액션 버튼(예: '새 템플릿 등록')
   헤더 안 검색 컨트롤이 32px 이라 기본 버튼(46px)을 그대로 두면 헤더가 들쭉날쭉해진다.
   padding 에 !important 인 이유: Metronic `.btn:not(...)×6` = (0,7,0) */
/* 🐛 글자가 아이콘보다 작고 위로 붙어 보였다 (2026-07-22 사용자 지적). 원인 둘:
     ① Metronic 의 `.btn i{font-size:1rem}` 은 **버튼 글자 크기를 따르지 않는다** →
        글자 `.9rem`(11.7px) 옆에 아이콘만 13px 로 서서 글자가 왜소해 보였다.
        → 글자를 `1rem` 으로 올려 아이콘과 **같은 크기**로 맞춘다(본문 기준 13px 이기도 하다).
     ② 아이콘은 `line-height:0`(Metronic)이라 상자가 글자 그 자체인데, 텍스트는 `1.5` 라
        상자가 더 크다. 둘을 가운데 정렬하면 **서로 다른 상자의 중심**을 맞추는 셈이라
        한글 글자가 위로 뜬다(실측: 아이콘 457~535 vs 텍스트 437.5~554.5).
        → 텍스트도 `line-height:1` 로 상자를 글자에 맞추면 두 상자가 **완전히 겹친다**(457~535).
     ⚠️ px 로 밀어 맞추지 말 것 — 폰트·크기가 바뀌면 그 보정값이 그대로 틀어진다. */
.ds .card > .card-header .card-toolbar .btn.ds-header-action {
    height: var(--ds-control-h);
    min-height: 0 !important;
    padding: 0 .9rem !important;
    font-size: 1rem;
    line-height: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

/* 목록 헤더의 검색 폼 — **남는 가로 공간을 전부 채운다** (2026-07-20 사용자 지시).
   등록 버튼 문구가 화면마다 달라 검색창 끝과 버튼 사이 간격이 들쭉날쭉했다.
   검색창이 남은 폭을 흡수하면 버튼 폭이 달라져도 헤더에 빈 구간이 생기지 않는다.
   ⚠️ 한때 `max-width:520px` 로 줄였다가 되돌렸다 — 그러면 헤더 중앙에 1100px 짜리
   빈 공간이 생긴다. 폭 상한을 다시 두지 말 것. */
.ds .card > .card-header .ds-search { width: 100%; max-width: none; }

/* 카드 타이틀 */
.ds .card > .card-header .card-title,
.ds .ds-title {
    font-size: 1.15rem;
    font-weight: 600;
    color: var(--ds-text-strong);
    line-height: 1.2;
}

/* -------------------------------------------------------------
   3) 입력 컨트롤 — 컴팩트
   ------------------------------------------------------------- */
.ds .form-control,
.ds .form-select {
    min-height: 0 !important;
    padding: .35rem .7rem !important;
    font-size: .9rem;
}
.ds input.form-control,
.ds select.form-select { height: var(--ds-control-h); }
.ds textarea.form-control { height: auto; line-height: 1.5; }

/* ⚠️ **모달·드로어는 `#kt_content.ds` 바깥이라 위 규칙이 닿지 않는다.**
   실측(25개 화면): 페이지 입력창은 100% 32px 인데 **모달 안만 42.9px** 이었다.
   같은 폼 컨트롤이 어디에 놓였느냐로 높이가 달라지면 안 된다 → 같은 값으로 맞춘다.
   ※ `.ds` 를 모달마다 붙이는 방법도 있으나 템플릿 수십 곳을 고쳐야 하고, 빠뜨리면 또 갈린다.

   🔥 **높이만 복사하면 안 된다** (2026-07-22 사용자 지적 "등록 모달 3종이 다 이상하다").
   위 `.ds` 블록은 **padding·font-size·height 세 줄이 한 세트**인데 모달엔 height 만 옮겨져 있었다.
   그래서 Metronic 기본 padding(9.75px 상하) + 글자 14.3px 가 32px 상자에 갇혀
   **글자가 위아래로 잘렸다.** 실측: 모달 입력창 padding `9.75px 13px` / font `14.3px` / height `32px`.
   → 높이를 줄이려면 **안쪽 여백과 글자도 함께** 줄여야 성립한다. 세 줄을 갈라놓지 말 것. */
.modal .form-control,
.modal .form-select,
.drawer .form-control,
.drawer .form-select {
    min-height: 0 !important;
    padding: .35rem .7rem !important;
    font-size: .9rem;
}
.modal input.form-control,
.modal select.form-select,
.drawer input.form-control,
.drawer select.form-select { height: var(--ds-control-h); }
.modal textarea.form-control,
.drawer textarea.form-control { height: auto; line-height: 1.5; }

/* ⚠️ **위 단축 `padding` 은 방향 유틸리티(`ps-*`·`pe-*`)를 통째로 덮는다** — 규칙이 아니라 경고다
      (2026-08-18, 구현 #93 후속).
   🔬 `.drawer .form-control{padding:… !important}` 는 (0,2,0) 이고 `.ps-12` 는 (0,1,0)+`!important` 라
      **여기가 이긴다** → 아이콘 자리를 비우려고 붙인 좌측 여백이 48px 에서 11.2px 로 줄어
      **돋보기 위에 글자가 겹쳤다.** `!important` 끼리는 특이도로 갈린다.
   🔑 **처방은 특이도를 올리는 규칙이 아니라 마크업을 바꾸는 것이었다** — 아이콘을 겹쳐 놓는 대신
      목록 화면처럼 `.input-group` + `.input-group-text` **addon** 으로 두면 겹칠 자리가 없다.
      (그래서 여기 있던 `.ps-12`/`.pe-12` 되살리기 규칙은 쓰는 곳이 0이 되어 지웠다.)
   🔥 모달·드로어 안에서 입력칸에 `ps-*`·`pe-*` 를 새로 붙이려거든 **먼저 addon 으로 풀 수 있는지**
      보라. 그래도 필요하면 `.drawer .form-control.ps-N`(0,3,0)처럼 클래스를 하나 더 붙여야 한다. */
/* ※ select 화살표 자리는 따로 안 준다 — 이 파일 아래쪽의
   `.form-select.form-select.form-select{padding-right:2.2rem!important}`(0,3,0)가
   위 padding 단축(0,2,0)을 **특이도로 이겨** 모달에도 그대로 적용된다(실측 확인).
   ⚠️ 여기에 `.modal .form-select` 를 콤마로 여러 번 나열해도 **특이도는 안 오른다** —
   콤마는 별개 선택자다. 올리려면 한 선택자 안에서 클래스를 반복해야 한다. */

/* input-group 도 `.ds` 와 같은 세트가 필요하다 — **검색줄이 깨진 진짜 원인이 이것이다.**
   입력창만 32px 로 줄고 검색 버튼은 Metronic 기본 42.9px 로 남아 줄이 어긋났다(실측 32 vs 42.9).
   위 `.ds .input-group` 블록과 값이 같아야 한다. 한쪽만 고치면 또 갈린다. */
.modal .input-group > .form-control,
.modal .input-group > .form-select,
.modal .input-group > .input-group-text,
.modal .input-group > .btn,
.drawer .input-group > .form-control,
.drawer .input-group > .form-select,
.drawer .input-group > .input-group-text,
.drawer .input-group > .btn {
    height: var(--ds-control-h);
    min-height: 0 !important;
    font-size: .9rem;
    display: flex;
    align-items: center;
}
.modal .input-group > .input-group-text,
.drawer .input-group > .input-group-text { padding: 0 .6rem !important; }
.modal .input-group > .btn,
.drawer .input-group > .btn { padding: 0 .9rem !important; justify-content: center; }
/* 🔥 svg 크기 줄이 **이 세트에만 빠져 있었다.** 아이콘 SVG 는 원본이 `width="24px"` 라
   줄이는 규칙이 없으면 24px 로 그려진다 → 모달 안 돋보기만 목록보다 커진다(실측 24 vs 16).
   ⚠️ 모달은 `#kt_content_container` **밖**이라 `.ds` 규칙이 닿지 않는다 —
      아래 `.ds` 블록의 같은 줄과 **값이 같아야 한다. 한쪽만 고치면 또 갈린다.** */
.modal .input-group > .input-group-text svg,
.drawer .input-group > .input-group-text svg { width: 16px; height: 16px; }

/* 📏 **모달 푸터의 버튼도 카드 푸터와 같은 크기**다 — `--ds-btn-h`(단독 버튼 2.2rem)
   (2026-08-18 사용자 지적 — 폰 실기기: *"모달 푸터의 버튼이 크다"*).
   🔬 실측(폭 500px · root 12px, 관리자 랜드사별 마진율 상세 ▸ 「카테고리 마진율 설정」):
      모달 푸터 버튼 **39.8px**(글자 13.2px) vs 같은 화면 카드 푸터 버튼 **26.4px**(12px) — **1.5배**다.
   🔑 선언 다섯 줄은 17절 `.ds .card .card-footer .btn` 과 **글자 그대로 같다.**
      🔥 **한쪽만 고치지 말 것** — 그래서 지금까지 갈려 있었다.
   ⚠️ **`.ds` 를 붙이면 안 된다** — 모달은 `#kt_content.ds` **바깥**이라 조용히 무시된다(위 3절과 같은 함정).
   🔥 **화면에서 `btn-sm`·px 로 맞추지 말 것** — 실제로 DMC 모달 3곳이 `btn-sm` 을 붙여 혼자 작았다
      (환불 등록 · 메모 추가 · 정산 신청). 2026-08-18 에 그 클래스를 걷어냈다.
   📌 `.btn-close`(× 버튼)는 `btn` 클래스를 갖지 않아 이 규칙에 걸리지 않는다 — 의도한 것이다. */
.modal .modal-footer .btn {
    height: var(--ds-btn-h);
    padding: 0 1rem !important;   /* Metronic `.btn:not(…)×6` 이 (0,7,0) 이라 !important 가 필요하다 */
    display: inline-flex;
    align-items: center;
    font-size: var(--ds-font-body);
    line-height: 1;
}

/* 📐 **모달 머리·발의 상하 여백은 카드와 같은 계열이다** (2026-08-18 사용자 지적 — 폰 실기기)

   > *"「마진율 변경 예약 추가」 모달에서 제목 줄과 버튼 줄의 위아래가 헐렁하다."*

   🕘 직전 작업이 **버튼 높이**만 위에서 맞추고 **여백은 벤더 기본 그대로** 두었다.
   🔬 실측(1280px, 관리자 「마진율 변경 예약 추가」):
        머리 `padding 28px`(1.75rem) → 높이 **84.61px**   ← 제목 글자는 27.61px 뿐이다
        발   `padding 24px`(1.5rem) + `.modal-footer>*{margin:.25rem}` → 높이 **92.19px**
             ← 버튼(35.19px)보다 **여백이 더 크다**
   🔑 값은 **2절·2-1절의 카드 규격 그대로**다 — 머리 `.6rem` · 발 `.9rem`.
      🔬 그러면 머리 높이가 **47.8px** 로 `.ds .card > .card-header` 의 **48px** 과 맞고,
         발 높이는 **65.0px** 로 카드 푸터와 같아진다. 🔥 여기에만 다른 숫자를 쓰지 말 것.
   🔑 **자식 여백(`.25rem`)을 걷고 `gap` 으로 바꾼다** — 카드 푸터(2-1 절)와 **같은 방식**이다.
      🔥 `margin` 으로 두면 **줄이 접힐 때 접힌 줄까지 들여써진다**(9절 배지에서 실제로 겪은 그 결함이다).
      📌 `me-auto` 같은 정렬 유틸리티는 `!important` 라 그대로 산다.
   ⚠️ **좌우는 건드리지 않는다** — 넓은 화면은 벤더 기본, 좁은 화면은 이미 아래 11절이 1rem 으로 맞춰 뒀다.
   ⚠️ **`!important` 를 붙이지 말 것** — 일부러 다르게 준 모달(`pb-0 border-0` 닫기 전용 머리,
      상품 이미지 라이트박스)까지 덮는다. 지금은 (0,2,0) 이라 벤더(0,1,0)만 이기고 유틸리티에는 진다.
      🕘 손으로 좁혀 두었던 `py-3`·`py-5` 는 **템플릿에서 걷어냈다**(그것이 이 규칙이 대신할 일이다).
   ⚠️ **`.ds` 를 붙이면 안 된다** — 모달은 `#kt_content.ds` 바깥이다(위 3절과 같은 함정). */
.modal .modal-header {
    padding-top: .6rem;
    padding-bottom: .6rem;
}
.modal .modal-footer {
    padding-top: .9rem;
    padding-bottom: .9rem;
    gap: .5rem;
}
.modal .modal-footer > * { margin: 0; }

/* input-group 은 구성원 높이가 하나라도 다르면 줄이 깨진다.
   .form-control 만 줄이면 addon(.input-group-text)·버튼이 46px 로 남아 어긋난다
   → 그룹 안의 모든 구성원을 같은 높이로 맞춘다.
   버튼 padding 에 !important 인 이유: Metronic `.btn:not(...)×6` = (0,7,0) */
.ds .input-group > .form-control,
.ds .input-group > .form-select,
.ds .input-group > .input-group-text,
.ds .input-group > .btn {
    height: var(--ds-control-h);
    min-height: 0 !important;
    font-size: .9rem;
    display: flex;
    align-items: center;
}
/* 🔴 검색영역 여백의 기준은 **검색어 입력창의 좌측 여백 `.7rem`(11.2px)** 이다 (2026-09-07 사장님 지시).
   🔥 `.6rem`·`.9rem` 처럼 제각각인 값으로 되돌리지 말 것 — 한 줄에 선 칸들의 글자 시작선이 어긋난다. */
.ds .input-group > .input-group-text { padding: 0 .7rem !important; }
/* 🔴 기준 `.7rem`(위 참고). 🔬 `.9rem`(14.4px)이던 때 「검색」 버튼은 안쪽이 24px 인데
   글자가 26px 이라 **이미 잘리고 있었다** — 기준으로 줄이니 안쪽 30.6px 이 되어 오히려 해소됐다. */
.ds .input-group > .btn { padding: 0 .7rem !important; justify-content: center; }
.ds .input-group > .input-group-text svg { width: 16px; height: 16px; }

/* 🔴 **목록 머리의 「신규 등록」은 어느 폭에서든 오른쪽 끝이다** (2026-09-01 사장님 지시 · 항목 67 ②)
   🔬 실측(전): 768·900·1100 에서 검색줄이 길어 툴바가 **다음 줄로 접히는데**, 카드 머리의
      `justify-content: space-between` 은 **혼자 남은 항목을 왼쪽에** 둔다 → 등록 버튼이
      「아래 왼쪽」에 섰다. 1440·1920 은 한 줄에 들어가 오른쪽이라 **폭마다 자리가 달랐다.**
   🔑 접힌 줄을 오른쪽으로 미는 것은 `margin-left: auto` 하나다(`justify-content` 로는 안 된다 —
      그것은 **줄 전체**를 정하지 그 줄에 혼자 있는 항목을 밀지 못한다).
   ⚠️ **좁은 화면(≤767.98px)의 짝이 따로 있다** — 거기서는 머리가 세로로 쌓이고 툴바에
      `order: -1` + 전폭이 걸려 있어 그 둘을 함께 풀어야 한다(아래 767.98px 블록).
      🔥 **한쪽만 고치면 폭에 따라 자리가 다시 갈린다.**
   📌 대상은 「검색 폼이 있는 머리」의 「폼을 품지 않은 툴바」= 등록 버튼뿐이다(성격으로 고른다). */
.ds .card > .card-header:has(form) > .card-toolbar:not(:has(form)) { margin-left: auto; }

/* 모바일: 검색 input-group 을 한 줄에 욱여넣으면 검색어 칸이 거의 사라진다
   → [날짜 ~ 날짜] / [검색어 + 버튼] 2줄로 스택. */
@media (max-width: 767.98px) {
    /* 컨테이너 px-10(30px)도 좁은 화면에선 과하다. 카드 패딩과 합쳐 좌우 84px 를 먹었다.
       Metronic 유틸이 !important 라 ID 를 함께 물어 이긴다(2,0,0).
       2026-07-17 사용자 지시로 .ds 스코프를 풀어 **모든 페이지**에 적용한다 (이 파일은 전역 로드).
       Trade.css(quote-content)·Main.css 에 있던 동일 값 규칙은 이 규칙이 대체/중복 커버한다. */
    #kt_content #kt_content_container {
        padding-left: 12px !important;
        padding-right: 12px !important;
    }

    /* 모달도 같은 이유로 좌우를 줄인다 (2026-07-22 사용자 지적 "모바일에서 모달 좌우 여백이 너무 크다").
       실측(375px): dialog 여백 6 + `.modal-body` 의 `mx-5`(15) + 패딩(21) = **한쪽 42px**,
       내용이 291px(78%)밖에 못 썼다. 페이지 컨테이너를 12px 로 줄여 놓고 모달만 42px 이었던 셈이다.
       ⚠️ `mx-5 my-5` 는 이 프로젝트 모달 22개 중 13개가 쓰는 **Metronic 관용구**라 한 화면만
          고치면 갈린다 → 여기서 전 모달 공통으로 중화한다(위 컨테이너 규칙과 같은 판단).
       ⚠️ Metronic 유틸(`.mx-5`)이 `!important` 라 (0,2,0)+`!important` 로 맞받아야 이긴다.
       ※ 상하(`my-5`)는 건드리지 않는다 — 세로는 스크롤이 받아 준다. */
    .modal .modal-body {
        margin-left: 0 !important;
        margin-right: 0 !important;
        padding-left: 1rem !important;
        padding-right: 1rem !important;
    }
    .modal .modal-header,
    .modal .modal-footer { padding-left: 1rem !important; padding-right: 1rem !important; }

    /* 헤더와 본문의 좌우 패딩은 반드시 같아야 한다(기준 §2) — 다르면 검색창과 목록의
       끝선이 어긋난다. 좁은 화면에선 2.25rem 이 과해 양쪽 다 1rem 으로 줄인다.
       🔴 **푸터도 같은 값이어야 한다** (2026-08-18 사용자 지적 — 폰 실기기).
          🕘 예전에는 이 줄이 **헤더·본문 둘뿐**이라 푸터만 2.25rem 으로 남았고, 그래서
             **푸터 버튼이 카드 안쪽으로 들여쓰여** 위 내용과 오른쪽 끝선이 어긋났다.
          🔑 이 카드의 네 층(머리·본문·발)은 **끝선이 하나여야 한다** — 한 층만 빠뜨리면
             그 층만 다른 규격으로 보인다. 🔥 셋을 함께 고칠 것. */
    /* 🔴 **세로 패딩은 0 · `min-height:48px` 은 그대로 둔다 — 좌우만 좁힌다** (2026-08-21).
       🕘 예전에는 이 줄이 `padding:.6rem 1rem !important; min-height:0 !important` 였다.
          🔥 **주석이 말하는 의도는 좌우 패딩뿐인데 선언이 세로까지 건드려**, 폰에서 2절의
             「카드 헤더 48px 통일」을 **통째로 무효화**하고 있었다.
       🔬 실측(관리자 1:1 문의 상세, 500px): 「문의 내용」 머리 **47.2px** vs 「답변」 머리 **31.9px** —
          같은 화면의 카드 머리가 제각각이었다. 고친 뒤 **48 / 48**.
          🔑 사용자가 2026-08-20 실주행에서 남긴 **「카드 헤더 세로 크기 통일」** 지침이
             폰에서는 애초에 걸린 적이 없었다는 뜻이다.
       🔬 같은 사고가 랜드사 `Trade.css` 좁은 화면 블록에도 사본으로 있었다(같은 날 함께 고쳤다) —
          **이 줄이 원본이고 그쪽이 지역 사본**이다.
       ⚠️ **검색 폼 머리(`:has(form)`)는 예외로 남긴다** — 그 머리는 세로로 쌓이므로(아래 규칙)
          자기 세로 패딩이 필요하고 `min-height` 도 뜻이 없다. 2절 주석이 그렇게 적어 둔 대로다.
          🔬 실측으로 확인했다: 업체 목록 검색 머리 **372.2px → 372.2px**(변화 없음).
       🔥 세로 패딩을 이 줄에 다시 넣지 말 것 — 넣는 순간 머리 높이가 **내용 크기를 따라** 제각각이 된다. */
    .ds .card > .card-header { padding: 0 1rem !important; min-height: 48px !important; }
    /* 🔴 **검색 폼이 든 머리는 상하좌우가 같다** (2026-08-21 사용자 지시: *"모바일은 상하좌우가
          같으면 좋겠는데?"*). 좌우가 이미 `1rem` 이므로 **네 방향 모두 `var(--ds-sp-4)`(=1rem)** 다.
       🔬 그전에는 `.6rem`(폰 root 12px 기준 **7.2px**)이라 검색 줄이 머리 테두리에 붙어 있었다 —
          넓은 화면은 2026-08-21 에 16px 로 고쳤는데 **이 좁은 화면 블록이 안 따라와** 폰만 남았다.
          📌 `screen-tokens.md` 「좁은 화면 규칙은 카드의 세 층을 함께 고친다」가 말하는 그 함정이다.
       🔥 **생 숫자로 되돌리지 말 것** — 척도 이름(`--ds-sp-4`)이라야 좌우 `1rem` 과 함께 움직인다. */
    .ds .card > .card-header:has(form) { padding: var(--ds-sp-4) !important; min-height: 0 !important; }
    /* 🔴 **2026-08-24 (H5) — `1rem` 을 `--ds-card-x`(17 / 30px)로 바꿨다.**
       🔑 **왜 여기까지 고쳐야 하나** — 새 규격(2-0-2 절)의 본문 규칙은 마크업의 `p-0` 을 존중하려고
          일부러 `!important` 를 안 붙였다. 그래서 **`!important` 가 붙은 이 줄이 이긴다** →
          375px 에서 머리는 17px 인데 본문만 14px(=1rem·root 14) 로 남아 끝선이 갈렸다(실측).
       🔥 `1rem` 으로 되돌리지 말 것 — 폰에서만 머리와 본문의 시작선이 3px 어긋난다. */
    .ds .card > .card-body { padding-left: var(--ds-card-x) !important; padding-right: var(--ds-card-x) !important; }
    .ds .card > .card-footer { padding-left: var(--ds-card-x) !important; padding-right: var(--ds-card-x) !important; }
    /* 🔴 **일부러 `p-0` 을 준 카드 본문은 그 뜻을 존중한다** (2026-08-22 구현 #48).
       🔬 랜드사 작업대 화면을 `.ds` 안으로 옥기면서 드러났다 — 미리보기 판은
          **안쪽 래퍼(`.p-5`)가 여백을 주는 구조**라 카드 본문에 일부러 `p-0` 을 줌다.
          위 규칙이 `!important` 로 그것을 덮으면 **여백이 두 겹**(12 + 15 = 27px)이 되고,
          실측상 폰에서 **368개 요소 중 102개의 폭**이 24px 씩 줄었다.
       🔑 특이도로 이긴다 — `.card-body.p-0` 은 (0,4,0) 이라 위 규칙(0,3,0)보다 세다.
       🔥 `p-0` 을 마크업에서 걷어내는 방식으로 고치지 말 것 — 그러면 넓은 화면에서
          안쪽 래퍼 여백과 겹쳐 미리보기 판이 또 어긋난다. */
    .ds .card > .card-body.p-0 { padding-left: 0 !important; padding-right: 0 !important; }
    /* 🔴 **2026-08-24 (H5) 좌우를 `--ds-card-x` 로 바꿨다** — 좁은 화면에서도 끝선이 하나여야 한다. */
    .ds .ds-filter-bar { padding: .5rem var(--ds-card-x) !important; }

    /* 검색이 여러 줄로 스택되므로 헤더도 세로로 쌓고, 우측 액션 버튼은 전폭으로 내린다.
       (Metronic `.card .card-header{align-items:center}` 를 덮는다)

       ⚠️ **검색/필터 폼이 있는 헤더에만** 적용한다(`:has(form)`).
       상세 화면의 섹션 카드에도 걸렸더니 '메모 추가' 같은 작은 버튼이 **섹션 제목 위로** 올라가
       무엇에 대한 버튼인지 읽히지 않았다 (2026-07-20, 이행 상세에서 발견).
       그쪽은 제목 좌 / 버튼 우의 기본 배치가 맞다.
       판별 기준을 `.ds-search` 가 아니라 `form` 으로 둔 이유: 분쟁·문의 목록처럼
       `.ds-search` 를 안 붙인 검색 헤더도 있어서다. 폼이 있으면 목록/필터 헤더다. */
    .ds .card > .card-header:has(form) { flex-direction: column; align-items: stretch; }
    /* 주 액션은 검색 위로. DOM 은 [검색][액션] 순이지만, 세로로 쌓이면 주 액션이
       검색 버튼 아래에 묻히고 파란 버튼 2개가 붙어 위계가 사라진다.
       🔴 **다만 그 툴바가 검색 폼 자체를 품고 있으면 대상이 아니다** (2026-08-27 · E26).
          🔬 이 규칙은 「툴바 = 등록 버튼」을 전제로 쓰였는데, 화면 다섯 곳은 **검색 폼을
             `.card-toolbar` 안에** 두고 있다(관리자 자동승인 · 여행사 상품/리뷰/견적 목록 ·
             랜드사 매입매출 대조). 그 다섯은 375·430px 에서 **검색줄이 카드 제목보다 위**로 올라가
             무슨 목록의 검색인지 읽히지 않았다(768 이상에서는 제목과 같은 줄이라 더 어긋나 보인다).
          🔑 판정은 **「이 툴바가 검색 폼을 품었는가」** 하나다 — 화면 이름으로 예외를 적지 말 것. */
    .ds .card > .card-header:has(form) .card-toolbar:not(:has(form)) { order: -1; }
    .ds .card > .card-header .card-toolbar .btn.ds-header-action { width: 100%; }
    /* 🔴 **「신규 등록」은 어느 폭에서든 검색줄 「아래 · 오른쪽」이다** (2026-09-01 사장님 지시 ·
          `구현/화면검증-수정대기.md` 항목 67 ②).
       > *"단 신규 등록 버튼을 우측 정렬해야할 듯"*
       🕘 **이 줄은 바로 위 두 규칙(2026-07-17 「주 액션은 검색 위로」)을 뒤집는다.**
          🔬 375px 실측(전): 툴바가 `order: -1` · `width: 317px`(전폭)이라 **검색 조건 위에 전폭 버튼**이
             섰다. 넓은 폭에서는 한 줄에 들어가 안 드러났고, 768~1100 에서는 줄이 접히며 **아래 왼쪽**에 섰다.
          🔥 **「flex-wrap 이라 접히는 순서가 뒤집힌다」는 진단은 절반만 맞다** — 좁은 폭의 실제 원인은
             `order: -1` 이고, 넓은 폭의 원인은 접힌 줄에 정렬이 없는 것이다. **둘 다 고쳐야 한다.**
       🔑 **대상은 성격으로 고른다** — 「검색 폼이 있는 카드 머리」의 「폼을 품지 않은 툴바」,
          즉 **등록 버튼 툴바**뿐이다. 🔥 화면 이름으로 예외를 적지 말 것.
          📌 검색 폼을 툴바 안에 둔 화면 다섯(위 E26 주석)은 `:not(:has(form))` 이 그대로 걸러 낸다. */
    .ds .card > .card-header:has(form) > .card-toolbar:not(:has(form)) {
        order: 0;
        width: auto;
        flex: 0 0 auto;
        margin-left: auto;
    }
    .ds .card > .card-header:has(form) > .card-toolbar:not(:has(form)) .btn.ds-header-action {
        width: auto;
    }
    /* 세로로 쌓이면 검색 영역의 우측 여백(me-5, 데스크톱에서 등록 버튼과 벌리는 용도)이
       그대로 남아 검색창만 15px 좁아진다 → 등록 버튼과 끝선이 어긋난다(2026-07-17 사용자 지적).
       Bootstrap 유틸 `.me-5`(!important)를 이기려면 !important 가 필요하다. */
    .ds .card > .card-header:has(form) .card-title {
        margin-right: 0 !important;
        width: 100%;
    }

    /* 필터 칩 → 드롭다운 (2026-07-17 사용자 결정)
       칩 19개가 랩핑되며 132px 를 먹어 목록이 화면 밖으로 밀렸다. 좁은 화면에서는
       칩 카드를 통째로 숨기고 검색 카드 헤더의 드롭다운(.ds-filter-row)으로 대체한다.
       · 드롭다운은 선택값이 라벨에 남아 **현재 필터를 항상 확인**할 수 있다
         (가로 스크롤 칩은 선택 칩이 스크롤 밖으로 나가면 안 보인다 — 그래서 탈락).
       · 이동은 common.js 의 `.ds-filter-select` 위임 핸들러가 처리한다. */
    .ds .ds-filter-card { display: none !important; }
    .ds .ds-filter-row {
        display: grid !important;
        /* 🔑 **트랙 최소폭은 「첫 항목이 잘리지 않는 폭」이다** (2026-08-18 사용자 실기기 지적:
              *"「여행 카테고리 · 전」 으로 잘린다"*).
           🔬 실측(375px): 트랙 150px − 좌여백 10.5 − 우여백(화살표 자리) 33 − 테두리 2 = **글자칸 104.5px**
              인데 「여행 카테고리 · 전체」는 **117.7px**(영어 `Travel Category · All` 은 **128.7px**)라
              뒤가 잘렸다. 「상태 · 전체」(64.3px)는 짧아서 들어갔다 — 그래서 칸마다 갈려 보였다.
           📐 필요 폭 = 128.7 + 45.5 = **174.2px = 11.61rem** → 여유를 둔 **11.75rem**.
              두 칸이 들어갈 폭이 안 되면 `auto-fit` 이 **스스로 한 칸으로 푼다**(폰) — 넓어지면 다시 둘이 된다.
              🔑 그래서 화면마다 특례를 만들지 않는다. 12화면 15개 셀렉트가 이 한 줄을 함께 쓴다.
           🔥 **px 로 되돌리지 말 것** — 글자 크기 3단계 토글(0-0 절)이 root rem 이라, px 로 박으면
              글자만 커지고 트랙은 그대로여서 큰 글자 사용자에게 다시 잘린다.
           ⚠️ `min(100%, …)` 을 빼지 말 것 — 줄이 11.75rem 보다 좁으면 트랙이 그대로 넘친다. */
        grid-template-columns: repeat(auto-fit, minmax(min(100%, 11.75rem), 1fr));
        /* 🔴 **검색 머리 안의 간격은 한 값이다 — `--ds-search-gap`** (2026-08-23). 🕘 `.4rem` 이었다: 이 줄만 4.8px 이고 바로 아래 검색 격자도 4.8px 인데 **머리의 `gap` 은 9px** 이라 「첫 줄 아래만 여백이 크다」로 보였다. 🔥 생 숫자로 되돌리지 말 것. */
        gap: var(--ds-search-gap);
        width: 100%;
        /* 🕘 **`margin-bottom: .4rem` 을 지웠다** (2026-08-21 사용자 지적: *"카테고리 전체 드롭다운
              박스 다음에 여백만 높아"*). 🔬 실측(375px): 부모 머리가 `flex-column` + `gap: 9px` 인데
              이 자식이 **4.8px 을 더 얹어** 그 칸만 **13.8px**, 나머지 칸은 9px 이었다.
           🔥 **자식에 `margin-bottom` 으로 간격을 주지 말 것 — 부모가 `gap` 으로 정한다**
              (`screen-layout.md` ⑤). 이 한 줄이 그 규칙이 막으려던 바로 그 증상이다. */
    }

    /* 2열 그리드. '~' 를 위해 가운데 auto 열을 두면 그 폭(21px)이 버튼 줄에서도
       그대로 벌어져 '검색 취소 | 검색' 사이가 28px 로 떠버린다.
       날짜 입력창 2개가 나란히 있으면 기간임이 읽히므로 addon 은 모바일에서 모두 숨긴다. */
    .ds .input-group {
        display: grid;
        grid-template-columns: 1fr 1fr;   /* [날짜][날짜] */
        /* 🔴 **줄 간격의 실제 주인은 여기다** (2026-08-23). 🔬 랜드사·여행사의 검색 폼은 직계 자식이 사실상 `.input-group` **하나뿐**이라, 좁은 화면에서 보이는 「줄과 줄 사이」는 전부 이 격자의 `row-gap` 이다 — `form` 에 `row-gap` 을 걸어도 아무 일이 일어나지 않는다. 🕘 `.4rem`(폰 root 12px 기준 4.8px)이었고 머리의 `gap` 은 9px 이라 두 값이 갈려 있었다. 📌 이 앱은 거르는 칸이 `.input-group` 밖이라 이 격자에는 검색어+버튼만 들어간다 — 그래도 **세 앱 한 벌**이므로 같은 값이어야 한다. 🔥 여기를 되돌리면 「검색창 내 여백 통일」이 통째로 풀린다. */
        gap: var(--ds-search-gap);
        align-items: center;
    }
    /* Bootstrap 의 `.input-group > .form-control{width:1%}` 은 flex 에선 flex-grow 가 상쇄하지만
       grid 에선 그대로 먹어 입력창이 19px 로 찌그러진다 → 폭을 되돌린다.

       🔴 **`.form-select` 도 같은 규칙을 받아야 한다** (2026-08-17 사용자 지적:
          *"화살표만 보이고 글자가 하나도 안 보여 무엇을 고르는 칸인지 알 수 없다"*).
       🔬 Bootstrap 의 그 선언은 `.input-group > .form-control, .input-group > .form-select` **둘 다**인데
          여기에는 `.form-control` 만 적혀 있었다 → 그리드 칸 안에서 셀렉트만 `width:1%` 가 살아남아
          **안쪽 여백(좌 10.5 + 우 33)과 테두리만 남은 45.5px** 로 찌그러졌다(375px 실측).
          글자가 들어갈 자리가 **2px** 라 화살표만 보인 것이 그 지적의 실체다. 고친 뒤 157.5px.
       🔥 이 줄에서 `.form-select` 를 빼지 말 것 — 빼는 순간 **오류 없이** 같은 화면으로 되돌아간다
          (셀렉트가 보이기는 하므로 「열리는가」 검사도, 상태코드 검사도 전부 통과한다).
       ⚠️ `max-width: none` 이 필요한 이유: 화면 몇 곳이 셀렉트에 인라인 `max-width` 를 박아 두었는데
          (넓은 화면에서 통화 칸을 좁히려는 것) 좁은 화면에서는 그것이 칸을 더 조인다. 인라인 스타일은
          `!important` 로만 이긴다.
       🕘 **2026-08-22 에 그 인라인 값들을 걷어내기 시작했다** — 넓은 화면 규격이 이제 **4-6 절**에 있다
          (거르는 칸은 내용만큼, 검색어 칸이 나머지를 먹는다). 🔥 **px 로 되돌리지 말 것.**
          ⚠️ **`max-width: none` 은 그대로 둔다** — 아직 인라인 값이 남은 화면이 있고(거래 목록 6화면),
             없어져도 이 선언은 아무것도 망가뜨리지 않는다. */
    .ds .input-group > .form-control,
    .ds .input-group > .form-select {
        width: 100% !important;
        max-width: none !important;
    }
    /* addon('~'·여백·검색아이콘)은 모바일에서 자리만 차지하므로 모두 숨김 */
    .ds .input-group > .input-group-text { display: none; }
    /* 검색어·버튼은 각각 전폭 한 줄 */
    .ds .input-group > input[type="text"],
    .ds .input-group > .btn { grid-column: 1 / -1; }
    /* 🔴 **거르는 칸이 홀수면 마지막 칸이 줄을 다 쓴다 — 반쪽짜리 빈 자리를 만들지 않는다** (2026-08-23 사용자 지적:
          *"좁은 화면에서 통화 칸이 한 줄에 반만 찬다"*).
       🔬 실측(375px, 여행사 견적 목록): 거르는 칸이 **통화 하나뿐**이라 2열 격자의 왼쪽 한 칸(153.6px)만 차고
          오른쪽 153.6px 이 통째로 비어 있었다. 「전체 통화」는 한 단어라 그 빈 자리가 더 눈에 띈다.
       🔑 **셈은 브라우저가 한다** — `:nth-of-type(2n+1)`(거르는 칸들 중 홀수 번째) + `:last-of-type`(마지막 거르는 칸)
          이 동시에 참인 것은 **짝이 없는 마지막 칸** 하나뿐이다. 칸이 1개면 그 하나, 3개면 셋째, 2·4개면 아무것도
          안 걸린다(둘씩 짝지어 줄을 채운다). 🔑 **화면마다 거르는 칸 수가 다른데 화면별 예외가 필요 없다.**
       🔴 **기간 칸을 반으로 쪼개서 채우지 말 것** — 값 `2026-09-15 ~ 2026-09-17` 이 글자 크기 3단계에서
          **126 / 137 / 147px** 인데 반 칸의 글자 자리는 **113px** 이라 **세 단계 모두 넘친다**.
          기간은 `input[type="text"]` 라 위 규칙이 이미 줄 전체를 준다 — 🔥 그 규칙에서 기간 칸을 빼지 말 것.
       ⚠️ `:nth-of-type` 은 **요소 이름(`select`)** 으로 센다 — 그래서 선택자를 `select.form-select` 로 적는다.
          숨은 `input[type="hidden"]` 은 다른 요소 이름이라 셈에 끼지 않는다.
       📌 **이 앱에는 걸릴 자리가 거의 없다** — 거르는 칸이 `.input-group` 밖(`form.d-flex.gap-3`)에 서고
          거기서는 `w-100` 이 이미 줄 전체를 준다. 그래도 **세 앱 한 벌**이므로 함께 둔다. 🔥 지우지 말 것. */
    .ds .input-group > select.form-select:nth-of-type(2n+1):last-of-type { grid-column: 1 / -1; }
    /* 단, '검색 취소'가 함께 나와 버튼이 2개일 때는 한 줄에 반반(각 1fr).
       버튼이 1개면 :nth-last-of-type(2) 가 매칭되지 않아 위의 전폭 규칙이 그대로 유지된다. */
    .ds .input-group > .btn:nth-last-of-type(2) { grid-column: 1 / 2; }
    .ds .input-group > .btn:nth-last-of-type(2) ~ .btn { grid-column: 2 / -1; }
    /* group 의 모서리 병합 규칙에 눌리지 않게 */
    .ds .input-group > .form-control,
    .ds .input-group > .btn { border-radius: .425rem !important; }
}

/* -------------------------------------------------------------
   4) 표
   ------------------------------------------------------------- */
.ds .ds-table-box {
    border: 1px solid var(--ds-line-soft);
    border-radius: .5rem;
}
.ds .ds-table-box > .table { margin-bottom: 0; }
.ds .ds-table-box thead th {
    background-color: var(--ds-fill);
    border-bottom: 1px solid var(--ds-line-soft);
}
.ds .ds-table-box thead tr:first-child th:first-child { border-top-left-radius: .5rem; }
.ds .ds-table-box thead tr:first-child th:last-child  { border-top-right-radius: .5rem; }
.ds .ds-table-box td,
.ds .ds-table-box th { padding-top: .45rem !important; padding-bottom: .45rem !important; }
/* gs-0 는 첫 칸 padding-left 를 0 으로 만들어 내용이 외곽선에 붙는다.
   `<=767px` 카드형에선 td 패딩이 0 이어야 하는데, `td:first-child` 는 (0,3,1) 이라
   카드형 규칙 `.ds .ds-card-table tbody td`(0,2,2) 를 이겨버린다 → 데스크톱으로 한정한다. */
@media (min-width: 768px) {
    .ds .ds-table-box th:first-child,
    .ds .ds-table-box td:first-child { padding-left: 1.5rem !important; }
    /* 마지막 칸도 같은 값으로 — Metronic `.table td:last-child{padding-right:0}` 때문에
       삭제 버튼이 표 외곽선에 딱 붙어 있었다(실측 8px, 그것도 버튼 자체 여백이었다).
       첫 칸만 열어두면 좌우가 짝이 안 맞는다. (2026-07-20 사용자 지적) */
    .ds .ds-table-box th:last-child,
    .ds .ds-table-box td:last-child { padding-right: 1.5rem !important; }
}

/* -------------------------------------------------------------
   4-1) 읽기 전용 목록표 — `<=767px` 카드형 전환 (`.ds-card-table`)
   좁은 화면에서 가로 스크롤 표는 우측 열(대개 '관리' = 수정/삭제)이 화면 밖으로
   밀려 조작 자체가 불가능해진다. 행을 그리드 카드로 바꿔 전부 보이게 한다.

   여기까지가 공통 골격이고, **칸 배치(grid-template-areas)는 화면별 CSS 가 정의**한다
   — 표마다 컬럼이 다르기 때문. 기본값은 전폭 1칸씩이라 배치를 안 주면 그냥 쌓인다.
   라벨은 `data-label` 을 ::before 로 출력 → 다국어는 템플릿의 #{} 가 담당.
   ------------------------------------------------------------- */
/* 🧮 **표의 합계는 표 안이 아니라 표 밖 한 줄이다 — `.ds-table-total`** (2026-08-18, 구현 #94 4차)

   🔴 **`<tfoot>` 을 쓰지 말 것.** 바로 아래 카드형 전환(4-1절)은 `thead` 를 감추고 `tbody` 를
      `display: block` 으로 바꾸는데 **`tfoot` 은 손대지 않는다** → 좁은 화면에서 본문은 카드가 되고
      합계 줄만 **표 조각으로 남아** 둘이 따로 논다.
      🔬 전 저장소에서 `ds-card-table` 에 `tfoot` 을 쓴 화면은 **0곳**이었다(그래서 드러난 적이 없다).
   🔑 표 밖으로 빼면 **두 모드에서 같게** 보인다 — 넓은 화면에서는 표 아래 굵은 한 줄,
      좁은 화면에서는 카드 묶음 아래 굵은 한 줄.
   🔢 `tabular-nums` 는 장식이 아니다 — 합계는 위 숫자들과 **자릿수가 세로로 맞아야** 한다.
   📌 발행 문서(견적서·계약서)의 합계는 여기 규격이 아니다 — 그쪽은 `menuLevel/Trade.css` 의
      `.invoice-total`(문서 양식)이고 랜드사 앱과 같아야 한다. */
.ds .ds-table-total {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: .75rem;
    /* 🔑 **표 상자와 띄운다** (2026-08-18 6차) — 표가 테두리 라운드 박스가 되면서(표 표준),
          이 줄의 윗선이 그 박스의 아랫선에 붙어 **두 줄이 겹쳐 보였다.**
       ⚠️ 값을 줄이지 말 것: 붙으면 「표의 마지막 행」으로 읽혀 합계가 눈에 안 띈다. */
    margin-top: .75rem;
    padding: .65rem .25rem 0;
    border-top: 2px solid var(--ds-line);
    font-size: var(--ds-font-strong);
    font-weight: var(--ds-weight-title);
    color: var(--ds-text-strong);
}
.ds .ds-table-total > .ds-table-total-key {
    font-size: var(--ds-font-field-k);
    font-weight: var(--ds-weight-label);
    letter-spacing: var(--ds-track-field-k);
    color: var(--ds-text-label);
}
.ds .ds-table-total > .ds-table-total-val { font-variant-numeric: tabular-nums; }

@media (max-width: 767.98px) {
    .ds .ds-card-table thead { display: none; }
    .ds .ds-card-table,
    .ds .ds-card-table tbody { display: block; }
    /* (0,2,2) — Metronic `.table.table-row-dashed tr`(0,2,1) 의 점선 밑줄을 덮는다 */
    .ds .ds-card-table tbody tr {
        display: grid;
        grid-template-columns: 1fr auto;
        gap: .2rem .75rem;
        align-items: center;
        border: 1px solid var(--ds-line-soft);
        border-radius: .5rem;
        /* Metronic 의 `.table tr:first-child{padding-left:0}` / `.table tr:last-child{padding-right:0}` 는
           tr 이 display:table-row 일 땐 무해하지만, 카드형으로 display:grid 를 주는 순간 살아나
           첫/마지막 카드만 좌우가 어긋난다 → 좌우를 개별 속성으로 못박는다. */
        padding: .7rem .9rem;
        padding-left: .9rem !important;
        padding-right: .9rem !important;
        margin-bottom: .6rem;
    }
    /* text-center 등 부트스트랩 유틸이 !important 라 이쪽도 !important 가 필요하다 */
    .ds .ds-card-table tbody td {
        display: block;
        grid-column: 1 / -1;
        border: 0 !important;
        padding: 0 !important;
        text-align: left !important;
    }
    .ds .ds-card-table tbody td[data-label]::before {
        content: attr(data-label) " :";
        margin-right: .3rem;
        color: var(--ds-text-label);
        font-weight: 400;
    }
    /* ===== 정렬 기준 — **표마다 나열하지 말고 여기서 한 번에** (2026-07-21 사용자 지시) =====
       · 항목명이 **붙는** 칸(`data-label` + `::before` 표시) → `라벨: ……… 값` 두 끝, 값은 우측.
       · 항목명을 **숨긴** 칸(`::before{content:none}`)        → 좌측 정렬이 기본.
       예전엔 표마다 칸 이름을 일일이 나열해서 칸을 추가할 때마다 네 곳(배치·콜론·정렬·nowrap)을
       손대야 했고 **실제로 계속 빠뜨렸다.** 골격에 두면 새 칸도 자동으로 따른다.
       ⚠️ 개별 칸을 다르게 하려면 **이 규칙 뒤에** 자기 규칙을 두면 된다(같은 특이도는 뒤가 이긴다). */
    .ds .ds-card-table tbody td[data-label] {
        display: flex;
        justify-content: space-between;
        align-items: baseline;
        gap: .75rem;
        text-align: right !important;
    }

    /* 카드형이라 넘칠 게 없다. 남겨두면 빈 가로 스크롤바가 생긴다.
       행마다 테두리가 생기므로 표 외곽선도 뺀다(견적 상세와 동일).
       (:has() 대신 래퍼에 클래스를 직접 준다) */
    .ds .ds-card-table-box { overflow-x: visible; border: 0 !important; }

    /* 카드에서만 뺄 칸 — PC 표에는 그대로 남는다(순번, 부차 정보 등). (2026-07-20)
       좁은 화면에서 카드가 길어지면 정작 중요한 값이 스크롤 밖으로 밀린다. */
    .ds .ds-card-table tbody td.ds-card-hide { display: none !important; }

    /* **값이 빈 칸은 카드에서 숨긴다.** NULL 허용 컬럼이 많은 표(예: 벤더 관리)에서
       라벨만 줄줄이 남아 카드가 빈 항목 목록처럼 보였다. 표에서는 열 정렬이 필요해 그대로 둔다. */
    .ds .ds-card-table tbody td:empty { display: none !important; }

    /* 카드의 제목 칸 — 그 행이 무엇인지 알려주는 값(이름·제목).
       라벨(data-label)을 달지 말 것: '사업자명 테스트 벤더' 처럼 군더더기가 된다.
       제목은 커지고, 나머지 값들만 라벨을 달아 구분한다. */
    .ds .ds-card-table tbody td.ds-card-title {
        font-size: 1.05rem;
        font-weight: 700;
        color: var(--ds-text);
        margin-bottom: .25rem;
        grid-column: 1;
        grid-row: 1;
    }
    /* 🔴 **행에 액션 칸이 없으면 제목도 줄 전체를 쓴다** (2026-09-01 · 항목 79 ⑧)
       🔬 **증상**: 카드형 표에서 **첫 칸만 오른쪽 여백이 9px 넓었다**(여행사 마이페이지 세 표 전부 —
          구성원·약관 동의 내역·로그인 로그. 첫 칸 오른쪽 **38px** / 나머지 **29px**).
       🔬 **원인은 배지가 아니다** — 행이 **2열 격자(`269.406px 0px`)** 인데 제목만 `grid-column: 1`
          (첫 열)에 놓이고 나머지 칸은 `grid-column: 1 / -1`(전체)이라, **제목만 `column-gap` 9px 을
          자기 오른쪽에 남긴다.** 🔥 **배지 탓으로 보지 말 것** — 배지가 없는 **로그인 로그의
          첫 칸도 38px** 이었다.
       ✅ **액션 칸(`.ds-card-act`)이 없는 행에서만** 제목을 `1 / -1` 로 편다 —
          액션이 있는 행은 「제목 | 삭제」 두 칸이 그 격자의 목적이므로 그대로 둔다.
       🔥 `.ds-card-act` 쪽을 지우거나 격자를 1열로 바꾸지 말 것 — 삭제 버튼이 제목 아래로 떨어진다.
       🎯 대상은 `ds-card-title` 을 쓰는 **세 앱 카드형 표 전부**다. */
    .ds .ds-card-table tbody tr:not(:has(td.ds-card-act)) td.ds-card-title {
        grid-column: 1 / -1;
    }
    /* 행 액션(삭제) 칸 — 제목과 같은 줄 오른쪽 끝. (2026-07-20)
       계약서 템플릿 목록은 Trade.css 의 grid-template-areas 로 같은 배치를 만드는데,
       그건 그 표의 컬럼 이름(tpl-*)에 묶여 재사용이 안 된다. 여기 두 클래스
       (ds-card-title / ds-card-act)만 붙이면 어느 목록에서든 같은 카드가 나온다.
       ⚠️ grid-template-areas 를 쓰는 표와 섞지 말 것 — areas 가 있으면 그쪽이 이긴다. */
    .ds .ds-card-table tbody td.ds-card-act {
        grid-column: 2;
        grid-row: 1;
        justify-self: end;
        align-self: center;
        display: flex;
        gap: .35rem;
    }
}

/* -------------------------------------------------------------
   4-1-1) **열이 여섯 개 넘는 읽기 전용 표는 카드형 문턱을 올린다** — `.ds-card-table-lg`
          (2026-09-01 · 항목 79 ⑨)

   🔬 **증상**: 여행사 마이페이지 「약관 동의 내역」(열 6개)이 **900px 에서 가로 넘침은 0인데 눌렸다** —
      머리 「구분」이 「구/분」으로 세로로 쪼개지고 「동의 버전」·「동의 일시」의 날짜가 두 줄이 됐다
      (칸 폭 72 · 144 · 128px). 카드형 전환이 `<=767.98px` 뿐이라 그 위 구간이 통째로 비어 있었다.

   🔑 **문턱은 「열 수」가 정한다 — 화면 이름이 아니다**(「비슷한 성격의 요소는 동일한 속성」).
      기준: **읽기 전용 카드형 표 중 열이 6개 이상**이면 이 클래스를 함께 붙인다.
      🔬 그 기준으로 이 앱에서 걸리는 것은 마이페이지 **「구성원」·「약관 동의 내역」 둘**이다
      (「약관 동의 이력」 5열 · 「로그인 로그」 4열은 대상이 아니다).

   🔴 **임계값은 국문·영문 중 큰 쪽으로 잡는다**(`screen-markup.md`). 영문 머리·배지가 더 길다 —
      `Agreed version` + `Updated (2026-01-01)` 배지가 한 칸에 서고 「관리」 열에는 `Withdraw` 가 온다.
      ⚠️ **이 값은 브라우저 실측이 아니라 열별 자연폭 추정치다**(이 작업은 브라우저 사용이 금지돼 있었다) —
      **다음에 화면을 열 때 `screenWidths.measure()` 로 검산하고, 필요하면 이 한 줄만 고칠 것.**
      추정: 약관 표의 자연 폭 ≈ 840px(영문) + 카드 좌우 여백 → 본문 ≈ 915px 필요 →
      본문이 그만큼 되는 뷰포트가 대략 1150~1200px 이라 **Bootstrap `xl` 경계(1199.98px)** 를 골랐다.

   🔑 **아래 블록은 4-1 절의 골격을 그대로 복사한 것**이다 — `@media` 조건에는 변수를 못 쓰므로
      임계값마다 한 벌씩 두는 수밖에 없다(4-4 절 `ds-fit-table` 이 1200·1480 을 그렇게 두고 있다).
      🔥 **4-1 절을 고치면 여기도 함께 고칠 것.** 두 벌이 갈리면 폭에 따라 카드 모양이 달라진다.
   📌 `<=767.98px` 에서는 4-1 절과 이 블록이 **둘 다** 걸린다(규칙이 같아 무해하다).
   ------------------------------------------------------------- */
@media (max-width: 1199.98px) {
    .ds .ds-card-table-lg thead { display: none; }
    .ds .ds-card-table-lg,
    .ds .ds-card-table-lg tbody { display: block; }
    .ds .ds-card-table-lg tbody tr {
        display: grid;
        grid-template-columns: 1fr auto;
        gap: .2rem .75rem;
        align-items: center;
        border: 1px solid var(--ds-line-soft);
        border-radius: .5rem;
        padding: .7rem .9rem;
        padding-left: .9rem !important;
        padding-right: .9rem !important;
        margin-bottom: .6rem;
    }
    .ds .ds-card-table-lg tbody td {
        display: block;
        grid-column: 1 / -1;
        border: 0 !important;
        padding: 0 !important;
        text-align: left !important;
    }
    .ds .ds-card-table-lg tbody td[data-label]::before {
        content: attr(data-label) "\00a0:";
        margin-right: .3rem;
        color: var(--ds-text-label);
        font-weight: 400;
    }
    .ds .ds-card-table-lg tbody td[data-label] {
        display: flex;
        justify-content: space-between;
        align-items: baseline;
        gap: .75rem;
        text-align: right !important;
    }
    .ds .ds-card-table-lg tbody td.ds-card-hide { display: none !important; }
    .ds .ds-card-table-lg tbody td:empty { display: none !important; }
    .ds .ds-card-table-lg tbody td.ds-card-title {
        font-size: 1.05rem;
        font-weight: 700;
        color: var(--ds-text);
        margin-bottom: .25rem;
        grid-column: 1;
        grid-row: 1;
    }
    /* 🔴 액션 칸이 없는 행에서는 제목도 줄 전체를 쓴다 — 4-1 절 항목 79 ⑧과 **같은 이유**다. */
    .ds .ds-card-table-lg tbody tr:not(:has(td.ds-card-act)) td.ds-card-title {
        grid-column: 1 / -1;
    }
    .ds .ds-card-table-lg tbody td.ds-card-act {
        grid-column: 2;
        grid-row: 1;
        justify-self: end;
        align-self: center;
        display: flex;
        gap: .35rem;
    }
    /* Metronic 의 `tbody tr:last-child{border-bottom:0!important}` 때문에 마지막 카드의 밑변만 사라진다
       (4-1 절이 `<=767.98px` 에서 같은 이유로 같은 줄을 갖고 있다). */
    .ds .ds-card-table-lg tbody tr {
        border-bottom: 1px solid var(--ds-line-soft) !important;
    }
}

/* -------------------------------------------------------------
   4-1-3) **행위 배너의 버튼은 좁은 화면에서 전폭이다** — `.ds-action-bar` (2026-09-02 사장님 지시)

   > *"375, 430 에서 버튼이 길어서 두 줄이 되는데 줄바꿈 된 버튼은 전폭을 사용하도록 하면 어때?"*

   🎬 **행위 배너**란 상세 화면 위쪽의 「아이콘 + 문구」(왼쪽) / 「행위 버튼」(오른쪽) 한 줄짜리 카드다
      (이행 시작 · 계약 취소 요청 · 수정 시작 · 취소 승인/거절 · 계약 작성 등).

   🔬 **증상**(실측 2026-09-02, 랜드사 계약 상세 · 영문): 375·430px 에서 「Request contract
      cancellation」 배너의 버튼이 **아랫줄로 내려가는데 제 폭(208px)만 써서** 줄(300px) 오른쪽에
      **92px 이 빈다.** 768px 이상에서는 접히지 않는다.

   🔴 **「접힌 것만」 전폭으로 하지 않는다 — 그러면 언어에 따라 동작이 갈린다.**
      🔬 같은 배너가 **국문 「계약 취소 요청」은 안 접히고 영문만 접힌다.** 접힘 여부로 규칙을 걸면
         한국어 화면과 영어 화면의 배치가 달라지고, 문구가 한 글자만 바뀌어도 그날로 어긋난다.
      🔑 그래서 **문턱은 폭 하나(`767.98px`)** 다 — 이 저장소가 **카드 머리 버튼**을 이미 그렇게
         하고 있다(2-0-2 절 `.btn.ds-header-action { width: 100% }`). 같은 성격이면 같은 속성이다.

   🔑 **버튼이 둘이면 반반으로 나눈다**(`flex: 1 1 0`) — 세로로 쌓지 않는다.
      🔬 승인/거절처럼 짝이 되는 두 버튼을 세로로 쌓으면 **어느 것이 주 액션인지**가 사라진다
         (거래 선택 모달의 「검색 취소 / 검색」이 이미 반반이다 · 항목 72).
   🔑 **글자는 가운데 정렬**한다 — 버튼이 `display:flex` 인데 `justify-content` 가 `normal` 이면
      **`text-align:center` 가 아무 일도 하지 않는다**(`screen-layout.md` C8 에서 실제로 겪었다).

   ⚠️ **`ds-action-bar` 는 표식 클래스다 — 이 이름으로 다른 규칙을 만들지 마라.**
      배너의 모양(테두리 색·여백)은 종전대로 마크업의 유틸리티가 정한다.
   ⚠️ **등록 화면의 「언어 탭 줄」에는 붙이지 마라** — 같은 유틸리티 조합(`d-flex
      justify-content-between flex-wrap gap-3`)을 쓰지만 **행위 버튼 줄이 아니다**
      (2026-09-02 전수에서 셋을 제외했다).
   ------------------------------------------------------------- */
@media (max-width: 767.98px) {
    .ds .ds-action-bar > * {
        flex: 1 1 100%;
        min-width: 0;
    }
    /* 버튼이 줄의 직계 자식인 배너(수정 시작 등) */
    .ds .ds-action-bar > .btn {
        width: 100%;
        justify-content: center;
    }
    /* 버튼이 묶음 안에 있는 배너(이행 시작·취소 요청·취소 승인/거절 등) */
    .ds .ds-action-bar > .d-flex > .btn {
        flex: 1 1 0;
        min-width: 0;
        justify-content: center;
    }
}

/* -------------------------------------------------------------
   4-1-2) **카드 전환을 「뷰포트」가 아니라 「그릇의 폭」으로 판정한다**
          `.ds-cq-card-table-box`(그릇) + `.ds-card-table-cq`(표) — 2026-09-01 · 항목 48 후속

   🔬 **증상**: 여행사 계약 상세의 결제 내역 표(6열)가 **영문에서 머리글 여섯 칸이 전부 두 줄**이 됐다
      (thead 48.5px → **72.5px**).
   🔴 **뷰포트 문턱을 올려서는 못 고친다.** 이 표가 든 그릇은 `.quote-workspace`(2단 작업영역)라
      **뷰포트가 넓어질수록 오히려 좁아지는 구간**이 있다.
      실측(2026-09-01 · 영문 · 사이드바 펼침 · root 16px):
        뷰포트 1250 → 그릇 **830px(2줄)** · 1366 → 946px(1줄) · 1440 → 1020px(1줄) ·
        1600 → 1180px(1줄) · **1920 → 862px(2줄)** ← 이 폭에서 미리보기 단이 붙어 그릇이 줄어든다.
      🔥 **뷰포트와 그릇 폭이 단조 관계가 아니다** → 미디어 쿼리로는 어떤 값을 골라도
         1250 과 1920 을 동시에 잡을 수 없다. 이것이 문턱을 올리는 처방이 실패하는 이유다.
   ✅ **판정을 그릇 폭으로 바꾼다** — `.claude/rules/screen-layout.md` ①
      「반응형 전환은 컨테이너 쿼리 하나로」. 카드 골격은 **4-1-1 과 같은 한 벌**이다.

   🔴 **문턱은 국문·영문 중 큰 쪽으로 잡는다**(`screen-markup.md`). 브라우저 실측(2026-09-01,
      `TreeWalker(SHOW_TEXT)` 로 **텍스트 노드**의 rect 를 모아 줄 수를 세었다 —
      요소에 Range 를 걸면 배지가 2줄로 잡힌다):
        · 국문 — 머리글이 한 줄로 서는 최소 그릇 폭 **732px**(본문까지 한 줄 733px)
        · 영문 — 머리글이 한 줄로 서는 최소 그릇 폭 **882px**(본문까지 한 줄 883px)
      → 큰 쪽(영문 883px) 위로 29px 여유를 두어 **57rem**(root 16px 에서 912px)으로 잡았다.
      🔑 **`rem` 으로 적는 이유**: 글자 크기 3단계(root 16/18/20px · 좁은 화면 12/13/14px)를
         따라와야 한다. `@container` 안의 `rem` 은 **루트 글꼴 크기**를 따르므로 함께 커진다.
         🔥 px 로 박으면 글자를 키웠을 때 표의 자연 폭만 넓어져 **넓은 화면에서도 머리글이 두 줄**이 된다
            (4-4 절이 2026-08-13 에 같은 이유로 임계값을 ×1.23 올렸다).
      ⚠️ 912px 은 「한 줄로 서는 가장 좁은 실측 그릇」(946px · 뷰포트 1366)보다 **34px 아래**다 —
         멀쩡히 들어가는 폭을 카드로 바꾸지 않으면서 883px 을 덮는다.

   🔑 **이 표에서는 `ds-card-table-lg`(뷰포트 ≤1199.98px)를 뗀다.** 두 판정을 겹치면
      「그릇은 넉넉한데 뷰포트가 좁아서 카드」가 생긴다 — 규칙 ①이 금지하는 모양이다.
      📌 `ds-card-table`(≤767.98px)은 **남긴다**: 그 폭에서는 그릇도 반드시 912px 아래라
         두 규칙이 **언제나 같은 답**을 낸다(합집합 = 컨테이너 규칙). 어긋날 수가 없다.
      🕘 **마이페이지 세 표(구성원·약관 동의 내역·이력)는 2026-09-19 에 4-1-1(뷰포트)에서 이 방식으로 옮겼다**
         (랜드사 2회차 H1-02 — 영문 768·992 에서 표가 카드보다 넓었다). 지금 4-1-1(`ds-card-table-lg`)을
         쓰는 마크업은 세 앱 0곳이다 — 규칙은 남겨 두되 새 표에는 이 절을 쓸 것.

   ⚠️ **컨테이너 쿼리는 컨테이너 자신을 스타일할 수 없다**(4-4 절이 같은 함정을 적어 두었다) →
      그릇에 줄 것(`container-type`)과 표에 줄 것(카드 골격)을 갈라 둔다.
   ⚠️ 그릇은 `.table-responsive`(`overflow-x: auto`)를 겸한다 — 카드 모드에서는 내용이 그릇보다
      좁아 **가로 스크롤 막대가 생기지 않는다**(4-4 절에서 같은 구조로 실측된 사실이다).
   ⚠️ `container-type: inline-size` 는 **절대 위치 자식의 기준 상자**가 된다 →
      🔥 **이 그릇 안에 드로워·모달·드롭다운을 넣지 말 것**(지금은 표 하나뿐이라 안전하다).
   ⚠️ **이름을 붙인 컨테이너**(`dscardtable`)다. 이름 없는 `@container` 는 **가장 가까운** 컨테이너에
      붙으므로, 이름을 빼면 4-4 절의 `@container (max-width: 1200px)` 가 이 그릇을 잡아 엉뚱하게 걸린다.
   ------------------------------------------------------------- */
.ds .ds-cq-card-table-box {
    container-type: inline-size;
    container-name: dscardtable;
}
/* 💰 **금액 칸(`td.text-end`)은 줄바꿈하지 않는다** (2026-09-18 여행사 2회차 C7-02)
   🔬 실측(여행사 계약 상세 · 억 단위 계약 · 1200px · 그릇 970px 표 모드): 「+1,100,000,000 KRW」가
      「+1,100,000,000」/「KRW」 두 줄 · 입금일 「2026-09-/17」 두 줄. 금액이 쪼개지면 금액이 아니다
      (`screen-tokens.md` 「금액은 nowrap」). 걸고 나서 같은 폭의 표 자연 폭 **968px = 그릇 968px** 로 들어갔다.
   📌 날짜 칸은 표마다 첫 칸이 날짜가 아니라서 여기서 걸지 않는다 — 여행사 `Trade.css` 가 그 표에만 건다. */
.ds .ds-card-table-cq tbody td.text-end { white-space: nowrap; }

@container dscardtable (max-width: 57rem) {
    .ds .ds-card-table-cq thead { display: none; }
    .ds .ds-card-table-cq,
    .ds .ds-card-table-cq tbody { display: block; }
    .ds .ds-card-table-cq tbody tr {
        display: grid;
        grid-template-columns: 1fr auto;
        gap: .2rem .75rem;
        align-items: center;
        border: 1px solid var(--ds-line-soft);
        border-radius: .5rem;
        padding: .7rem .9rem;
        padding-left: .9rem !important;
        padding-right: .9rem !important;
        margin-bottom: .6rem;
    }
    .ds .ds-card-table-cq tbody td {
        display: block;
        grid-column: 1 / -1;
        border: 0 !important;
        padding: 0 !important;
        text-align: left !important;
    }
    .ds .ds-card-table-cq tbody td[data-label]::before {
        content: attr(data-label) "\00a0:";
        margin-right: .3rem;
        color: var(--ds-text-label);
        font-weight: 400;
    }
    .ds .ds-card-table-cq tbody td[data-label] {
        display: flex;
        justify-content: space-between;
        align-items: baseline;
        gap: .75rem;
        text-align: right !important;
    }
    .ds .ds-card-table-cq tbody td.ds-card-hide { display: none !important; }
    .ds .ds-card-table-cq tbody td:empty { display: none !important; }
    .ds .ds-card-table-cq tbody td.ds-card-title {
        font-size: 1.05rem;
        font-weight: 700;
        color: var(--ds-text);
        margin-bottom: .25rem;
        grid-column: 1;
        grid-row: 1;
    }
    /* 🔴 액션 칸이 없는 행에서는 제목도 줄 전체를 쓴다 — 4-1 절 항목 79 ⑧과 **같은 이유**다. */
    .ds .ds-card-table-cq tbody tr:not(:has(td.ds-card-act)) td.ds-card-title {
        grid-column: 1 / -1;
    }
    .ds .ds-card-table-cq tbody td.ds-card-act {
        grid-column: 2;
        grid-row: 1;
        justify-self: end;
        align-self: center;
        display: flex;
        gap: .35rem;
    }
    /* Metronic 의 `tbody tr:last-child{border-bottom:0!important}` 때문에 마지막 카드의 밑변만 사라진다
       (4-1 · 4-1-1 절이 같은 이유로 같은 줄을 갖고 있다). */
    .ds .ds-card-table-cq tbody tr {
        border-bottom: 1px solid var(--ds-line-soft) !important;
    }
}

/* -------------------------------------------------------------
   4-2) 아이콘 버튼은 **기본 배경 없음** (2026-07-20 사용자 결정)

   Metronic `btn-light-*` 는 연한 배경 사각형을 깐다. 아이콘 하나짜리 버튼에까지 깔리니
   카드마다 색 블록이 늘어서 "배경 때문에 답답해" 보였다. 아이콘 버튼을 새로 만들 때마다
   화면별로 지우고 있었는데(이행 상세에서만 세 번), **여기서 한 번에 끈다.**

   - 대상은 `.btn-icon`(아이콘 전용)만이다. 텍스트가 있는 `btn-light-primary` 버튼은
     배경이 있어야 버튼으로 읽히므로 건드리지 않는다.
   - **색은 유지한다** — 배경이 사라지면 primary/danger 구분이 색밖에 안 남는다.
   - **hover 는 Metronic 기본(solid 배경 + 흰 아이콘)을 그대로 쓴다** (2026-07-22 사용자 지시).
   - `!important` 가 필요하다: Metronic `.btn:not(...)×6` = (0,7,0).

   🔥 **hover 배경만 연하게 덮으면 아이콘이 사라진다** (2026-07-22 사용자 지적으로 발견).
      Metronic 은 hover 를 **두 줄로** 정의한다:
        `.btn.btn-light-danger:hover…{background:#f1416c!important}`  +  `…:hover… i{color:#fff}`
      배경만 `--ds-danger-soft` 로 덮으면 **연분홍 위 흰 아이콘** = 대비 1.1 로 안 보인다.
      → 배경을 바꾸려면 **아이콘 색도 같이** 되돌려야 한다. 그럴 바엔 Metronic 기본이 낫다는 판단이다.
      (`.ds-row-del` 은 이 문제를 알고 `i{color:inherit}` 까지 넣어 두었었다 — 그 자리만 예외였다.)

   ⚠️ **`.ds` 스코프를 쓰지 않는다** — 10절과 같은 이유다. 상세·편집 화면 12개가 `ds` 없이
      `quote-content` 만 쓰기 때문에 `.ds` 로 묶으면 정작 그 화면들이 빠진다.
   ⚠️ `:not(:hover)` 로 **비호버일 때만** 끈다. 그냥 끄면 hover 배경까지 지워진다.
   ------------------------------------------------------------- */
.btn-icon.btn-light-primary:not(:hover),
.btn-icon.btn-light-danger:not(:hover),
.btn-icon.btn-light-success:not(:hover),
.btn-icon.btn-light-warning:not(:hover),
.btn-icon.btn-light-info:not(:hover),
.btn-icon.btn-light:not(:hover) { background-color: transparent !important; }

/* -------------------------------------------------------------
   4-5) 세금계산서 승인번호 — 24자리 (2026-07-21 사용자 지적)

   국세청 전자세금계산서 승인번호는 **24자리**(하이픈 표기 시 8-8-8, 26자)다.
   기본 폭·비례폭 글꼴로는 목록에서 잘리거나 줄바꿈됐다.
   → 등폭 글꼴(자릿수 세기 쉬움) + 자간 정리 + 목록에서 줄바꿈 금지.
   ------------------------------------------------------------- */
/* 🔑 **24자리 세금계산서 승인번호 전용**이다 (2026-07-23 범위 확정).
      그 값은 손으로 옮겨 적으며 자릿수를 세어야 해서 등폭 글꼴이 실제로 도움이 된다.
   ⚠️ **짧은 식별자에 붙이지 말 것** — 사업자번호(10자리)·계좌번호에도 붙어 있었는데,
      정보 목록 안에서 **그 한 줄만 글꼴이 달라 보였다**(사용자 지적). 자릿수를 셀 일이 없는 값은
      본문 글꼴 그대로 두는 것이 맞다. */
.ds .ds-taxno {
    font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
    letter-spacing: .01em;
}
/* 목록·카드에서 승인번호는 한 덩어리다 — 쪼개지면 자릿수 확인이 불가능해진다 */
.ds .sh-tax, .ds td.sh-tax { white-space: nowrap; }
.ds .sh-tax > span:first-child,
.ds .settle-taxno {
    font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
    font-size: .95em;
}

/* -------------------------------------------------------------
   4-0) 🕘 **없앴다 — 「툴바가 없으면 `me-5` 를 지운다」 예외** (2026-09-04 L-3)

   🕘 무엇이었나(2026-07-21 사용자 지적):
      `.ds .card-header > .card-title.flex-grow-1:last-child { margin-right: 0 !important; }`
      표준이 `.card-title.flex-grow-1.me-5`(검색) + `.card-toolbar`(액션 버튼)였는데,
      액션 버튼이 없는 화면(정산 대상·정산 이력)에서는 `me-5` 가 빈 여백으로 남아
      검색창 끝선이 목록 끝선보다 안쪽에 섰다 → 형제가 없으면 그 여백을 지웠다.
   🔴 **되살리지 말 것 — 지울 여백 자체가 없어졌다.** 위 2절이 카드 머리에 `gap` 을 준 뒤로
      `me-5` 는 **이중**이었고, 머리가 줄바꿈되면 툴바 줄은 끝선에 서는데 **제목 줄만 20px 짧아졌다**
      (실측 900px · 랜드사 상품 목록: 제목 오른쪽 795 / 툴바 오른쪽 815 · 마진 20px).
      → 세 앱 템플릿 **18곳**(랜드사 12 · 여행사 6 · 관리자 0)에서 `me-5` 를 걷고 이 예외도 없앴다.
   🔥 **카드 머리의 요소 간격은 `gap` 하나가 정한다 — 마크업에 `me-*` 를 다시 적지 말 것.**
   ------------------------------------------------------------- */

/* -------------------------------------------------------------
   4-4) 열이 많은 표 — **가로 스크롤 대신 카드형** (`ds-fit-table`)

   대조표(8열)는 좁아지면 셀마다 줄바꿈이 났다. 처음엔 min-width 로 가로 스크롤을 만들었는데,
   사용자 결정으로 **가로 스크롤을 없애고 그 전에 카드형으로 바꾼다** (2026-07-20).
   금액 비교표에서 가로 스크롤은 열을 화면 밖으로 밀어내 비교 자체를 못 하게 만든다.

   ⚠️ **뷰포트 미디어쿼리로는 안 된다.** 표가 쓸 폭은 사이드바 상태에 따라 달라진다
   (실측: 뷰포트 2108px → 컨테이너 1720px, 차이 388px). → **컨테이너 쿼리**로 표 자신의 폭을 본다.

   ⚠️ **임계값은 표마다 다르다** — 열 수·내용이 달라 자연 폭이 다르다.
   실측(2026-07-20, root 13px 시절): 대조표 965px / 정산 요청 1110px / 정산 이력 1159px.
   → 기본 `ds-fit-table` **1200px**, 넓은 표는 `ds-fit-lg` 를 함께 붙여 **1480px**.
   🕘 2026-08-13 에 root 를 13px→16px 로 올리면서 **임계값도 같은 비율(×1.23)로** 올렸다
      (980→1200 · 1200→1480). 글자만 키우고 임계값을 그대로 두면 표의 자연 폭이 그만큼
      넓어져 **넓은 화면에서도 카드형으로 바뀐다.**
   **새 표를 붙일 땐 자연 폭(`width:max-content` 로 실측)을 재고 고를 것.**
   `@container` 조건엔 CSS 변수를 못 쓰므로 골격을 임계값마다 한 벌씩 둔다.
   ------------------------------------------------------------- */
.ds .ds-fit-table-box {
    container-type: inline-size;
    /* 🔴 **넘치면 이 상자 안에서만 가로로 민다** (2026-08-27 · D-E27).
       🕘 여기는 원래 `visible` 이었고 근거는 *"좁아지면 카드형이 되므로 넘칠 일이 없다"* 였다.
          🔬 **그 전제가 틀렸다** — 카드형 전환은 상자가 **1200px 아래**로 내려갈 때만 일어나는데,
             열이 많은 표는 상자가 그보다 **넓어도** 자연 폭이 더 넓을 수 있다.
             실측(2026-08-27, 1920px · 영문): 관리자 거래액 통계의 원본 표가 상자 **1500px** 에
             자연 폭 **1879px** → `visible` 이라 그대로 새어 나가 **문서 전체에 가로 스크롤 309px**.
             랜드사 매출 통계(D-E27)도 같은 모양이었다(1969 > 1905).
       🔑 **페이지 본문의 가로 스크롤은 금지**이므로, 넘치는 몫은 **표 상자가 스스로** 삼킨다.
          카드형일 때는 내용이 상자보다 좁아 스크롤 막대가 생기지 않는다.
       ⚠️ `overflow-x: auto` 는 세로도 함께 잘라내므로 **이 상자 안에 드롭다운을 넣지 말 것**
          (실측 2026-08-27: 이 상자를 쓰는 세 앱 17화면에 드롭다운이 **0곳**이라 지금은 안전하다). */
    overflow-x: auto;
    /* ⚠️ **컨테이너 쿼리는 컨테이너 자신을 스타일할 수 없다** — 자손만 대상이다.
       그래서 `@container` 안에서 이 박스의 테두리를 지우려던 규칙은 **조용히 무시됐고**,
       카드형인데도 바깥 테두리가 남아 카드 위에 겹쳐 보였다 (2026-07-21 사용자 지적).
       → 테두리를 박스가 아니라 **표(자손)** 에 준다. 자손이라야 카드 모드에서 끌 수 있다. */
    border: 0 !important;
}
/* 표 모드에서만 보이는 바깥 틀 — 카드 모드에선 아래 @container 블록이 끈다 */
.ds .ds-fit-table {
    border: 1px solid var(--ds-line-soft);
    border-radius: .5rem;
    border-collapse: separate;
    border-spacing: 0;
}
.ds .ds-fit-table th,
.ds .ds-fit-table td { white-space: nowrap; }
.ds .ds-fit-table td.ds-truncate { max-width: 20rem; overflow: hidden; text-overflow: ellipsis; }

/* 기본 임계값 1200px — `ds-fit-lg` 가 붙은 표는 아래 블록이 맡는다 */
@container (max-width: 1200px) {
    .ds .ds-fit-table:not(.ds-fit-lg):not(.ds-fit-xl) thead { display: none; }
    /* 카드 모드: 표의 바깥 틀 제거 (행마다 테두리가 생긴다) */
    .ds .ds-fit-table:not(.ds-fit-lg):not(.ds-fit-xl) { border: 0; border-radius: 0; }
    .ds .ds-fit-table:not(.ds-fit-lg):not(.ds-fit-xl),
    .ds .ds-fit-table:not(.ds-fit-lg):not(.ds-fit-xl) tbody { display: block; }
    .ds .ds-fit-table:not(.ds-fit-lg):not(.ds-fit-xl) tbody tr {
        display: grid;
        grid-template-columns: 1fr auto;
        gap: .15rem .75rem;
        align-items: center;
        /* 겉모습은 **다른 목록 화면의 카드와 같아야 한다** (2026-07-21 사용자 지적).
           그 화면들은 표가 아니라 카드 그리드(`.card.border-gray-300.border-dashed`)를 쓰는데,
           여기만 실선 테두리라 혼자 달라 보였다. 실측값을 그대로 맞춘다. */
        border: 1px dashed var(--ds-line);
        border-radius: .475rem;
        box-shadow: 0 0 20px 0 rgba(76, 87, 125, .02);
        padding: 1rem;
        padding-left: 1rem !important;
        padding-right: 1rem !important;
        margin-bottom: .75rem;
        white-space: normal;
    }
    /* Metronic `.table tbody tr:last-child{border-bottom:0 !important}` 를 맞받는다 */
    .ds .ds-fit-table:not(.ds-fit-lg):not(.ds-fit-xl) tbody tr:last-child {
        border-bottom: 1px dashed var(--ds-line) !important;
    }
    .ds .ds-fit-table:not(.ds-fit-lg):not(.ds-fit-xl) tbody td {
        display: block;
        border: 0 !important;
        padding: 0 !important;
        text-align: left !important;
        white-space: normal;
    }
    .ds .ds-fit-table:not(.ds-fit-lg):not(.ds-fit-xl) tbody td[data-label]::before {
        content: attr(data-label) " :";
        margin-right: .3rem;
        color: var(--ds-text-label);
        font-weight: 400;
    }
    /* ===== 정렬 기준 — **표마다 나열하지 말고 여기서 한 번에** (2026-07-21 사용자 지시) =====
       · 항목명이 **붙는** 칸(`data-label` + `::before` 표시) → `라벨: ……… 값` 두 끝, 값은 우측.
       · 항목명을 **숨긴** 칸(`::before{content:none}`)        → 좌측 정렬이 기본.
       예전엔 표마다 칸 이름을 일일이 나열해서 칸을 추가할 때마다 네 곳(배치·콜론·정렬·nowrap)을
       손대야 했고 **실제로 계속 빠뜨렸다.** 골격에 두면 새 칸도 자동으로 따른다.
       ⚠️ 개별 칸을 다르게 하려면 **이 규칙 뒤에** 자기 규칙을 두면 된다(같은 특이도는 뒤가 이긴다). */
    .ds .ds-fit-table:not(.ds-fit-lg):not(.ds-fit-xl) tbody td[data-label] {
        display: flex;
        justify-content: space-between;
        align-items: baseline;
        gap: .75rem;
        text-align: right !important;
    }

    .ds .ds-fit-table:not(.ds-fit-lg):not(.ds-fit-xl) tbody td.ds-truncate { max-width: none; overflow: visible; text-overflow: clip; }
    /* 계약번호·금액은 한 덩어리라 쪼개지면 안 된다 */
    .ds .ds-fit-table:not(.ds-fit-lg):not(.ds-fit-xl) tbody td.rc-no,
    .ds .ds-fit-table:not(.ds-fit-lg):not(.ds-fit-xl) tbody td.rc-sales,
    .ds .ds-fit-table:not(.ds-fit-lg):not(.ds-fit-xl) tbody td.rc-deposit,
    .ds .ds-fit-table:not(.ds-fit-lg):not(.ds-fit-xl) tbody td.rc-purchase,
    .ds .ds-fit-table:not(.ds-fit-lg):not(.ds-fit-xl) tbody td.rc-profit { white-space: nowrap; }
    /* 카드 안 들여쓰기 되돌림: `.ds .ds-table-box td:first-child{padding-left:1.5rem!important}`(0,3,1) 가
       카드 규칙(0,2,2)을 이겨 카드가 통째로 36px 밀렸다. 그 규칙은 `min-width:768px` 로만 막혀 있는데
       여기 전환은 **컨테이너 폭** 기준이라 넓은 화면에서도 카드가 될 수 있다. */
    .ds .ds-fit-table-box.ds-table-box tbody td:first-child,
    .ds .ds-fit-table-box.ds-table-box tbody td:last-child {
        padding-left: 0 !important;
        padding-right: 0 !important;
    }
}

/* 넓은 표 — 골격은 같고 임계값만 1480px */
@container (max-width: 1480px) {
    .ds .ds-fit-lg thead { display: none; }
    /* 카드 모드: 표의 바깥 틀 제거 (행마다 테두리가 생긴다) */
    .ds .ds-fit-lg { border: 0; border-radius: 0; }
    .ds .ds-fit-lg,
    .ds .ds-fit-lg tbody { display: block; }
    .ds .ds-fit-lg tbody tr {
        display: grid;
        grid-template-columns: 1fr auto;
        gap: .15rem .75rem;
        align-items: center;
        /* 겉모습은 **다른 목록 화면의 카드와 같아야 한다** (2026-07-21 사용자 지적).
           그 화면들은 표가 아니라 카드 그리드(`.card.border-gray-300.border-dashed`)를 쓰는데,
           여기만 실선 테두리라 혼자 달라 보였다. 실측값을 그대로 맞춘다. */
        border: 1px dashed var(--ds-line);
        border-radius: .475rem;
        box-shadow: 0 0 20px 0 rgba(76, 87, 125, .02);
        padding: 1rem;
        padding-left: 1rem !important;
        padding-right: 1rem !important;
        margin-bottom: .75rem;
        white-space: normal;
    }
    /* Metronic `.table tbody tr:last-child{border-bottom:0 !important}` 를 맞받는다 */
    .ds .ds-fit-lg tbody tr:last-child {
        border-bottom: 1px dashed var(--ds-line) !important;
    }
    .ds .ds-fit-lg tbody td {
        display: block;
        border: 0 !important;
        padding: 0 !important;
        text-align: left !important;
        white-space: normal;
    }
    .ds .ds-fit-lg tbody td[data-label]::before {
        content: attr(data-label) " :";
        margin-right: .3rem;
        color: var(--ds-text-label);
        font-weight: 400;
    }
    /* ===== 정렬 기준 — **표마다 나열하지 말고 여기서 한 번에** (2026-07-21 사용자 지시) =====
       · 항목명이 **붙는** 칸(`data-label` + `::before` 표시) → `라벨: ……… 값` 두 끝, 값은 우측.
       · 항목명을 **숨긴** 칸(`::before{content:none}`)        → 좌측 정렬이 기본.
       예전엔 표마다 칸 이름을 일일이 나열해서 칸을 추가할 때마다 네 곳(배치·콜론·정렬·nowrap)을
       손대야 했고 **실제로 계속 빠뜨렸다.** 골격에 두면 새 칸도 자동으로 따른다.
       ⚠️ 개별 칸을 다르게 하려면 **이 규칙 뒤에** 자기 규칙을 두면 된다(같은 특이도는 뒤가 이긴다). */
    .ds .ds-fit-lg tbody td[data-label] {
        display: flex;
        justify-content: space-between;
        align-items: baseline;
        gap: .75rem;
        text-align: right !important;
    }

    .ds .ds-fit-lg tbody td.ds-truncate { max-width: none; overflow: visible; text-overflow: clip; }
    /* 계약번호·금액은 한 덩어리라 쪼개지면 안 된다 (`44,200,000` / `KRW` 로 갈렸다) */
    .ds .ds-fit-lg tbody td.sr-no, .ds .ds-fit-lg tbody td.sh-no,
    .ds .ds-fit-lg tbody td.sr-amount, .ds .ds-fit-lg tbody td.sr-req, .ds .ds-fit-lg tbody td.sr-avail,
    .ds .ds-fit-lg tbody td.sr-fee, .ds .ds-fit-lg tbody td.sr-refund,
    .ds .ds-fit-lg tbody td.sh-supply, .ds .ds-fit-lg tbody td.sh-vat, .ds .ds-fit-lg tbody td.sh-settle,
    .ds .ds-fit-lg tbody td.sh-reqdate,
    /* ⚠️ 금액 칸은 **전부** 나열해야 한다 — `sd-supply`/`sd-vat` 를 빠뜨려
       `20,000,000` / `KRW` 로 갈렸다 (2026-07-21 사용자 지적). 칸을 추가할 때 같이 넣을 것. */
    .ds .ds-fit-lg tbody td.sd-amount,
    .ds .ds-fit-lg tbody td.sd-supply,
    .ds .ds-fit-lg tbody td.sd-vat { white-space: nowrap; }
    /* 카드 안 들여쓰기 되돌림: `.ds .ds-table-box td:first-child{padding-left:1.5rem!important}`(0,3,1) 가
       카드 규칙(0,2,2)을 이겨 카드가 통째로 36px 밀렸다. 그 규칙은 `min-width:768px` 로만 막혀 있는데
       여기 전환은 **컨테이너 폭** 기준이라 넓은 화면에서도 카드가 될 수 있다. */
    .ds .ds-fit-table-box.ds-table-box tbody td:first-child,
    .ds .ds-fit-table-box.ds-table-box tbody td:last-child {
        padding-left: 0 !important;
        padding-right: 0 !important;
    }
}

/* 아주 넓은 표 — 골격은 같고 임계값만 **120rem**(root 16 에서 1920px) (2026-09-02 · L-7·A-3)
   🔬 **왜 세 번째 단계가 필요했나** — 통계 「원천 데이터」 표는 열이 11~12개라 자연 폭이
      **영문 최대 1,914px**(관리자 매출)이다. `ds-fit-lg`(1480px)로는 카드가 안 되고,
      1920px 창에서도 표 상자는 **1,500px** 뿐이라 **그대로 가로 스크롤**이 났다
      (실측: 관리자 매출 414px · 거래 271px · 랜드사 매출 223px 넘침).
   🔴 이 저장소 규칙: *「열이 많은 표는 가로 스크롤 대신 카드형 — 가로 스크롤은 열을 화면 밖으로
      밀어내 비교 자체를 못 하게 만든다」*(`screen-markup.md`).
   🔑 **열을 줄여서 풀 수 없다** — 11개 열이 각각 110~264px 로 전부 정상 폭이다(실측).
   🔥 **`ds-fit-lg` 를 120rem 으로 올리지 말 것** — 정산 요청·이력 표(자연 폭 ≈1,425px)와
      통계 7화면(819~1,477px)까지 **1920px 화면에서 카드가 된다**. 잘 들어가는 표를 접는 것은 손해다.
   🔑 **`rem` 이라 문턱이 글자 크기 3단계를 따라 커진다**(root 12 → 1,440px · 16 → 1,920px · 20 → 2,400px).
      상자 폭(px)은 그대로이므로 「아주 크게」에서 한 단계 먼저 카드가 된다 — 의도한 동작이다.
   📌 **새 표를 이 단계에 붙일 때는 자연 폭을 `width:max-content` 로 재고 고를 것**
      (국문·영문 중 **큰 쪽**. 실측상 영문이 늘 더 넓었다). */
@container (max-width: 120rem) {
    .ds .ds-fit-xl thead { display: none; }
    /* 카드 모드: 표의 바깥 틀 제거 (행마다 테두리가 생긴다) */
    .ds .ds-fit-xl { border: 0; border-radius: 0; }
    .ds .ds-fit-xl,
    .ds .ds-fit-xl tbody { display: block; }
    .ds .ds-fit-xl tbody tr {
        display: grid;
        grid-template-columns: 1fr auto;
        gap: .15rem .75rem;
        align-items: center;
        /* 겉모습은 **다른 목록 화면의 카드와 같아야 한다** (2026-07-21 사용자 지적).
           그 화면들은 표가 아니라 카드 그리드(`.card.border-gray-300.border-dashed`)를 쓰는데,
           여기만 실선 테두리라 혼자 달라 보였다. 실측값을 그대로 맞춘다. */
        border: 1px dashed var(--ds-line);
        border-radius: .475rem;
        box-shadow: 0 0 20px 0 rgba(76, 87, 125, .02);
        padding: 1rem;
        padding-left: 1rem !important;
        padding-right: 1rem !important;
        margin-bottom: .75rem;
        white-space: normal;
    }
    /* Metronic `.table tbody tr:last-child{border-bottom:0 !important}` 를 맞받는다 */
    .ds .ds-fit-xl tbody tr:last-child {
        border-bottom: 1px dashed var(--ds-line) !important;
    }
    .ds .ds-fit-xl tbody td {
        display: block;
        border: 0 !important;
        padding: 0 !important;
        text-align: left !important;
        white-space: normal;
    }
    .ds .ds-fit-xl tbody td[data-label]::before {
        content: attr(data-label) "\00a0:";
        margin-right: .3rem;
        color: var(--ds-text-label);
        font-weight: 400;
    }
    /* ===== 정렬 기준 — **표마다 나열하지 말고 여기서 한 번에** (2026-07-21 사용자 지시) =====
       · 항목명이 **붙는** 칸(`data-label` + `::before` 표시) → `라벨: ……… 값` 두 끝, 값은 우측.
       · 항목명을 **숨긴** 칸(`::before{content:none}`)        → 좌측 정렬이 기본.
       예전엔 표마다 칸 이름을 일일이 나열해서 칸을 추가할 때마다 네 곳(배치·콜론·정렬·nowrap)을
       손대야 했고 **실제로 계속 빠뜨렸다.** 골격에 두면 새 칸도 자동으로 따른다.
       ⚠️ 개별 칸을 다르게 하려면 **이 규칙 뒤에** 자기 규칙을 두면 된다(같은 특이도는 뒤가 이긴다). */
    .ds .ds-fit-xl tbody td[data-label] {
        display: flex;
        justify-content: space-between;
        align-items: baseline;
        gap: .75rem;
        text-align: right !important;
    }

    .ds .ds-fit-xl tbody td.ds-truncate { max-width: none; overflow: visible; text-overflow: clip; }
    /* 계약번호·금액은 한 덩어리라 쪼개지면 안 된다 (`44,200,000` / `KRW` 로 갈렸다) */
    .ds .ds-fit-xl tbody td.sr-no, .ds .ds-fit-xl tbody td.sh-no,
    .ds .ds-fit-xl tbody td.sr-amount, .ds .ds-fit-xl tbody td.sr-req, .ds .ds-fit-xl tbody td.sr-avail,
    .ds .ds-fit-xl tbody td.sr-fee, .ds .ds-fit-xl tbody td.sr-refund,
    .ds .ds-fit-xl tbody td.sh-supply, .ds .ds-fit-xl tbody td.sh-vat, .ds .ds-fit-xl tbody td.sh-settle,
    .ds .ds-fit-xl tbody td.sh-reqdate,
    /* ⚠️ 금액 칸은 **전부** 나열해야 한다 — `sd-supply`/`sd-vat` 를 빠뜨려
       `20,000,000` / `KRW` 로 갈렸다 (2026-07-21 사용자 지적). 칸을 추가할 때 같이 넣을 것. */
    .ds .ds-fit-xl tbody td.sd-amount,
    .ds .ds-fit-xl tbody td.sd-supply,
    .ds .ds-fit-xl tbody td.sd-vat { white-space: nowrap; }
    /* 카드 안 들여쓰기 되돌림: `.ds .ds-table-box td:first-child{padding-left:1.5rem!important}`(0,3,1) 가
       카드 규칙(0,2,2)을 이겨 카드가 통째로 36px 밀렸다. 그 규칙은 `min-width:768px` 로만 막혀 있는데
       여기 전환은 **컨테이너 폭** 기준이라 넓은 화면에서도 카드가 될 수 있다. */
    .ds .ds-fit-table-box.ds-table-box tbody td:first-child,
    .ds .ds-fit-table-box.ds-table-box tbody td:last-child {
        padding-left: 0 !important;
        padding-right: 0 !important;
    }
}

/* 매입매출 대조표는 **표가 아니라 `div.card` 그리드**(9절 `.ds-list-card`)다 — 2026-07-21 전환.
   여기 있던 `@container (max-width:980px)` 표-카드 배치 블록은 `recon-table` 클래스가
   템플릿에서 사라지면서 **선언이 하나도 없는 껍데기**로 남아 있었다(2026-07-23 제거).
   ⚠️ 그때 얻은 교훈은 CLAUDE.md 에 있다 — `:not()` 이 들어간 골격 선택자는 안쪽까지 세므로
      특이도가 한 단계 높다(그래서 `계약번호:` 라벨 숨김이 조용히 무시됐다). */

/* 정산 요청 / 정산 이력 카드 배치 — **임계값은 `ds-fit-lg` 와 같은 1480px** 이어야 한다.
   골격과 배치가 다른 폭에서 켜지면 칸 이름만 붙고 배치표가 없어 전부 겹쳐 쌓인다.
   순번(#)은 카드에서 뺀다 — 목록 위치일 뿐 정보가 아니고 '총 N개'가 이미 규모를 알려준다. */
@container (max-width: 1480px) {
    /* 2026-07-21 사용자 지적으로 재설계. 세 가지가 문제였다:
       ① **계약 상태(종료) 배지가 카드 우측 상단**에 있어 '정산이 종료됐다'로 읽혔다.
          → 계약번호 **바로 옆**으로 붙인다(계약의 상태임이 자리로 드러난다).
       ② 금액과 금액 아닌 항목이 똑같이 좌측 정렬로 줄줄이 나열됐다.
          → 금액 블록은 구분선 아래 `라벨 … 값(우측)` 으로 갈라 놓는다.
       ③ 신청 버튼이 카드 좌측 하단에 파묻혀 있었다. → 우측 하단(주 액션 자리).

       ⚠️ 1열을 `auto` 로 두면 계약번호 폭에 맞춰지고, 배지가 그 옆(`justify-self:start`)에 붙는다.
          `1fr auto` 로 두면 배지가 카드 끝까지 밀려 예전 문제로 되돌아간다. */
    
    
    /* 계약번호는 이제 **본문**이다 — 머리글이 아니므로 크기를 키우지 않는다 */
    
    /* ===== 카드 **머리** = 신청일시 + 상태. 정산 대상 목록 카드와 같은 규격(배경 없이 점선만).
       ⚠️ `column-gap: 0` + 상태 칸 `stretch` 가 없으면 선이 중간에서 끊긴다(다른 카드에서 겪은 함정).
       ⚠️ 두 칸 `align-self: stretch` — 높이가 다르면 선이 어긋난다. */
    
    
    
    
    /* 여긴 **정산 건의 상태**라 우측 상단이 맞다 (정산 대상 목록의 '계약 상태' 와 다르다) */
    
    
    
    
    
    /* ===== 카드 **발** = 액션 (2026-07-21 사용자 지시) — 정산 대상 목록 카드와 같은 규격.
       머리(신청일시·상태)와 짝을 이뤄 `머리 / 본문 / 발` 세 구역이 된다. 배경 없이 점선만.
       ⚠️ 이 표는 `ds-fit-lg` 공용 골격이라 `tr` 에 `padding:1rem` 이 있다 → 선을 카드 끝까지
          빼려면 **음수 마진**이 필요하다(골격의 패딩을 건드리면 같은 골격을 쓰는 다른 표가 흔들린다).
       ⚠️ `justify-self: end` 로 두면 칸이 **버튼 폭**이라 선도 거기까지만 그어진다 —
          칸은 `stretch` 로 늘리고 **버튼만** `text-align: right` 로 민다(이 함정은 이미 두 번 밟았다). */
    /* 🐛 **이 세션에서 네 번째로 밟은 함정.** `.ds .ds-fit-table-box.ds-table-box tbody
       td:last-child{padding-right:0!important}`(0,4,2)가 이겨 **버튼이 카드 외곽선에 딱 붙는다**(여백 1px).
       머리/발 칸이 `:first-child`·`:last-child` 면 **항상 이 규칙부터 의심할 것.**
       박스 클래스를 함께 물어 (0,5,2)로 올린다. */
    
    
    
    
    
    /* 금액 블록은 정산 대상 목록과 같은 규격 — `라벨 … 값` + 구분선 */
    /* ⚠️ 두 끝으로 벌어진 행에는 **콜론을 붙이지 않는다** (2026-07-21).
       라벨과 값 사이가 이미 비어 있어 콜론만 허공에 남는다(`신청금액:` …… 값).
       콜론은 라벨과 값이 **붙어 있는** 행 전용이다. */
    /* ⚠️ **두 끝으로 벌어진 행에도 콜론을 붙인다** (2026-07-21 사용자 재요청).
       한때 "라벨과 값 사이가 이미 비어 있으니 콜론이 허공에 남는다" 며 뺐다가 되돌렸다 —
       사용자 판단은 **항목명임이 분명히 보이는 쪽**이다. 카드형 라벨은 예외 없이 콜론을 붙인다. */
    /* ⚠️ **날짜도 두 끝 정렬**이다 (2026-07-21 사용자 지적 — 값이 없을 때 `-` 만 왼쪽에 남았다).
       금액만 우측이고 날짜만 왼쪽이면 카드 안에서 값의 끝선이 어긋난다. */
    
    
    
    /* 카드 **머리글(신청일시)** 은 라벨을 달지 않는다 — 정산 대상 상세의 이력 카드와 같은 규격이다.
       (라벨을 달면 머리 칸이 `라벨: 값` 두 끝이 되어 값의 끝선이 아래 금액들과 어긋난다 — 실측 303 vs 338) */
    
    

    /* 정산 대상 **상세**의 신청 이력 (2026-07-21).
       이 표는 우측 열(col-lg-7 ≈ 440px) 안에 있어 6열이 들어갈 자리가 애초에 없다 —
       고치기 전에는 '정산일' 헤더가 글자 단위로 세로로 쪼개져 있었다.
       계약이 이미 정해진 화면이라 계약번호·상품·여행사가 없다. 금액이 머리글이다. */
    .ds .settle-detail-table tbody tr {
        grid-template-columns: 1fr auto;
        grid-template-areas:
            "date    status"
            "supply  supply"
            "vat     vat"
            "amount  amount"
            "confirm confirm"
            "reject  reject"
            "act     act";
    }
    /* ── 발: 행위 버튼 (U6 표준, 2026-07-23 사용자 지시) ──────────────────────
       🔑 **카드 안 행위 버튼은 카드 푸터에 둔다.** 아이콘 삭제(×)·상태 배지만 머리 우측이다.
       🕘 '증빙 제출' 버튼이 머리에 있던 시절, 바로 위 `td.sd-status .btn` 규칙
          (배지 크기 1.7rem)이 **텍스트 버튼까지 22×22 로 줄여** 글자가 잘렸다.
          사용자가 "증빙 제출 UI 를 찾을 수 없다"고 한 원인이 이것이다 — 기능은 정상이었다.
       ⚠️ 버튼이 없을 때는 **칸을 통째로 접는다.** `th:if` 로 td 를 지우면 표 모드에서
          열이 어긋나므로 td 는 늘 두고, 비었을 때 CSS 로 숨긴다(`:not(:has(.btn))`). */
    /* ⚠️ `border-top`·`text-align` 에 **`!important` 가 필수**다 — 골격의
          `.ds .ds-fit-lg tbody td { border:0 !important; text-align:left !important }` 는
          **특이도와 무관하게 이긴다.** 빼면 선이 조용히 사라지고 버튼이 좌측에 붙는다
          (2026-07-23 실측으로 잡았다 — 눈으로는 "왜 선이 없지?" 로만 보인다). */
    .ds .settle-detail-table tbody td.sd-act {
        grid-area: act;
        border-top: 1px dashed var(--ds-line) !important;
        margin: .6rem -1rem 0;
        padding: .6rem 1rem 0 !important;
        text-align: right !important;
    }
    .ds .settle-detail-table tbody td.sd-act::before { content: none; }
    .ds .settle-detail-table tbody td.sd-act:not(:has(.btn)) { display: none; }
    /* 발 버튼 높이는 `.ds-lc-foot` 과 **같은 규격**(`--ds-btn-h`)이다 — 표 골격을 쓴다는 이유로
       여기만 Metronic `btn-sm` 기본값(34.3px)이면 정산 이력 카드(28.6px)와 나란히 놓았을 때 어긋난다.
       ⚠️ padding 은 `!important` 가 필요하다(Metronic `.btn:not(…)×6` = (0,7,0)).
       ⚠️ 높이를 고정하면 세로 가운데 정렬이 풀리므로 `inline-flex` 로 다시 맞춘다. */
    .ds .settle-detail-table tbody td.sd-act .btn {
        height: var(--ds-btn-h);
        padding-top: 0 !important;
        padding-bottom: 0 !important;
        display: inline-flex;
        align-items: center;
    }
    /* ⚠️ 카드 **좌상단을 비워 두지 말 것** (2026-07-21 사용자 지적).
       배지를 자기 행 우측에 두었더니 그 줄 왼쪽이 통째로 비어 카드가 머리가 없어 보였다.
       이 표엔 계약번호 같은 식별자가 없으므로(`TB_SETTLEMENT` 에 정산번호 컬럼 자체가 없다)
       **신청일시가 그 건의 이름**이다 → 좌상단으로 올리고 아래 날짜 행에서는 뺀다.
       ※ 정산번호를 새로 채번하려면 거래번호 쌍코드 체계를 건드려야 한다 — 별건이다. */
    /* ⚠️ **한 카드 안에서 정렬 방식을 섞지 말 것** (2026-07-21 사용자 지적: "꼴사나운 카드 구성").
       금액은 `라벨 … 값` 두 끝으로 벌리고 날짜는 `라벨: 값` 으로 붙여 놨더니
       한 카드에 정렬 체계가 둘이 되어 흐트러져 보였다. **전 행을 두 끝 정렬로 통일**한다 —
       바로 옆 정산 현황 카드(`trade-info-row`)와 같은 규격이라 나란히 놓아도 어긋나지 않는다.
       ⚠️ 그래서 `"supply status"` 처럼 배지를 금액 옆에 두면 안 된다. 그 행만 값의 우측 끝이
          배지 앞에서 끊겨 **아래 금액들과 오른쪽 끝선이 안 맞는다.** 배지는 자기 행(맨 위)으로. */
    .ds .settle-detail-table tbody td.sd-supply,
    .ds .settle-detail-table tbody td.sd-vat,
    .ds .settle-detail-table tbody td.sd-amount,
    .ds .settle-detail-table tbody td.sd-confirm {
        display: flex;
        justify-content: space-between;
        align-items: baseline;
        gap: .75rem;
        text-align: right !important;
    }
    /* ⚠️ 두 끝으로 벌어진 행에는 **콜론을 붙이지 않는다.** 라벨과 값 사이가 이미 비어 있어
       콜론만 허공에 남는다(`공급가액:` … 값). 콜론은 라벨과 값이 **붙어 있는** 행 전용이다. */
    /* ⚠️ **두 끝으로 벌어진 행에도 콜론을 붙인다** (2026-07-21 사용자 재요청).
       한때 "라벨과 값 사이가 이미 비어 있으니 콜론이 허공에 남는다" 며 뺐다가 되돌렸다 —
       사용자 판단은 **항목명임이 분명히 보이는 쪽**이다. 카드형 라벨은 예외 없이 콜론을 붙인다. */

    /* ⚠️ **머리글은 공급가액이다 — 실지급액이 아니다** (2026-07-21 사용자 지적).
       예전엔 실지급액(공급가+세액)이 머리글이라 카드를 눈으로 더하면 화면의 다른 숫자와 안 맞았다:
       카드 22,000,000 + 19,548,000 = 41,548,000 인데 **정산 신청 누계는 39,548,000**.
       이 화면의 모든 금액(계약금액·입금액·잔액·누계)이 **공급가 기준**이고 잔액을 깎는 것도
       `C_SUPPLY_AMOUNT` 다 — 머리글이 홀로 다른 단위면 대조가 안 된다. */
    .ds .settle-detail-table tbody td.sd-supply { grid-area: supply; font-size: 1.05rem; font-weight: 700; }
    /* 머리글이라도 **라벨은 남긴다** — 공급가액과 실지급액이 한 카드에 같이 있어
       숫자만 있으면 어느 쪽인지 구분되지 않는다. 라벨은 값보다 작게. */
    .ds .settle-detail-table tbody td.sd-supply::before { font-size: .95rem; font-weight: 400; }
    .ds .settle-detail-table tbody td.sd-vat { grid-area: vat; }
    /* 실지급액 = 공급가 + 부가세 **합계**다 → 입금 내역 총계(`ff-total-row.is-strong`)처럼 선으로 끊는다.
       ⚠️ 금액 3줄(공급가·부가세·실지급)은 **붙여 두고** 날짜는 아래로 모은다 —
          한때 신청일이 공급가액과 부가세액 사이에 끼어 `공급가 + 부가세 = 실지급액` 흐름이 끊겼다. */
    .ds .settle-detail-table tbody td.sd-amount {
        grid-area: amount;
        font-weight: 600;
        margin-top: .25rem;
        padding-top: .5rem !important;
        border-top: 1px dashed var(--ds-line) !important;
    }
    /* 신청일시 = 카드 머리(좌상단). 라벨 없이 값만 — 배지와 같은 줄이라 짧아야 한다.
       ⚠️ 여기만 `::before` 를 끄므로 **thead 의 '신청일' 은 그대로 둔다**(표 모드에서 필요). */
    /* 신청일시 = 카드 머리글. **입금 내역 카드의 금액(`sp-amount`)과 같은 폰트**로 맞춘다
       (2026-07-21 사용자 요청) — 나란히 놓이는 두 카드의 머리글이 달라 보이면 안 된다.
       ⚠️ 굵기가 **700 이 아니라 500** 인 이유: 상대(`sp-amount`)가 마크업에 `fw-bold` 를 달고 있는데
          Metronic 의 `.fw-bold{font-weight:500!important}` 라 실제로는 500 이다.
          여기에 700 을 주면 "같은 폰트"가 아니게 된다(실측으로 잡았다). */
    /* ⚠️ 머리글 칸은 라벨을 숨겼으므로 **좌측**이다. 공용 `td[data-label]{justify-content:space-between}`
       을 물려받은 채로 두면 자식이 둘 이상 될 때 갑자기 양끝으로 벌어진다 — 명시적으로 되돌린다. */
    .ds .settle-detail-table tbody td.sd-date {
        justify-content: flex-start;
        grid-area: date; font-size: 1.05rem; font-weight: 500;
        color: var(--ds-text-strong);
        text-align: left !important;
    }
    .ds .settle-detail-table tbody td.sd-date::before { content: none; }

    /* ===== 카드 **머리** = 신청일시 + 상태 + 취소 (2026-07-21 사용자 요청) =====
       정산 대상 목록 카드와 같은 규격 — **배경 없이 구분선만**.
       ⚠️ 여기선 **음수 마진**으로 카드 끝까지 뺀다(목록 카드는 `tr` 패딩을 0 으로 두는 방식이었다).
          이 표는 `ds-fit-lg` 공용 골격을 쓰는데, 그 패딩을 건드리면 정산 이력 목록 등
          **같은 골격을 쓰는 다른 표까지** 흔들린다. 음수 마진은 이 표에만 닿는다.
       ⚠️ `column-gap: 0` 이라야 선이 일시와 배지 **사이에서 끊기지 않는다.**
          다른 행은 두 칸을 모두 span 하므로 gap 0 이어도 영향이 없다. */
    .ds .settle-detail-table tbody tr { column-gap: 0; }
    .ds .settle-detail-table tbody td.sd-date,
    .ds .settle-detail-table tbody td.sd-status {
        margin-top: -1rem;
        padding-top: .5rem !important;
        padding-bottom: .5rem !important;
        /* ⚠️ `!important` 필수 — 골격의 `td{border:0!important}` 는 특이도와 무관하게 이긴다 */
        border-bottom: 1px dashed var(--ds-line) !important;
        margin-bottom: .5rem;
        /* ⚠️ `align-self: center` 로 두면 **두 칸의 높이가 달라** 선이 어긋난다 —
           취소(×) 버튼이 일시보다 커서 배지·버튼 쪽 선만 아래로 내려왔다(2026-07-21 사용자 지적).
           `stretch` 로 둘을 같은 높이로 만들고 **내용만** flex 로 세로 가운데 정렬한다. */
        align-self: stretch;
        display: flex;
        align-items: center;
    }
    /* ⚠️ 취소(×) 버튼이 **34×34** 라 이력 카드 머리만 48px, 입금 내역 카드는 36px 이었다
       (2026-07-21 사용자 지적). 나란히 놓이는 두 카드의 머리 높이가 다르면 안 된다.
       → 버튼을 **배지와 같은 22px** 로 줄인다. 그러면 머리 높이가 `22 + 상하 패딩` 으로 같아진다.
       ⚠️ Metronic `.btn:not(…)×6` 이 (0,7,0) 이라 padding/height 는 `!important` 가 필요하다. */
    /* ⚠️ **px 로 박지 말 것.** 배지는 root font-size 를 따라 데스크톱 22px / 모바일 20px 로 변하는데
       버튼만 22px 로 고정하면 좁은 화면에서 다시 2px 어긋난다(실측: root 13px → 12px).
       `1.7rem` 이면 배지와 같이 줄어든다(13×1.7≈22 / 12×1.7≈20).
       🔑 그 값이 곧 **`--ds-badge-h` 토큰**이다 — 🕘 그전에는 `1.7rem` 을 **손으로 베껴** 두었다.
          0-3 절 「인접 규칙」이 *배지 옆 버튼 → `var(--ds-badge-h)`* 라고 이미 못 박고 있고,
          같은 크기를 쓰는 다른 자리(자격증빙 ×·칩·상품 요약 줄)는 전부 토큰을 읽는다.
          🔥 값을 다시 손으로 적지 말 것 — 토큰을 바꾸면 여기만 남는다. */
    .ds .settle-detail-table tbody td.sd-status .btn {
        width: var(--ds-badge-h); height: var(--ds-badge-h);
        min-height: 0 !important;
        padding: 0 !important;
        display: inline-flex; align-items: center; justify-content: center;
    }
    .ds .settle-detail-table tbody td.sd-status .btn i { font-size: .8rem; line-height: 1; }
    .ds .settle-detail-table tbody td.sd-date   { margin-left: -1rem; padding-left: 1rem !important; }
    .ds .settle-detail-table tbody td.sd-status { margin-right: -1rem; padding-right: 1rem !important; }
    /* 정산일은 결과값 — 금액과 같은 두 끝 정렬, 작고 흐리게 */
    .ds .settle-detail-table tbody td.sd-confirm { grid-area: confirm; font-size: .9rem; margin-top: .35rem; }
    .ds .settle-detail-table tbody td.sd-idx { display: none; }
    /* 반려 사유 (2026-07-22).
       ⚠️ **금액 행들의 두 끝 정렬을 따르지 않는다** — 문장이라 우측 정렬하면 읽기 어렵다.
          `라벨: 문장` 으로 붙이고 좌측 정렬한다(카드형 정렬 규칙의 '항목명이 붙는 칸').
       ⚠️ 전폭 칸이라 `white-space: normal` 이 필요하다 — 이 표의 다른 칸은 nowrap 이다. */
    .ds .settle-detail-table tbody td.sd-reject {
        grid-area: reject;
        text-align: left !important;
        white-space: normal;
        margin-top: .35rem;
    }
    /* 🔑 **항목명은 한 줄이다 — 줄어드는 것은 문장 쪽** (2026-09-18 · 랜드사 1회차 S2-21 · 정본 P63ⓕ).
       🔬 칸이 flex 라 `::before`(항목명)도 줄어드는 항목이었다 → 사유 문장이 길면 「반려 사유 :」가
          **두 줄로 쪼개졌다**(라벨 폭 360 34px · 992 40px · 1200 54px, 영문 「Rejection Reason :」 58~70px). */
    .ds .settle-detail-table tbody td.sd-reject::before { flex-shrink: 0; white-space: nowrap; }
    /* 🔑 신청일시(카드 머리)도 한 줄이다 — 영문 배지 「Awaiting Evidence」에 밀려 992 에서
          「2026-09-」/「08 23:54」로 끊겼다(S2-21). 날짜·시각은 끊으면 읽을 수 없다. */
    .ds .settle-detail-table tbody td.sd-date { white-space: nowrap; }
    /* 상태 배지 + 취소 버튼은 **자기 행**(맨 위 우측). 위 ⚠️ 참고 — 금액 행에 얹으면 끝선이 깨진다. */
    .ds .settle-detail-table tbody td.sd-status {
        grid-area: status; justify-self: stretch;
        justify-content: flex-end;   /* 위에서 flex 가 됐으므로 text-align 대신 이것으로 우측 정렬 */
    }
    .ds .settle-detail-table tbody td.sd-status::before { content: none; }

    /* 정산 대상 상세의 입금 내역 (2026-07-21). 합계만 있고 건별이 없어
       "왜 잔액이 이것뿐인가" 를 알 수 없었다. */
    .ds .settle-payment-table tbody tr {
        grid-template-columns: 1fr auto;
        grid-template-areas:
            "amount    status"
            "date      date"
            "depositor depositor";
    }
    /* ⚠️ 머리글 칸은 라벨을 숨겼으므로 **좌측**이다. 공용 `td[data-label]{justify-content:space-between}`
       을 물려받은 채로 두면 자식이 둘 이상 될 때 갑자기 양끝으로 벌어진다 — 명시적으로 되돌린다. */
    .ds .settle-payment-table tbody td.sp-amount    { grid-area: amount; justify-content: flex-start; font-size: 1.05rem; font-weight: 700; white-space: nowrap; }
    .ds .settle-payment-table tbody td.sp-status    { grid-area: status; justify-self: stretch; justify-content: flex-end; }
    /* ===== 카드 **머리** = 금액 + 상태 (2026-07-21 사용자 요청) =====
       정산 신청 이력 카드와 같은 규격 — 배경 없이 점선만, 음수 마진으로 카드 끝까지.
       ⚠️ `column-gap: 0` + `sp-status{justify-self:stretch}` 가 없으면 선이 금액과 배지 사이에서,
          또는 배지 폭에서 끊긴다. ⚠️ 두 칸 `align-self: stretch` — 높이가 다르면 선이 어긋난다. */
    .ds .settle-payment-table tbody tr { column-gap: 0; }
    .ds .settle-payment-table tbody td.sp-amount,
    .ds .settle-payment-table tbody td.sp-status {
        margin-top: -1rem;
        padding-top: .5rem !important;
        padding-bottom: .5rem !important;
        border-bottom: 1px dashed var(--ds-line) !important;
        margin-bottom: .5rem;
        align-self: stretch;
        display: flex;
        align-items: center;
    }
    .ds .settle-payment-table tbody td.sp-amount { margin-left: -1rem; padding-left: 1rem !important; }
    /* 🐛 `.ds .ds-fit-table-box.ds-table-box tbody td:last-child{padding-right:0!important}`(0,4,2)가
       이 칸(마지막 td)을 눌러 **상태 칩이 카드 외곽선에 딱 붙었다**(2026-07-21 사용자 지적 — 여백 1px).
       목록 카드에서 이미 겪은 함정이다. 박스 클래스를 함께 물어 **(0,5,2)** 로 올려 이긴다. */
    .ds .ds-fit-table-box.ds-table-box .settle-payment-table tbody td.sp-status {
        margin-right: -1rem; padding-right: 1rem !important;
    }
    .ds .settle-payment-table tbody td.sp-date      { grid-area: date; }
    .ds .settle-payment-table tbody td.sp-depositor { grid-area: depositor; }
    .ds .settle-payment-table tbody td.sp-amount::before,
    .ds .settle-payment-table tbody td.sp-status::before { content: none; }
}

/* 정산 현황 카드 — 값이 **전부 금액**인 카드 (2026-07-21 사용자 지적).
   ① 숫자와 통화 코드는 한 덩어리다: `44,200,000` / `KRW` 로 갈리면 안 된다.
   ② 금액은 우측정렬(프로젝트 공통 규칙) — 계약 요약 카드(좌측정렬)와 시각적으로 갈린다.
   값마다 클래스를 붙이는 대신 카드에 한 번만 건다. */
.ds .settle-money-rows .trade-info-value {
    white-space: nowrap;
    text-align: right;
}

/* 거부 사유 — 신청 이력 행 안에 붙는다(예전엔 표 바깥에 따로 쌓였다).
   컨테이너 쿼리 **밖**이다: 표 모드에서도 보여야 한다. */
.ds .sd-reject-box {
    margin-top: .5rem;
    padding: .5rem .75rem;
    border-radius: .475rem;
    background: var(--ds-danger-soft);
    white-space: normal;
}

/* =============================================================
   정산 대상 목록 — **전 구간 카드형** (2026-07-21 사용자 요청)

   열이 12개까지 늘면서 표 모드가 깨졌다: `정산 가능 잔액` 칸의 숫자가 사라지고 `KRW` 만 남았다.
   금액이 억 단위가 되면 더 나빠진다 → **넓은 화면에서도 카드로 간다.**
   `ff-purchase-table`(이행 상세 매입 표)과 같은 방식이다 — 컨테이너 쿼리 **밖**에 골격을 둔다.

   ⚠️ 그래서 이 표에는 `ds-fit-table`/`ds-fit-lg` 를 붙이지 않는다. 붙이면 넓을 때
      표 모드 규칙과 이 규칙이 겹쳐 **어느 쪽이 이겼는지 추적이 안 된다**(매입 표에서 겪은 일).
   ⚠️ 표 모드가 없으므로 **행마다 td 개수가 달라도 안전하다** — `th:if` 로 빈 칸을 빼도 된다.
      (표 모드가 있는 표에서는 열이 어긋나므로 금지다.)
   ============================================================= */

/* ⚠️ **한 줄에 카드 하나를 고집하지 않는다** (2026-07-21 사용자 요청).
   화면이 넓으면 2~3개씩 놓는다. `auto-fill` 이라 임계값을 손으로 관리할 필요가 없다 —
   카드가 380px 아래로 내려가지 않는 선에서 열 수가 알아서 정해진다.
   ⚠️ 카드 폭이 좁아지므로 카드 **안**을 다시 2열로 벌리지 말 것(예전에 그렇게 했다가 되돌렸다). */

/* Metronic `.table tbody tr:last-child{border-bottom:0 !important}` 를 맞받는다 */

/* ⚠️ `.ds .ds-table-box td:first-child{padding-left:1.5rem!important}`(0,3,1) 가
   이 규칙을 이겨 **계약번호만 19.5px 들여쓰기**된다 (2026-07-21 사용자 지적 — 이 파일에
   이미 적어둔 함정을 또 밟았다). `padding: 0 !important` 만으로는 부족해
   `padding-left/right` 를 따로 `!important` 로 다시 눌러야 한다. */

/* 🐛 위 규칙(0,2,2)만으로는 **못 이긴다.** `.ds .ds-table-box td:first-child{padding-left:1.5rem!important}`
   가 (0,3,1)이라 클래스 수에서 앞선다 → 실측 19.5px 들여쓰기가 그대로 남았다.
   `:first-child` 를 함께 물어 (0,3,2)로 올려야 이긴다. **둘 다 `!important` 면 특이도로 갈린다.** */

/* ⚠️ 이 표는 **자체 골격**이라 공용 `td[data-label]::before` 규칙이 닿지 않는다 —
   라벨과 정렬 규칙을 **여기서 직접** 줘야 한다. 2026-07-21에 공용 규칙만 믿고 개별 규칙을
   지웠다가 **라벨이 통째로 사라졌다.** 골격을 따로 쓰는 표는 이 짝을 항상 함께 둘 것. */

/* 항목명이 붙는 칸 = `라벨: ……… 값`(우측) / 숨긴 칸 = 좌측 (공용 골격과 같은 기준) */

/* 계약번호·금액은 한 덩어리라 쪼개지면 안 된다 (`44,200,000` / `KRW` 로 갈렸다) */

/* ⚠️ **여행기간·순번(#)은 뺐다** (2026-07-21 사용자 요청 — 항목이 늘어 레이아웃이 깨졌다).
   여행기간은 정산 판단에 쓰이지 않고(계약 상세에 있다), 순번은 목록 위치일 뿐이다.
   ⚠️ 금액 체인의 시작은 **계약금액이 아니라 확인된 입금액**이다.
      계약금액을 맨 위에 두면 `2,000,000 − 120,000 = 1,880,000` 처럼 읽히는데
      실제 잔액은 −120,000 이다(확인 입금이 0이라서). **눈으로 더해서 맞아야 한다.** */

/* ===== 카드 3구역: 머리=식별 / 본문=근거 / 발=결론+액션 (2026-07-21 사용자 결정) =====
   예전엔 전부 한 평면이라 **결론(정산 가능 잔액)이 근거 금액들 틈에 섞여** 있었다.
   ⚠️ `<tr>` 이라 Metronic 의 `.card-header`/`.card-footer` 를 쓸 수 없다 — 배경+구분선으로 흉내낸다.
   ⚠️ 그래서 좌우 여백은 **카드가 아니라 각 칸이** 준다(위 `padding:0`). 칸을 새로 추가하면
      좌우 패딩을 직접 줘야 한다 — 안 주면 그 줄만 카드 끝에 딱 붙는다. */

/* 머리 — 계약번호 + 상태 배지.
   ⚠️ **배경색을 넣지 않는다** (2026-07-21 사용자 요청). 구분선만으로 충분하고,
      카드가 여러 장 깔리면 회색 띠가 반복돼 시끄럽다. */

/* 발 — **액션만**. 배경 없이 구분선만. */

/* ⚠️ 배지와 계약번호는 **같은 정렬**이어야 한다. tbody 가 `align-items:start` 라
   그냥 두면 배지가 6.5px 내려앉아 줄이 안 맞는다 (2026-07-21 사용자 지적). */

/* ⚠️ `justify-self: start` 면 칸이 배지 폭(52px)에 맞춰져 **헤더 띠가 거기서 끊긴다**(실측).
   `stretch` 로 늘리고 배지는 `text-align:left` 로 계약번호 옆에 붙여 둔다. */
/* ⚠️ 배지는 **우측 상단**으로 전 화면 통일 (2026-07-21 사용자 지시).
   🕘 한때 이 목록만 계약번호 **바로 옆(좌측)** 이었다 — '종료' 배지가 우측 상단에 있으니
      '정산이 종료됐다'로 읽힌다는 지적 때문이었다. 지금은 배지가 중립색(`badge-light`)이고
      `title` 로 '계약상태'를 밝히므로 오독 위험이 낮다고 보고 통일 쪽을 택했다.
      **다시 좌측으로 옮길 일이 생기면 그때는 네 화면을 함께 옮길 것.** */

/* ⚠️ 칸을 추가하면 **grid-areas 와 이 목록에 함께** 넣을 것 — 빠뜨리면 자동 배치돼 겹친다 */

/* ⚠️ `justify-self: end` 면 칸이 **버튼 폭(85px)** 이라 발 구분선도 거기까지만 그어진다
   (2026-07-21 사용자 지적 — 금액 구분선과 **같은 종류의 실수**다).
   칸은 전폭으로 늘리고 **버튼만** `text-align:right` 로 우측에 둔다. */

/* 금액 블록 — `라벨 … 값` 두 끝으로 벌린다. td 는 카드 모드에서 `display:block` 이라
   flex 로 되돌려야 `::before`(라벨)와 값이 양끝으로 간다. */
/* ⚠️ **두 끝으로 벌어진 행에도 콜론을 붙인다** (2026-07-21 사용자 재요청).
   한때 "라벨과 값 사이가 이미 비어 있으니 콜론이 허공에 남는다" 며 뺐다가 되돌렸다 —
   사용자 판단은 **항목명임이 분명히 보이는 쪽**이다. 카드형 라벨은 예외 없이 콜론을 붙인다. */

/* 금액 영역의 시작을 선으로 알린다 (여행사까지가 계약 정보).
   ⚠️ 선은 **여행사 칸의 아래**에 그린다. `sr-deposit` 에 `border-top` 을 주면
      그 칸이 `width:min(420px,100%)` 라 **선이 우측 일부만 그어진다**(2026-07-21 사용자 지적).
      여행사 칸은 **이미 카드 전폭**이라(카드 padding 이 0, 좌우 여백은 칸의 padding) 선도 전폭이 된다. */

/* ⚠️ 넓은 화면에서 금액 행이 카드 전폭(1180px)을 쓰면 **라벨과 값 사이가 통째로 빈다**
   (2026-07-21 사용자 지적 — 정산 현황 카드에서 이미 겪은 증상이다).
   → 금액 블록만 우측에 모은다. 좁아지면 `100%` 라 그대로 전폭이 된다. */

/* -------------------------------------------------------------
   4-3) 날짜 입력 — 아이콘·자리표시자를 **네이티브가 안 그리는 곳에서만 우리가 그린다**
        (2026-07-20 신설 → 2026-07-23 재작성)

   🕘 예전엔 브라우저가 그리는 `::-webkit-calendar-picker-indicator` 를 오른쪽 끝으로
      옮기기만 했다. 데스크톱과 **개발자도구 모바일 뷰**에서는 멀쩡했다.
   🔥 그런데 **실기기 iOS(Safari·iOS Chrome 모두 WebKit)는 값이 없는 `input[type=date]` 를
      완전히 빈 칸으로 그리고 달력 아이콘도 그리지 않는다**(2026-07-23 사용자 지적).
      사용자는 그게 날짜 칸인 줄 모른다.
      ⚠️ **개발자도구 모바일 뷰는 데스크톱 렌더링 엔진이라 이 차이를 절대 못 잡는다.**
         "모바일 뷰에서 정상"은 실기기 검증을 대신하지 못한다.

   🔑 **네이티브가 그리는 곳에는 손대지 않는다.** 전 브라우저에 우리 아이콘을 깔면
      **아이콘이 두 개가 된다** — Chrome 계열은 `::-webkit-calendar-picker-indicator` 를
      투명하게 만들 수 있지만 **Firefox 는 그 의사요소를 모른다**(자기 달력 아이콘을 계속 그린다).
      그래서 조건을 좁혀 **"아무것도 안 그리는 플랫폼"에만** 우리 것을 넣는다.
   ⚠️ 조건은 두 개를 **함께** 걸어야 한다:
      · `@supports (-webkit-touch-callout: none)` — WebKit 만. (Blink 는 이 속성을 버렸다)
      · `hover:none + pointer:coarse` — 터치 기기만. **없으면 데스크톱 Safari 까지 걸린다**
        (거기선 네이티브가 `yyyy-mm-dd` 를 그리므로 **글자가 겹친다**).
   ⚠️ 값이 있는지 **CSS 로는 알 수 없다**(`:placeholder-shown` 은 date 에 안 걸린다) →
      `common.js` 의 `Common.initDateInputs` 가 `data-empty`/`data-placeholder` 를 붙인다.
      **CSS 와 JS 는 세트다** — 한쪽만 옮기면 자리표시자가 사라지거나 값 위에 겹친다.
   ⚠️ **`.ds` 스코프를 쓰지 않는다.** 날짜 입력은 `.ds` 가 없는 화면(회원가입·견적 상세·상품 등록)
      에도 있다. **회원가입이 정확히 그 경우라 예전 규칙은 아예 닿지도 않았다.**
   ⚠️ 선택자는 `input.form-control[type="date"]`(0,2,1) 이어야 한다 — 위쪽
      `.ds .form-control{padding:…!important}`(0,2,1) 과 같은 특이도라야 소스 순서로 이긴다.
      `.form-control[type="date"]`(0,2,0) 로 낮추면 **조용히 진다.**
   ------------------------------------------------------------- */
:root {
    /* 달력 아이콘. **URL 이라 회색 토큰으로 표현할 수 없어** 테마별로 한 벌씩 둔다.
       색은 배경 대비 실측값이다(라이트 3.31 / 다크 6.3 — 비문자 요소 기준 3:1 이상). */
    --ds-date-icon: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%238C8C99' stroke-width='1.3' stroke-linecap='round'%3E%3Crect x='1.7' y='3' width='12.6' height='11.3' rx='1.6'/%3E%3Cpath d='M1.7 6.5h12.6M5.2 1.7v2.6M10.8 1.7v2.6'/%3E%3C/svg%3E");
}
:root[data-theme="dark"] {
    --ds-date-icon: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23A0A0AE' stroke-width='1.3' stroke-linecap='round'%3E%3Crect x='1.7' y='3' width='12.6' height='11.3' rx='1.6'/%3E%3Cpath d='M1.7 6.5h12.6M5.2 1.7v2.6M10.8 1.7v2.6'/%3E%3C/svg%3E");
}

/* (가) 네이티브 아이콘이 있는 플랫폼 — 위치만 오른쪽 끝으로 고정한다 (2026-07-20 사용자 요청).
   값 바로 뒤에 붙어 필드 한가운데 뜨던 것을 다른 입력의 우측 아이콘과 줄 맞춘다.
   ⚠️ 의사요소 선택자는 **콤마로 묶지 말 것** — 모르는 접두사가 하나라도 있으면 규칙 전체가 버려진다. */
input.form-control[type="date"] {
    position: relative;
    /* ⚠️ 위쪽 `.ds .form-control{padding:… !important}` 를 이겨야 한다 (실측: 8.4px 로 밀렸었다). */
    padding-right: 2.2rem !important;
}
input.form-control[type="date"]::-webkit-calendar-picker-indicator {
    position: absolute;
    right: .6rem;
    top: 50%;
    transform: translateY(-50%);
    margin: 0;
    padding: 0;
    cursor: pointer;
}

/* (다) **피커를 붙인 칸**(`.js-date` / `.js-datetime` / `.js-daterange`) — `type="text"` 라
   네이티브가 **아무것도 안 그린다**
   🔑 **세 이름의 차이는 「무엇을 고르는가」뿐**이다 — `.js-date` = 날짜 하나(`yyyy-MM-dd`) ·
      `.js-datetime` = 날짜+시각(`yyyy-MM-dd HH:mm`) · `.js-daterange` = **기간 한 칸**
      (`2026-07-01 ~ 2026-08-31`, 규격은 4-4 절). **겉모습은 같아야 하므로 규칙은 한 벌**이다.
      🔥 셋 중 하나라도 빠뜨리지 말 것 — 빠뜨린 쪽만 아이콘을 잃는다.
      🔬 2026-08-17 사용자 지적으로 **`.js-daterange` 가 빠져 있던 것을 잡았다**: 기간 칸을 새 이름으로
         만들면서 이 목록에 더하지 않아, 같은 피커를 쓰는데 **화면에 따라 달력 아이콘이 있다 없다** 했다
         (실기기 확인: 랜드사 견적 목록 · 매출 통계 · 관리자 회원 목록 · 여행사 정산 내역 넷 다 없었다).
      📐 **오른쪽 여백(2.2rem)은 기간 칸 기준으로도 충분하다** — 기간 값은 23자로 날짜 하나(10자)의
         두 배가 넘지만, 칸 폭이 **16rem**(4-4 절)이라 값·자리표시자 모두 아이콘에 닿지 않는다(실측).
         🔥 이 값을 줄이지 말 것 — 줄이면 기간 칸에서 **아이콘이 글자 위를 덮는다.**
   🔬 2026-08-17 사용자 지적: *"피커 나오는 입력창에 플레이스홀더와 달력 아이콘이 사라졌어."*
      🕘 원인은 **`datetime-local` → `text` 전환**이다(피커가 `Y-m-d H:i` 형식을 쓰는데
         `datetime-local` 은 그 글자를 **거부해 값이 빈 채로 저장이 나간다**).
         그 대가로 브라우저가 그려 주던 **달력 아이콘과 형식 안내**를 함께 잃었다.
   🔑 그래서 **(가)·(나)와 같은 모양을 우리가 그린다** — 아이콘은 같은 토큰(`--ds-date-icon`),
      자리는 같은 값(오른쪽 .6rem · 크기 1.05rem)이라 **네이티브 칸과 나란히 놓아도 어긋나지 않는다.**
   🔑 **여기는 조건을 걸지 않는다** — (나)와 달리 이 칸은 **어느 브라우저에서도** 네이티브가
      아무것도 안 그리므로 **전 플랫폼에서 우리가 그린다.** 🔥 아이콘이 두 개가 될 걱정이 없다.
   ⚠️ 자리표시자는 **`placeholder` 속성 그대로** 쓴다(`type="text"` 라 정상 동작한다) —
      🔥 (나)처럼 `data-placeholder`+`::before` 로 흉내 내지 말 것. 그 장치는 `type="date"` 가
         자리표시자를 못 받아서 만든 우회로다. 여기서는 불필요하고 두 벌이 된다.
   🔴 **이 칸들은 `readonly` 다 — 값은 선택기로만 들어온다** (2026-08-19 구현 #97 사용자 지시).
      🔑 **마크업에 적지 않는다** — `common.js` 가 판을 붙이면서 함께 건다(62칸에 손으로 적으면
         새로 만드는 화면이 빠진다). 🔥 그러니 화면에서 `readonly` 를 지우려 하지 말 것.
      ⌨️ 접근성은 그대로다 — 포커스를 받고, Enter·Space·↓ 로 판이 열린다.
      🗑 값을 비우는 것은 **판 아래 「지우기」 버튼**이다(25-4 절).
      🕘 이 자리에는 *"읽기 전용이 아니다 · `readonly` 를 붙이지 말 것"* 이 적혀 있었다 —
         근거가 「손으로 지우는 것이 비우는 유일한 길」이었는데 구현 #97 이 그 길을 대신했다. */
.ds input.form-control.js-date,
.ds input.form-control.js-datetime,
.ds input.form-control.js-daterange,
input.form-control.js-date,
input.form-control.js-datetime,
input.form-control.js-daterange {
    background-image: var(--ds-date-icon);
    background-repeat: no-repeat;
    background-position: right .7rem center;
    background-size: 1.05rem 1.05rem;
    /* 🔴 **여백의 기준은 「검색어 입력창의 좌측 여백」 = `.7rem`(11.2px)** (2026-09-07 사장님 지시:
       *"검색어 입력창 내 좌측 여백 있지? 그걸 기준으로 검색영역에 요소들의 좌우 여백을 모두 맞춰"*).
       🔑 **아이콘까지가 「내부 요소」이고, 아이콘 오른쪽의 빈 공간이 진짜 우측 여백**이다
          (사장님: *"그 아이콘들 우측의 여백이 드롭다운박스 내의 우측 여백"*).
       📐 그래서 **`background-position: right .7rem`**(아이콘 오른쪽 여백 = 기준) ·
          **`padding-right = .7rem + 아이콘 1.05rem + 여유 .2rem = 1.95rem`**.
       🔬 실측(관리자 정산 목록): 좌 11.2 · 아이콘 오른쪽 11.2 로 **검색어 칸과 완전히 같아졌고**
          「증빙 제출 대기」 잘림이 사라졌다.
       🕘 한때 「좌우 여백 통일」을 `padding` 숫자로만 읽어 **왼쪽을 2.2rem 으로 키웠다가 되돌렸다** —
          사장님이 화면을 보고 *"왜 좌측 여백이 저렇게 커진거야?"* 라고 바로잡으셨다.
          왼쪽에는 아이콘이 없으니 거기에 맞추면 **24px 이 낭비**되고 글자 자리가 그만큼 준다.
       🔥 **`padding` 숫자만 보고 좌우를 맞추지 말 것** — 아이콘이 그려지는 자리는 여백이 아니다.
       📌 판정 기준: **그 공간에 무언가가 그려지는가.** 그려지면 여백이 아니다. */
    padding-right: 1.95rem !important;
}

/* (나) 네이티브가 **아무것도 안 그리는** 플랫폼(iOS 계열 터치 기기) — 아이콘도 자리표시자도 우리가 그린다 */
@supports (-webkit-touch-callout: none) {
    @media (hover: none) and (pointer: coarse) {
        input.form-control[type="date"] {
            background-image: var(--ds-date-icon);
            background-repeat: no-repeat;
            background-position: right .7rem center;
            background-size: 1.05rem 1.05rem;
        }
        input.form-control[type="date"][data-empty]::before {
            content: attr(data-placeholder);
            position: absolute;
            left: 0;
            right: 2.2rem;
            top: 50%;
            transform: translateY(-50%);
            padding-left: inherit;   /* 입력창의 좌패딩을 그대로 따라간다 */
            color: var(--ds-text-faint);
            white-space: nowrap;
            overflow: hidden;
            pointer-events: none;
        }
        /* 포커스가 들어가면 네이티브 편집 UI 가 뜬다 — 자리표시자는 비켜야 한다 */
        input.form-control[type="date"][data-empty]:focus::before { content: ""; }
    }
}

/* -------------------------------------------------------------
   4-4) 기간은 **한 칸**으로 고른다 — `.ds-daterange` (2026-08-17 신설 → 같은 날 한 칸으로 합침)

   🔴 **보이는 칸은 하나, 서버로 가는 이름은 둘이다.**
      · 보이는 칸 = `input.js-daterange` — **`name` 이 없다.** 글자는 `2026-07-01 ~ 2026-08-31`.
      · 그 짝으로 `type="hidden"` 두 개(`dateFrom`·`dateTo`)가 서 있고 **그 둘만 서버로 간다.**
      🔑 **왜 이렇게 나눴나** — 이 저장소의 페이징·검색 유지·엑셀 내려받기는 **폼을 통째로 직렬화**하거나
         서버가 `params` 로 링크를 다시 그린다. 이름이 그대로 남으면 **매퍼·컨트롤러·페이저를 하나도
         안 고쳐도** 동작하고, **북마크된 옛 URL**(`?dateFrom=…&dateTo=…`)도 그대로 열린다.
      🔥 **보이는 칸에 `name` 을 주지 말 것** — `dateFrom=2026-07-01 ~ 2026-08-31` 이라는 엉뚱한
         파라미터가 함께 가서 조건이 통째로 어긋난다.
      🔴 **이 칸도 `readonly` 다**(2026-08-19 구현 #97) — 기간을 **비우는 길은 판 아래 「지우기」 버튼**
         이고, 누르면 **hidden 두 개까지 함께** 빈다(그 상태로 검색하면 「전체 기간」이다).
         🕘 그전에는 *"손으로 지우는 것이 유일한 길이므로 `readonly` 금지"* 였다. 🔥 되돌리지 말 것.
      🔑 **한쪽만 고르는 검색을 잃지 말 것** — 시작일만 넣고 끝을 비우면 「그날 이후 전부」다.
         글자는 `2026-07-01 ~` 로 끝나고 hidden `dateTo` 는 빈 문자열이다.

   🔑 **`~` 는 마크업이 아니라 여기서 정한다** — 아래 `--ds-daterange-sep` **하나뿐**이다.
      🔬 그전에는 화면마다 손으로 적혀 있었다(2026-08-17 실측, 세 앱 33화면:
         **아무것도 없는 곳 15 · `text-muted` 2 · `input-group-text` 16**).
      🔥 i18n 문구에 넣지 말 것 — 기호는 언어를 타지 않는다(`.ds-note-warn` 의 `※` 와 같은 규칙).
      🔴 **읽는 쪽이 CSS 가 아니라 `common.js` 다** — 칸의 **값**에 들어가는 글자라 의사요소로는
         그릴 수 없다(`input` 은 대체 요소라 `::before` 가 아예 생성되지 않는다 —
         오류가 아니라 아무 일도 안 일어난다). 그래서 규격은 여전히 이 한 곳이고
         `getComputedStyle(:root)` 로 읽어 간다.
         🔥 이름(`--ds-daterange-sep`)을 바꾸려면 `common.js` 날짜 선택기 블록도 함께 볼 것.
      🕘 `.ds-daterange-sep`(빈 `<span>` + `::before`)은 **없앴다** — 두 칸 사이에 세우던 것인데
         칸이 하나가 되어 세울 자리가 없다(세 앱 마크업 사용처 0곳). 🔥 되살리지 말 것.
   ⚠️ **`.ds` 스코프를 쓰지 않는다** — 검색 폼은 `.ds` 가 없는 화면에도 있다(4-3 절과 같은 이유).

   🔴 **골격이 두 가지인 것은 그대로다 — 하나로 통일하려 하지 말 것** (2026-08-17 실측 판단):
      · **(가) `.ds-daterange` 로 감싼다** — 검색 폼이 `d-flex gap-3 flex-wrap` 인 화면(관리자 12화면).
        감싼 덕에 **hidden 두 칸이 보이는 칸과 한 덩어리**로 남는다(`common.js` 가 이 묶음 안에서 찾는다).
      · **(나) 감싸지 않고 `.ds-daterange-input` 만** 준다 — 칸이 Bootstrap **`.input-group` 안에**
        다른 필터(통화·평점·검색어)와 **한 줄로 서 있는** 화면(랜드사·여행사 20화면).
        🔥 여기에 `.ds-daterange` 를 끼워 넣지 말 것 — `.input-group > .form-control` 의 flex·모서리
           규칙이 **직계 자식에만** 걸려 있어, 한 겹 감싸는 순간 그 줄의 폭 배분과 모서리가 깨진다.
        🔑 그래서 **hidden 두 칸은 `.input-group` 밖, 같은 `<form>` 안**에 둔다.
           🔥 `.input-group` 안에 넣지 말 것 — `:last-child` 가 되면 **바로 앞 요소의 오른쪽 모서리**가
              펴진다(`display:none` 이라도 자식 선택자에는 그대로 걸린다).
      📌 라벨이 칸에 붙은 화면(여행사 정산 이력)은 라벨이 **하나**가 된다 — 칸이 하나이기 때문이다.
   🔑 **「종료일이 시작일보다 이를 수 없다」는 이제 규칙이 아니라 구조다** — 한 판에서 기간을 고르므로
      어긋난 값을 만들 수 없다. 🔥 손으로 거꾸로 쳐 넣으면 `common.js` 가 **뒤집어** 받는다(비우지 않는다).
   ------------------------------------------------------------- */
:root {
    /* 🔑 시작일과 종료일 사이의 글자. **여기가 유일한 출처**이고 `common.js` 가 읽어 간다. */
    --ds-daterange-sep: "~";
    /* 🔴 **기간 칸의 폭 — 세 골격이 이 하나를 본다** (2026-08-23 사용자 지적: *"기간 검색 칸이 넓다"*).
       🕘 **16rem(256px) 이었다.** 그 값은 자리표시자 `YYYY-MM-DD ~ YYYY-MM-DD`(**189px**)에 맞춘 것인데,
          자리표시자는 **값이 들어오면 사라지는 글자**다. 그것 때문에 칸이 커져 있었던 셈이다.
       🔑 **기준은 「실제 값」이다** — `2026-09-15 ~ 2026-09-17`. 🔬 실측(여행사 견적 목록, 1440px):
          | 글자 크기 | root | 값 폭 | 안쪽 여백+테두리 | 필요 폭 | rem 환산 |
          | 기본     | 16px | 168.2 | 48.4 | 216.6 | **13.54rem** |
          | 크게     | 18px | 189.3 | 54.2 | 243.5 | **13.53rem** |
          | 아주 크게 | 20px | 210.3 | 60.0 | 270.3 | **13.52rem** |
          🔑 세 단계의 rem 환산이 **같다** — 글자도 여백도 rem 이라 함께 커진다.
             → **rem 값 하나로 세 단계가 모두 안 잘린다.** 🔥 px 로 박으면 큰 글자에서 그날로 잘린다.
       📐 **14rem** = 필요 폭 13.54rem + 여유(0.46rem ≈ 7px). 여유를 둔 이유는 글꼴 대체(fallback)와
          로케일에 따라 값 폭이 몇 px 흔들리기 때문이다.
       🔥 **13rem 이하로 내리지 말 것 — 값이 잘린다**(잘려도 오류가 안 나고 눈으로만 잡힌다).
          🔬 사용자 제안값 「190px 안팎」은 **기본 글자 크기에서도 26.6px 모자란다**(216.6 필요).
       🔑 축 이름 자리표시자(「여행일자」 53px · 「입금 기한」 등)는 이 폭에 **넉넉히** 들어간다 —
          글자 자리가 176px 이다. 🔥 자리표시자를 다시 양식(`YYYY-MM-DD ~ …`)으로 되돌리면
          **189px 이라 잘린다**(`common.js` 의 기간 칸 자리표시자 절과 짝이다).
       📌 이 앱은 (가) 묶음 골격(`.ds-daterange`)이 18곳으로 가장 많다 — 그 칸들이 이 토큰을 본다.
       🔑 **줄어든 32px 은 검색어 칸이 가져간다**(`flex:1 1 auto`) — 2026-08-23 사용자 확정:
          *"남는 공간은 검색어 칸이 전체 먹는 건 유지해."* 🔥 검색 폼·검색어 칸에 최대 폭을 걸지 말 것. */
    --ds-daterange-w: 14rem;
}
.ds-daterange {
    display: flex;
    align-items: center;
    flex-wrap: nowrap;
    /* 좁은 화면 — 검색 폼의 다른 칸들처럼 한 줄을 통째로 쓴다 */
    flex: 1 1 100%;
    min-width: 0;
}
/* 🔥 폭을 Metronic 유틸리티(`w-md-150px`)로 주지 말 것 — 정의된 단계만 있어 맞출 수 없고,
      없는 단계는 **오류가 아니라 아무 일도 안 일어나** `w-100` 이 이겨 줄이 무너진다.
   📐 폭은 **`--ds-daterange-w`(위 `:root`)** 하나가 정한다 — 세 골격((가) 묶음 · (나) 한 칸 · 통계 필터
      바)이 같은 이름을 본다. 🕘 예전에는 세 곳에 `16rem` 이 손으로 적혀 있었다(2026-08-23 에 묶었다).
   ⚠️ px 로 박지 말 것(글자 크기 3단계 토글이 root 기준 rem 이다). */
@media (min-width: 768px) {
    .ds-daterange {
        flex: 0 1 auto;
        width: var(--ds-daterange-w);
    }
}
/* 🔑 폭은 **묶음이 정하고 칸이 채운다** — 칸에 폭을 박으면 좁은 화면에서 넘친다.
      `min-width: 0` 이 없으면 flex 항목의 기본 최소폭 때문에 **묶음이 부모를 뚫고 나간다.** */
.ds-daterange > .form-control {
    flex: 1 1 0;
    width: auto;
    min-width: 0;
}
/* (나) 골격 — `.input-group` 의 **직계 자식**이라 선택자를 그 모양으로 맞춘다.
   ⚠️ `.input-group > .form-control`(0,2,0) 과 **같은 특이도**라야 소스 순서로 이긴다
      (`.ds-daterange-input` 만 적으면 (0,1,0) 이라 부트스트랩의 `width:1%` 에 조용히 진다). */
.input-group > .ds-daterange-input {
    flex: 1 1 100%;
}
@media (min-width: 768px) {
    .input-group > .ds-daterange-input {
        flex: 0 1 auto;
        width: var(--ds-daterange-w);
    }
}

/* -------------------------------------------------------------
   4-5) 검색 줄 — **거르는 칸은 떨어지고 검색 덩어리는 붙는다** (2026-08-19 구현 #97)

   🔬 사용자 지적: *"검색창에 기간과 통화가 붙어있어"*.
      실측(랜드사 견적 목록, 창 2158px): 기간 칸 right **597** / 통화 칸 left **596**
      → 사이 **−1px**. 떨어진 것이 아니라 **1px 겹쳐** 한 덩어리로 보였다.
   🔎 **원인은 마크업이 아니라 Bootstrap 이다.** 랜드사·여행사의 검색 줄은 필터와 검색어가
      **하나의 `.input-group`** 안에 선다(4-4 절 (나) 골격). 그런데 Bootstrap 의
      `.input-group > :not(:first-child):not(…)` 이 **모든** 칸에 `margin-left:-1px` +
      왼쪽 모서리 0 을 걸어 **줄 전체를 용접**한다. 검색아이콘+검색어+버튼은 그래야 맞지만
      **기간·통화는 서로 다른 조건**이라 붙어 있을 이유가 없다.
   🔑 그래서 **「용접할 것」과 「떼어 놓을 것」을 클래스로 가른다.** 떼어 놓을 것은
      `.form-select`(거르는 칸)과 `.ds-daterange-input`(기간) **둘뿐**이다.
      🔬 전 저장소 실측(2026-08-19): `.input-group` 의 **직계 자식**으로 이 둘이 오는 곳은
         **검색 줄 23화면뿐**이고, 나머지 `.input-group`(인원+「명」·금액+통화·사업자번호+조회 등)
         에는 하나도 없다 → **화면별 예외 없이 이 한 규칙으로 끝난다.**
         🔥 그러니 화면 CSS(`menuLevel/*.css`)에 같은 간격을 또 만들지 말 것.
   🔑 모서리도 함께 되살린다 — 용접이 풀린 칸은 **제 모서리를 가져야** 한다. 안 그러면
      기간 칸만 오른쪽이 각지고 통화 칸만 왼쪽이 각진 채 떨어져 더 어색해진다.
   ⚠️ **`<=767px` 에는 걸지 않는다.** 그쪽은 위 3절이 `.input-group` 을 통째로
      `display:grid; gap:.4rem` 으로 바꾸므로 **이미 떨어져 있다.** 여기서 margin 을 더하면
      그리드 칸 폭이 어긋나 검색 칸이 화면을 넘는다.
   📐 값은 **관리자 앱 검색 폼과 같은 12px** 다 — 관리자는 필터가 `.input-group` **밖**
      `d-flex gap-3 flex-wrap` 에 있어 처음부터 벌어져 있었다(그래서 관리자에는 이 지적이 없었다).
      🔬 실측(2026-08-19, 관리자 회원 목록): 필터 사이 **12px**. 🔥 이것을 `1rem`(16px)으로 보지 말 것 —
         **Metronic 의 여백 눈금은 부트스트랩 기본과 다르다**(`gap-3` = `.75rem` = 12px).
         세 앱이 같아 보이도록 그 실측값을 그대로 쓴다.
   📌 **관리자 앱에서는 이 규칙이 걸릴 자리가 없다**(위 이유로 필터가 `.input-group` 밖이다).
      그래도 두는 이유는 이 파일이 **세 앱 한 벌**이기 때문이다 — 갈리면 다음 사람이 어느 쪽이
      맞는지 모른다. 🔥 지우지 말 것.
   ⚠️ `!important` 가 필요한 이유: 위 Bootstrap 선언이 `:not()` 6개로 **(0,7,0)** 이라
      선택자를 아무리 길게 써도 이길 수 없다(`.input-group > .form-select` 는 (0,2,0)).

   🔴 **2026-08-24 — 「검색 머리 안」에서는 이 절이 아니라 4-7 절이 산다.**
      4-7 절이 `.ds .card > .card-header .input-group:not(.ds-search-main) > *` 로
      **`margin-left` 를 0 으로 되돌리고 간격을 `gap` 이 만들게** 한다((0,5,0) 이라 이 절 (0,2,0)을 이긴다).
      🔑 **왜 바꿨나** — 이 절의 `margin-left` 방식은 검색 덩어리가 아랫줄로 내려갔을 때
         **그 줄 왼쪽에 12px 들여쓰기를 남긴다**(사용자 지적).
   🔥 **그래도 이 절을 지우지 말 것 — 검색 머리 밖에도 닿는다.**
      🔬 실측(2026-08-24 세 앱 전수): 랜드사 상품 템플릿 등록 화면의 **아이템 검색 모달**이
         `.input-group > .form-select` + 검색어 + 버튼 골격이다(`travelProduct/templateRegist.html`).
         거기는 `.card-header` 가 아니라 `.modal-body` 라 4-7 절이 닿지 않고, 이 절이 그 줄을 벌린다.
   ------------------------------------------------------------- */
:root {
    /* 🔑 **검색 줄의 모든 간격**이 이 하나다 — 넓은 화면의 「거르는 칸」 사이도, 좁은 화면에서 줄과 줄 사이도 같은 값이다. **여기가 유일한 출처**다.
       🔴 2026-08-23 사용자 지시: *"전체 페이지에 검색창 내 여백을 통일해 줘."* — 한 화면 안에서도, 화면끼리도, 세 앱끼리도 한 값이어야 한다.
       📐 값은 **척도의 `--ds-sp-3`(12px)** 다. 새로 고른 값이 아니라 **이미 가장 많이 쓰이던 값**이라 변경 폭이 가장 작다:
          · 넓은 화면 검색 줄이 쓰던 값(이 토큰의 원래 값 `.75rem`) · `.ds .card > .card-header` 의 `gap` · **이 앱 18개 목록 화면의 검색 폼(`form.d-flex.gap-3`, Metronic `gap-3` = `.75rem`)**.
          🔬 어긋나 있던 것은 좁은 화면 블록의 `.ds-filter-row`·`.ds .input-group` 두 선언(`.4rem`)뿐이었다.
       🔥 생 숫자로 되돌리지 말 것 · 🔥 이 토큰을 검색 줄 밖에서 쓰지 말 것(쓰면 이 값이 검색 줄 전용이라는 뜻이 사라진다). */
    --ds-search-gap: var(--ds-sp-3);
}
@media (min-width: 768px) {
    /* ① 떼어 놓을 칸 — 오른쪽 모서리를 되살린다 */
    .input-group > .ds-daterange-input:not(:last-child),
    .input-group > .form-select:not(:last-child) {
        border-top-right-radius: .475rem !important;
        border-bottom-right-radius: .475rem !important;
    }
    /* ② 그 **바로 뒤 칸** — 간격을 벌리고 왼쪽 모서리를 되살린다.
          🔑 뒤 칸이 검색 아이콘이면 **거기서부터가 「검색 한 덩어리」의 머리**가 된다. */
    .input-group > .ds-daterange-input + *,
    .input-group > .form-select + * {
        margin-left: var(--ds-search-gap) !important;
        border-top-left-radius: .475rem !important;
        border-bottom-left-radius: .475rem !important;
    }
}

/* -------------------------------------------------------------
   4-6) 검색 줄의 **폭 배분** — 거르는 칸은 필요한 만큼, 검색어 칸이 나머지를 먹는다
        (2026-08-22 사용자 지시)

   > *"검색창의 드롭다운은 쓸데없이 길게 늘이지 마라 — 필요한 만큼만 차지하고,
   >   남는 가로 공간은 「검색어 입력창」이 늘어나 채운다."*

   🔎 **원인은 마크업이 아니라 Bootstrap 이다.**
      `.input-group > .form-control, .input-group > .form-select{flex:1 1 auto; width:1%; min-width:0}`
      → 거르는 칸과 검색어 칸이 **똑같이** 늘어난다. 넓은 화면에서 남는 폭이 셋·넷으로 나뉘어
      「전체 노출」 한 단어짜리 셀렉트가 검색어 칸만큼 길어진다.
   🔑 그래서 **늘어나는 것은 검색어 칸 하나**로 못 박는다. 거르는 칸은 `flex: 0 0 auto` 다.
      · `width: auto` = **내용(가장 긴 항목) 만큼** — 화면마다 폭을 손으로 고르지 않아도 된다.
      · 화면이 `w-md-150px`(정의된 단계)로 폭을 정해 두었으면 `!important` 라 그쪽이 이긴다 —
        그때도 **늘어나지는 않는다**(이 규칙이 정하는 것은 `flex` 이지 `width` 가 아니다).
      🔥 그러니 셀렉트 폭을 **인라인 `style="max-width:…px"`** 로 잡지 말 것 —
         글자 크기 3단계 토글(0-0 절)이 root 기준 rem 이라 px 는 큰 글자에서 그날로 어긋난다.
   🔥 **`min-width: 0` 을 빼지 말 것** — flex 항목인 `input` 의 `min-width` 초기값 `auto` 는
      **min-content 폭**(`size` 기본 20자)이라, 글자를 키운 사용자에게만 검색어 칸이 묶음 밖으로
      밀려난다. **넓은 화면·기본 글자 크기에서는 보이지 않는다**(silent-traps 에 같은 함정이 있다).
   ⚠️ **기간 칸은 제외**한다 — 4-4 절이 `.ds-daterange-input` 을 `16rem` 으로 이미 정했다.
      빼지 않으면 이 규칙이 그 칸을 늘려 「2026-07-01 ~ 2026-08-31」 규격이 깨진다.
      📌 관리자 앱의 검색 폼은 대개 `.ds-daterange` 로 **감싸는** (가) 골격이라 이 제외가 걸릴 일이
         적지만, 두 골격이 한 저장소에 함께 사는 이상 선택자는 세 앱이 같아야 한다.
   ⚠️ **`<=767.98px` 에는 걸지 않는다** — 그쪽은 3절이 `.input-group` 을 `display: grid` 로 바꾸고
      칸마다 `width: 100% !important` 를 주므로 flex 속성이 아무 뜻이 없다.
   🔬 `.input-group` 의 **직계 자식**으로 `.form-select` 가 오는 곳은 세 앱 전수로 **검색 줄뿐**이다
      (2026-08-22 재확인 · 4-5 절의 2026-08-19 실측과 같은 결과) → 화면별 예외가 필요 없다.
      📌 관리자 앱은 거르는 칸이 `.input-group` **밖**(`d-flex gap-3 flex-wrap`)에 서고 검색어만
         `.input-group.flex-grow-1.w-auto` 안에 있어 **이 규칙에 걸리는 칸이 거의 없다** —
         그래도 세 앱이 같은 파일을 쓰므로 함께 둔다(한 앱만 고치는 것이 이 저장소의 최다 실패다).
      ⚠️ 금액+통화 같은 복합 칸은 `.amount-input-group`(`.d-flex`)이라 여기에 걸리지 않는다 —
         🔥 **이름이 비슷하다고 이 규칙의 선택자를 넓히지 말 것.**

   🔴 **2026-08-24 — 「검색 머리 안」에서는 ①이 아니라 4-7 절이 산다.**
      4-7 절이 거르는 칸을 `flex: 1 1 auto`(늘어난다) + `max-width: none` 으로 덮는다.
      🔑 **왜 뒤집었나** — 여기 ①의 `flex: 0 0 auto` 는 「검색어 칸이 남는 폭을 다 먹는다」를 지키려는
         것인데, 4-7 절은 **검색 세트에 `flex-grow: 1000`** 을 줘서 같은 것을 더 확실히 지킨다.
         그러고 나면 거르는 칸이 늘어나도 검색어를 잡아먹지 않고, **세트가 아랫줄로 내려간 뒤**
         윗줄에 남은 빈자리를 **균등하게** 채울 수 있다(사용자가 요구한 두 번째 조건).
      🔑 **14rem 상한도 검색 머리 안에서는 푼다** — 위 이유로 상한의 목적이 이미 달성돼 있고,
         상한이 남아 있으면 **긴 항목이 잘린다.**
   🔥 **그래도 이 절을 지우지 말 것 — ②는 검색 머리 밖의 `.input-group` 에도 닿는다**
      (`min-width: 0` 이 없으면 글자를 키운 사용자에게만 입력칸이 묶음 밖으로 밀려난다).
   ------------------------------------------------------------- */
@media (min-width: 768px) {
    /* ① 거르는 칸 — 내용만큼만 차지하고 **늘어나지 않는다** */
    .input-group > .form-select {
        flex: 0 0 auto;
        width: auto;
        /* 📐 항목 글자가 아무리 길어도 검색어 칸을 잡아먹지 않도록 **상한만** 둔다.
              🔥 px 로 박지 말 것(글자 크기 3단계 토글이 root 기준 rem 이다). */
        max-width: 14rem;
    }
    /* ② 검색어 칸 — 남는 가로 공간을 **혼자** 채운다.
          🔑 기간 칸은 제 폭이 있으므로(4-4 절) 뺀다. */
    .input-group > .form-control:not(.ds-daterange-input) {
        flex: 1 1 auto;
        min-width: 0;
    }
    /* ── ③ **넓은 화면에서 검색 줄이 접히면 두 줄이 맞붙는다** (2026-08-23 실측으로 발견) ─────────
       🔬 실측(랜드사 상품 목록, 768px): 거르는 칸이 많아 `.input-group` 이 **두 줄로 접히는데**
          Bootstrap 의 `.input-group` 은 `flex-wrap: wrap` 이면서 `row-gap` 이 **0** 이라
          윗줄 컨트롤 바닥과 아랫줄 꼭대기 사이가 **0px** 이었다(가로 간격은 4-5 절이 12px 로 벌려 둔 채).
          → 같은 검색창 안에서 **가로는 12px, 세로는 0px** 이라 「검색창 내 여백 통일」이 깨진다.
       🔑 **`row-gap` 만 준다** — `column-gap` 을 주면 4-5 절이 「용접할 것/뗄 것」을 갈라 놓은 규칙과
          겹쳐 검색 아이콘·검색어·버튼 사이까지 벌어진다(그 셋은 붙어 있어야 한 덩어리로 읽힌다).
       🔑 **좁은 화면(≤767.98px)은 3절이 이미 `gap: var(--ds-search-gap)` 으로 격자를 만든다** —
          그래서 이 규칙은 `min-width: 768px` 안에 있다. 두 구간이 같은 값을 본다.
       ⚠️ 검색 머리 안으로 **범위를 좁혔다** — `.input-group` 은 금액+통화·인원+「명」 같은
          「한 줄 묶음」에도 쓰이고 그쪽은 접힐 일이 없다(접히면 그것대로 다른 물음이다).
       📌 이 앱은 거르는 칸이 폼(`gap-3`)에 있어 접혀도 이미 12px 이다 — 검색어 묶음이 접히는
          경우를 위해 **세 앱에 같은 규칙**을 둔다. 🔥 지우지 말 것. */
    .ds .card > .card-header .input-group { row-gap: var(--ds-search-gap); }
}

/* -------------------------------------------------------------
   4-7) 검색 줄이 **접히는 방식** — 검색어 칸에 바닥값을 주고 「검색 세트」를 통째로 내린다
        (2026-08-24 사용자 지시 · 일곱 폭 실물 화면판(375·430·768·900·1100·1440·1920)으로 확정)

   🔬 **무엇이 문제였나** — 랜드사 견적 목록 768px 에서 검색어 칸이 **40px** 이라 자리표시자
      「견적번호 · 여행사 · 상품명」이 **두 글자**만 보였다(900px 에서도 172px 로 끝이 잘렸다).
      원인은 검색어 칸에 **바닥값이 없어** 남는 폭을 전부 내주고 자기가 찌그러지는 것이다.

   🔑 **칸마다 「최소폭 지표」가 다르다** (사용자 확정)
      · 일자·일시·기간 → **값 자체**(`2026-09-15 ~ 2026-09-17`, 실측 217px). 4-4 절의
        `--ds-daterange-w`(14rem)가 이미 그 값이다 — 여기서는 그것을 **폭이 아니라 바닥값**으로 바꿔 쓴다.
      · 드롭다운 → 최장 옵션과 자리표시자 중 긴 쪽. **내용 기준(`width: auto`)이라 이미 충족**한다.
      · 검색어 → **자리표시자 길이**. 화면마다 다르므로 그 화면이 인라인 `--ds-kw-min` 으로 준다.
      · 최소값보다 좁아지면 **그 항목이 전폭으로 내려가고**, 윗줄에 남은 항목들이 **늘어나** 빈자리를 채운다.
      · 🔴 **전폭이 된 항목 앞에는 여백이 없어야 한다.**

   🔑 **① `margin` 이 아니라 `gap` 으로 벌린다** — 4-5 절은 Bootstrap 의 용접을 되받아
      「떼어 놓을 칸」 **뒤 칸**에 `margin-left: 12px` 를 준다. 그 방식이면 세트가 아랫줄로 내려갔을 때
      **왼쪽에 12px 들여쓰기가 그대로 남는다**(사용자가 지적한 그 여백). 용접이 필요한 것은
      **세트 안쪽뿐**이므로 바깥은 `gap` 으로 바꾼다 → 줄바꿈 시 여백이 저절로 없어진다.

   🔑 **② 검색 「세트」를 `div.ds-search-main` 으로 묶는다** — 🔍 아이콘 · 검색어 · 검색취소 · 검색.
      🔬 묶지 않으면 검색어만 줄바꿈되고 **아이콘 37px 이 윗줄에 홀로 남는다**(브라우저 실험으로 확인).
      🔑 세트가 **언제 아랫줄로 내려가는가**는 안에 실제로 든 것들의 합이 정한다 — 줄바꿈 판정은
         flex 항목의 **희망 크기**로 하고 세트의 희망 크기는 `flex-basis: auto`(= 내용 폭)이다 →
         **「검색 취소」가 나타나면 그만큼 더 필요해져 더 일찍 내려간다.**
         🔥 **취소 버튼 폭을 어디에도 박지 말 것** — 세는 일은 브라우저에게 맡긴다.
         🕘 **2026-08-24 에 세트의 `min-width` 를 `auto` 에서 `0` 으로 바꿨다**(이 절 맨 끝 규칙).
            🔥 **「세트의 `min-width` 가 `auto` 라서 이렇게 된다」로 되돌리지 말 것** — 줄바꿈 판정은
               `min-width` 가 아니라 희망 크기로 하므로 위 문장은 그대로 성립한다.

   🔑 **③ 세트는 `flex: 1000 1 auto`** — 한 줄일 때 남는 폭을 사실상 전부 먹어
      **「남는 공간은 검색어 칸이 전부 먹는다」**(2026-08-23 사용자 확정)가 유지되고,
      세트가 내려간 줄에서는 남은 항목들이 `grow: 1` 씩이라 **균등하게** 빈자리를 나눠 갖는다.
      🔑 **두 요구가 이 한 값으로 동시에 만족된다** — 화면별 예외가 필요 없다.

   🔑 **④ 세트 안쪽 용접을 되살린다** — div 한 겹이 끼면 Bootstrap 의
      `.input-group > :not(:first-child)` 도, 3 절의 `.ds .input-group > …` **높이 규칙**도 안 걸린다.
      🔥 높이 규칙을 빠뜨리면 「검색」 버튼이 **세로 두 줄로 깨진다**(실험에서 실제로 그랬다).

   🔑 **⑤ 칸 순서는 `[드롭다운들][기간][.ds-search-main]`** — 성격으로 묶는다
      (고르는 것 ▸ 값을 넣어 좁히는 것). 기간 칸이 둘인 화면은 둘 다 세트 앞에 붙여 둔다.
      📱 **좁은 화면(≤767.98px)의 배치는 바뀌지 않는다** — 3 절의 2열 격자가 그대로 만든다:
         `[상태|통화] / [기간 전폭] / [검색어 전폭] / [검색 전폭]`(취소가 있으면 `[취소|검색]` 반반).

   🔴 **골격이 셋이다 — 선택자를 하나로 합치려 하지 말 것**
      · **(가) 랜드사·여행사** — 거르는 칸과 세트가 **한 `.input-group`** 안에 선다(4-4 절 (나) 골격).
        → 세트는 `.input-group` **안의 새 div** 다.
      · **(나) 관리자** — 거르는 칸이 `.input-group` **밖**에 서고 **폼 자신이 `d-flex flex-wrap`** 이다
        (4-4 절 (가) 골격). → **세트는 원래부터 있던 `.input-group` 자신**이라 감쌀 div 를 만들지 않고
        `ds-search-main` **클래스만 더한다.** 「윗줄이 균등하게 늘어난다」는 폼의 직계 자식에게 따로 준다.
        🔥 그래서 (가)의 규칙들은 전부 `:not(.ds-search-main)` 을 물고 있다 — 안 물리면
           **관리자의 세트가 통째로 12px 씩 벌어지고 용접이 풀린다.**
      · **(다) 여행사 정산 이력 하나** — 검색 카드가 `card-header` 가 아니라 `card-body` 에 있고
        라벨이 칸 위에 붙으며 `.input-group` 도 🔍 아이콘도 없다(4-4 절이 이미 이 예외를 적어 두었다).
        → 묶을 세트가 없으므로 **검색어 바닥값만** 같은 방식으로 준다.

   🔥 **4-5·4-6 절을 지우지 말 것** — 그 둘은 **검색 머리 밖의 `.input-group`** 에도 닿는다
      (실측 2026-08-24 세 앱 전수: 랜드사 상품 템플릿 등록 모달의 아이템 검색 줄이
      `.input-group > .form-select` 골격으로 **딱 한 곳** 남아 있다).
      **검색 머리 안에서는 이 절이 덮는다** — 선택자가 (0,5,0) 이상이라 (0,2,0) 을 이긴다.
   ------------------------------------------------------------- */
/* (공통) 세트 안쪽도 「그룹 구성원」 규격을 갖는다 — 3 절의 직계자식 규칙을 잃으므로 복원한다.
   🔥 선언 다섯 줄은 3 절 `.ds .input-group > …` 과 **글자 그대로 같다.
      한쪽만 고치면 같은 줄 안에서 높이가 갈린다.** */
.ds .ds-search-main > .form-control,
.ds .ds-search-main > .form-select,
.ds .ds-search-main > .input-group-text,
.ds .ds-search-main > .btn {
    height: var(--ds-control-h);
    min-height: 0 !important;
    font-size: .9rem;
    display: flex;
    align-items: center;
}
.ds .ds-search-main > .input-group-text { padding: 0 .7rem !important; }   /* 기준 = 검색어 칸 좌측 여백 */
/* 🔥 `white-space: nowrap` 을 빼지 말 것 — 세트가 좁아지는 순간 「검색 취소」가 두 줄로 접힌다. */
.ds .ds-search-main > .btn {
    /* 🔴 기준 `.7rem`(= 검색어 입력창 좌측 여백, 2026-09-07 사장님 지시).
       🔬 `.9rem`(14.4px)이던 때 「검색」 버튼은 안쪽이 **24px 인데 글자가 26px** 이라 이미 잘리고 있었다 —
          기준으로 줄이니 안쪽이 30.6px 이 되어 **오히려 해소**됐다.
       🔥 이 규칙이 `.ds .input-group > .btn` 보다 세다 — **여기를 안 고치면 아무 일도 일어나지 않는다.** */
    padding: 0 .7rem !important;
    justify-content: center;
    white-space: nowrap;
}
.ds .ds-search-main > .input-group-text svg { width: 16px; height: 16px; }

@media (min-width: 768px) {
    /* ── (가) 랜드사·여행사 — 거르는 칸과 세트가 한 `.input-group` 안에 선다 ─────────────── */
    .ds .card > .card-header .input-group:not(.ds-search-main) {
        flex-wrap: wrap;
        gap: var(--ds-search-gap);
    }
    /* 🔴 Bootstrap 의 용접(`-1px`)과 4-5 절의 `margin-left` 를 **여기서 끈다** — 간격은 위 `gap` 이 만든다.
          그래야 세트가 아랫줄로 내려갔을 때 **왼쪽 들여쓰기가 남지 않는다.**
       ⚠️ `!important` 가 필요한 이유는 4-5 절과 같다(Bootstrap 선언이 `:not()` 6개로 (0,7,0)). */
    .ds .card > .card-header .input-group:not(.ds-search-main) > * { margin-left: 0 !important; }
    .ds .card > .card-header .input-group:not(.ds-search-main) > .form-control,
    .ds .card > .card-header .input-group:not(.ds-search-main) > .form-select {
        border-radius: .475rem !important;
    }
    /* 거르는 칸 — 내용만큼 서되(`basis: auto`) **빈자리는 균등하게 나눠 갖는다**(4-6 ①을 여기서만 덮는다).
       🔑 `max-width: none` 인 이유: 4-6 의 14rem 상한은 「검색어 칸을 잡아먹지 말라」는 뜻인데
          여기서는 세트가 `grow: 1000` 이라 그 걱정이 없고, 상한을 두면 긴 항목이 잘린다. */
    .ds .card > .card-header .input-group:not(.ds-search-main) > .form-select {
        flex: 1 1 auto;
        width: auto;
        max-width: none;
    }
    /* 기간 칸 — 4-4 절의 `width` 를 **바닥값**으로 바꿔 쓴다(값이 잘리지 않는 최소치다). */
    .ds .card > .card-header .input-group:not(.ds-search-main) > .ds-daterange-input {
        flex: 1 1 auto;
        width: auto;
        min-width: var(--ds-daterange-w);
    }
    /* 세트 — 한 줄이면 남는 폭을 전부 먹고, 내려가면 윗줄을 남은 칸들에게 넘긴다 */
    .ds .card > .card-header .input-group:not(.ds-search-main) > .ds-search-main {
        display: flex;
        flex: 1000 1 auto;
    }

    /* ── (나) 관리자 — 거르는 칸이 폼의 직계 자식이고 폼이 `d-flex flex-wrap` 이다 ────────── */
    /* 🔥 마크업에서 `flex-grow-1` 을 걷어냈다 — Metronic 유틸리티는 `!important` 라 아래 `flex: 1000`
          을 **조용히 1 로 되돌린다**(배치 규칙 ④: 이길 규칙이 아니라 겹치지 않을 규칙을 쓴다).
       🔥 `w-auto` 는 그대로 둘 것 — `.input-group` 은 `width: 100%` 라 그것이 없으면 검색창이
          통째로 아래 줄로 내려간다(구현 #32 에서 실제로 겪었다). */
    .ds .card > .card-header form > .form-select { flex: 1 1 auto; }
    .ds .card > .card-header form > .ds-daterange {
        flex: 1 1 auto;
        width: auto;
        min-width: var(--ds-daterange-w);
    }
    .ds .card > .card-header form > .input-group.ds-search-main { flex: 1000 1 auto; }

    /* ── (공통) 세트 **안쪽만** 용접한다 ──────────────────────────────────────────── */
    /* 🔴 **세트 자신은 절대 줄바꿈하지 않는다** (2026-08-24 사용자 지적).
       🔬 증상: 관리자 시스템 오류 768px · 글자 「아주 크게」에서 검색 줄이
          `[🔍][검색어]` / `[검색]` **두 줄로 갈라졌다**(세트 563px 안에 42 + 522 = 564).
       🔬 **원인은 4-7 절이 아니다** — 위 (가) 규칙은 전부 `:not(.ds-search-main)` 을 물고 있어
          관리자 세트에 **걸리지 않는다**(그 점은 정상이다). 걸린 것은 **벤더 기본**,
          `style.bundle.css` 의 `.input-group{position:relative; display:flex; flex-wrap:wrap; …}` 다.
          🔑 **관리자 세트는 그 자신이 `.input-group`** 이라(마크업
             `class="input-group ds-search-main w-auto"` — 세 앱 **19곳**) 벤더 기본을 그대로 받는다.
             랜드사·여행사 세트는 `.input-group` **안의 맨 `div`**(**25곳**)라 애초에 `nowrap` 이었다.
          🔥 그래서 「`:not()` 을 물렸는데도 wrap 이 걸린다」를 **우리 규칙의 누락으로 읽지 말 것** —
             `:not()` 은 *더하지 않는다*는 뜻이지 *벤더가 준 것을 끈다*는 뜻이 아니다.
       📐 특이도: 벤더 `.input-group` = **(0,1,0)**, 이 규칙 `.input-group.ds-search-main` = **(0,2,0)**
          → 이긴다(`!important` 불필요). 게다가 이 파일이 번들보다 **뒤에** 실린다(`head.html`).
          🔥 `.ds .ds-search-main` 으로만 적지 말 것 — 같은 (0,2,0) 이지만 `.ds` 가 없는 화면의
             검색 폼에는 닿지 않는다(4-4·4-5 절이 같은 이유로 `.ds` 스코프를 쓰지 않는다).
       ⚠️ **좁은 화면(≤767.98px)은 대상이 아니라서 이 `@media` 안에 둔다** — 그쪽은 3 절이
          `.ds .input-group` 을 `display: grid` 로 바꿔 검색어·버튼을 각각 **전폭 줄**에 놓는다
          (의도된 배치다). 격자에서 `flex-wrap` 은 아무 일도 하지 않지만,
          **이 자리에 있다는 것 자체가 「넓은 화면 전용」이라는 뜻**이다. */
    .input-group.ds-search-main { flex-wrap: nowrap; }
    .ds .ds-search-main > * { border-radius: 0 !important; }
    .ds .ds-search-main > :not(:first-child) { margin-left: -1px !important; }
    .ds .ds-search-main > :first-child {
        border-top-left-radius: .475rem !important;
        border-bottom-left-radius: .475rem !important;
    }
    .ds .ds-search-main > :last-child {
        border-top-right-radius: .475rem !important;
        border-bottom-right-radius: .475rem !important;
    }
    /* 🔴 **검색어 칸의 희망폭** — 그 화면이 인라인 `style="--ds-kw-min: …em"` 으로 준다.
       🔑 **`em` 이다** — 글자 크기 3단계 토글(0-0 절)이 root 기준 rem 이라 px 로 박으면 큰 글자에서 잘린다.
       🔑 **인라인인 이유**: 자리표시자가 화면마다 다르다. 여기에 화면별 규칙 40여 개를 만드는 것보다
          그 화면이 자기 값을 들고 있는 편이 낡지 않는다.

       🔴 **2026-08-24 — 이 값을 `min-width` 가 아니라 `flex-basis` 로 준다. 되돌리지 말 것.**
       🔬 **왜 바꿨나** — `min-width` 는 **절대 바닥**이라 자리가 모자라도 양보하지 못한다 →
          칸이 줄어드는 대신 **부모 밖으로 밀려난다.** 실측(관리자 시스템 오류 `/admin/system-error`,
          768px, 글자 「아주 크게」): 검색어 504 + 아이콘 42 + 버튼 69 = **615px** 인데 폼이 줄 수 있는
          폭은 **563px** → 세트 박스는 95~658 로 줄었는데 **안쪽 「검색」 버튼은 639~708 까지** 그려져
          세트 밖 **+50px** · 카드 밖 **+5px** 이었다.
          🔥 **박스만 재면 안 보인다** — 세트(658)만 보고 「여유 45px」이라 잘못 읽었다.
             **안쪽 마지막 자식(검색 버튼)의 `right`** 로 재야 드러난다.
       🔬 **양보해도 자리표시자는 안 잘린다** — `TextMetrics` 가 희망폭을 낼 때 **안쪽 여백 1.85em +
          여유 0.5em** 을 이미 얹어 두므로 **여유분부터 깎인다.** 실측(768px, 글자 「아주 크게」):
          `/setting/privacy-log`(가장 긴 축) 희망 468 → 실제 454(글자에 필요한 폭 367 / 남은 자리 426) ·
          `/admin/member/list` 477 → 454(384 / 426) · `/admin/system-error` 504 → 454.
          **셋 다 자리표시자가 전 폭 온전**했다.
       ⚠️ **`min(var(--ds-kw-min), 100%)` 으로 바꾸지 말 것 — 그것으로는 안 풀린다.** 여기서 `100%` 는
          **아직 폭이 정해지지 않은 부모(세트)** 를 가리켜 순환이 되고, 고유 크기 계산 중의 백분율은
          **0 으로** 풀린다(가로 스크롤 예방선에서 이미 한 번 겪은 함정이다).
       📱 **좁은 화면(≤767.98px)은 대상이 아니다** — 3 절이 격자로 바꾸므로 아래 `@media` 밖의 일이고,
          실측상 375·430px 은 이 변경으로 **아무것도 바뀌지 않았다.** */
    .ds .ds-search-main > input[name="keyword"] {
        /* 🔑 **희망폭** — 자리가 있으면 여기까지 늘고, 없으면 스스로 양보한다. */
        flex: 1 1 var(--ds-kw-min, 12em);
        /* 🔑 **절대 바닥** — 여기까지만 양보한다. 이 아래로는 안 줄어든다. */
        min-width: 8em;
        /* ✂️ 그래도 모자라면 자리표시자가 글자 중간에서 뚝 끊기지 않고 「…」로 끝난다. */
        text-overflow: ellipsis;
    }
    /* 🔤 **자리표시자가 들어갈 만큼은 양보하지 않는다 — 모자라면 세트가 제 줄로 내려간다** (2026-09-17 여행사 1회차
          B2-07·C1-03·C4-02·C6-02·F1-04)
       🔬 바닥이 8em 이라 같은 줄의 기간·선택 칸에 밀려 **자리표시자 폭 아래까지** 줄었다 — 영문 견적 목록 1200px
          「Quotation no. · Land operator · P」(필요 273 / 칸 233), 계약 목록 768px(263 / 217), 리뷰 992px(263 / 251).
       ✅ 바닥을 **희망폭 − 여유 0.5em**(= `TextMetrics` 가 얹은 여유분만 양보)으로 올린다. 그 아래가 필요하면
          flex-wrap 이 **세트를 전폭 줄로 내린다** — 이 절 머리의 「최소값보다 좁아지면 전폭으로 내린다」가 그 설계다.
          실측(영문): 견적·예약 1200 **잘림 → 0**(머리 70 → 121px, 두 줄) · 계약 768 **→ 0** · 리뷰 768·992 **→ 0**.
       🔴 **상한 `22em` 을 지우지 말 것** — 위 2026-08-24 절의 사고(바닥이 부모보다 커서 버튼이 카드 밖 +50px)가
          그대로 돌아온다. 22em 은 768px·글자 「아주 크게」(입력 글자 18px)에서도 396 + 아이콘·버튼 111 = 507px 로
          한 줄(563px)에 들어가는 값이다. 그보다 긴 자리표시자는 위의 말줄임이 받는다.
       ⚠️ **속성 선택자 `[style*="--ds-kw-min"]` 을 빼지 말 것** — 인라인 변수가 없는 칸에서 `calc(var(...))` 는
          계산 시점 무효가 되어 `min-width` 가 **`auto` 로 풀린다**(8em 바닥까지 사라진다. 폴백은 규칙상 못 쓴다).
       📌 `TextMetrics` 의 희망폭 자체가 모자란 칸이 하나 있다(여행사 상품 목록 영문 「Search by product or DMC name」 —
          필요 245.8px / 희망 16.5em = 237.6px) — 그건 공용 모듈의 소문자 폭 계수 문제라 여기서 메우지 않았다(말줄임이 받는다). */
    .ds .ds-search-main > input[name="keyword"][style*="--ds-kw-min"] {
        min-width: min(calc(var(--ds-kw-min) - .5em), 22em);
    }
    /* 🔑 **세트 자신도 부모를 넘지 않는다** — flex 항목의 자동 최소 크기(`min-width: auto` = min-content)를 푼다.
       🔴 **선택자를 좁히지 말 것** — `.ds-search-main` 은 (가) 랜드사·여행사에서는 `.input-group` **안의 div**,
          (나) 관리자에서는 **`.input-group` 자신**이다. 한쪽에만 걸면 다른 앱에서 이 규칙이 조용히 사라진다.
       📐 특이도 (0,1,0) 으로 충분하다 — 이 두 속성을 다투는 규칙이 벤더에도 이 파일에도 없다.
          Bootstrap 의 `min-width: 0` 은 `.input-group > .form-control, .input-group > .form-select` 에만
          걸려 **div 인 (가) 세트에는 애초에 닿지 않는다.** */
    .ds-search-main { min-width: 0; max-width: 100%; }
    /* (다) 여행사 정산 이력 — 세트가 없으므로 바닥값만 준다.
       🔥 **폴백을 두지 말 것.** 인라인 변수가 없는 화면(관리자 공통코드의 좌측 `col-xl-4` 칼럼)에서는
          선언이 무효가 되어 `auto` 로 남아야 한다 — 그 칸은 1280px 에서 입력칸이 **106px** 뿐이라
          바닥값을 주면 그대로 넘친다(그 화면 주석에 실측이 적혀 있다). */
    .ds .card > .card-body form input[name="keyword"] { min-width: var(--ds-kw-min); }
}
@media (max-width: 767.98px) {
    /* 📱 좁은 화면 — 세트는 **줄 전체를 쓰는 2열 격자**가 된다(3 절이 `.input-group` 에 하는 것과 같다).
       🔑 관리자는 세트 자신이 `.input-group` 이라 3 절이 이미 같은 일을 한다 →
          **아래 값들은 3 절과 같아야 한다. 한쪽만 고치면 앱마다 폰 화면이 갈린다.** */
    .ds .input-group > .ds-search-main {
        grid-column: 1 / -1;
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: var(--ds-search-gap);
    }
    .ds .ds-search-main > .input-group-text { display: none; }
    .ds .ds-search-main > .form-control {
        width: 100% !important;
        max-width: none !important;
        grid-column: 1 / -1;
        border-radius: .425rem !important;
    }
    .ds .ds-search-main > .btn {
        grid-column: 1 / -1;
        border-radius: .425rem !important;
        margin-left: 0 !important;
    }
    /* 「검색 취소」가 함께 나와 버튼이 둘일 때만 한 줄에 반반.
       버튼이 하나면 `:nth-last-of-type(2)` 가 매칭되지 않아 위의 전폭 규칙이 그대로 산다. */
    .ds .ds-search-main > .btn:nth-last-of-type(2) { grid-column: 1 / 2; }
    .ds .ds-search-main > .btn:nth-last-of-type(2) ~ .btn { grid-column: 2 / -1; }
    /* 🔴 **숨긴 버튼(`d-none`)은 「함께 나온 버튼」이 아니다 — 이 줄을 지우지 말 것**
       (2026-08-25 사장님 지적 · 구현 #49: *"375, 430에 검색 버튼도 반쪽만 차지하는게 이상한데"*)
       🔬 `:nth-last-of-type()` 는 **`display:none` 인 형제도 그대로 센다.** 통계 필터 바는
          「검색 취소」를 서버가 아니라 **JS 가 `d-none` 으로 토글**해서 DOM 에는 늘 버튼이 둘이다
          → 위 두 줄이 언제나 걸려 **검색 버튼이 오른쪽 절반(147px)만 차지하고 왼쪽 절반이 빈다.**
       🔑 특이도가 위 줄과 같은 (0,5,0) 이라 **순서로 이긴다** — 이 규칙을 위로 옮기지 말 것.
       📌 조건부 렌더(`th:if`)로 버튼을 아예 안 그리는 화면에는 애초에 해당이 없다. */
    .ds .ds-search-main > .btn.d-none ~ .btn { grid-column: 1 / -1; }

    /* 🔴 **(나) 관리자 — 좁은 화면에서 세트가 줄 전체를 쓰게 하는 일은 여기가 맡는다.**
       🔬 그 앱의 세트는 **폼(`d-flex flex-wrap`)의 flex 항목**이고, 마크업의 `flex-grow-1` 이
          그 일을 하고 있었다. 2026-08-24 에 그것을 걷어냈으므로(위 (나) 블록의 이유) **여기서 되살린다.**
       🔥 이 줄을 빼면 세트가 **내용 폭으로 줄어** 검색어 칸이 폰에서 다시 찌그러진다 —
          넓은 화면은 멀쩡하므로 **눈으로 잡기 어렵다.**
       📌 랜드사·여행사는 해당 없다 — 그쪽 세트는 `.input-group` 격자의 칸이고
          위 `grid-column: 1 / -1` 이 이미 줄 전체를 준다. */
    .ds .card > .card-header form > .input-group.ds-search-main { flex: 1 1 100%; }
}

/* -------------------------------------------------------------
   5) 행 삭제 아이콘 버튼 — 평소 회색 / hover 시 danger
   opacity 로 흐리게 하지 말 것(표마다 달라 보인다)

   ※ 이 블록만 `.ds` 스코프가 없다.
     클래스명(.ds-row-del)이 고유해 스코프 없이도 다른 화면과 충돌하지 않고,
     아직 `.ds` 로 전환하지 않은 화면(quote-content 계열 = 견적/예약/계약 상세)에서도
     같은 삭제 버튼을 써야 하기 때문이다. (`.hdr-page-desc` 와 같은 이유)
   ------------------------------------------------------------- */
/* 크기에 !important 인 이유: Metronic `.btn.btn-icon.btn-sm{height:calc(1.5em + 1.1rem + 2px)}` 가
   (0,3,0) 이라 이 규칙(0,2,0)을 이겨 34px 이 된다.
   실제로 이 26px 규칙은 그동안 한 번도 적용된 적이 없었다(색만 먹고 크기는 34px 이었다). */
.ds-row-del.btn-icon {
    /* 🕘 26px 이었다 — 카드 안 아이콘 버튼을 22px(배지 높이)로 통일하면서 **여기만 남아** 있었다
       (2026-07-22 카탈로그로 확인). 값을 토큰으로 바꿔 한 곳에서 관리한다. */
    width: var(--ds-badge-h) !important;
    height: var(--ds-badge-h) !important;
    min-width: var(--ds-badge-h) !important;
    padding: 0 !important;
    background-color: transparent !important;
    border: 0;
    /* 🕘 회색(`--ds-text-label`)이었다 — 목록에서 삭제가 너무 튀지 않게 하려던 것이다.
       2026-07-22 사용자 지시로 **모든 삭제 버튼을 `btn-light-danger` 하나로 통일**하면서
       이 색만 남아 카탈로그에서 혼자 회색으로 드러났다. 색도 기본(danger)을 따른다. */
}
/* 🕘 여기 있던 `hover{background:danger-soft; color:danger}` 를 **걷어냈다** (2026-07-22 사용자 지시).
   hover 는 **Metronic 기본(solid danger + 흰 아이콘)** 으로 통일한다 — 템플릿 화면과 같은 모양이다.
   ⚠️ 연한 배경으로 덮는 방식은 아이콘 색까지 함께 되돌려야만 성립한다(아래 `i{color:inherit}`).
      한 곳이라도 빠뜨리면 **연분홍 위 흰 아이콘**이 되어 버튼이 사라진다 — 실제로 정산 신청 이력
      카드에서 그렇게 보였다. 규칙이 두 줄로 얽히느니 기본을 쓰는 편이 안전하다. */
/* 아이콘은 항상 버튼 색을 따른다 — hover 에서 Metronic 이 버튼 색을 흰색으로 바꾸면 함께 따라간다. */
.ds-row-del.btn-icon i,
.ds-row-del.btn-icon:hover i { color: inherit !important; }
.ds-row-del.btn-icon i { font-size: .8rem; line-height: 1; }

/* 목록 표(벤더 관리·계약서 템플릿)용 변형 — 평소에도 danger 색으로 보인다. (2026-07-20 사용자 결정)
   편집 표(계약 상세의 항목 행)는 삭제 아이콘이 행마다 있어 회색이 맞지만,
   목록은 행당 하나뿐이라 눈에 띄는 편이 낫다. 아이콘 모양·크기(26px)는 기본형 그대로 쓴다. */
/* 배경 없이 아이콘만 (2026-07-20 사용자 결정) — 연분홍 배경이 카드 안에서 버튼처럼 튀었다.
   hover 에서만 옅은 배경을 깔아 눌리는 자리임을 알린다. */
/* 카드 안의 삭제 버튼은 stretched-link 위로 올린다 (2026-07-20).
   목록을 카드 격자로 바꾸면서 제목에 `stretched-link` 를 걸어 카드 전체를 수정 링크로 만들었는데,
   그 ::after 가 카드를 통째로 덮어(z-index:1) **삭제 버튼이 눌리지 않았다**
   (elementFromPoint 로 확인: 버튼 위치에서 잡히는 건 링크였다). position 만으로는 부족하다. */
.card .ds-row-del { position: relative; z-index: 2; }

/* -------------------------------------------------------------
   6) 행 추가 버튼 — 점선 전폭. 표 안 마지막 행(별도 tbody)에 둔다.
   padding/border 에 !important 인 이유: Metronic
   `.btn:not(.btn-outline):not(.btn-dashed):not(.border-hover):not(.border-active):not(.btn-flush):not(.btn-icon)`
   가 (0,7,0) 이라 선택자만으로는 못 이긴다.
   테두리색이 --kt-border-color 면 흰 배경 대비 1.12:1 로 안 보여 primary 톤을 쓴다.
   ------------------------------------------------------------- */
.ds .btn.ds-add-row {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: .35rem;
    width: 100%;
    border: 1px dashed rgba(0, 158, 247, .5) !important;
    background-color: transparent;
    color: var(--ds-primary);
    border-radius: .425rem;
    padding: .45rem !important;
    font-size: .9rem;
    font-weight: 600;
    line-height: 1.5;
}
.ds .btn.ds-add-row:hover {
    border-color: var(--ds-primary) !important;
    background-color: var(--ds-primary-soft);
}
/* 클릭 후 focus 가 남아 hover 색이 유지되는 것 방지 */
.ds .btn.ds-add-row:focus:not(:hover) {
    border-color: rgba(0, 158, 247, .5) !important;
    background-color: transparent;
    color: var(--ds-primary);
}
.ds .btn.ds-add-row i { font-size: .75rem; line-height: 1; }

/* -------------------------------------------------------------
   7) '더보기' 버튼 — 테두리 + 행 우측 끝 + 아이콘 1개
   ------------------------------------------------------------- */
.ds .ds-more-btn {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    margin-left: auto;
    border: 1px solid var(--ds-line-soft);
    background: var(--ds-surface);
    color: var(--ds-primary);
    border-radius: .4rem;
    padding: .26rem .7rem;
    font-size: .95rem;
    font-weight: 600;
    line-height: 1.5;
    cursor: pointer;
    white-space: nowrap;
    flex: none;
}
.ds .ds-more-btn:hover {
    border-color: var(--ds-primary);
    background: var(--ds-primary-soft);
}
.ds .ds-more-btn i { font-size: .8rem; line-height: 1; }

/* -------------------------------------------------------------
   8) 목록 화면 — 상태 필터 바
   ------------------------------------------------------------- */
/* 모바일 전용 필터 드롭다운 — 데스크톱에서는 칩이 보이므로 숨긴다
   (모바일 규칙은 위 @media 블록의 .ds-filter-row 참고, 2026-07-17) */
.ds .ds-filter-row { display: none; }

/* 🔴 **2026-08-24 (H5) 좌우를 `--ds-card-x`(17 / 30px)로 바꿨다** — 이 줄도 카드 본문이라
   위·아래 카드와 **끝선이 하나**여야 한다(🔬 실측: 여기만 45px 로 남아 목록 카드보다 15px 안쪽이었다).
   🔥 `2.25rem` 으로 되돌리지 말 것. */
.ds .ds-filter-bar { padding: .5rem var(--ds-card-x) !important; }
/* 🔴 **탭 줄은 접지 않는다 — 그 줄 안에서 가로로 민다** (2026-08-27 · D-E6).
   🔬 **왜** — 아이템 유형 탭이 **16개**라 접히면서 필터 카드가 통째로 커졌다.
      실측(랜드사 아이템 목록, 영문): 768 = **4줄 167px** · 900 = 3줄 124 · 1100 = **4줄 167** ·
      1440 = 3줄 124 · **1920 도 2줄 82px** — 가장 넓은 화면에서도 한 줄에 안 들어간다.
      고친 뒤 **일곱 폭 전부 1줄 53px**(탭 줄의 자연 폭은 2,071px).
   🔑 **문서 가로 스크롤은 그대로 0** 이다 — 미는 것은 **이 `.nav` 안에서만**이다
      (규칙: 페이지 본문은 가로 스크롤 금지). 실측: 일곱 폭 전부 문서 가로 **0**.
   🔑 **탭이 다 들어가는 화면에는 막대가 안 생긴다**(`auto`) — 열 수가 적은 목록은 그대로다.
   ⚠️ **`overflow-y` 를 그냥 `hidden` 으로 두지 말 것** — 이 줄은 내용이 세로로 **2px** 넘쳐 있어
      그대로 자르면 무엇이 잘리는지 모르는 채로 잘린다. `--ds-sp-1` 만큼 아래 여백을 줘
      **넘침 자체를 없앴다**(실측 `scrollHeight == clientHeight`).
   ⚠️ 마크업의 `flex-wrap`(Metronic 유틸리티, `!important`)은 **네 화면에서 걷어냈다** —
      한 요소의 줄바꿈을 마크업과 CSS 가 함께 정하면 반드시 어긋난다(배치 규칙 ④).
      🔥 새 필터 탭 줄에 `flex-wrap` 을 다시 붙이지 말 것. */
.ds .ds-filter-bar .nav {
    gap: .25rem;
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    /* 🔴 **안쪽 위아래 여백은 같은 값이다** (2026-09-07 사장님 확정: *"요소 안쪽 위아래, 좌우측 여백은 무조건 통일"*)
       🕘 아래만 `--ds-sp-1`(4px)이고 위는 0 이었다 — 실측 T0 / B4.
       🔑 **위를 같은 토큰으로 채운다**(0 으로 맞추지 않는다) — 아래 값은 이 줄의 세로 넘침 2px 을
          없애려고 넣은 것이라 지우면 그 결함이 돌아온다(바로 위 ⚠️ 참고).
       🔑 밑줄은 벤더 `.nav-line-tabs` 의 `border-bottom` 이 그린다 — 여백이 아니다. */
    padding-top: var(--ds-sp-1);
    padding-bottom: var(--ds-sp-1);
    scrollbar-width: thin;
    overscroll-behavior-x: contain;
}
.ds .ds-filter-bar .nav > .nav-item { flex: 0 0 auto; }
/* 🔴 **768px 이상에서는 다시 접는다 — 가로 밀기는 575.99~767.98px 구간에만 남는다** (2026-08-28 사장님 결정)
   > *"768에서 카테고리가 왜 두줄로 안나오고 가로 스크롤이 생기지? 가로 스크롤 금지야."*
   > *"768 이상에서만 줄바꿈. 단 줄바꿈 될때 모든 줄의 첫번째 탭은 좌측 여백이 없도록 설정"*

   🔬 실측(2026-08-28 · 여행사 `/travelProduct/list` 한국어): **768px 에서 탭 내용 640 / 보이는 폭 613**
      → **27px 넘침**. 900 이상은 남는다. 🔑 「768px 의 문제」가 아니라 **탭 개수·문구 길이가 폭을 넘으면
      어디서든 나는 것**이라 영문·카테고리 추가에서 더 넓은 폭에도 나타난다.
   🔑 **「모든 줄의 첫 탭 왼쪽 여백 0」은 이미 지켜져 있다** — 18절이 2026-08-23 에 벤더의
      `margin: 0 1rem` + `:first-child{margin-left:0}` 을 걷고 **부모의 `column-gap`** 으로 옮겼다
      (`:first-child` 는 **첫 줄만** 잡아 둘째 줄이 16px 밀렸던 그 결함이다).
      🔥 그 규칙을 margin 으로 되돌리지 말 것 — 되돌리는 순간 이 결정이 그날로 깨진다.
   ⚠️ **`overflow-x`·`overflow-y` 를 함께 `visible` 로** 돌린다 — 한쪽만 `visible` 이면 CSS 가 그것을
      `auto` 로 계산해 **막대가 그대로 남는다.**
   📌 **575.98px 이하는 대상이 아니다** — 그 구간은 `.nav` 를 감추고 `select` 로 바꾸므로(위 `.ds-filter-row`)
      375·430 에는 애초에 탭 줄이 없다.

   🕘 **이 규칙은 2026-08-27 D-E6(위 절)의 `nowrap` 을 768px 이상에서 되돌리는 것이다 — 알고 하는 것이다.**
      🔬 D-E6 이 밀기를 고른 이유는 **랜드사 아이템 목록의 탭이 16개**여서다(접으면 768=4줄 167px ·
         1920 도 2줄 82px 로 필터 카드가 통째로 커졌다). **그 대가가 되돌아온다.**
      🔥 사장님이 그 대가를 **알고 B(768 이상 줄바꿈)를 고르셨다** — 「카드가 커진다」를 이유로
         `nowrap` 으로 되돌리지 말 것. 감당하기 어려운 화면이 나오면 **되돌리지 말고 사장님께 여쭐 것.**
      🕘 검토했으나 채택되지 않은 갈래: ⓒ 한 화면만 접게(화면마다 규칙이 갈린다) ·
         ⓓ 좁을 때는 드롭다운(경계를 768까지 올린다 — 제안됐으나 사장님이 B 로 정하셨다).
         🔥 ⓓ 로 되돌리지 말 것.
   📌 이 앱은 `nav-line-tabs` 를 쓰는 화면이 **7개**다 — 전부 함께 바뀐다. */
@media (min-width: 768px) {
    .ds .ds-filter-bar .nav {
        flex-wrap: wrap;
        overflow-x: visible;
        overflow-y: visible;
    }
}
/* 🕘 **`.ds-filter-chip`(알약형 칩) 규격은 없앴다** (2026-08-21 구현 #47 사용자 결정 —
      *"F-4 는 밑줄 있는 탭으로 바꿔야 할것 같아"*).
   🔑 목록 필터는 이제 **밑줄형 탭 골격**을 쓴다:
      `ul.nav.nav-tabs.nav-line-tabs.nav-line-tabs-2x` + 링크 `nav-link px-3 py-2`.
      밑줄은 **벤더의 `nav-line-tabs`** 가 그리고, 크기·활성색은 **18절 한 곳**이 준다.
   🔥 **CSS 로 밑줄을 흉내 내는 규격을 다시 만들지 말 것** — 그러면 「탭 만드는 방법」이 두 벌이 되어
      「탭은 밑줄형 한 가지다」라는 규칙이 그날로 무너진다. 세 앱 12화면이 이 골격을 함께 쓴다.
   📌 **이 앱에는 쓰는 화면이 없다**(칩을 쓰던 12화면은 랜드사 7 · 여행사 5다) —
      🔥 **안 쓴다고 이 주석째 지우지 말 것**: 세 앱 CSS 가 갈리면 그날부터 화면이 앱마다 달라진다. */

/* 🔑 필터 바의 일반 버튼(조회 등)은 **옆 입력창(32px)과 높이를 맞춘다** — 위 0-3) '인접 규칙'.
   Metronic `btn-sm` 기본이 34.3px 이라 그냥 두면 같은 줄에서 2.3px 어긋난다
   (실측 위반: `/stats/category` 의 조회 버튼 34.3 vs 입력창 32).
   ⚠️ 화면별 CSS 로 각자 고치지 말 것 — 필터 바는 전 목록이 공유하는 골격이다.
   ⚠️ Metronic `.btn:not(…)×6` = (0,7,0) 이라 padding 은 `!important` 가 필요하다.
   ⚠️ 칩(.ds-filter-chip)과 아이콘 버튼은 자기 규격이 따로 있으므로 제외한다. */
.ds .ds-filter-bar .btn:not(.btn-icon):not(.ds-filter-chip) {
    height: var(--ds-control-h);
    padding: 0 .9rem !important;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    white-space: nowrap;
}

/* -------------------------------------------------------------
   9) 헤더 슬롯(layout:fragment="headerExtra") 용
   ※ 이 요소들은 #kt_header 안이라 `.ds`(#kt_content) 스코프가 닿지 않는다.
     클래스명이 고유해 스코프 없이도 다른 화면과 충돌하지 않는다.
   ------------------------------------------------------------- */
.hdr-page-desc {
    margin-left: 1.15rem;
    padding-left: 1.15rem;
    border-left: 1px solid var(--ds-line-soft);
    font-size: .95rem;
    color: var(--ds-text-faint);
    white-space: nowrap;
}
/* 좁은 화면에선 페이지 타이틀 존이 콘텐츠로 내려가므로 부제는 감춘다 */
@media (max-width: 991.98px) {
    .hdr-page-desc { display: none; }
}

/* 🔴 **유틸바의 페이지 명은 줄바꿈하지 않는다 — 줄어들어야 하는 것은 설명이 먼저다**
   (2026-08-25 사장님 지시 · 구현 #49)
   > *"유틸바에 페이지 명이 줄바꿈 되면 안돼. 페이지 명을 건드리기 전에 페이지 설명이 말줄임되어야 하고,
   >   페이지 설명이 없을 때에는 페이지 명이 말줄임 되면 될듯."*

   🔬 실측(여행사 파트너 통계 · 1100px · 영어): 제목 `Partners & Products` 가 **229px 안에서 두 줄**로
      접혀 유틸바가 60px 이 됐다. 옆의 설명(`.hdr-page-desc`)은 `white-space: nowrap` 이라
      **한 글자도 안 물러나고** 제목만 접힌 것이다.
   🔑 **말줄임 규칙은 원래 있었다 — 다만 `@media (max-width: 991.98px)` 안에만 있었다.**
      좁은 화면은 설명을 아예 감추므로 제목만 줄이면 됐지만, 넓은 화면에서는 **설명이 살아 있어서**
      제목과 자리를 다툰다. 그래서 같은 규칙이 넓은 화면에도 필요하다.
   🔑 **순서는 「제목은 0, 설명은 그 밖」으로 정한다**(아래 두 블록) — 비율로는 안 된다.
   🔥 **`white-space: nowrap` 을 빼지 말 것** — 빼는 순간 제목이 다시 두 줄이 된다.
   🔥 **제목에 `display: flex` 를 주지 말 것** — `text-overflow` 가 동작하지 않는다
      (좁은 화면 규칙에 같은 경고가 이미 있다). */
/* ⚠️ **사슬의 모든 단계에 `min-width: 0` 이 있어야 한다 — 네 줄이 세트다.**
   flex 자식의 기본값 `min-width: auto` 는 **내용보다 작아지지 않는다**는 뜻이라, 한 단계라도
   빠지면 그 단계가 내용 폭만큼 부풀어 아래의 `flex-shrink` 가 **아무 일도 하지 않는다.**
   🔬 실측(2026-08-25): 이 네 줄을 넣기 전에는 1100px 화면에서 `#kt_header_nav` 가 **1756px** 로
      부풀어 설명이 1,477px 그대로 서 있었다(줄어들 자리가 없으니 말줄임도 안 걸린다).
   🕘 같은 네 줄이 좁은 화면(≤991.98px) 블록에도 있다 — **그쪽을 지우지 말 것**(거기서는 설명을
      감추고 제목만 줄이는 다른 계산이다). */
#kt_header > .container-fluid,
#kt_header_wrapper,
#kt_header_nav { min-width: 0; }

/* 📏 **머리(헤더)의 좌우 여백은 본문 컨테이너와 같은 값이다** (2026-09-18 여행사 2회차 I1-06)
   🔬 실측(여행사 계약 목록): 머리 `.container-fluid` 는 Metronic 기본 **15px(<992) / 30px(≥992)** 인데
      본문 `#kt_content_container` 는 `px-10` = **2.5rem(40px)** · 좁은 화면 **12px**(1-x 절) 이라
      사용자 아바타 오른끝과 카드 오른끝이 **768px 25px · 992·1200·1920 10px · 360 3px** 어긋났다.
   ✅ 머리를 본문 값에 맞춘다 — 본문 82화면이 이미 그 값이라 **고칠 자리는 여기 한 줄**이다.
   ⚠️ 대가: 머리 안 요소가 안쪽으로 10px(≥992) · 25px(768~991) 들어오고 그만큼 제목·부제가 쓸 폭이 준다
      (부제는 「넓은 화면에서는 머리 제목이 말줄임으로 물러난다」 규칙이 받는다).
   🔑 `rem` 이다 — 본문 `px-10` 도 rem 이라 글자 크기 3단계에서 둘이 **함께** 커진다(px 로 박으면 다시 갈린다).
   🔥 본문 쪽(`px-10`·12px)을 머리에 맞춰 줄이지 말 것 — 카드 여백 규격(H5)이 그 값을 전제로 확정됐다. */
#kt_header > .container-fluid { padding-left: 2.5rem; padding-right: 2.5rem; }
@media (max-width: 767.98px) {
    #kt_header > .container-fluid { padding-left: 12px; padding-right: 12px; }
}

/* 🔴 **제목은 물러나지 않는다(`flex-shrink: 0`) — `200 : 1` 같은 비율로 두지 말 것.**
   🔬 실측(2026-08-25): 설명 쪽에 `flex-shrink: 200`, 제목에 `1` 을 줬더니 **둘 다 말줄임**됐다
      (사장님 지적: *"설명이 말줄임 되면 페이지명은 그대로 있어야지"*). flex 는 모자란 폭을
      **비율로 한 번에 나눠 깎기** 때문에, 비율이 아무리 기울어도 제목 몫이 **0 이 되지는 않는다.**
   🔑 「먼저/나중」은 비율이 아니라 **`0` 과 그 밖**으로 가른다. */
#kt_header_nav > .hdr-page-h1 {
    min-width: 0;
    flex-shrink: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* 설명이 없는 화면에서는 줄일 것이 제목뿐이다 → 그때만 제목이 말줄임된다(사장님 지시).
   ⚠️ 좁은 화면(≤991.98px)에서는 `.page-title` 이 본문으로 내려가 이 조건이 저절로 참이 되는데,
      그것이 곧 그 폭의 기존 동작(설명은 감추고 제목만 말줄임)이라 어긋나지 않는다. */
#kt_header_nav:not(:has(.hdr-page-desc)) > .hdr-page-h1 { flex-shrink: 1; }
#kt_header_nav > .page-title { min-width: 0; flex-shrink: 200; }
.hdr-page-desc {
    min-width: 0;
    flex-shrink: 200;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* 🔴 **부제가 있는 머리 줄은 접히지 않는다 — 부제가 먼저, 그다음 배지가 말줄임으로 물러난다**
   (2026-09-18 · 랜드사 1회차 S1-01 영문 · 정본 P63ⓝ).
   🔬 `.page-title` 마크업에 `flex-wrap` 이 있어 랜드사 영문 정산 신청 목록 992·1200px 에서 부제와 정책 배지
      (「Settlement Policy: After Contract Closed」 315px)가 **두 줄로 쌓였고**(머리 높이 30 → 53px) 부제가 머리 바
      위 끝에 붙었다 — 위 `flex-shrink: 200` 말줄임이 **접힘에 밀려 한 번도 일하지 못했다.**
   🔑 순서는 위 규칙 그대로(부제 200 · 배지 1). 부트스트랩 `flex-wrap` 이 `!important` 라 여기도 필요하다.
   ⚠️ 좁은 화면(≤991.98px)은 부제를 감추고 `.page-title` 이 본문으로 내려가므로 대상이 아니다. */
@media (min-width: 992px) {
    #kt_header_nav > .page-title:has(> .hdr-page-desc) { flex-wrap: nowrap !important; }
    #kt_header_nav > .page-title > .badge { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
}

/* ── 헤더 타이틀 존: 식별번호 배지 + 상태 배지 + 날짜 (2026-07-22 Trade.css 에서 이관) ──
   🕘 원래 Trade.css(메뉴 전용)에 있었는데 **12개 비-trade 화면**(정산·이행·상품·고객센터…)이
      이걸 쓰려고 Trade.css 를 통째로 로드하고 있었다. "전 메뉴 공통 CSS 는 전역에" 원칙에 따라
      이리로 옮긴다. 이름에 `quote` 가 들었지만 견적 전용이 아니다(개명은 전 템플릿을 건드려 보류).
   ⚠️ `.ds`(#kt_content) 스코프를 쓰지 않는다 — headerExtra 는 `#kt_header` 안이라 닿지 않는다.
   페이지 타이틀("견적")과 메타 사이의 구분선·간격. Metronic 기본 구분선은 common.css 가
   헤더에서 display:none 처리하므로 직접 그린다. */
/* 🗑 **`.ds-field-act`(항목 안 행위 버튼)는 2026-08-17 에 없앴다** — 쓰는 곳이 0이 됐다.
   🕘 「이 랜드사 기본 마진율」의 설정 버튼이 항목 안에 있어 만들었던 규격인데,
      그 버튼이 **기본 마진율 카드의 푸터**로 가면서 카드 푸터 규격이 그대로 걸렸다.
   🔑 **되살리기 전에 먼저 물을 것** — 처리 버튼을 항목 안에 두어야 하는가?
      이 저장소의 답은 **카드 푸터**다(크기 규격이 거기에만 걸리고, 값 옆의 연한 버튼은
      **상태 배지로 읽힌다** — 2026-08-15 구현 #58 에서 지적받은 그 형태다). */
/* 🔘 **검색폼이 없는 카드 머리의 행위 버튼은 푸터 버튼과 같은 크기다** (2026-08-17 사용자 지적)
   🔬 `.ds-header-action` 은 `--ds-control-h`(≈38.4px)를 쓴다 — **검색창과 끝선을 맞추기 위해서**다
      (같은 머리에 검색폼이 있는 목록 화면의 「등록」 버튼이 그 자리다).
   🔥 그런데 **검색폼이 없는 카드 머리**에서는 맞출 상대가 없고, 같은 화면의 카드 푸터 버튼
      (`--ds-btn-h` ≈35.2px)과 **크기만 갈린다.** 랜드사별 마진율 상세에서 「이 랜드사 기본 마진율 ▸ 설정」과
      카테고리 카드의 「설정」이 **같은 모달을 여는 같은 행위인데** 3px 달라 보였다.
   🔑 그래서 **`:has(form)` 이 아닌 머리에서만** 푸터와 같은 토큰을 쓴다 —
      검색 화면의 정렬 근거는 **그대로 살아 있다.**
   ⚠️ 화면에서 `btn-sm`·px 로 맞추지 말 것(그러면 이 규칙이 있는 줄도 모르고 또 어긋난다). */
.ds .card > .card-header:not(:has(form)) .card-toolbar .btn.ds-header-action {
    height: var(--ds-btn-h);
}
/* 🔻 **카드 제목과 계수 배지 사이 세로 구분선** (2026-08-17 사용자 지시)
   🔑 제목 옆 배지가 **제목의 일부인지 별개인지** 안 읽혀서 둘 사이를 가른다.
      선례는 헤더의 `hdr-title-sep`(페이지 제목 ↔ 부제)이고 여기는 **카드 안**의 같은 일이다.
   🔥 유틸리티 조합(`h-20px border-gray-200 border-start mx-4`)으로 화면에 적지 말 것 —
      `border-gray-200` 은 계조를 직접 고르는 것이라 다크에서 갈리고, 화면마다 값이 달라진다.
   ⚠️ 높이는 **`em`** 이다(글자 크기 3단계 토글을 따라 함께 늘고 준다). px 로 박지 말 것.
   📌 배지 쪽 좌우 여백은 이 선이 만든다 — 배지에 `ms-3` 을 함께 주지 말 것(간격이 두 벌이 된다). */
/* 🔴 **`.ds` 만으로는 부족하다 — 거래 화면의 뿌리는 `.quote-content` 다** (2026-08-20 구현 #97).
   🔬 실측: 세 앱에서 `quote-content` 를 뿌리로 쓰는 화면이 **21개**(DMC 13 · Agency 4 · Admin 4)인데
      그 화면들은 `.ds` 가 없어 이 규칙이 **조용히 안 걸렸다**(선이 안 그려지고, 선이 만들던 여백도 없어진다).
      그래서 계약 상세에 구분선 대신 `ms-3` 을 손으로 넣는 일이 생겼다.
   🔥 화면에서 `ms-*` 로 때우지 말 것 — 여백이 두 벌이 되고 화면마다 값이 달라진다. */
:is(.ds, .quote-content) .card-title > .ds-title-sep {
    display: inline-block;
    width: 1px;
    height: 1.05em;
    margin: 0 .85rem;
    background: var(--ds-line-soft);
    vertical-align: -.15em;
}
/* 🔑 **뒤따르는 배지가 감춰져 있으면 선도 없다** (2026-09-18 · 랜드사 1회차 S2-19 · 정본 P63ⓔ)
   🔬 랜드사 정산 신청 상세 「정산 신청 이력」이 0건일 때 배지는 서버가 `d-none` 으로 그리는데
      선은 JS 가 **다시 그릴 때만** 감춰져, 첫 화면에 선만 남았다.
   🔑 짝을 CSS 가 판정하므로 서버 렌더·JS 토글 어느 쪽에서도 선이 따로 남지 않는다. */
:is(.ds, .quote-content) .card-title > .ds-title-sep:has(+ .d-none) {
    display: none;
}
/* 🏷 **제목 옆 「항목명 + 값 배지」 묶음은 제목과 한 줄에 선다 — 좁으면 항목명이 먼저 접힌다** (2026-09-19 랜드사 2회차 S2-04 · 정본 P69ⓒ)
   🔬 실측(랜드사 정산 신청 상세 · 영문 「Settlement Status | Platform Margin Rate 6.00%」): 묶음이 `text-nowrap` 한 덩어리라
      제목 160 + 구분선 27 + 묶음 215 = 402px 가 카드 머리 내용 폭(1200px 348 · 1920px 396)을 넘어
      **묶음 통째로 둘째 줄**로 떨어졌다(title top 258 / badge 284 · 제목 칸 높이 53). 구분선만 첫 줄 끝에 남았다.
   🕘 그 전(P63ⓝ, 2026-09-18)은 **배지만** 떨어져 항목명과 값이 갈렸고, 그래서 한 덩어리로 묶었던 것이다 —
      🔥 **`text-nowrap` 으로 되돌리지 말 것**(그러면 다시 통째로 떨어진다). 항목명과 값이 한 줄에 서는 것은 그대로 지킨다.
   ✅ 묶음의 기준 폭을 0 으로 둔다(`flex: 1 1 0`) — 제목 줄에 넣을지를 **최소 폭**(가장 긴 낱말 + 배지)으로 판정하고,
      남는 폭을 채운 뒤 **항목명 글자만** 줄을 바꾼다. 배지는 줄지 않는다(`flex-shrink:0`).
      정말 좁아 최소 폭도 안 들어가면 묶음이 통째로 다음 줄로 간다 — 그때도 항목명과 값은 붙어 있다.
   📌 이 앱에는 아직 쓰는 화면이 없다(2026-09-19 기준 랜드사 1곳) — 세 앱 한 벌이라 같이 둔다.
   📌 간격은 `column-gap` 한 벌이다 — 항목명에 `me-*` 를 함께 주지 말 것. */
:is(.ds, .quote-content) .card-title > .ds-title-kv {
    display: inline-flex;
    align-items: center;
    column-gap: .5rem;
    flex: 1 1 0;
}
:is(.ds, .quote-content) .card-title > .ds-title-kv > .ds-title-kv-k {
    min-width: 0;
    white-space: normal;
    line-height: 1.3;
}
:is(.ds, .quote-content) .card-title > .ds-title-kv > .badge {
    flex-shrink: 0;
}
/* 🏷 **배지 색은 화면이 정하지 않는다 — 역할 이름으로 고른다** (2026-08-20 사용자 지시 · 구현 #97)
   > *"같은 성격의 배지는 같은 색이어야해."* · *"색상 같은 건 하드 코딩 된 부분이 없도록 해 줘."*
   🔬 계기 — 같은 뜻·같은 i18n 키(`company.approve.pending`)인 「승인 대기」가 마진율 메뉴에서는
      `badge-light-dark`(회색), 업체 메뉴에서는 `badge-light-warning`(노랑)이었다. 화면마다 색을
      손으로 적었기 때문이다. 🔬 실측: 세 앱 템플릿·JS 에 하드코딩된 `badge-light-*` 가 **627곳**.

   🔑 **색을 정하는 곳은 둘뿐이다.**
   | 배지 종류 | 색을 정하는 곳 |
   |---|---|
   | 값에 따라 달라지는 것(상태·이력·유형) | **DB** — `TB_COMMON_CODE.C_SEMANTIC_TYPE_CODE_SEQ` |
   | 언제나 같은 표식(다국어·필수·NEW …) | **아래 다섯 클래스** |

   🔥 **템플릿·JS 에 `badge-light-*` 를 적지 말 것.** 아래 이름으로 **역할**을 고르면 색은 여기가 준다.
   🔑 다섯은 **DB 의 의미 구분(`SEMANTIC_TYPE`)과 같은 축**이다 — 두 경로가 같은 색으로 모인다.
   ⚠️ 색을 바꾸고 싶으면 **여기 한 곳**을 고친다(화면을 훑지 않아도 된다).
   🔥 **세 앱이 같은 블록이다 — 한 앱만 고치지 말 것.**
   ⚠️ **`--bs-*` 를 직접 쓰지 않는다** — 이 파일 0)절이 `--ds-*` 로 한 겹 감싸 두었고
      테마 대응이 그 층에 걸려 있다(원시 변수를 쓰면 그 층을 건너뛴다). */
/* 🔴 **글자색은 교정 토큰(`--ds-badge-*-fg`)이다 — 원색(`--ds-primary` 등)을 쓰지 말 것.**
      🔬 이 파일 아래쪽(「배지 글자 대비」 절)이 `.badge-light-*` 의 글자색을 이미 교정해 뒀다:
         노랑 1.47 → 4.60 · 초록 1.92 → 4.62 · 파랑 2.75 → 4.63 · 빨강 3.44 → 4.62.
      🔥 원색을 쓰면 **역할 클래스로 옮긴 배지만 글자가 흐려진다** — 같은 뜻의 배지가 두 밝기가 된다.
         (2026-08-20 실측으로 잡았다. 처음에 원색으로 짰다가 바로잡은 것이다.)
      📌 배경은 원색 계열(`--ds-*-soft`)이 맞다 — `.badge-light-*` 와 **같은 값**이다. */
.badge.ds-badge-info  { background-color: var(--ds-primary-soft); color: var(--ds-badge-primary-fg); }  /* 분류·표식·수량 */
.badge.ds-badge-wait  { background-color: var(--ds-warning-soft); color: var(--ds-badge-warning-fg); }  /* 대기·미설정·확인 필요 */
.badge.ds-badge-ok    { background-color: var(--ds-success-soft); color: var(--ds-badge-success-fg); }  /* 완료·인증·동의 */
.badge.ds-badge-stop  { background-color: var(--ds-danger-soft);  color: var(--ds-badge-danger-fg);  }  /* 중지·필수·중요·회수 */
/* 🔑 중립 표식은 `--ds-fill`(gray-100) + `--ds-text` 다 — Metronic `badge-light-dark` 와 같은 값이고,
      `badge-light-secondary` 는 흰 카드 위에서 거의 안 보인다(`screen-tokens.md` 규칙). */
.badge.ds-badge-mute  { background-color: var(--ds-fill);         color: var(--ds-text);    }  /* 부가 표식(공통 등) */
/* 🆕 **NEW 배지는 자기 이름을 갖는다** (2026-09-01 사장님 지시 — *"NEW 배지는 연두색이 좋겠는데?"*)
   🕘 그전에는 `ds-badge-info`(보라·파랑 계열)였다.
   🔴 **`ds-badge-ok` 를 재사용하지 않는다** — 그 클래스는 **「완료·인증·동의」**용이라 색을 같이 쓰면
      **한 색이 두 뜻**을 갖게 된다. 🔑 **색이 같아도 뜻이 다르면 이름이 달라야** 나중에 갈라 칠 수 있다.
   🎯 쓰는 곳은 **여행사·랜드사의 공지 목록 · 공지 상세 · 고객센터 개요**다.
      📌 이 앱에는 아직 쓰는 곳이 없다 — 🔥 **안 쓴다고 지우지 말 것**(세 앱 공용 파일은 한 벌로 유지).
   🔥 **아래 「배지 글자 대비」 절의 30% 외곽선 목록에도 이 이름을 넣어야 한다** — 색만 정의하면
      외곽선이 안 걸려 그 배지만 테두리가 없다(2026-09-01 에 실제로 빠뜨려 지적받았다). */
.badge.ds-badge-new   { background-color: var(--ds-success-soft); color: var(--ds-badge-success-fg); }  /* 7일 안 등록 표식 */

.hdr-quote-meta { display: flex; align-items: center; gap: .6rem; margin-left: 1.15rem; padding-left: 1.15rem; border-left: 1px solid var(--ds-line-soft); }
.hdr-quote-no   { font-weight: 700; color: var(--ds-text); background: var(--ds-primary-soft); border-radius: .4rem; padding: .26rem .65rem; font-size: .95rem; white-space: nowrap; }
.hdr-quote-date { font-size: .88rem; color: var(--ds-text-faint); white-space: nowrap; margin-left: .25rem; }
/* 📅 **날짜는 폭과 무관하게 보인다** (2026-09-19 랜드사 2회차 B1-05 · P69ⓐ)
   🕘 여기 있던 `@media (max-width: 1199.98px) { .hdr-quote-date { display: none; } }` 를 **지웠다.**
      견적 상세 유틸바에 요청일이 있던 시절(Trade.css) 자리를 아끼려던 줄인데, 요청일은 2026-08-14 에
      요약 카드로 옮겨 갔고 지금 이 클래스를 쓰는 곳은 **세 앱 메인의 「회사명(접속자)·오늘 날짜」뿐**이다.
   🔬 그 줄 때문에 1199px 이하(992 헤더 안 · 768·360 본문으로 내려온 줄)에서 **회사명만 남고 날짜가 사라졌다.**
   🔑 992 이상은 아래 「머리 제목이 말줄임으로 물러난다」가 제목 칩을 먼저 줄이고 날짜는 `flex-shrink:0`,
      991 이하는 `.hdr-quote-meta` 가 `flex-wrap` 이라 좁으면 날짜가 다음 줄로 접힌다 — 숨길 이유가 없다. */
/* 🔥 **좁은 화면에서는 배지 줄이 접혀야 한다** (2026-08-17 글꼴 교체 중 실측).
   이 줄은 flex 인데 자식이 전부 `white-space: nowrap` 이라 **줄바꿈이 안 되고 그대로 넘친다.**
   🔬 실측(375px, 관리자 업체 목록): 머리 배지 3개(승인 대기·거래 준비 미비·정산 준비 미비)가
      Pretendard 에서는 360px 안에 간신히 들어갔는데 Noto Sans KR 에서 **361.3px** 가 되어
      페이지 전체에 **가로 스크롤**(scrollWidth 373 vs 360)이 생겼다.
   🔑 배지 **사이에서만** 접는다 — 각 배지의 `white-space: nowrap` 은 그대로 둔다
      (배지 안에서 글자가 잘리면 숫자와 라벨이 갈라진다).
   ⚠️ **넓은 화면은 그대로다** — 이 규칙은 이미 타이틀 존이 본문으로 내려오는
      구간(991.98px 이하)에만 걸린다. 위쪽 기본 규칙에 `flex-wrap` 을 옮기지 말 것. */
@media (max-width: 991.98px)  { .hdr-quote-meta { margin-left: 0; padding-left: 0; border-left: 0; flex-wrap: wrap; } }
/* 📱 **좁은 화면에서는 머리 제목(`.hdr-quote-no`)도 줄을 바꾼다** (2026-09-17 여행사 1회차 E2-37·E2-40)
   🔬 이 자리에는 번호(CT-…)만 오는 것이 아니라 **공지·문의·분쟁 제목, 상품명, 회사명**이 온다.
      360px 영문 공지 상세에서 제목 span 이 `nowrap` 이라 **379px** 로 서서 문서 가로 스크롤 **31px**.
   ✅ 줄을 바꾸게 두되 **상자 폭은 줄 폭까지**(`max-width:100%`). 번호는 360px 에서도 한 줄에 들어가
      (실측 160px 안팎) 실제로는 제목만 접힌다. 🔑 넓은 화면(헤더 안)은 한 줄 규칙 그대로다.
   ⚠️ 규칙은 아래 「제목과 배지 규격 통일」 블록 **뒤**에 둔다 — 그 블록이 같은 특이도로 `nowrap` 을 다시 건다. */

/* 🔥 **이 줄이 간격까지 함께 지워 브레드크럼과 배지가 딱 붙었다** (2026-08-18 사용자 지적 — 폰 실기기).
   🔬 실측(375px · root 12px · 관리자 업체 상세): 브레드크럼 `right 131.13` / 배지 `left 131.13` — **간격 0**.
      넓은 화면에서는 위 규칙의 `margin-left`+`padding-left`+세로 구분선(합 2.3rem+1px)이 벌려 주는데,
      좁은 화면에서 셋을 함께 0 으로 만들면서 **간격이 통째로 사라졌다.**
   ✅ **세로 구분선은 그대로 없애 둔다** — 줄이 접히면 선이 엉뚱한 자리(둘째 줄 맨 앞)에 선다.
   🔑 간격은 **`.hdr-quote-meta` 의 `margin-left` 가 아니라 부모의 `column-gap`** 으로 준다.
      🔥 **`margin-left` 로 되돌리지 말 것** — `.page-title` 은 `flex-wrap` 이라 배지 묶음이
         **제 줄로 접히는 화면이 이미 있고**(실측: 관리자 업체 **목록**은 375px·320px 에서 접힌다),
         그때 margin 은 **접힌 줄까지 들여써** 브레드크럼과 왼쪽 끝이 어긋난다.
         `column-gap` 은 **같은 줄에 나란히 설 때만** 걸려 접힌 줄의 왼쪽 끝(12px)이 그대로 남는다.
      ⚠️ **`gap` 으로 적지 말 것** — 세로에도 걸려 접힌 줄이 함께 내려간다(지금 세로 간격은 0 이다).
   ⚠️ **px 로 박지 말 것** — rem 이라 글자 크기 3단계 토글(0-0 절)을 따라 함께 늘고 준다.
   🔬 실측(전/후): 375px **0 → 12px** · 320px **0 → 12px** · 768px·991px **0 → 16px**.
      넓은 화면(≥992px)은 이 규칙 밖이라 그대로다(실측 1280px: `margin-left 18.4px` · `padding-left 18.4px` · 구분선 1px 유지). */
@media (max-width: 991.98px)  { .page-title { column-gap: 1rem; } }

/* 제목(.hdr-quote-no)과 배지 규격 통일 — 실측상 글자 크기·패딩·높이·모서리가 전부 달랐다.
   두 요소를 같은 규격으로 못박고 굵기만 제목 700 / 배지 600 으로 위계를 남긴다.
   Metronic `.badge` 의 padding·font-size 를 이기려면 !important 가 필요하다. */
.hdr-quote-meta > .hdr-quote-no,
.hdr-quote-meta > .badge {
    display: inline-flex;
    align-items: center;
    font-size: .95rem !important;
    line-height: 1.2 !important;
    padding: .35rem .7rem !important;
    border-radius: .45rem !important;
    white-space: nowrap;
}
.hdr-quote-meta > .hdr-quote-no { font-weight: 700; }
.hdr-quote-meta > .badge        { font-weight: 600; }
/* 📱 좁은 화면의 머리 제목 줄바꿈 — 근거는 위 「좁은 화면에서는 머리 제목도 줄을 바꾼다」 주석. */
/* ✂️ **띄어쓰기 없는 긴 제목도 접힌다** (2026-09-18 랜드사 1회차 P65ⓗ).
   🔬 실측(랜드사 아이템·템플릿 상세 · 공백 없는 200자 관리명): `white-space: normal` 만으로는 **끊을 자리가 없어**
      칩이 한 줄로 서서 문서 가로 스크롤 **360px 1,444px · 768px 1,938px** 이 생겼다. 관리자 앱도 같은 머리 조각을 쓴다.
   ✅ `overflow-wrap: anywhere` — **끊을 자리가 없을 때만** 아무 데서나 끊는다(평소 낱말 줄바꿈은 그대로).
      🔥 `break-word` 로 바꾸지 말 것 — min-content 계산에 안 들어가 flex 부모가 여전히 200자 폭으로 선다.
   ✅ 사슬(`.hdr-quote-meta` → 칩)에 `min-width: 0` — 칩이 `inline-flex` 라 글자가 익명 flex 항목이 되고,
      그 항목과 부모 둘 다 `min-width:auto`(=min-content) 면 줄지 않는다. */
@media (max-width: 991.98px)  {
    .hdr-quote-meta { min-width: 0; max-width: 100%; }
    .hdr-quote-meta > .hdr-quote-no { white-space: normal; max-width: 100%; min-width: 0; overflow-wrap: anywhere; }
}
/* ✂️ **넓은 화면(≥992px)에서는 머리 제목이 말줄임으로 물러난다** (2026-09-18 여행사 2회차 B3-11·E2-37·E4-56·E5-63)
   🔬 실측(여행사 상품 상세 · 78자 상품명): 제목 칩이 `nowrap` 이고 `.hdr-quote-meta` 가 `min-width:auto` 라
      내용 폭(895px)에서 줄지 않았다 → 992px 에서 칩 오른끝 1307(뷰포트 밖) · 1200px 에서 메시지·알림·프로필
      아이콘 위를 덮고, 줄어들 수 있는 것이 페이지 명뿐이라 「상품 상세」가 「상품 …」로 잘렸다.
   🔑 위 「유틸바의 페이지 명」 규칙과 같은 원리다 — **페이지 명은 0, 머리 제목(부제)이 먼저 물러난다.**
      사슬(`.page-title` → `.hdr-quote-meta` → 칩)의 모든 단계에 `min-width:0` 이 있어야 한다.
   ⚠️ 칩은 `display:block` 이어야 말줄임이 걸린다(`inline-flex` 면 글자가 익명 flex 항목이 되어 안 걸린다).
      높이는 줄높이 1.2 + 위아래 패딩 그대로라 옛 `inline-flex` 와 같다.
   📌 배지·날짜는 줄지 않는다(`flex-shrink:0`) — 줄어드는 것은 사람이 친 글(제목·상품명)뿐이다.
   📌 전체 글자는 `common.js` 가 **잘렸을 때만** `title` 로 붙인다(마우스를 올리면 보인다). */
@media (min-width: 992px) {
    #kt_header_nav:has(.hdr-quote-meta) > .hdr-page-h1 { flex-shrink: 0; }
    .hdr-quote-meta { min-width: 0; }
    .hdr-quote-meta > .badge,
    .hdr-quote-meta > .hdr-quote-date { flex-shrink: 0; }
    .hdr-quote-meta > .hdr-quote-no { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
}

/* 🔥 **폭을 줄였다 늘리면 제목과 배지의 순서가 뒤바뀐다** (2026-08-15 사용자 지적:
      *"브라우저 너비를 줄였다가 다시 늘리면 타이틀과 배지 순서가 뒤바뀜"*).

   🔬 원인은 Metronic 의 **KTSwapper** 다. `fragments/header.html` 의 `.page-title` 은
      `data-kt-swapper-parent="{default:'#kt_content_container','lg':'#kt_header_nav'}"` +
      **`mode="prepend"`** 로 붙어 있고, 스와퍼는 `resize` 마다
      `부모가 다르면 → parent.prepend(요소)` 만 한다(`scripts.bundle.js` 의 `KTSwapper`).
      · 처음 열 때(넓음): 이미 `#kt_header_nav` 안이라 **아무 일도 안 한다** → 제목 → 배지 (정상)
      · 좁히면: `#kt_content_container` 로 **prepend**(본문 맨 위 — 이것은 의도된 동작)
      · 다시 넓히면: `#kt_header_nav` 에 **prepend** → **제목(h1) 앞에 꽂힌다** → 순서가 뒤집힌 채 굳는다
   🔑 그 컨테이너는 `d-flex` 라 **`order` 로 보이는 순서를 못 박는다** — 스와퍼가 DOM 어디에 꽂든
      제목이 먼저다. ⚠️ 좁은 화면에서는 `.page-title` 이 이 컨테이너 밖(본문)이라 영향이 없다.
   ⚠️ **`mode` 를 `append` 로 바꿔 고치지 말 것** — 그러면 좁은 화면에서 페이지 타이틀 존이
      본문 **맨 아래**로 내려간다(브레드크럼이 화면 끝에 붙는다).
   ⚠️ 세 앱의 `header.html` 이 같은 구조다 — 이 규칙도 세 앱 공용 한 벌이다. */
#kt_header_nav { display: flex; }
#kt_header_nav > .hdr-page-h1 { order: 0; }
#kt_header_nav > .page-title  { order: 1; }

/* -------------------------------------------------------------
   10) 오버레이 z-index — 고정 헤더(#kt_header)가 z-index:100 이라
   그 이하면 드로어 상단(타이틀/닫기)이 헤더에 가려진다.
   ------------------------------------------------------------- */
.ds-overlay-scrim { z-index: 105; }
.ds-overlay        { z-index: 106; }
.ds-overlay-scrim2 { z-index: 107; }
.ds-overlay2       { z-index: 108; }

/* ── 🌫 **막(scrim)과 판(모달·드로워)의 경계는 여기 한 곳이 정한다** (2026-08-18 사용자 지적 — 폰 실기기)

   > *"다크모드에서 모달이나 드로워가 뜰 때 바깥 암전 레이어가 없는 것처럼 보여."*

   🔬 **왜 다크에서만 그런가**(실측 — 관리자 「카테고리 마진율 설정」 모달, **막 세기를 .40 으로 맞춰** 비교):
        라이트 : 판 #FFFFFF  vs 막 뒤 배경 #9B9FA4 → **2.66**
        다크   : 판 #1E1E2D  vs 막 뒤 배경 #0D0D14 → **1.18**
      다크는 판과 배경이 **둘 다 거의 검정**이라, 막을 **완전 불투명 검정**으로 해도 이 대비의
      **상한이 1.28** 이다(계산·실측 일치). 🔥 **그래서 「막을 더 진하게」만으로는 절대 안 고쳐진다.**
   🔑 그래서 **두 가지를 함께** 쓴다 — ① 막을 진하게(`--ds-scrim`) ② 판에 테두리(`--ds-panel-edge`).
      🔥 **둘 중 하나만 되돌리지 말 것.** ②를 빼면 다크에서 판의 경계가 다시 사라진다
      (그림자는 검정 위 검정이라 안 보인다). ①을 빼면 뒤 화면이 그대로 살아 있어 판이 떠 보이지 않는다.
   ⚠️ **`.ds` 스코프를 쓰지 않는다** — 모달·Swal 은 `#kt_content.ds` 바깥이다(3절과 같은 이유).
   ⚠️ **특이도가 벤더와 같아 「나중에 로드된다」로만 이긴다**(head.html 순서) — 이 파일을 위로 옮기지 말 것. */
.modal-backdrop      { background-color: var(--ds-scrim); }
/* 세기는 토큰의 alpha 가 쥔다. `.fade` 의 `opacity:0` 은 그대로라 **서서히 나타나는 것은 유지**된다. */
.modal-backdrop.show { opacity: 1; }
/* SweetAlert2 의 확인창도 **같은 자리**다(전 화면에서 쓰인다) — 벤더 기본이 `rgba(0,0,0,.4)` 라 증상이 같았다. */
.swal2-container.swal2-backdrop-show,
.swal2-container.swal2-noanimation { background: var(--ds-scrim); }

/* 📏 **확인창 안에 세로 스크롤이 생기던 것** — 높이 상한은 「내용 200px」이 아니라 **「화면에 들어오는 만큼」**이다
   (2026-08-19 구현 #97 사용자 지적: *"모달에 스크롤 안생기게 해줘야 할 듯"*).

   🔬 **층별로 재서** 범인을 골랐다(랜드사 견적 발송 확인창 · 뷰포트 2353px · 짐작으로 정하지 않았다):
        `.swal2-container`      max-height `none` · overflow-y **hidden**(벤더 `!important`) → 안 흐른다
        `.swal2-popup`          max-height `none` · overflow **visible**                      → 안 흐른다
        `.swal2-html-container` max-height **200px** · overflow **auto** ·
                                scrollHeight **206** > clientHeight **200**  → **여기가 흘렀다**
      → 범인은 SweetAlert2 기본이 아니라 **Metronic 번들의
        `.swal2-container .swal2-html-container { max-height: 200px }`** 한 줄이다.
        특이도가 같아 **나중에 로드되는 이 파일**이 이긴다(바로 위 절과 같은 이유 — 이 파일을 위로 옮기지 말 것).
   🕘 **이번에 드러난 것**이다 — 같은 날 그 확인창을 `text:` → `html:` 로 고치면서 메시지 입력칸이 비로소
      그려졌다(구현 #96). 그전에는 태그가 글자로 찍혀 높이가 달랐다.
   🔑 값 **`calc(100vh - 22rem)`** 의 22rem(352px)은 **팝업이 내용 말고 쓰는 높이의 실측치**다 —
      팝업 안쪽 533 − 내용 200 = **333px**(아이콘·버튼·상하 패딩) + 컨테이너 여백 20px.
      → 뷰포트에 들어오는 한 상한에 **닿지 않아 스크롤이 안 생기고**, 넘치면 `overflow:auto` 가 그대로 흘린다.
   🔥 **`none` 으로 풀지 말 것** — `.swal2-container` 가 `overflow-y: hidden !important` 라
      팝업이 화면보다 커지면 **버튼이 잘린 채 스크롤도 안 된다**. 그래서 상한 자체는 남긴다.
   🔥 **`60vh` 로 바꾸지 말 것**(2026-08-16 구현 #75 가 지운 규격의 주석에 남아 있는 값이다) —
      낮은 창에서 `0.6h + 333px` 이 뷰포트를 넘어 **같은 잘림**이 난다(뷰포트 727px 아래).
      `calc` 은 **어떤 뷰포트 높이에서도 종전(200px)보다 더 잘리지 않는다** — 뷰포트가 552px 밑이면
      상한이 200px 보다 작아져 **스크롤이 늘 뿐**이고, 그것이 잘리는 것보다 낫다.
   ⚠️ **`input:'textarea'` 로 뜨는 창의 입력칸은 `.swal2-html-container` 밖**이라 이 22rem 에 안 들어간다 —
      그 창들은 종전과 같은 만큼만 쓰므로 이 변경으로 나빠지지 않는다. */
.swal2-container .swal2-html-container { max-height: calc(100vh - 22rem); }

/* 📐 **확인창 본문은 왼쪽 정렬이고, 좌우 여백은 판의 여백 하나로 끝낸다** (2026-08-20 사용자 지시)
   🔬 실측(세 앱 전부 같았다 — 팝업 512px 기준):
      `.swal2-popup` 좌우 **32px** + `.swal2-html-container` 자체 좌우 **28.16px**(1.6em) = **60.16px**.
      안내 문구가 길어 두 줄이 되면 가운데 정렬이라 **줄마다 시작점이 달라져** 읽기 나쁘다.
   🔴 **짧은 문구는 가운데, 긴 문구는 왼쪽**이어야 한다 (2026-08-20 사용자 지적).
      🕘 처음엔 왼쪽 정렬 + 여백 0 으로 했다가 *"「견적을 요청하시겠습니까?」 같은 짧은 문구를
         좌측 정렬해 놓으니 어색하다"* · *"여백이 너무 작다"* 는 지적을 받았다.
      ✅ **글상자를 내용 크기(`width: fit-content`)로 줄여 가운데 둔다** — 한 줄이면 상자가 글자만큼만
         차지해 **가운데로 보이고**, 길어지면 상자가 폭을 다 써서 **왼쪽 정렬이 드러난다.**
      🔑 **줄 수를 세는 것이 아니다** — `fit-content` 는 「글자를 한 줄로 폈을 때의 폭」과 「쓸 수 있는
         폭」 중 **작은 쪽**을 잡는다. 그래서 **줄바꿈 문자가 없는 긴 한 줄**도 정확히 가려낸다
         (실제 확인창에 `<br>` 없는 긴 문장을 넣어 3줄로 접히며 왼쪽 정렬되는 것을 확인했다).
      🔥 `text-align: center` 로 되돌리지 말 것 — 두 줄이 되는 순간 **줄마다 시작점이 달라진다.**
      🔥 `width: fit-content` 를 빼지 말 것 — 빼면 짧은 문구가 왼쪽에 붙어 어색해진다.
   📌 **여백은 라이브러리 기본값 1.6em 그대로**다(판의 32px 과 합쳐 60px). 줄이지 말 것.
   ⚠️ 제목(`.swal2-title`)은 **가운데 정렬 그대로**다 — 지시는 「안내 문구 노출부」였다.
   🔥 `!important` 를 붙이지 않았다 — 라이브러리 기본값과 특이도가 같아 **나중에 로드되는 이 파일이
      이긴다**(바로 위 절과 같은 이유). 이 파일을 위로 옮기지 말 것.
   📌 개별 창이 가운데 정렬이 필요하면 그 창의 `html:` 안에서 정하면 된다(여기서 예외를 만들지 말 것).
   🔥 **세 앱이 같은 규칙이다 — 한 앱만 고치지 말 것.**
   ⚠️ 이 앱에는 `menuLevel/Setup.css` 가 `.swal2-html-container.setup-missing-body` 로 따로 손대는
      자리가 있다(초기 설정 미비 목록). 그쪽은 목록이라 왼쪽 정렬이 맞으므로 충돌하지 않는다. */
.swal2-container .swal2-html-container {
    text-align: left;
    width: fit-content;
    max-width: 100%;
    margin-left: auto;
    margin-right: auto;
    /* 🔤 **낱말 중간에서 자르지 말고 띄어쓰기에서 자른다** (2026-08-20 사용자 지시).
       🔬 한글은 `word-break` 기본값(`normal`)에서 **글자 단위로 끊긴다** — 「예상액입니다」가
          「예상액입」/「니다」로 갈린다. `keep-all` 이면 **공백에서만** 줄이 바뀐다.
       ⚠️ 그러면 **공백 없는 긴 덩어리**(URL·계약번호)가 판 밖으로 나갈 수 있다 →
          `overflow-wrap: break-word` 를 함께 둔다(그때만 강제로 자른다).
       📌 **마침표 뒤 줄바꿈은 CSS 가 아니라 `common.js` 의 훅**이 한다(문장부호 + 공백에서 `<br>`).
          🔥 여기에 같은 일을 하는 규칙을 만들지 말 것 — 두 벌이 되면 빈 줄이 생긴다. */
    word-break: keep-all;
    overflow-wrap: break-word;
    /* 📏 **알림창 본문에서는 body 의 `text-wrap-style: pretty` 를 끈다** (2026-09-18 사장님 지적 · A안 확정).
       🔬 실측(여행사 영문 로그인 「Your account is pending administrator approval.」): 글자 자리 392px 에
          문장 폭 약 393px 이라 두 줄이 되는데, `pretty` 가 두 줄을 **195 / 193px 로 반씩** 나눴다.
          두 줄이면 위 규칙대로 왼쪽 정렬이고 상자(fit-content)는 판 폭 그대로라 **글이 왼쪽에 몰리고
          오른쪽 약 250px 이 비었다**(*"줄바꿈이 왜 이래"*).
       🔑 `auto` 면 첫 줄이 폭을 채우고 넘친 낱말만 다음 줄로 간다 — 2026-08-20 의 「두 줄이면 왼쪽」은 그대로다.
       ⚠️ 대가: 둘째 줄에 낱말 하나가 남을 수 있다(일반 문단과 같은 모양 — 사장님이 알고 고르심).
       🔥 단축형 `text-wrap` 으로 적지 말 것(위 body 절과 같은 이유) · 🔥 세 앱이 같은 규칙이다. */
    text-wrap-style: auto;
}

/* 📏 **입력칸이 든 확인창은 본문 상자를 판 폭까지 편다** (2026-08-31 사장님 지시 · 항목 24)
   > *"사유 입력창 너비가 고정이 아니고 입력창 위 텍스트의 너비를 따르고 있어.
   >  텍스트는 한줄이면 중앙 두줄 이상이면 좌측 정렬되면 되고, 사유 입력창은 풀 사이즈 고정이어야지."*

   🔬 **실측(여행사 견적 상세 · 일곱 폭)** — 입력창 폭이 **안내 문구 글자 폭을 따라갔다**:
      | 폭 | 팝업 안쪽(버튼줄) | 문구칸 | 입력창 |
      |---|---|---|---|
      | 375 · 430 | 297 · 336 | **278.2** | **236.0** |
      | 768~1920 | 448 | **370.9** | **314.6** |
      거절 모달은 문구가 짧아 더 심했다 — 입력창 **235.0 @ 왼쪽에서 138.5px**(버튼줄은 448 @ 32).
   🔬 **원인** — 팝업은 `display:grid` 에 448px 한 칸인데, 바로 위 절이 본문 상자를
      **`width: fit-content`**(= 글자 폭)로 잡아 두었다. 입력칸은 그 상자 **안**에 있어 함께 좁아지고,
      상자의 좌우 안쪽 여백 **28.16px**(1.6em)만큼 더 들어간다.
   🔑 **그래서 「문구 정렬」과 「입력칸 폭」을 한 상자가 겸할 수 없다** →
      상자는 **판 폭까지 펴고**, 「짧으면 가운데 · 길면 왼쪽」은 **문구 감싸개(`.swal2-msg`)로 내린다.**
      (수법은 바로 위 절과 같다 — `fit-content` + `margin-inline:auto`. 줄 수를 JS 로 세지 않는다.)
   🔬 **적용 후**: 입력창이 버튼줄과 **같은 폭·같은 x**(375·430 → 297/336 @24 · ≥768 → 448 @32).

   🔴 **범위를 `:has()` 로 좁혔다 — 「입력 요소가 든 확인창」에만 건다.**
      🔥 **모든 확인창에 걸면 2026-08-20 사장님 결정이 무너진다** — 그날 *"「견적을 요청하시겠습니까?」
         같은 짧은 문구를 좌측 정렬해 놓으니 어색하다"* 는 지적으로 위 절의 `fit-content` 가 생겼다.
         입력칸이 없는 확인창에는 `.swal2-msg` 감싸개가 없으므로, 범위를 안 좁히면 그 창들의 짧은 문구가
         전부 왼쪽에 붙는다. 🔥 **`:has()` 를 지우지 말 것.**
      📌 감싸개를 넣는 호출부는 여행사 `Trade.js`(`_askReason` 외) · 랜드사 `Trade.js` ·
         `TravelProduct.js` · 관리자 `RbacRoles.js` 다.
   📌 좌우 여백(1.6em)을 0 으로 만드는 것은 **이 경우에만**이다 — 위 절의 *"여백은 기본값 1.6em 그대로"*
      는 **입력칸이 없는 확인창**에 대한 것이라 그대로 살아 있다.
   ⚠️ 이 앱에는 `menuLevel/Setup.css` 가 `.swal2-html-container.setup-missing-body` 로 따로 손대는
      자리가 있다 — 그쪽은 입력 요소가 없어 이 규칙에 안 걸린다.
   🔥 **세 앱이 같은 규칙이다 — 한 앱만 고치지 말 것.** */
.swal2-popup .swal2-html-container:has(.form-control, .form-select, select, textarea, input) {
    width: 100% !important;
    padding-inline: 0 !important;
}
.swal2-popup .swal2-html-container .form-control,
.swal2-popup .swal2-html-container .form-select {
    width: 100% !important;
}
/* 🔤 안내 문구 감싸개 — 한 줄이면 가운데, 두 줄 이상이면 왼쪽(위 절과 같은 수법) */
.swal2-popup .swal2-msg {
    display: block;
    width: fit-content;
    max-width: 100%;
    margin-inline: auto;
    text-align: left;
}

/* 📝 **확인창 안의 입력 폼** — `.ds-swal-*` (2026-08-20 · 문서 다국어)
   🕘 이 파일 아래쪽에 *"Swal 본문에 폼을 넣는 화면이 생기면 되살릴 것"* 이라며 지워 둔 옛 규격
      (`.ds-swal-form`)의 주석이 있다. 🔴 **그 규격을 그대로 되살리지 않았다** — 그것이 처방하던 둘이
      **이미 다른 규칙으로 해결돼 있기 때문**이다:
        ① `max-height: 60vh` → 바로 위 절의 **`calc(100vh - 22rem)`** 이 더 낫다(60vh 는 낮은 창에서 잘린다).
        ② 좌우 여백 제거 + 팝업 폭 34em → 그 처방은 **안내 + 셀렉트 + textarea** 짜리 폼의 것이다.
      🔑 지금 필요한 것은 **선택 상자 한 개**뿐이라 팝업 폭을 넓힐 이유가 없다.
      🔥 다시 큰 폼(입력칸 여러 개)이 생기면 그때 그 옛 주석의 처방을 함께 볼 것.
   🔑 **바로 위 `width: fit-content` 규칙을 건드리지 않는다** — 그 규칙이 「짧은 문구는 가운데,
      긴 문구는 왼쪽」을 만든다. 대신 **폼 쪽에 바닥 폭**(`min-width`)을 준다 — 안 주면 글상자가
      안내 문구 길이만큼만 잡혀 **선택 상자가 좁아 보인다.**
   ⚠️ 확인창은 `#kt_content.ds` **바깥**이다 — 선택자에 `.ds` 를 붙이지 말 것(조용히 무시된다).
   🔥 세 앱이 같은 규칙이다 — 한 앱만 고치지 말 것.
   📌 이 앱에는 아직 쓰는 화면이 없다(문서 언어는 **여행사 앱**의 견적 요청 확인창에서 고른다) —
      🔥 **안 쓴다고 지우지 말 것**: 세 앱 CSS 가 갈리면 그날부터 화면이 앱마다 달라진다. */
.swal2-container .ds-swal-text { margin-bottom: .25rem; }
.swal2-container .ds-swal-field {
    display: block;
    min-width: 16rem;               /* 🔑 `fit-content` 글상자의 바닥 폭 — 없으면 상자가 좁아진다 */
    margin-top: 1rem;
    text-align: left;
}
.swal2-container .ds-swal-label {
    display: block;
    margin-bottom: .35rem;
    font-size: .875rem;
    font-weight: 600;
    color: var(--ds-text-label);
}
.swal2-container .ds-swal-field .form-select { width: 100%; }
/* 고를 것이 하나뿐일 때 — **감추지 않고 고정 표시**한다(문서 언어를 모른 채 요청하지 않도록). */
.swal2-container .ds-swal-fixed {
    padding: .35rem .75rem;
    border: 1px solid var(--ds-line);
    border-radius: .425rem;
    background-color: var(--ds-fill);
    color: var(--ds-text-strong);
}
/* 입력칸 아래 고정 설명 — 23절 `.ds-hint` 와 **같은 역할**이지만 확인창은 `.ds` 밖이라 자기 규칙을 쓴다.
   🔥 경고색(`--ds-note-warn`)을 쓰지 말 것 — 경고가 아니라 안내다. */
.swal2-container .ds-swal-hint {
    margin-top: .35rem;
    font-size: .8rem;
    color: var(--ds-hint-text);
}

/* 판의 테두리는 **`box-shadow` 링**이다.
   🔥 **`border` 로 주지 말 것** — 라이트에서 투명 1px 이 **자리를 차지해** 판이 2px 커진다.
   ⚠️ **`outline` 도 안 된다** — 둥근 모서리를 무시해 각진 사각형이 덧그려진다(17절 `.ds-anchor-hit` 참고).
   📌 뒤에 이어 붙인 값은 Metronic `.modal-content` 의 기본 그림자 **그대로**다 →
      링이 투명한 라이트에서는 **종전과 완전히 같은 그림자**가 된다(무변경). */
.modal .modal-content {
    box-shadow: 0 0 0 1px var(--ds-panel-edge), 0 3px 6px 0 rgba(0, 0, 0, .1);
}

/* -------------------------------------------------------------
   11) 모바일 보정 — 목록 카드 그리드 / 카드형 표
   ------------------------------------------------------------- */
@media (max-width: 767.98px) {
    /* 목록 카드 그리드: g-10(2.5rem=30px)은 한 줄에 카드 하나뿐인 모바일에선 과하다.
       가로 거터는 단일 컬럼이라 의미가 없고 카드 폭만 깎으므로 0 으로 둔다. */
    .ds .row.g-10 {
        --bs-gutter-y: 1rem;
        --bs-gutter-x: 0;
    }
    /* 카드 안 여백도 좌우(12px)와 맞춘다 — p-9(27px)는 데이터 카드엔 과하다 */
    .ds .row.g-10 > [class*="col-"] > .card > .card-body {
        padding-top: 1rem !important;
        padding-bottom: 1rem !important;
    }
    /* 카드형 표: Metronic `.table tbody tr:last-child{border-bottom:0!important}` 때문에
       마지막 카드의 밑변만 사라진다 (위 Trade.css 의 견적 항목표와 같은 문제) */
    .ds .ds-card-table tbody tr {
        border-bottom: 1px solid var(--ds-line-soft) !important;
    }
}

/* -------------------------------------------------------------
   정산 신청 — 수수료 내역 (2026-07-21)
   신청액 − 수수료 = 계산서 발행액. 계산서를 얼마로 끊어야 하는지 그 자리에서 알려준다.
   ------------------------------------------------------------- */
.settle-breakdown {
    background: var(--ds-fill);
    border-radius: .475rem;
    padding: .85rem 1rem;
}
.settle-breakdown .sb-row {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: .5rem; padding: .2rem 0; font-size: .95rem;
}
.settle-breakdown .sb-val { font-weight: 700; white-space: nowrap; }
.settle-breakdown .sb-minus .sb-val { color: var(--ds-danger); }
.settle-breakdown .sb-total {
    border-top: 1px solid var(--ds-line-soft);
    margin-top: .35rem; padding-top: .5rem;
}
.settle-breakdown .sb-total .sb-val { color: var(--ds-primary); font-size: 1.1rem; }

/* ===================================================================
   9) 목록 카드 공통 골격 — `.ds-list-card` (2026-07-21 사용자 결정)

   **모든 목록의 데이터 카드는 이 골격 하나만 쓴다.**
   기준은 정산 대상 목록 카드의 실측값이다(머리/본문/발 3구역, 점선 구분).

       [머리]  제목 ·············· 배지 · ×삭제      ← 하단 점선
       [본문]  라벨 ········· 값
               라벨 ········· 값
       [발]    ······················ [버튼]         ← 상단 점선. **버튼 있을 때만**

   🕘 예전엔 목록마다 구조가 달랐다: 정산 대상·정산 이력은 `<table><tr>` 을 CSS 로 카드처럼
      위장했고(원래 표였던 잔재), 나머지 12개는 `div.card` 였다. 배지 위치도 제목 위/옆으로 갈렸다.
      → **마크업을 `div.card` 로 통일**하고 배치를 이 골격이 정한다.
      ⚠️ 매입매출 대조표만 예외로 표 모드를 유지한다 — 금액 **비교**가 목적이라 열 정렬이 필요하다.

   ⚠️ `grid-template-areas` 방식으로 돌아가지 말 것. 칸을 하나 추가할 때마다 CSS 네 곳
      (배치·라벨·정렬·nowrap)을 함께 고쳐야 했고 **실제로 계속 빠뜨렸다.**
   =================================================================== */
.ds-list-grid {
    display: grid;
    gap: var(--ds-gap-card);
    /* 넓으면 폭이 허락하는 만큼 열이 는다. `auto-fill` 이라 임계값을 손으로 관리하지 않는다.
       🕘 **(2026-09-07 에 이 상한을 걷었다)** ⚠️ `(100% - gap×2)/3` 이 트랙 최소폭이라
          **4열이 물리적으로 못 들어간다**(상한).
       🕘 **(2026-09-07 에 이 상한을 걷었다)** ⚠️ **gap 2개분을 숫자로 박지 말 것** — 한때 `1.5rem`
          하드코딩이라 `--ds-gap-card` 를 바꾸면 상한이 어긋났다(2026-07-22 간격을 넓히며 발견).
          🔑 식에서 gap 이 통째로 빠졌으므로 **이제 그 걱정 자체가 없다.**
       ⚠️ `min(100%, …)` 가 없으면 컨테이너가 트랙 바닥값보다 좁을 때 그대로 넘친다(실측: 375px 에서 29px).

       🔴 **트랙 바닥값은 `rem` 이다 — px 로 되돌리지 말 것** (2026-08-24 사용자 확정).
       🕘 `380px` 이었다. px 이라 **글자 크기 3단계 토글(0-0 절)을 따라가지 않아**, 글자를 키우면
          카드 안 내용만 커지고 **열 수는 그대로**여서 카드가 상대적으로 좁아졌다.
       🔬 실측(관리자 시스템 오류 · 글자 「아주 크게」): 배지 묶음이 154 → **220px** 로 커졌는데
          1100px 은 여전히 2열이라 카드가 **400px** 이었다 — 900px 1열(**695px**)보다 좁다.
          그래서 배지 묶음이 카드 밖으로 **+84px** 넘쳤다(1440px 2열 475px 에서 +9 · 1920px 3열 469px 에서 +14).
       🕘 바닥값은 `23.75rem`(root 16px 에서 **380px**)이었다 — 글자를 키운 사용자에게만 427 / 475px 로
          함께 커져 **열이 한 단계 줄고 카드가 넓어진다**는 것이 그 값의 뜻이었다.
          🔬 실측(1920px · 가장 큰 글씨): 회원·업체·분쟁·상품검수·관리자견적 다섯 화면이 3열 → 2열,
             넘침 0 · 가로 스크롤 0 · 같은 줄 카드 높이 일치.

       🔴 **2026-09-07 사장님 확정 — 3열 상한을 걷고 바닥값을 `--ds-lc-min` 으로 뺐다.**
          > *"구간별로 요소별 카드 최소 너비를 설정하고 넘어가는 데이터는 줄바꿈"*
          > *"목록은 한 페이지 12개씩이므로 열 갯수는 무조건 12의 공약수"*
          🔬 **왜 걷었나** — `(100% - gap×2)/3` 이 트랙 최소폭이라 UHD(3840px)에서 **열이 3에서 멈추고
             카드가 1128px 로 벌어졌다**(실측). 화면이 넓을수록 카드만 커지는 자리였다.
          🔑 바닥값은 **380px → 480px**(`--ds-lc-min`, 0-4 절) — 상품 목록 카드가 줄바꿈 없이
             필요한 폭이 **482px** 이다. 🔑 **넘치는 데이터는 줄바꿈으로 받는다**(열을 넓히지 않는다).
          📐 그래서 열 수가 전 구간에서 **12의 약수**다: 360·768 → 1 · 992·1200 → 2 · 1920 → 3 · 3840 → 6.
          🔥 **`max(...)` 와 `/ 3` 을 다시 넣지 말 것** — 그 둘이 곧 상한이다. 열을 제한해야 할 화면이
             생기면 **그 화면에서 `--ds-lc-min` 을 다시 정한다**(이 식을 고치지 않는다).
       🔴 **바닥값은 여전히 `rem` 이다 — px 로 되돌리지 말 것**(위 2026-08-24 확정 그대로). */
    grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--ds-lc-min)), 1fr));
    /* 아래 `subgrid` 가 나눠 쓰는 줄 트랙 — 카드 한 장이 두(또는 세) 행을 함께 차지한다. */
    grid-auto-rows: auto;
}

/* ══ 📐 구간별 카드 최소 너비 (2026-09-07 사장님 확정: *"구간별로 나눠"*) ═══════════════
   🔴 **열 수는 12의 약수(1·2·3·4·6)여야 한다** — 목록이 한 페이지 **12건**이라 5·7열이면
      마지막 줄이 들쭉날쭉해진다(사장님: *"열 갯수는 무조건 12의 공약수여야 해"*).
   🔴 **넘치는 데이터는 줄바꿈한다** — 그래서 이 값이 나중에 안 맞아도 **깨지지 않고 줄이 하나 는다.**
      🔥 자동 계산(JS·서버)을 하지 마라: JS 는 리사이즈마다 다시 재야 하고, 서버 계산은
         **페이지마다 열 수가 달라져** 넘길 때 배치가 바뀐다. 둘 다 검토했다가 버렸다.

   🔬 **실측으로 고른 값**(여행사 상품 목록, 크롬):
   | 구간 | 값 | 열 | 카드 폭 |
   |---|---|---|---|
   | ~767   | 30rem | 1열 | 285px  (격자가 `min(100%)` 로 잘려 어차피 1열) |
   | 768~   | 21rem | 2열 | 339px |
   | 992~   | 25rem | 2열 | 408px |
   | 1200~  | 29rem | 2열 | 475px |
   | 1920~  | 30rem | 3열 | 478px  · 3840 에서 **6열 556px** |

   🔴 **`1920` 경계가 여기서만 필요하다** (2026-09-07 사장님 승인).
      🔬 `1200~` 을 한 값으로 두면 **1200 에서 2열**이려면 `min ≤ 474.5px`, **3840 에서 7열을 막으려면**
         `min ≥ 474px` — **0.5px 폭**에 걸어야 한다. 데이터가 조금만 바뀌어도 깨진다.
      🔥 그러니 이 경계를 지우지 마라. 지우면 UHD 가 **7열**(약수 아님)이 되거나 1200 이 **1열**(카드 966px)이 된다.

   🕘 **한때 `23.75rem` 하나로 전 구간을 덮었다** — 그래서 UHD 에서 카드가 **1128px** 로 벌어졌고
      1200 에서는 1열이 됐다. 🔥 **값 하나로 되돌리지 마라.**
   ⚠️ `rem` 이라 **글자 크기 3단계 토글을 따라간다**(좁은 화면 root 12px · 넓은 화면 16px). px 금지. */
@media (min-width:  768px) { :root { --ds-lc-min: 21rem; } }
@media (min-width:  992px) { :root { --ds-lc-min: 25rem; } }
@media (min-width: 1200px) { :root { --ds-lc-min: 29rem; } }
@media (min-width: 1920px) { :root { --ds-lc-min: 30rem; } }

/* ══ 🕳 「데이터가 없습니다」 — `.ds-empty` (2026-08-16 구현 #71 사용자 지시) ═══════════════
   🔑 **가운데 정렬 한 곳**이다. 그전에는 화면마다 유틸리티 조합이 달라
      (`text-center text-gray-500 my-10` / `text-muted fs-8` / 그냥 `text-muted`) 같은 문구가
      어떤 카드에서는 가운데, 어떤 카드에서는 왼쪽에 붙어 있었다.
   🔑 **색은 낮추되 크기는 본문 그대로** 둔다 — 「없다」는 것도 읽어야 하는 답이다(보조 설명이 아니다).
   ⚠️ 카드 본문·드로워·목록 어디에 놓아도 같아야 하므로 **여백은 세로만** 준다(좌우는 부모가 정한다).
   🔥 화면에서 `text-center`·`my-10` 같은 유틸리티로 다시 칠하지 말 것 — 그렇게 갈렸다. */
/* ⚠️ **모달·드로어는 `#kt_content.ds` 바깥이라 `.ds .ds-empty` 가 닿지 않는다**
      (2026-08-18, 구현 #93 후속). 위 `.modal/.drawer .form-control` 블록과 **같은 이유·같은 처방**이다.
   🔬 실제로 당했다: 메시지 드로워에서 대화 목록이 비면 **흰 공간만** 남아, 사용자가
      *"왜 메세지가 다 날아갔지?"* 라고 물었다(DB 는 멀쩡했다). **「없다」를 말하지 않으면
      「사라졌다」로 읽힌다.**
   🔥 화면에서 `text-center`·`py-10` 같은 유틸리티로 다시 칠하지 말 것 — 그렇게 갈렸었다. */
.ds .ds-empty,
.modal .ds-empty,
.drawer .ds-empty {
    display: block;
    text-align: center;
    padding: 1.5rem 0;
    color: var(--ds-text-faint);
}

/* 🔴 **「없다」 한 줄뿐인 카드는 이웃 카드 높이를 따라가지 않는다** (2026-08-28 · D-6 E16 · A-E19).
   🔬 실측(여행사 통계 ▸ 지출, 데이터 0건): 「분해축 랭킹」 카드가 `card h-100` 이라 옆의 차트 카드에
      맞춰 **465px** 로 늘어났고, 그 안에는 「등록된 내용이 없습니다.」 **한 줄**뿐이었다
      (1920px 에서 **638×465**). 고친 뒤 **638×186** — 1100·1440 도 같이 줄었다(465 → 186).
   🔑 판정은 **「빈 상태 말고 그릴 것이 있는가」** 하나다 — 차트(`canvas`·`svg`)·표·목록 격자·
      항목 그리드·폼·지표 줄이 하나라도 있으면 **그 카드는 늘어나야 한다**(옆 카드와 줄이 맞아야 한다).
   ⚠️ **`!important` 가 필요하다** — Bootstrap 의 `.h-100` 이 `height: 100% !important` 라
      그것을 되돌리려면 같은 무게여야 한다. 🔥 선택자를 더 늘려 이기려 하지 말 것(규칙 ④).
   🔥 **`h-100` 을 마크업에서 걷어내는 방식으로 바꾸지 말 것** — 데이터가 있을 때는 그 카드가
      옆 차트와 높이를 맞춰야 하고, 그것을 마크업이 미리 알 수 없다.
   📌 목록 카드(`.ds-list-card`)는 대상이 아니다 — 그쪽 높이는 `.h-100` 유틸리티가 아니라 9절이 쥔다. */
.ds .card.h-100:has(.ds-empty):not(:has(canvas, svg, table, .ds-list-grid, .ds-fields, form, .ds-kpi-row)) {
    height: auto !important;
}

/* 대시보드 안의 '최근 …' 목록 — **트랙 최소폭을 좁힌 변형** (2026-08-09)
   🔑 대시보드 카드는 `col-xl-8`(실측 773px) 안에 들어간다. 본문 목록 기준(380px)이면
      `380*2 + gap` 이 그 폭을 **간발의 차로** 넘겨 1열이 되고, 항목이 9~10개면
      카드 높이가 561px → 1131px 로 **두 배**가 된다(옆 칸이 그만큼 빈다).
   🔑 여기 카드는 본문 목록과 달리 **본문 행이 2~3줄뿐**이라 좁아도 읽힌다.
   ⚠️ 상한이 **2열**이다(`/2`). 대시보드는 훑어보는 자리라 3열까지 늘리면 글자가 잘게 쪼개진다.
   ⚠️ 본문 목록에는 쓰지 말 것 — 거기는 라벨·값이 6~7줄이라 380px 이 필요하다. */
.ds-list-grid.is-compact {
    grid-template-columns: repeat(auto-fill, minmax(min(100%, max(340px, (100% - var(--ds-gap-card)) / 2)), 1fr));
}

.ds-list-card {
    display: flex;
    flex-direction: column;
    height: 100%;                 /* 같은 줄 카드 높이를 맞춘다 */
    border: 1px dashed var(--ds-line);
    border-radius: .475rem;
    box-shadow: 0 0 20px rgba(76, 87, 125, .02);
    background: transparent;
}

/* 🔴 **같은 줄에 선 카드는 같은 행을 나눠 쓴다 — `subgrid`** (2026-08-24 사용자 확정,
       일곱 폭 실물 화면판으로 검증)

   🔑 **무엇을 푸는가** — 아래 `.ds-lc-head` 는 이제 **모든 폭에서 접힌다**(그래야 배지 묶음이
      카드를 넘지 않는다). 접히면 카드마다 머리 높이가 갈리는데, 카드가 `subgrid` 로 **부모 격자의
      같은 행을 공유**하면 한 카드의 머리가 두 줄이 될 때 **그 줄 전체의 머리 행이 함께 늘어난다.**
      🔬 실측(2026-08-24): 여섯 카드의 머리 높이가 전부 **106px**, 카드 높이가 전부 **284px** 로 같았다.
   ⚠️ `height: 100%` 를 `auto` 로 되돌린다 — 높이를 맞추는 일은 이제 격자가 한다.

   🔴 **`grid-row` 의 span 은 「카드 직계 자식 수」와 같아야 한다.**
      서브그리드 축에는 **암시 트랙이 만들어지지 않아**, 남는 자식이 마지막 행으로 클램프되어
      **본문 위에 겹쳐 그려진다**(오류가 아니라 그냥 겹친다 — 눈으로만 잡힌다).
   🔬 실측(2026-08-24 세 앱 전수 · `.ds-list-grid` 직계 카드 **61곳**):
      자식 둘(머리+본문) **39곳** / 자식 셋(+`.ds-lc-foot` 또는 카드 직계 `.ds-lc-meta`) **22곳**.
      → 그래서 아래 `:has()` 한 줄이 셋짜리에 **span 3** 을 준다. 자식 넷은 0곳이다.
   🔑 **한 `.ds-list-grid` 안의 카드는 구성이 언제나 같다** — 같은 `th:each` 가 찍고,
      조건부 발(`th:if`)도 `canWrite`·`canWriteSetting` 같은 **화면 단위 플래그**뿐이라
      행마다 갈리지 않는다(2026-08-24 세 앱 전수 확인).
      🔥 **행마다 있고 없고가 갈리는 발·꼬리표를 만들지 말 것** — 그 순간 한 격자 안에서 span 이
         갈려 줄이 통째로 어긋난다. (「푸터는 두려면 전부, 빼려면 전부」 규칙과 같은 이유다.) */
.ds-list-grid > .ds-list-card {
    display: grid;
    grid-template-rows: subgrid;
    grid-row: span 2;
    height: auto;
}
.ds-list-grid > .ds-list-card:has(> .ds-lc-foot),
.ds-list-grid > .ds-list-card:has(> .ds-lc-meta) { grid-row: span 3; }

/* ── 머리: 제목 + 배지 + 삭제 ── */
/* 🔴 **카드 안쪽 칸에도 `min-width: 0` 이 있어야 한다 — 지우지 말 것** (2026-08-25 구현 #49)
   🔬 `.ds-list-card` 는 격자이고 그 칸(`.ds-lc-head`·`.ds-lc-body` 등)은 기본값이 `min-width: auto`,
      곧 **「내용의 최소 폭보다 작아지지 않는다」**다. 카드 제목(`.ds-lc-title`)이 `white-space: nowrap`
      이라 **제목 한 줄 전체 폭**이 그대로 그 최소값이 된다.
   🔬 실측(관리자 공지 목록 · 영어 · 가장 큰 글씨 · 1920px): 제목 63자가 743px 이라 머리가 **783px** 이
      되고, 카드의 트랙이 그만큼 벌어져 **479px 카드에서 303px 이 밖으로** 나갔다 →
      페이지에 **가로 스크롤 405px**. 한국어 제목은 짧아 안 드러났고 **영어에서만 보였다.**
   🔑 `min-width: 0` 이면 제목이 자기 `overflow: hidden` 으로 말줄임되고 카드는 트랙 폭을 지킨다.
   📌 이것이 `screen-layout.md` 「⑥ 격자 칸에 `min-width: 0` 을 빼지 마라」의 목록 카드 판이다
      (그 규칙은 `.ds-grid > *` 에만 적혀 있었다). */
.ds-list-card > * { min-width: 0; }

.ds-lc-head {
    display: flex;
    align-items: center;
    /* 🔴 **모든 폭에서 접힌다** (2026-08-24). 근거·경위는 아래 「카드 머리는 접혀야 한다」 주석. */
    flex-wrap: wrap;
    gap: .5rem;
    padding: var(--ds-lc-pad-y) var(--ds-lc-pad-x);
    border-bottom: 1px dashed var(--ds-line);
    /* 🔑 **아주 연한 배경** — 카드가 여러 장 나란히 놓일 때 어디서 끊기는지 눈으로 잡힌다
          (2026-08-12 사용자 지시: "데이터 카드들 서로간에 구분이 쉽도록").
       ⚠️ **`--ds-fill`(gray-100) 보다 더 연해야 한다.** 카드 머리는 제목·배지가 얹히는 자리라
          배경이 진해지면 배지의 연한 의미색(`badge-light-*`)이 바탕에 묻힌다.
          그래서 채도 없는 반투명 검정을 아주 얇게 깐다 — 어떤 카드 배경 위에서도 같은 만큼만 어두워진다.
       🔑 **다크에서는 반대로 얇게 밝힌다.** 다크 카드(#1E1E2D)에 검정을 깔면 구분이 아니라
          그냥 더 어두운 덩어리가 된다.
       ⚠️ 위쪽 모서리를 함께 깎지 않으면 배경이 카드 테두리 밖으로 각지게 삐져나온다.
       🔬 값을 여러 번 고쳤다(2026-08-12). `.022` → `.012` → `.04` → **`.02`**(사용자 선택).
          🔥 **가운데 두 번은 화면에 그려진 적이 없다** — 아래 🐛 의 주석 사고로 이 선언이 죽어 있었다.
             「연하게 내렸더니 안 보인다」는 지적의 원인은 값이 아니라 그 사고였다.
          실제로 다섯 단계(.04/.03/.025/.02/.012)를 나란히 비교한 뒤 **`.02` = #FAFAFA** 로 정했다.
       🔑 **배지와 다투지 않는 이유는 「연해서」가 아니라 「무채색이라서」다.**
          `badge-light-*` 바탕은 의미색이 섞인 옅은 색(예: light-primary ≈ #F1FAFF)이라
          같은 밝기라도 **색상이 달라** 구분된다. 그래서 여기에는 의미색을 쓰지 않는다.
       📌 중립 배지 `badge-light-dark` 의 바탕은 **#EFF2F5** 로, 이 배경(#FAFAFA)보다 **진하다** —
          즉 배지가 바탕에 파묻히는 구간이 애초에 아니다. 연하게 내릴 때 잃는 것은 배지 대비가 아니라
          **카드 경계**다. ⚠️ 더 내리지 말 것 — `.012`(#FCFCFC) 부터는 배경이 있는지 알 수 없다.
       🐛 이 주석을 고칠 때 **주석 닫기 기호를 중간에 남겨** 뒤 내용이 CSS 로 파싱됐고,
          오류 복구가 다음 세미콜론까지 삼키면서 **바로 아래 background-color 선언만 사라졌다**
          (테두리 반경은 살아남아 "규칙은 있는데 배경만 없는" 상태가 된다 — 2026-08-12 에 두 번 밟았다.
           두 번째는 이 경고문 안에 그 기호를 예시로 적어서였다. 여기에 그 기호를 쓰지 말 것). */
    background-color: rgba(0, 0, 0, .02);
    border-top-left-radius: .475rem;
    border-top-right-radius: .475rem;
}
html[data-theme="dark"] .ds-lc-head {
    background-color: rgba(255, 255, 255, .05);
}
.ds-lc-title {
    font-size: var(--ds-font-title);
    font-weight: var(--ds-weight-title);
    color: var(--ds-text-strong);
    min-width: 0;                 /* 긴 제목이 배지를 밀어내지 않게 */
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ds-lc-title:hover { color: var(--ds-primary); }
/* 🔴 **제목을 한 겹 감싼 래퍼의 `min-width` 를 푼다** (2026-08-24).
   🔬 위 `.ds-lc-title` 에는 말줄임 3종(`overflow`·`text-overflow`·`white-space`)이 **이미 있는데
      안 먹는 화면**이 있었다. 원인은 제목이 `div.d-flex` 로 **한 겹 더 감싸져** 있고 그 래퍼에
      `min-width: 0` 이 없다는 것 — flex 항목의 `min-width` 초기값 `auto` 는 **min-content** 라
      래퍼가 거기서 멈추고, 안쪽 제목은 줄어들 자리를 못 받는다.
   🔬 「IllegalStateException」처럼 **끊을 데가 없는 영어 한 단어**는 233px 에서 더 줄지 않는다.
   ⚠️ **다른 목록이 멀쩡해 보이는 것은 제목이 한글이라 글자 단위로 줄어들기 때문**이다 —
      배지가 하나만 더 붙으면 똑같이 넘친다(실측: 다른 목록 배지 묶음 160~180px,
      시스템 오류 220px). 🔥 「그 화면만의 문제」로 읽지 말 것.
   📌 제목이 `<a class="ds-lc-title">` 처럼 래퍼 없이 바로 오는 카드에서는 이 선택자가
      아무것도 잡지 않는다(그 카드는 원래부터 말줄임이 걸려 있었다). */
.ds-lc-head > div:first-child { min-width: 0; }
/* 배지·삭제는 **한 묶음으로 우측 끝**에. `margin-left:auto` 가 제목과 갈라 놓는다.
   🔴 **`flex-shrink: 0` 으로 되돌리지 말 것** (2026-08-24) — 묶음이 줄지도 접히지도 못하면
      카드보다 넓어지는 순간 **그대로 카드를 넘는다**(실측 1100px 에서 **+84px**). */
/* 🔴 **`min-width: 0` 을 지우지 말 것** (2026-08-28 D-4). flex 항목의 `min-width` 초기값은 `auto`(min-content)라
   `flex-shrink: 1` 을 줘도 **가장 넓은 배지 폭 아래로는 안 줄어든다** — 배지가 넷 붙는 카드(업체 목록의
   승인상태·재확인·유형·유형미설정)에서 그 바닥값이 카드 폭을 넘으면 그대로 카드를 넘는다.
   🔬 실측(2026-08-28, 일곱 폭 × 한국어·영문 × normal·xlarge): 넣기 전후 모두 **넘침 0** —
   지금 데이터에서는 화면이 한 픽셀도 안 바뀐다. **긴 상태 이름·긴 유형 이름이 들어올 때를 위한 바닥값**이다.
   ⚠️ 배지 자신의 `white-space: nowrap` 은 그대로 둔다(9절 「카드 머리는 접혀야 한다」). */
.ds-lc-head-end { margin-left: auto; display: flex; align-items: center; gap: .4rem; flex-shrink: 1; flex-wrap: wrap; justify-content: flex-end; min-width: 0; }
/* ⚠️ 삭제 버튼은 **배지와 같은 높이**여야 한다. px 로 박지 말 것 —
   배지는 여백이 em 기반이라 root 13px→모바일 12px 에서 22px→20px 로 준다.
   `1.7rem` 이면 함께 줄어든다(이 함정은 정산 이력 카드에서 이미 겪었다). */
.ds-lc-head-end .ds-row-del.btn {
    width: var(--ds-badge-h); height: var(--ds-badge-h);
    padding: 0 !important;        /* Metronic `.btn:not(…)×6` = (0,7,0) 이라 !important 가 필요하다 */
    display: inline-flex; align-items: center; justify-content: center;
}

/* 🔥 **카드 머리는 접혀야 한다** — 2026-08-17 좁은 화면에서 발견, **2026-08-24 에 모든 폭으로 넓혔다.**

   🕘 **여기에는 「⚠️ 넓은 화면은 손대지 말 것」이라는 경고가 있었다.**
      *"기본 규칙에 `flex-wrap` 을 올리면 1440px 에서도 접힐 수 있게 되어 카드 머리 높이가
      카드마다 달라진다"* — 그래서 두 선언을 `@media (max-width: 767.98px)` 안에만 두었다.
   🔴 **2026-08-24 에 그 경고의 근거가 없어졌다.** 위 `.ds-list-grid > .ds-list-card` 의
      `subgrid` 가 **같은 줄 카드의 머리 행을 하나로 묶어**, 한 카드의 머리가 두 줄이 되면
      **그 줄 전체의 머리 높이가 함께 늘어난다.**
      🔬 실측(2026-08-24): 여섯 카드 머리 높이 전부 **106px** · 카드 높이 전부 **284px**.
   🔑 그래서 두 선언을 **기본 규칙으로 올렸고**(위 `.ds-lc-head` · `.ds-lc-head-end`),
      좁은 화면 블록은 **글자 그대로 중복**이 되어 지웠다. 🔥 다시 만들지 말 것 —
      같은 값을 두 곳에 두면 한쪽만 고쳐져 폭 구간마다 갈린다.

   🕘 **왜 접혀야 하나**(2026-08-17 실측 — 이 기록은 그대로 유효하다):
      머리는 flex 인데 `flex-wrap` 이 없고 `.ds-lc-head-end` 가 `flex-shrink: 0` 이라,
      제목 + 배지 묶음의 자연 폭이 카드 폭을 넘으면 **줄이 바뀌지도 줄어들지도 못해 그대로 넘쳤다**
      → 페이지 전체에 가로 스크롤이 생긴다.
      🔬 실측(375px 뷰포트, clientWidth 360): 관리자 수금 `scrollWidth 399` ·
         관리자 시스템 오류 `433` · 랜드사 상품 `410`. 넘치는 요소는 셋 다 `.ds-lc-head-end` 였다.
      🕘 **글꼴 교체(Pretendard→Noto)와 무관한 기존 결함이다** — Pretendard 로 되돌려도 넘쳐서
         A/B 로 확인했다(399/421/396). 글꼴 탓으로 돌리지 말 것.

   🔴 **「제목은 이미 `flex-shrink` 로 줄어 생략부호가 된다」는 사실이 아니었다** (2026-08-24 정정).
      제목을 감싼 `div.d-flex` 래퍼에 `min-width: 0` 이 없어 **말줄임이 아예 걸리지 않았다** —
      위 `.ds-lc-head > div:first-child` 가 그것을 푼다.
      🔑 그래서 우선순위는 **카드폭 보장 → (모자라면) 배지 줄바꿈 → (그래도 넘치면) 제목 말줄임**
         세 단계다(2026-08-24 사용자 확정). 셋 중 하나만 고쳐서는 넘침이 남는다.
   🔥 **배지 자체의 `white-space: nowrap` 은 그대로 둔다** — 배지 안에서 글자가 쪼개지면
      숫자와 라벨이 갈라져 지금보다 나쁘다.
   ⚠️ **컨테이너 쿼리를 여기에 적지 말 것** — 목록 카드에는 `container-type` 이 없다
      (이 저장소에서 컨테이너 쿼리를 쓰는 것은 4절 표 카드형뿐이다). `@container` 를 적으면
      조상 컨테이너가 없어 **조용히 무시된다.** */

/* ── 본문: 라벨 · 값 (두 열 격자) ── */
/* 🔴 **라벨과 값을 카드의 두 끝으로 벌리지 않는다 — 두 열 격자다** (2026-08-28 · D-6 R2).
   🔬 실측(관리자 업체 목록·1:1 문의 목록, 국문 기본 글자): `justify-content: space-between` 이라
      라벨 오른쪽 끝과 값 왼쪽 끝 사이가 **375px 240 · 768px 531 · 900px 663px** 벌어져
      **어느 라벨의 값인지 눈으로 잇지 못했다.** 좁은 폭에서도 카드 폭의 3분의 2가 빈 자리였다.
   🔑 처방은 **본문을 `max-content | 1fr` 두 열 격자로 만들고 줄이 `subgrid` 로 그 열을 물려받는 것**이다.
      ① 라벨 열은 **그 카드에서 가장 긴 라벨**만큼만 넓어진다 → 사람이 px 을 지어내지 않는다
      ② 값은 전부 **같은 x 에서 시작**하므로 열이 맞는다(라벨마다 시작점이 들쭉날쭉하지 않다)
      ③ 라벨과 값 사이는 언제나 `--ds-sp-3` 하나다 — 실측 후 **전 폭 12px**(375·430 은 root 가 작아 9px).
   🔥 **`justify-content: space-between` 을 되살리지 말 것** — 그것이 곧 이 결함이다.
   🔥 **값을 우측 정렬로 되돌리지 말 것.** 우측 정렬이 「두 끝으로 벌어짐」의 나머지 절반이었다.
   🔬 마크업 전수(관리자 목록 17화면 `.ds-lc-row` **468줄**): **전부 「라벨 1 + 값 1」 두 자식**이라
      subgrid 자동 배치가 어긋날 자리가 없다. 그래도 열을 **`grid-column` 으로 못 박아** 두었다.
   ⚠️ `.is-plain`(라벨 없이 한 줄을 다 쓰는 값)은 **두 열을 통째로** 쓴다(아래 규칙).
   ⚠️ 본문 안에 놓인 `.ds-lc-meta` 는 `margin-top:auto` 로 카드 바닥에 붙던 줄이다 — 격자에서는
      그 auto 가 안 먹으므로 `align-content: start` 로 위에서부터 쌓는다.
      🔬 실측(관리자 1:1 문의·공지·약관·FAQ 목록 = 본문 안 meta 48곳, 일곱 폭): 바꾸기 전후로
      **카드 아래끝 ↔ meta 아래끝이 9px/11px 그대로**였다(같은 틀의 카드라 원래 남는 자리가 없다). */
.ds-lc-body {
    flex: 1 1 auto;
    display: grid;
    grid-template-columns: max-content minmax(0, 1fr);
    align-content: start;
    row-gap: var(--ds-lc-row-gap);
    column-gap: var(--ds-sp-3);
    padding: var(--ds-lc-pad-y) var(--ds-lc-pad-x);
}
/* 🔴 **라벨이 유난히 긴 카드만 라벨 열에 상한을 준다 — 값이 잘리지 않게** (2026-09-23 · P97 보정).
   🔬 결함: 이 앱 **영문** 360px `/settlement/withholding` 에서 라벨 열이 **152.7px**
      (가장 긴 라벨 `Residency (Business Type)` 의 `max-content`)를 먹어 값 칸이 **95.3px** 만 남았고,
      `498,200,000 KRW`(98px)가 아래 `.ds-lc-num` 규칙의 말줄임에 걸려 **`498,200,000 K…`** 가 되어
      **통화 코드가 사라졌다.** 🔑 금액은 통화를 잃으면 **읽을 수 없는 값**이라 말줄임이 손해다.
   🔑 처방은 **값을 줄이는 것이 아니라 칸을 넓히는 것**이다(사장님 결정).
      `fit-content(50%)` = `min(max-content, max(min-content, 50%))` → **좁을 때만** 라벨 열을
      카드 안폭의 절반으로 묶고 긴 라벨을 두 줄로 접는다.
      🔬 실측 360px 영문: 라벨/값 **152.7/95.3 → 128.5/119.5**(넘침 3건 → 0건 · 카드 높이 +18px).
   🔑 **넓은 화면에서는 아무 일도 하지 않는다** — 50% 가 `max-content` 보다 크면 식이 곧 `max-content` 다
      (실측 1280px: 적용 전후 라벨·값 열이 같은 px).
   🔴 **`.ds-lc-body` 전체에 주지 말 것 — 화면이 골라서 켠다.** 라벨 열이 절반을 넘는 카드는 다른 목록에도
      있지만 **그 카드들은 값이 잘리지 않는다**(`.ds-lc-num` 이 아니면 줄을 바꿔 흘러간다). 전체에 주면
      **고칠 이유가 없는 목록들의 라벨이 다 같이 두 줄로 접힌다.**
   ⚠️ 그래서 **저절로 낫지 않는다** — 새 화면이 같은 증상이면 그 화면 `.ds-lc-body` 에 이 클래스를 단다.
      판정은 「360px 영문에서 `.ds-lc-num` 값이 잘리는가」이고, 잘리면 `common.js` 의
      `Common.markClippedValues` 가 `title` 을 달아 두므로 **`[title]` 이 붙은 값 칸이 곧 표식**이다.
   📌 지금 켠 화면은 `settlement/withholding.html` **한 곳**이다(`.ds-lc-body is-keycap`). */
.ds-lc-body.is-keycap { grid-template-columns: fit-content(50%) minmax(0, 1fr); }
.ds-lc-row {
    grid-column: 1 / -1;
    display: grid; grid-template-columns: subgrid; align-items: baseline;
}
.ds-lc-key { color: var(--ds-text-label); grid-column: 1; }
/* ⚠️ **항목명 뒤에는 콜론을 붙인다** — 전 화면 공통 규칙이다(2026-07-21 사용자 재지시).
   한때 "두 끝으로 벌어진 행은 사이가 이미 비어 있어 콜론이 허공에 남는다"며 뺐다가
   **사용자 요청으로 되돌렸다.** 판단 기준은 *항목명임이 분명히 보이는가* 다.
   ⚠️ 마크업에 `:` 를 직접 넣지 말 것 — 다국어 문구에 섞이고 빠뜨리기 쉽다. */
.ds-lc-key::after { content: " :"; }
.ds-lc-val { grid-column: 2; text-align: left; color: var(--ds-text-strong); min-width: 0; }
/* 🔴 **배지가 둘 이상 붙는 값 칸도 배지 사이가 벌어져야 한다** (2026-09-07 사장님 확정 —
      *"인접한 동일 요소 사이에는 여백이 있어야 해"*. 위 `.ds-lc-langs` 와 같은 결정이다.)
   🔬 실측 대상: 관리자 계약 목록(초과 입금·조정 미처리·증빙 미비 최대 3개) · 속성 정의(필수·다중).
      값 칸은 격자 칸이라 기본이 블록이고 배지는 인라인이라, **사이가 「줄바꿈이 남긴 공백」 한 칸**뿐이었다
      (설계된 여백이 아니라 마크업 들여쓰기의 부산물이다 — 한 줄로 적으면 0px 가 된다).
   🔑 **`+` 로 「둘 이상일 때만」 켠다** — 값 칸 대부분은 글자라 무조건 flex 로 만들면
      아이콘·글자·배지가 섞인 칸(랜드사 이행 목록)의 줄바꿈이 통째로 달라진다. 🔥 조건을 빼지 말 것.
   📐 값은 `.ds-lc-head-end`·`.ds-lc-langs` 와 같은 `.4rem` 이다 — 카드 안 배지 간격은 한 값이다.
   🔥 마크업에 `ms-1` 같은 유틸리티로 간격을 주지 말 것 — 접힌 줄까지 들여쓴다(배치 규칙 ⑤). */
.ds-lc-val:has(> .badge + .badge) { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; }
/* 라벨 없이 한 줄을 다 쓰는 값(상품명·여행사 등)은 **좌측 정렬**이 기본이다
   — 오늘 확정한 전역 정렬 규칙과 같다. ⚠️ 두 열을 통째로 쓴다(2026-08-28 · R2). */
.ds-lc-row.is-plain { display: block; grid-column: 1 / -1; text-align: left; color: var(--ds-text-faint); }
.ds-lc-row.is-plain .ds-lc-val { text-align: left; color: inherit; }

/* ⚠️ **항목명 없는 데이터와 항목명 있는 데이터 사이에는 구분선**을 둔다 (2026-07-21 사용자 지적).
   두 묶음은 읽는 방식이 다르다(그냥 읽는 값 / 라벨과 짝지어 읽는 값). 선이 없으면 뒤섞여 보인다.
   표 기반 카드일 때는 '여행사' 칸의 `border-bottom` 이 이 역할을 했는데, `div.card` 로 옮기면서
   **빠뜨렸다.** → 마지막 `is-plain` **다음에 오는 첫 라벨 행**이 위쪽 선을 갖는다.
   ⚠️ 선은 **전폭**이어야 한다. 좁은 칸에 주면 그 폭만큼만 그어진다(이 프로젝트에서 세 번 겪었다). */
/* 🔴 **여기 `padding-top` 은 「안쪽 여백」이 아니라 「구분선과 글자 사이」다 — 0 으로 만들지 마라**
      (2026-09-07 · 여백 대칭 점검에서 「T8 / B0」으로 잡힌 자리다).
   🔬 이 줄에서 대칭의 축은 **상자가 아니라 선**이다: `margin-top .5rem` 이 선 **위** 8px,
      `padding-top .5rem` 이 선 **아래** 8px — **선을 가운데 두고 이미 대칭**이다.
   🔥 `padding-top: 0` 으로 맞추면 선 위 12.6px(row-gap 4.6 + margin 8) · 선 아래 0 이 되어
      **선이 아래 글자에 달라붙는다** — 대칭을 고치려다 비대칭을 만든다.
   🔑 두 값은 **언제나 같이 움직인다.** 한쪽만 바꾸면 선이 한쪽으로 쏠린다. */
.ds-lc-row.is-plain + .ds-lc-row:not(.is-plain) {
    border-top: 1px dashed var(--ds-line);
    margin-top: .5rem;
    padding-top: .5rem;
}
/* 금액은 한 덩어리라 끊기면 안 된다 */
/* 🔴 **넘치면 말줄임(…)으로 「잘렸다」를 보인다** (2026-09-23 · P95P97 의 P97 — 사장님 결정: 「말줄임(…)을 달기」).
   🔬 결함: 값 칸이 `overflow: visible` + `text-overflow: clip`(둘 다 **초기값**)이라 넘쳐도 **표시가 없었다.**
      실측(관리자 **영문** 360px) — `/trade/signed/list` 「6,500,000 to collect back」 상자 132 / 내용 140(+8) ·
      「1,005,400,000 still to pay」 132 / 141(+9) · `/settlement/withholding` 「…KRW」 95 / 98(+3) **3건**.
      지금은 카드를 안 뚫지만(글 오른끝 307 · 카드 312) **금액이 한 자리만 더 길면 뚫는다.**
   🔴 **`.ds-lc-val` 전체가 아니라 `.ds-lc-num` 인 이 줄에만 준다.**
      🔬 가로로 넘칠 수 있는 자리는 **`nowrap` 인 여기뿐**이다 — 나머지 값 칸은 body 의
         `word-break: keep-all` + `overflow-wrap: break-word` 로 **줄을 바꿔** 넘치지 않는다.
         실측: 관리자 12화면 360px 에서 넘친 값 칸 **5개가 전부 `.ds-lc-num`**(비-num 0건)이었다.
      🔥 `.ds-lc-val` 전체로 넓히지 말 것 — 안 넘치는 칸에 `overflow: hidden` 을 씌우면 배지·초점 테두리가
         잘릴 위험만 늘고 얻는 것이 없다.
   ⚠️ **`overflow: hidden` 을 빼면 `ellipsis` 는 아무 일도 하지 않는다** — 초기값이 `visible` 이라
      「`text-overflow` 만 주면 된다」가 아니다. 🔬 그 조합(visible+clip)이 바로 이 결함이었다.
   ⚠️ 안 넘치는 값에는 아무 일도 일어나지 않는다 — **넓은 화면은 그대로다**(실측으로 확인).
   🔴 **말줄임에는 `title` 이 함께 붙는다** — `.ds-ellipsis`(28-5-1절)의 「`title` 없이 쓰지 마라」와
      같은 규칙이다. 값이 서버 렌더라 CSS 로는 못 붙이므로 **`common.js` 의 `Common.markClippedValues`**
      가 **넘친 칸에만** 단다(2026-09-23 · P97 보정). 🔥 그 패스를 지우면 이 줄이 규칙 위반이 된다.
   🔑 **금액이 잘리는 카드는 말줄임으로 두지 말고 값 칸을 넓힌다**(위 `.ds-lc-body.is-keycap`) —
      `498,200,000 K…` 처럼 **통화 코드가 사라지면 읽을 수 없는 값**이 된다. 말줄임이 맞는 자리는
      잘려도 뜻이 남는 **문장**이다(계약 목록의 「… to collect back」·「… still to pay」). */
.ds-lc-num .ds-lc-val { font-weight: var(--ds-weight-strong); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* 🔴 **값에 딸린 괄호 부연은 값 옆이 아니라 값 아래 제 줄을 갖는다** (2026-08-28 · D-6 D-E19).
   🔬 실측(랜드사 정산 대상 목록): 「매입금액 +1,200,000 KRW **(플랫폼 마진 72,000 차감)**」이
      한 줄에 붙어 있었는데, 이 줄은 `.ds-lc-num` 이라 **`white-space: nowrap`** 이다 →
      괄호 문구가 줄바꿈 없이 **205px** 뻗어 375px 에서 **135px**, 430px 에서 **80px** 넘쳤고
      2열 구간(1100px)에서는 **옆 카드의 글자와 맞붙었다**(28px 넘침).
   🔑 부연은 **금액 덩어리가 아니다** — 끊기면 안 되는 것은 숫자와 통화이지 설명이 아니다.
      그래서 이 자식만 `white-space: normal` 로 풀고 제 줄로 내린다.
   ⚠️ 상속을 덮는 것이라 **특이도 싸움이 없다**(자식의 명시 선언이 부모의 상속값을 언제나 이긴다).
   🔥 화면에서 `fs-8`·`text-gray-*`·`ms-1` 유틸리티 조합으로 다시 만들지 말 것 — 그 조합이
      **가로로 붙이는 것**을 전제하고 있었고, 그것이 이 결함이었다. */
.ds-lc-val > .ds-lc-note {
    display: block;
    white-space: normal;
    margin-top: var(--ds-sp-1);
    font-size: var(--ds-font-field-sub);
    font-weight: 400;
    color: var(--ds-text-faint);
}
.ds-lc-row.is-strong .ds-lc-val { font-size: var(--ds-font-strong); }

/* ── 바닥 메타 행: [부가 배지 …] ······ [등록자·등록일] ──
   ⚠️ **발(`.ds-lc-foot`)이 아니다.** 발은 '할 일(버튼)' 자리라 구분선을 갖지만,
      이건 '이 데이터에 대한 부가 정보'라 선이 없다. 둘을 섞지 말 것.
   `margin-top:auto` 로 **카드 바닥에 붙는다** — 카드 높이가 서로 달라도 아래 끝이 맞는다
   (`.ds-list-card{height:100%}` 와 짝이다). */
/* 🔴 **좌·우·아래 여백이 없었다** (2026-08-18 사용자 실기기 지적 — 구현 #94).
   🔬 `padding-top` 하나뿐이라 배지와 등록일이 **카드 테두리에 딱 붙어** 있었다.
      바로 위 `.ds-lc-body` 는 `var(--ds-lc-pad-x)` 를 좌우로 갖는데 이 줄만 0 이라
      **같은 카드 안에서 좌측 끝선이 두 개**였다.
   🔑 좌우는 본문과 **같은 토큰**(`--ds-lc-pad-x`), 아래는 발과 같은 토큰(`--ds-lc-foot-y`)이다 —
      새 값을 지어내지 않았다. 위쪽 `.5rem` 은 그대로 둔다(본문과의 간격이라 성격이 다르다).
   ⚠️ **세 앱이 함께 쓰는 골격**이다 — 관리자 목록 17화면이 이 한 줄로 함께 고쳐진다.
      🔥 랜드사·여행사 앱의 같은 규칙도 함께 고쳐야 갈리지 않는다(이식 대기로 남긴다). */
/* 🔴 **안쪽 위아래 여백은 같은 값이다** (2026-09-07 사장님 확정: *"요소 안쪽 위아래, 좌우측 여백은 무조건 통일"*)
   🕘 위 `.5rem`(8px) / 아래 `--ds-lc-foot-y`(.6rem = 9.6px)로 **1.6px 어긋나 있었다.**
   🔑 **아래 값으로 맞춘다**(위를 9.6px 로 올린다) — 카드 아래끝↔meta 아래끝 관계를 그대로 두기 위해서다.
   🔥 한쪽만 숫자로 적지 말 것: 두 값이 갈리는 것이 이 결함이었다. */
/* 🔴 **위에 구분선을 둔다** (2026-09-08 사장님 지적: *"언어 배지 위에 구분선 안넣니?"*)
   🕘 **바로 위 주석이 「이건 부가 정보라 선이 없다」고 적고 있었다 — 그 결정이 뒤집혔다.**
      🔥 그 문장을 근거로 선을 다시 빼지 말 것.
   🔬 **왜 필요한가** — 이 줄은 본문 데이터와 **성격이 다른 것**(등록 언어·등록자·등록일)인데
      선이 없으면 마지막 데이터 행과 이어져 보인다. 실측: 본문 안에 놓인 언어 배지
      (`.ds-lc-body > .ds-lc-langs`)에는 **이미 같은 점선이 있었다** → 같은 배지가 자리에 따라
      선이 있기도 없기도 했다. 이 한 줄로 그 불일치가 사라진다.
   🔑 **`.ds-lc-foot` 과 같은 점선·같은 토큰**이다(`1px dashed var(--ds-line)`) —
      「비슷한 성격의 요소는 같은 속성」(2026-08-24 사장님 확정). 굵기·색을 달리 하지 말 것.
   ⚠️ **발과 메타가 한 카드에 함께 있는 화면이 4곳 있다**(이 앱에는 없고 DMC 2·Agency 2) —
      거기서는 점선이 두 줄이 된다. 자리가 다르므로(발 = 버튼, 메타 = 부가 정보) 의도된 모양이다. */
.ds-lc-meta {
    margin-top: auto;
    padding: var(--ds-lc-foot-y) var(--ds-lc-pad-x);
    border-top: 1px dashed var(--ds-line);
    display: flex; align-items: center; justify-content: space-between; gap: .5rem;
    color: var(--ds-text-faint);
}
.ds-lc-meta > :only-child { margin-left: auto; }   /* 한쪽만 있으면 우측에 붙인다 */
/* 🔴 **인접한 같은 요소 사이에는 여백이 있어야 한다** (2026-09-07 사장님 지적 · 확정:
      *"여백은 좌우가 대칭되어야 하고 … 인접한 동일 요소 사이에는 여백이 있어야 해"*)
   🔬 목록 카드의 언어 배지 두 개(`KO`·`EN`)가 **여백 없이 붙어 있었다**(실측 간격 **0px**).
      원인은 감싸개 `.ds-lc-langs` 가 그 자리에서 **`display: block`** 이라 `gap` 이 안 먹은 것이다.
   🔑 그래서 **선택자를 `.ds-lc-meta` 안으로 좁히지 않는다** — 바디 안·카드 직계 어디에 놓여도
      배지 사이가 벌어져야 한다. 🔥 `.ds-lc-meta .ds-lc-langs` 로 다시 좁히지 말 것(그것이 이 결함이었다).
   📐 값은 **머리 배지 묶음(`.ds-lc-head-end`)의 `gap` 과 같은 `.4rem`(6.4px)** 이다 —
      새로 고른 값이 아니다. 한 카드 안에서 배지 간격이 두 종류가 되면 안 된다
      (「비슷한 성격의 요소는 같은 속성」, 2026-08-24 사장님 확정).
      🕘 여기 값은 `.25rem`(4px) 이었다 — 머리 배지와 갈려 있던 자리다. */
.ds-lc-langs { display: flex; flex-wrap: wrap; gap: .4rem; }

/* 🔴 **언어 배지 줄은 「구분선 + 대칭 여백」을 갖는다** (2026-08-28 사장님 확정 · 2026-09-07 재확인).
   🔥 **이 규칙은 세 번 빠졌다.** 2026-09-01 에 「반영 완료」로 적혔는데 실제로는 구분선이 없었고,
      그 규격이 적힌 문서(`구현/화면검증-수정대기.md` 항목 14)를 2026-09-06 에 지워 **기준마저 사라졌다.**
      🔑 그래서 규격을 **여기(코드 옆)에** 적는다 — 문서를 지워도 이건 안 사라진다.

   📐 확정 규격 (기준은 사장님이 **「랜드사명 아래」**로 지정하셨다)
   | 자리 | 768↑ | 375·430 |
   |---|---|---|
   | ① 랜드사명 → 구분선1        | 13.6 | 10.2 |
   | ② 구분선1 → 여행 기간       | 13.6 | 10.2 |
   | ③ 가격 → **구분선2**        | 13.6 | 10.2 |
   | ④ **구분선2 → 배지**        | 13.6 | 10.2 |
   | ⑤ 머리 구분선 → 첫 내용     | 27.2 | 20.4 |
   | ⑥ **배지 → 카드 끝**        | 27.2 | 20.4 |
   🔑 ①~④ 는 **모두 같은 값**, ⑤⑥ 은 **그 두 배**다(사장님: *"구분선의 위 아래 4개 모두 같도록"*).

   🔴 **`grid-column: 1 / -1` 을 빼지 마라 — 이것 때문에 2026-09-07 에 또 틀렸다.**
      `.ds-lc-body` 는 **2열 격자**(`max-content minmax(0,1fr)`)라, 이게 없으면 배지 줄이 **첫 칸에만**
      들어가서 **구분선이 카드 중간에서 끊긴다.** 사장님 지적: *"구분선이 우측 까지 쭉 가야지"*.
   ⚠️ 바디가 `grid` 라 **`row-gap`(5.6px)이 `margin-top` 위에 얹힌다** → `margin-top = 13.6 − 5.6 = 8px`.
   ⚠️ 구분선 규격은 **구분선1과 같아야** 한다(`1px dashed` · 같은 색 토큰). 새 값을 고르지 마라. */
.ds-lc-body > .ds-lc-langs {
    grid-column: 1 / -1;
    border-top: 1px dashed var(--ds-line);
    margin-top: 8px;          /* ④ 13.6 − row-gap 5.6 */
    padding-top: 13.6px;      /* ③ */
}
.ds-list-card > .ds-lc-body { padding-bottom: 27.2px; }   /* ⑥ ⑤와 대칭 */
@media (max-width: 767.98px) {
    .ds-lc-body > .ds-lc-langs { margin-top: 4.6px; padding-top: 10.2px; }
    .ds-list-card > .ds-lc-body { padding-bottom: 20.4px; }
}
/* 🔴 **발(`.ds-lc-foot`)이 붙은 카드의 본문 아래 여백은 위와 같은 `--ds-lc-pad-y` 다** (2026-09-18 · 랜드사 1회차 S1-08·S3-06 · 정본 P63ⓓ — P48 재발).
   🔬 목록 격자(`.ds-list-grid`) 안의 카드는 `subgrid` 라 **부모의 행 간격을 물려받아** 머리↔본문·본문↔발 사이에
      그 간격(768↑ 16.8px · 좁은 폭 12.6px)이 **양쪽에 똑같이** 들어간다. 그런데 위 ⑥ 의 `27.2px` 은 **본문이 카드 끝인**
      카드용 값이라, 발이 있는 카드에서는 간격 위에 또 얹혀 **머리선→첫 글자 27.2 / 마지막 글자→발선 44px**(좁은 폭 20.4/33)이 됐다.
   🔑 발이 있으면 아래도 위와 같은 토큰을 쓴다 — 물려받은 간격이 양쪽에 같으므로 대칭이 된다. */
.ds-list-card:has(> .ds-lc-foot) > .ds-lc-body { padding-bottom: var(--ds-lc-pad-y); }
/* 🌐 **지원 언어 배지는 언제나 좌측**이다 (2026-08-22 사용자 지시: *"카드 좌측 하단에 지원 언어 배지"*).
   🔬 바로 위 `:only-child` 규칙은 「꼬리표가 하나뿐이면 우측」이라는 뜻인데, 등록자·등록일을 안 그리는
      카드(여행사 상품 목록)에서는 **언어 배지가 그 규칙에 걸려 오른쪽으로 넘어간다.**
   🔑 `.ds-lc-langs` 는 **좌측 칸이라는 뜻의 이름**이므로 그 자리를 이름으로 못 박는다 —
      화면 CSS(`menuLevel/*.css`)에 정렬을 다시 적지 말 것.
   🔥 위 `:only-child` 규칙을 지우지 말 것 — 등록일만 있는 카드가 그것으로 우측에 선다. */
.ds-lc-meta > .ds-lc-langs:only-child { margin-left: 0; }
/* 📅 **카드 직계로 놓인 메타 줄은 우측 정렬** (2026-09-01 사장님 지시 · 항목 62②)
   > *"등록일자는 카드 푸터에 우측정렬로 한번 바꿔 줘봐"* → 푸터를 보시고 *"나로 하자"*(= `.ds-lc-meta` 그대로).
   🔴 **`.ds-lc-foot` 으로 옮기지 않는다** — 발은 **「할 일(버튼)」 자리라 구분선을 갖고**, 등록일은
      **부가 정보**라 선이 없다(바로 위 주석의 구분 그대로).
   🕘 **「여기에 `border-top` 을 붙이지 말 것」이던 줄은 2026-09-08 에 뒤집혔다** — 사장님이
      *"언어 배지 위에 구분선 안넣니?"* 로 선을 요구하셨다. 🔥 그 옛 문장을 근거로 선을 빼지 말 것.
   🕘 검토했으나 안 고른 갈래: ⓐ `.ds-lc-foot` 으로 이동(그 자리의 정의가 흐려진다).
   🔑 **범위를 「카드 직계」로 좁힌 이유** — 본문(`.ds-lc-body`) 안에 놓인 메타는 본문 흐름의 일부라
      성격이 다르다. 카드 직계 메타는 9절의 `:has(> .ds-lc-meta) → grid-row: span 3` 규격이 걸리는
      **카드 바닥 줄**이고, 이 정렬은 그 줄에 대한 것이다.
   ⚠️ **언어 배지는 그대로 왼쪽**이다 — 위 「지원 언어 배지는 언제나 좌측」 결정을 이 규칙이 덮지 않도록
      `margin-right: auto` 로 못 박는다. 🔥 이 한 줄을 지우면 그 결정이 조용히 뒤집힌다.
   ⚠️ 이 앱의 카드 직계 메타에는 **등록자·등록일 두 칸이 함께** 놓인 자리가 있다 — 그때는 둘 다
      오른쪽으로 모인다(사장님이 확정하신 것은 공지·1:1 문의 목록이다). 어색한 화면이 나오면
      **되돌리지 말고 그 화면에만 걸리는 선택자로 예외**를 둘 것. */
.ds-list-card > .ds-lc-meta { justify-content: flex-end; }
.ds-list-card > .ds-lc-meta > .ds-lc-langs { margin-right: auto; }

/* 🔴 **구분선은 「카드 폭 전체」가 아니라 본문과 같은 안쪽 폭으로 긋는다** (2026-09-08 사장님 지적)
   > *"아씨 점선을 왜 맨날 콘텐츠 길이만큼만 넣는거야 벌써 몇번째 이 이야기를 하는건지 알아?"* ·
   > *"랜드사는 또 완전 100% 그었네... **여행사 여행상품 페이지 보고 하라고**"* ·
   > *"구분선 위 아래 여백도 여행사거랑 맞추고"*
   🔬 **왜 랜드사만 100% 였나** — `border` 는 요소의 **패딩 바깥**에 그려진다. 카드 직계 메타는
      좌우를 `padding` 으로 갖고 있어 선이 그 바깥, 즉 **카드 끝에서 끝까지** 그어졌다.
      본문 안 메타·언어 배지는 `.ds-lc-body` 가 이미 패딩을 갖고 있어 저절로 안쪽 폭이었다.
   ✅ **처방은 좌우를 `padding` → `margin` 으로 옮기는 것** 하나다. 요소 폭이 곧 선 길이가 된다.
      🔥 `box-shadow: inset` 으로 대신하지 말 것 — 점선(dashed)을 만들 수 없다.
   📐 **정본은 여행사 여행상품 목록**이고, 위아래 값은 바로 위 `.ds-lc-body > .ds-lc-langs` 와 한 벌이다:
      선 위 **13.6** · 선 아래 **13.6** · 배지 → 카드 끝 **27.2**(좁은 화면 10.2 / 10.2 / 20.4).
   ⚠️ **선 위 간격은 「카드 격자의 `row-gap`」이 만든다 — 패딩으로 더하면 두 번 준다.**
      🔬 카드는 `grid-template-rows: subgrid` 라 **부모 목록 격자의 gap(`--ds-gap-card`, 실측 16.8px)** 을
         본문과 메타 사이에 그대로 쓴다. 그래서 본문에 `padding-bottom: 13.6` 을 주면 **30.4px** 이 된다
         (2026-09-08 에 실제로 그렇게 재어 나왔다).
      ✅ 그래서 본문 아래 패딩을 **0** 으로 두고 **음수 `margin-bottom` 으로 gap 을 목표값까지 줄인다.**
      🔥 `row-gap` 을 카드에서 재정의해 풀지 말 것 — subgrid 는 행이 하나라 **머리↔본문 간격
         (「머리 구분선 → 첫 내용 27.2」)까지 함께 바뀐다.**
      🔥 `.ds-lc-meta` 의 `margin-top: auto`(카드 바닥 붙임)를 상쇄에 쓰지 말 것 — 그 줄이 죽는다. */
.ds-list-card > .ds-lc-meta {
    padding-left: 0; padding-right: 0;
    margin-inline: var(--ds-lc-pad-x);
    padding-top: 13.6px; padding-bottom: 27.2px;
}
.ds-list-card:has(> .ds-lc-meta) > .ds-lc-body {
    padding-bottom: 0;
    margin-bottom: calc(13.6px - var(--ds-gap-card));
}
@media (max-width: 767.98px) {
    .ds-list-card > .ds-lc-meta { padding-top: 10.2px; padding-bottom: 20.4px; }
    .ds-list-card:has(> .ds-lc-meta) > .ds-lc-body { margin-bottom: calc(10.2px - var(--ds-gap-card)); }
}

/* 🏷 **배지를 제목 위로 — 고객센터 개요와 같은 규격** (2026-09-01 사장님 지시 · 항목 62③ · 65)
   🔴 **옵트인이다(`.ds-list-grid.is-badges-top`). 전역으로 켜지 말 것.**
      🔬 **왜** — `.ds-lc-head-end` 는 **세 앱 56화면 58곳**이 쓰는 공용 골격이다(이 앱만 **28곳**).
         규칙을 `.ds-lc-head` 에 그대로 걸면 58곳이 전부 바뀌고 카드마다 세로가 **33~44px** 늘어난다.
         그러면 **56화면을 폭별로 다시 봐야 한다.**
      🔑 사장님이 보고 확정하신 것은 **여행사 공지 목록 · 1:1 문의 목록**(+ 개요)이다 → 그 화면들만 켠다.
      🕘 **2026-09-03 에 이 앱의 고객센터 목록 넷도 켰다**(사장님 지시 「관리자도 배지 위로 올려」) —
         분쟁 `dispute/list` · 공지 `customer/noticeList` · 1:1 문의 `customer/inquiryList` ·
         FAQ `customer/faqList`. 🔬 세어 보니 이 앱의 고객센터 목록은 **셋이 아니라 넷**이었다.
         🔥 **나머지 24파일(거래·정산·회원·설정 …)은 켜지 않았다** — 성격이 다른 목록이라
            사장님 확정 없이 넓히지 말 것(실측: 이 앱의 `.ds-lc-head-end` 는 **28파일 30곳**).
   🔴 **`margin-left: 0`·`margin-inline-start: 0` 두 줄이 없으면 배지가 오른쪽에 남는다.**
      🔬 실측: `justify-self: start` 만 걸었더니 배지 줄이 **`left: 321px`**(머리 폭 500px) —
         `.ds-lc-head-end` 의 `margin-left: auto` 가 이겼다. 🔥 **두 줄을 지우지 말 것.**
   ⚠️ **`.ds-lc-head-end` 는 원래 「머리 오른쪽 끝」이라는 뜻의 이름**이다 — 여기서는 왼쪽에 서므로
      이름과 하는 일이 어긋난다. **이름을 바꾸려면 58곳을 함께 고쳐야 하므로 그대로 두었다.** */
.ds-list-grid.is-badges-top .ds-lc-head {
    display: grid;
    grid-template-areas: "badges" "title";
    row-gap: .5rem;
}
.ds-list-grid.is-badges-top .ds-lc-head-end {
    grid-area: badges;
    justify-self: start;
    justify-content: flex-start;
    margin-left: 0;
    margin-inline-start: 0;
}
.ds-list-grid.is-badges-top .ds-lc-title {
    grid-area: title;
    min-width: 0;
}
/* 🔴 **배지 줄에 「행위 버튼」이 함께 든 목록은 그 버튼만 오른쪽 끝에 남긴다** (2026-09-03)
   🔬 **왜 필요한가** — 관리자 공지·FAQ 목록은 `.ds-lc-head-end` 안에 배지와 **삭제(X) 버튼**을
      함께 담고 있다. 위 규칙만으로는 그 줄이 통째로 왼쪽으로 가서 **삭제 버튼이 배지 옆**에 선다 →
      `screen-layout.md` 의 「**행위 버튼은 오른쪽 끝**」(삭제·닫기 한 개짜리도 마찬가지다)을 어긴다.
   🔑 **버튼이 있는 카드에서만** 줄을 늘리고(`:has(> .btn)`), 늘어난 폭을 `margin-left: auto` 가 먹는다.
      → 버튼이 없는 목록(분쟁·1:1 문의)은 **한 픽셀도 안 바뀐다.**
   🔥 **버튼을 `.ds-lc-head-end` 밖으로 꺼내 해결하지 말 것** — 크기·정렬을
      `.ds-lc-head-end .ds-row-del.btn`(위 9절)이 쥐고 있어 꺼내는 순간 배지와 높이가 갈린다. */
.ds-list-grid.is-badges-top .ds-lc-head-end:has(> .btn) { justify-self: stretch; }
.ds-list-grid.is-badges-top .ds-lc-head-end > .btn { margin-left: auto; }
/* 🔴 **본문 안에 놓이면 좌우 패딩을 두 번 준다** (2026-08-20 사용자 지적: "날짜 옆 여백이 크다").
   🔬 실측(랜드사 견적 목록 · 1440px): 카드 안 다른 콘텐츠의 끝선은 카드에서 **17px**(테두리 1 + 1rem)
      인데 등록일만 **33px** 이었다 — `.ds-lc-body` 와 `.ds-lc-meta` 가 같은 `--ds-lc-pad-x` 를
      **겹쳐 줘서** 16px 을 더 먹은 것이다. 사용자 눈에는 「날짜 옆 여백만 크다」로 보인다.
   🔥 **`.ds-lc-meta` 의 패딩을 통째로 지우지 말 것** — 이 앱의 거래 5화면(견적/예약 목록·상세,
      계약 목록)은 이 줄을 `.ds-lc-body` **밖**(카드 직계)에 두고 있어, 거기서 좌우를 빼면
      등록일이 **카드 끝선에 붙는다.** 그래서 **본문 안일 때만** 0 으로 만든다.
   📌 상하는 건드리지 않는다 — 위 `.5rem` 은 마지막 데이터 행과의 간격이라 겹침이 아니다. */
/* 🔴 **본문 안 메타는 「줄 전체」를 차지한다 — `grid-column: 1 / -1`** (2026-09-08 사장님 지적:
      *"아씨 점선을 왜 맨날 콘텐츠 길이만큼만 넣는거야 벌써 몇번째 이 이야기를 하는건지 알아?"*)
   🔬 `.ds-lc-body` 는 **2열 격자**(`max-content minmax(0,1fr)`)라, 이게 없으면 메타가 **첫 칸에만**
      들어가 선이 그 칸 폭만큼만 그어진다. 실측(관리자 상품 검수 목록): 카드 558.5px 인데 **선 67.8px**.
   🔥 **바로 위 `.ds-lc-body > .ds-lc-langs` 가 이미 같은 이유로 이 줄을 갖고 있다** — 같은 자리에
      놓이는 두 부품인데 한쪽에만 있어서 이 결함이 났다. 새 부품을 그 자리에 놓을 때도 함께 줄 것.
   📐 위아래 값도 그 언어 배지 줄과 **한 벌**이다(선 위 13.6 = margin 8 + row-gap 5.6 · 선 아래 13.6).
      바닥은 `.ds-list-card > .ds-lc-body` 의 `padding-bottom: 27.2` 가 마감한다. */
.ds-lc-body > .ds-lc-meta {
    padding-left: 0; padding-right: 0;
    grid-column: 1 / -1;
    margin-top: 8px;
    padding-top: 13.6px;
    padding-bottom: 0;
}
@media (max-width: 767.98px) {
    .ds-lc-body > .ds-lc-meta { margin-top: 4.6px; padding-top: 10.2px; }
}

/* ── 발: 액션만 ──
   🔑 **카드 안 '행위 버튼'은 예외 없이 이 발에 둔다** (2026-07-23 사용자 지시).
      · 발  = 텍스트 행위 버튼 (정산 신청 · 증빙 제출 · 답글 쓰기 · 정산 현황 …)
      · 머리 = **상태 배지 + 아이콘 삭제(×)** 만
   ⚠️ 머리는 「카드 안 아이콘 버튼 = 배지 크기(`--ds-badge-h` ≈ 22px)」 규칙이 걸리는 자리라
      텍스트 버튼을 두면 **22×22 로 찌그러져 글자가 잘린다.** 실제 사고: 정산 대상 상세의
      '증빙 제출' 버튼이 머리에 있어 사용자가 "제출 UI 를 찾을 수 없다"고 했다(기능은 정상이었다).
   ⚠️ 표 골격을 쓰는 카드(`settle-detail-table` 등)는 `.ds-lc-foot` 대신 **전용 발 칸**을 만든다 —
      `td` 라 이 클래스를 붙일 수 없다. 규칙(상단 점선 · 우측 정렬 · 버튼만)은 똑같이 지킬 것. */
/* 🔴 **버튼 사이 간격은 척도(`--ds-sp-2`)가 준다 — `ms-1`·`me-1` 로 띄우지 말 것** (2026-09-04 A-1).
   🔬 그전에는 `text-align: right` 뿐이라 버튼 둘 사이가 **인라인 공백 하나**였다 —
      실측 **영문 375·430px 2.70px · 국문 375px 3.38px**(관리자 수금 목록의 「입금 내역」·「상세」).
      글꼴·언어가 바뀌면 그 값도 바뀌므로 **간격이라고 부를 수 없는 값**이다.
   🔑 `flex` + `gap` 이면 마크업이 무엇을 넣든 간격이 하나다. 여백 유틸리티를 쓰던 4곳
      (`ms-1` 3곳 · `me-1` 1곳)도 함께 걷었다 — 남겨 두면 gap 위에 4px 이 더 얹혀 다시 갈린다.
   ⚠️ **`flex-wrap: wrap` 을 빼지 말 것** — 종전 인라인 배치는 좁은 화면에서 저절로 접혔다.
      빼면 버튼 둘이 안 들어가는 폭에서 카드 밖으로 밀려난다.
   🕘 `text-align: right` 는 `justify-content: flex-end` 가 대신한다(자식이 flex 항목이 되어
      그 속성은 더 이상 버튼을 밀지 못한다). */
/* 🔴 **발의 구분선은 「카드 폭 전체」다 — 메타 줄과 일부러 다르다** (2026-09-08 사장님 확정)
   > (발과 메타가 한 카드에 함께 있어 선 길이가 갈리는데 맞춰야 하느냐는 물음에)
   > *"카드 푸터의 구분선은 폭 전체가 맞아"*
   🔑 **발은 카드를 가로지르는 「판 아래 띠」**이고, 메타는 **본문에 딸린 부가 정보 줄**이다.
      그래서 발은 좌우를 `padding` 으로 갖고(선이 카드 끝까지), 메타는 `margin` 으로 갖는다
      (선이 본문 끝선에 맞는다 — 바로 위 `.ds-list-card > .ds-lc-meta` 절).
   🔥 **한 카드 안에 선 길이가 둘인 것은 결함이 아니다 — 맞추려고 이 패딩을 margin 으로 바꾸지 말 것.**
      🔬 함께 서는 화면 4곳: 랜드사 `customer/noticeList`·`fulfillment/vendor/list` ·
         여행사 `customer/noticeList`·`product/productList`. */
.ds-lc-foot {
    padding: var(--ds-lc-foot-y) var(--ds-lc-pad-x);
    border-top: 1px dashed var(--ds-line);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: var(--ds-sp-2);
}
/* 발의 버튼은 **단독 버튼 표준 높이**(`--ds-btn-h` = 2.2rem ≈ 28.9px)다 (2026-07-23).
   ⚠️ Metronic `btn-sm` 기본이 **34.3px** 이라, 클래스만 붙이면 목록마다 발 버튼 높이가 갈린다
      (실측: 정산 대상·정산 이력만 34.3, 나머지 화면의 단독 버튼은 28.9). 골격에서 한 번에 잡는다.
   ⚠️ `padding` 은 `!important` 가 필요하다 — Metronic 의 `.btn:not(…)×6` 이 **(0,7,0)** 이다.
   ⚠️ 높이를 고정하면 세로 가운데 정렬이 풀리므로 `inline-flex` 로 다시 맞춘다. */
.ds .ds-lc-foot .btn:not(.btn-icon) {
    height: var(--ds-btn-h);
    padding-top: 0 !important;
    padding-bottom: 0 !important;
    display: inline-flex;
    align-items: center;
    /* 🔤 **글자·좌우 여백도 카드 푸터 버튼과 같게** (2026-08-17 사용자 지적: *"조금 큰 것 같은데?"*)
       🔬 실측: 높이는 셋 다 35.19px 로 같았는데 **이 버튼만 글자 17.6px · 좌우 25px** 이었다
          (카드 푸터 버튼은 16px · 16px). 높이가 같아도 **글자와 여백이 크면 더 커 보인다** —
          「크기가 다르다」는 지적의 실체가 높이가 아니라 이 둘이었다.
       🔥 화면에서 `fs-*`·`px-*` 로 맞추지 말 것 — 목록 카드는 세 앱 17화면이 쓰는 골격이라
          한 화면만 맞추면 나머지가 그대로 갈린다. */
    font-size: var(--ds-font-body);
    padding-left: 1rem !important;
    padding-right: 1rem !important;
}

/* ===== 정산 이력 목록 — **표 기반 유지** (2026-07-21 사용자 결정) =====
   🕘 한때 `div.card` 그리드로 바꿨다가 되돌렸다. 넓은 화면에서는 표가 낫다는 판단이다.
   좁아지면 `ds-fit-lg` 골격이 카드로 전환한다(임계 1200px).
   ⚠️ 카드 모드의 배치는 아래 `grid-template-areas` 가 정한다 — 칸을 추가하면
      **배치·라벨·정렬·nowrap 네 곳**을 함께 고쳐야 한다(빠뜨리기 쉬운 자리다).
   ※ 새로 만드는 목록은 이 방식 말고 9절 `.ds-list-card` 를 쓸 것. */


/* ===================================================================
   10) 카드 안 아이콘 버튼 크기 — **배지와 같게** (2026-07-22 사용자 지시)

   상세/편집 화면의 카드 안 아이콘 버튼(↑↓ 이동·× 삭제 등)이 Metronic 기본 `btn-sm`
   그대로라 **34×34** 였다. 한 줄에 두세 개씩 붙는 자리라 그만큼 **행 높이가 통째로 커졌다**
   (일차·스케줄 카드가 특히 눈에 띄었다).
   → 기준은 **정산 신청 이력 카드의 취소(×) 버튼**, 즉 배지 높이(`--ds-badge-h` = 1.7rem ≈ 22px).

   ⚠️ **`rem` 이라 root 13px→모바일 12px 에서 22→20px 로 배지와 함께 준다.** px 로 박지 말 것
      (배지 여백이 em 기반이라, px 로 고정하면 좁은 화면에서 배지와 2px 어긋난다 — 이미 겪은 함정).
   ⚠️ Metronic `.btn:not(…)×6` = **(0,7,0)** 이라 padding 은 `!important` 가 필요하다.
   ⚠️ 아이콘 글자 크기도 같이 줄여야 한다 — 버튼만 줄이면 아이콘이 넘쳐 잘린다.

   ※ 제외되는 것들(의도한 것):
     · 언어 칩의 삭제(`.btn-del-lang`)는 `span` 이라 이 규칙에 안 걸린다(사용자가 제외 지정).
     · 목록 카드의 삭제는 9절 `.ds-lc-head-end .ds-row-del` 이 이미 같은 값으로 잡는다.
     · 카드 **밖**(헤더 툴바·드로어 닫기)은 대상이 아니다 — `.card` 안으로 스코프한다.
   =================================================================== */
/* ⚠️ **이 규칙만 `.ds` 스코프를 쓰지 않는다.** 이 파일의 다른 규칙은 전부 `.ds` 로 묶여
   "붙인 화면만" 바뀌는데, 여기서는 일부러 전역으로 건다.
   이유: 상세·편집 화면 **12개가 `#kt_content` 에 `ds` 없이 `quote-content` 만** 쓰고 있다
   (견적 상세·계약 상세·계약서 작성·상품/템플릿/아이템 등록·메인 …).
   `.ds` 로 묶으면 정작 버튼이 많은 그 화면들이 빠져 "모든 상세 페이지" 요구를 못 지킨다.
   ⚠️ 되돌릴 때 주의 — `.ds` 를 다시 붙이면 그 12개 화면에서 조용히 34px 로 돌아간다. */
.card .btn.btn-icon {
    width: var(--ds-badge-h);
    height: var(--ds-badge-h);
    min-height: 0;
    padding: 0 !important;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
/* ⚠️ `!important` 가 필요하다 — 버튼마다 `fs-8`/`fs-9`/`fs-7` 이 붙어 있고 그 유틸리티가
   `!important` 라, 안 붙이면 **같은 삭제 버튼인데 아이콘만 9.8 / 11.1px 로 갈린다**
   (2026-07-22 사용자 지적: "이건 다른 삭제 버튼들과 아이콘 크기 자체가 다른데?"). */
.card .btn.btn-icon > i,
.card .btn.btn-icon > .bi { font-size: .8rem !important; line-height: 1; }

/* 🔴 아바타 위젯 예외 — **사람이 「취소」를 누를 수 없던 것을 고친다** (2026-09-09 실측).
   🔬 증상: 프로필 사진을 바꾼 뒤 「취소(×)」 한가운데를 실제로 찍으면 `elementFromPoint` 가
      **「삭제」를 돌려준다.** 두 버튼이 25×25 로 **완전히 같은 좌표**에 포개져 있고 삭제가 위다.
      세 앱 마이페이지 3화면에서 전부 재현했다(랜드사 1309,286 · 여행사 1313,286 · 관리자 1973,574).
   🔬 원인: Metronic 은 사진을 바꾸면 삭제를 감추도록
      `.image-input.image-input-changed [data-kt-image-input-action="remove"] { display:none }` 를
      두는데 그 특이도가 **(0,3,0)** 이다. 위 `.card .btn.btn-icon` 도 **(0,3,0)** 이라 같고,
      `design-system.css` 가 **나중에** 실려서 우리 쪽이 이겨 삭제가 안 사라졌다.
   🔑 위 규칙은 건드리지 않는다 — 상세·편집 화면 12개가 `.ds` 없이 만들어져 있어 일부러 전역으로 건
      것이다(바로 위 주석). 대신 **여기서 한 단계(0,4,0)만 올려 되돌린다.**
   ⚠️ 이 세 줄을 지우면 세 앱 마이페이지에서 「취소」가 다시 안 눌린다. 지우지 말 것.

   🔬 덮어쓴 자리가 **둘**이었다 — 「삭제를 감춘다」와 「취소를 감춘다」 양쪽이다.
      Metronic 의 `.image-input [data-…="cancel"] { display:none }` 은 **(0,2,0)** 이라
      위 `.card .btn.btn-icon`(0,3,0)에 **순서와 무관하게** 진다. 그래서 사진을 바꾸기 전에도
      「취소」가 늘 떠 있었다. 아래 두 줄이 그 짝을 (0,4,0)·(0,5,0)으로 되살린다.
      🔑 순서가 중요하다 — 감추는 줄이 먼저, 되살리는 줄이 나중이다. */
.card .image-input.image-input-changed [data-kt-image-input-action="remove"] { display: none; }
.card .image-input [data-kt-image-input-action="cancel"] { display: none; }
.card .image-input.image-input-changed [data-kt-image-input-action="cancel"] { display: flex; }

/* ===================================================================
   11) 셀렉트는 **화살표 자리를 항상 남긴다** (2026-07-22 사용자 지적)

   Metronic `.form-select` 기본은 `padding: .75rem 3rem .75rem 1rem` 이다 —
   **오른쪽 3rem 이 화살표(배경 이미지, 16px, `right 1rem center`) 자리**다.
   컴팩트하게 만들려고 `padding: .35rem .7rem` 처럼 **네 방향을 한 번에** 줄이면
   그 자리가 사라져 **글자가 화살표 밑으로 들어간다.**
   실측(스케줄 카테고리): 폭 117 / 글자 끝 106 / 화살표 시작 101 → 5px 겹침.

   ⚠️ 세로 여백을 줄이는 건 괜찮다. **줄이면 안 되는 건 오른쪽뿐이다.**
   ⚠️ `padding` 단축 속성으로 덮어쓰는 규칙이 여럿 있어 `!important` 가 필요하다.
   ⚠️ `.ds` 스코프를 쓰지 않는다 — 상세·편집 12개 화면에 `ds` 가 없다(10절 참고).
   =================================================================== */
/* ⚠️ 클래스를 **세 번 반복**해 특이도를 (0,3,0)으로 올린다.
   `select.form-select`(0,1,1) + `!important` 로도 안 먹었다 — 더 센 `!important` 규칙이 있다.
   `!important` 끼리는 **특이도로 승부가 갈린다**(이 프로젝트에서 반복해 밟은 함정).
   화살표는 `background-position: calc(100% - 13px)` 에 16px 폭으로 그려지므로
   오른쪽 여백은 최소 29px ≈ 2.2rem 이 필요하다.

   🔴 **기준은 「검색어 입력창의 좌측 여백」 = `.7rem`(11.2px)** (2026-09-07 사장님 지시:
      *"검색어 입력창 내 좌측 여백 있지? 그걸 기준으로 검색영역에 요소들의 좌우 여백을 모두 맞춰"*).
   🔑 **아이콘까지가 「내부 요소」이고, 아이콘 오른쪽의 빈 공간이 진짜 우측 여백**이다
      (사장님: *"그 아이콘들 우측의 여백이 드롭다운박스 내의 우측 여백"*).
      📐 그래서 **화살표를 `right .7rem` 에 놓고**(= 우측 여백이 기준값),
         `padding-right = .7rem + 화살표 16px + 여유 = 1.9rem`.
      ✅ **왼쪽은 `.7rem` 그대로.** 좌 11.2 · 아이콘 오른쪽 11.2 로 **검색어 칸과 완전히 같다.**
   🕘 한때 「좌우 여백 통일」을 `padding` 숫자로만 읽어 **왼쪽을 2.2rem 으로 키웠다가 되돌렸다** —
      사장님이 화면을 보고 *"왜 좌측 여백이 저렇게 커진거야?"* 라고 바로잡으셨다.
      🔬 그때 안쪽이 **104 → 80px** 이 되어 **「증빙 제출 대기」가 잘렸다**(필요 86px · 영문 5화면 추가).
   🔥 **`padding` 숫자만 보고 좌우를 맞추지 말 것.**
   📌 판정 기준: **그 공간에 무언가가 그려지는가.** 그려지면 여백이 아니다. */
.form-select.form-select.form-select {
    padding-right: 1.9rem !important;
    background-position: calc(100% - .7rem) 50% !important;
}

/* ===================================================================
   13) 페이징 좌우 화살표 — **Metronic 이 그려주지 않는다** (2026-07-22)
   ===================================================================
   목록 12곳이 `<i class="previous">` / `<i class="next">` 를 쓰는데
   **이 번들에 그 아이콘 정의가 없다.** 실측: `::before` 의 `content` 가 `none`,
   요소는 11.4×11.4px 로 자리만 차지하고 **아무것도 안 보인다.**
   증상이 조용하다 — 콘솔 에러도 없고 클릭은 되므로, 눌러보지 않으면 버튼이 있는 줄도 모른다.

   ⚠️ 템플릿을 `bi bi-chevron-*` 로 하나씩 바꾸지 않는다. 8개 템플릿 + **JS 가 그리는
   모달 페이징**(TravelProduct.js 의 아이템 검색)까지 흩어져 있어 또 빠뜨린다.
   → 이미 로드된 Bootstrap Icons 글리프를 여기서 한 번에 입힌다.
   ※ 나머지 3개 화면(공지·문의·분쟁)은 원래 `bi bi-chevron-left` 를 쓴다 — 이 규칙과 무관하며
     결과 모양이 같다(같은 폰트·같은 글리프). 굳이 통일하려고 건드리지 말 것.  */
.page-link i.previous::before,
.page-link i.next::before {
    font-family: "bootstrap-icons";
    font-style: normal;
    line-height: 1;
    display: inline-block;
}
.page-link i.previous::before { content: "\f284"; }  /* bi-chevron-left  */
.page-link i.next::before     { content: "\f285"; }  /* bi-chevron-right */

/* ===================================================================
   14) 아이콘 + 텍스트 버튼의 세로 정렬 (2026-07-22 사용자 지적)
   ===================================================================
   정산 대상 상세 하단 '여행사 입금 내역' 버튼의 아이콘이 글자보다 아래로 처져 있었다.
   실측: 아이콘 중심이 텍스트 중심보다 **1.7px 아래**.

   원인은 `.btn` 이 **`display:block`** 이라 아이콘이 **baseline 에 걸리는** 것이다.
   Metronic 이 아이콘에 `line-height:0` 을 주기 때문에 밑선 기준으로 내려앉는다.
   ⚠️ **`.card .btn.btn-icon{width/height:22px}` 규칙과는 무관하다** — 이건 `btn-icon` 이 아니라
   텍스트 버튼 안에 아이콘이 든 형태다(메모에 '별도 원인'이라 적어둔 그것).

   두 후보를 실제로 재서 골랐다:
     A) 버튼을 `inline-flex; align-items:center`  → 편차 **0**, 버튼 높이 42.9px 그대로
     B) 아이콘에 `vertical-align:middle`          → 편차 1.7px **그대로**(효과 없음)
   → A 채택. `vertical-align` 은 `line-height:0` 인 요소에는 듣지 않는다.

   ⚠️ `btn-icon` 은 제외한다 — Metronic 이 이미 flex 로 가운데 정렬하고 있고,
      13절 위쪽의 22px 규칙과 겹쳐 어느 쪽이 이겼는지 추적이 어려워진다.
   ⚠️ `.ds` 스코프를 쓰지 않는다 — 상세·편집 12개 화면에 `ds` 가 없다(10절과 같은 이유). */
.btn:has(> i):not(.btn-icon) {
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

/* ===================================================================
   15) 이미지/문서 드롭존 — `.dropzone.js-instant-upload` (2026-07-23 전역으로 이관)
   -------------------------------------------------------------------
   🕘 원래 `menuLevel/TravelProduct.css` 에 있었다. 마이페이지 **사업자등록증**이
      여행상품 아이템과 **같은 업로드 UI** 를 쓰게 되면서(사용자 요청) 한 화면 전용이 아니게 됐다.
      "전 메뉴 공통 CSS 는 메뉴 CSS 가 아니라 전역 CSS 에 둔다"(2026-07-22 사용자 원칙).

   ⚠️ `.ds` 로 스코프하지 않는다 — 아이템 등록/수정 화면은 `#kt_content` 에 `ds` 가 없고
      `quote-content` 만 쓴다(이 파일의 아이콘 버튼 절과 같은 이유). 스코프를 붙이면
      정작 원래 쓰던 화면에서 규칙이 사라진다.
   =================================================================== */

/* ══════════════════════════════════════════════════════════════════════════════════════
   🔴 **드롭존 4종은 「같은 상자」다 — 화면 마크업에서 칠하지 말 것** (2026-09-08 사장님 지적)
   ══════════════════════════════════════════════════════════════════════════════════════
   🔬 **고치기 전 실측**(랜드사 아이템 등록, 같은 화면 안에서):
        · 이미지 드롭존 `class="… dropzone-image-area bg-light-primary border-primary border-dashed"`
          → 배경 `rgb(241,250,255)` · 테두리 `1px dashed rgb(0,158,247)`
        · 문서 드롭존   `class="… dropzone-queue mb-2"`
          → 배경 **투명** · 테두리 **없음**
      **같은 성격의 자리인데 상자가 갈려 있었다.** 이름도 뜻이 안 맞았다(`-image-area` 인데 문서 서류가
      들어가고, `-queue` 는 걷어낸 Metronic 큐 마크업의 이름이었다).
   ✅ **어느 쪽으로 통일했나 — 「테두리 있는 옅은 상자」다.** 근거는 「다수가 그렇다」가 아니라
      **드롭존은 「여기에 떨어뜨려라」를 눈으로 말해야 하는 자리**라는 것이다. 배경도 테두리도 없는
      상자는 조준할 표적이 없어, 파일을 어디에 놓아야 하는지 화면이 알려 주지 못한다.
      점선 테두리는 이 저장소가 이미 24곳에서 쓰던 그 표적이다.
   🔴 **판정 클래스(`js-instant-upload`)로 건다** — 상자 모양과 「이 자리가 즉시 업로드인가」가
      **같은 조건**이 되어 갈릴 수가 없다. 🔥 화면 마크업에 `bg-light-primary`·`border-primary` 같은
      Metronic 유틸리티를 다시 적지 말 것 — **그것이 갈림의 원인이었다**(그 유틸리티는 `!important` 라
      여기서 무엇을 적어도 못 이긴다).
   🔑 색은 역할 토큰이다 — `--ds-primary-soft` 는 라이트에서 `#f1faff`(= 걷어낸 `bg-light-primary` 와
      **같은 값**)이고 다크에서는 어두운 파랑이 된다. 🔥 hex 로 되돌리지 말 것(0절).
   🕘 **`.dropzone-image-area` 는 마크업에서 걷어냈다**(세 앱 24곳 — 이 앱은 5곳). 그 클래스에 걸려
      있던 규칙 중 살아 있던 것은 이 절과 아래 `×`·`dz-message` 절로 옮겼고, 죽어 있던 것
      (`dz-started` 격자 · 125px `!important` 3줄)은 지웠다 — 미리보기 컨테이너가 드롭존의 **자식**이라
      `dz-started` 가 붙지 않고, 125px 은 아래 `.dropzone > .ds-upload-files` 절이 이미 준다.
      🔬 그 `!important` 3줄이야말로 **문서 드롭존이 화면마다 다르게 보이던 원인**이었다 —
         서류 드롭존은 상자 규격 때문에 `.dropzone-image-area` 를 함께 달고 있어서, 줄(list) 형식인데도
         `width:125px !important` 가 걸려 아래 줄 규칙이 `!important` 로 맞서야 했다.
   ══════════════════════════════════════════════════════════════════════════════════════ */
.dropzone.js-instant-upload {
    background-color: var(--ds-primary-soft) !important;
    border: 1px dashed var(--ds-primary) !important;
    padding: 1.5rem 2rem !important;
    min-height: auto !important;
    border-radius: 0.475rem !important;
}

/* 🕘 **(2026-07-23) 여기는 원래 「파일이 하나라도 있으면 안내 문구를 감춘다」였다.**
      Dropzone 은 원래 `.dropzone.dz-started .dz-message{display:none}`(0,2,0) 로 감추는데,
      우리 마크업이 `class="dz-message needsclick d-flex …"` 라 **Bootstrap 의 `.d-flex{display:flex!important}`
      가 이겨서** 미리보기 옆에 "파일을 업로드하세요."가 계속 떠 있었다.
   ⚠️ `!important` 가 없으면 못 이긴다. 상대가 `!important` 라 특이도만 올려서는 소용없다
      (그 사정은 값이 `flex` 로 바뀐 지금도 그대로다 — 지우지 말 것).

   🔴 **2026-09-08 사장님이 화면을 보고 뒤집었다 — 이제 「남긴다」(`flex`)다.**
   🔬 감추던 시절에는 **문서 드롭존과 이미지 드롭존이 갈려 있었다** — 문서 쪽은 `previewsContainer`
      가 드롭존 자신이 아니라 `dz-started` 가 안 붙어 문구가 그대로 남았고, **이미지 갤러리만**
      한 장을 올리는 순간 문구가 통째로 사라졌다. **같은 성격의 자리가 서로 다르게 동작한 것이다.**
   🔑 **문구가 남아야 확장자·용량·개수·교체 안내를 적을 자리가 생긴다**(그 문구는 `Common.InstantUpload`
      가 채운다 — 관리자 앱에서는 초기 설정·플랫폼 설정의 브랜딩 이미지와 업체 서류 조각이 그 위젯을 쓴다).
   🔥 **`none` 으로 되돌리지 말 것.** 되돌리면 위 「두 쪽이 갈린다」가 그날로 돌아온다. */
.dropzone.dz-started .dz-message { display: flex !important; }

/* ══════════════════════════════════════════════════════════════════════════════════════
   🔴 **안내 문구를 감추는 규칙을 여기에 다시 만들지 말 것** (2026-09-08 · 넣었다가 같은 날 걷었다)
   ══════════════════════════════════════════════════════════════════════════════════════
   🕘 이 자리에 `.dropzone.ds-upload-full .dz-message { display:none !important }` 와
      `.dropzone.ds-upload-full > .ds-upload-files { margin-top:0 }` 두 줄이 있었다
      (단일 드롭존이 한 장 차면 안내를 감추는 규칙 — 판정은 `instant-upload.js` 가 했다).
      📌 이 앱의 브랜딩 이미지(로고·파비콘 아홉 장)가 전부 그 자리라 **여기서 가장 넓게 드러났다.**
   🔬 **왜 걷었나** — 그 안내 블록(`.dz-message`)에는 행동 유도만 든 것이 아니라
      **규격 설명**(허용 형식 · 파일당 최대 MB · 장수, `js-upload-desc`)이 **같이** 들어 있다.
      감추면 그것까지 사라진다. 실측(랜드사 아이템 등록 · 2026-09-08):
        · 다중 이미지 `flex` 「JPG · GIF · PNG · 파일당 최대 10MB · 20장까지」(37자)  → 보인다
        · 다중 문서   `flex` 확장자 13종 + 10MB + 20개까지(100자)                    → 보인다
        · **단일 문서 `none`** — 같은 형식의 **113자가 통째로 안 보였다**
        · **단일 이미지 `none`** — 같은 형식의 **50자가 통째로 안 보였다**
      사장님 지적: *"단일 문서 업로드 결과에 설명 텍스트 부분이 없다"* · *"단일 이미지도 설명이 사라지네"*.
   🔑 **단일 자리에서도 안내는 거짓말이 아니다** — 단일은 「더 못 올린다」가 아니라
      **「새로 올리면 교체된다」**이고 그 말은 문구(`label.upload.desc.single`)가 이미 하고 있다.
   🔥 **되살리려면 규격 설명을 그 블록 밖으로 먼저 빼야 한다.** 그 전에는 되살리지 말 것.
      📌 짝이 되는 JS 도 함께 지웠다(`instant-upload.js` 의 `snapshot()`).
   ══════════════════════════════════════════════════════════════════════════════════════ */

/* 🕘 그런데 **즉시 업로드 드롭존은 미리보기를 안에 두지 않는다** (2026-08-10 사용자 지시).
      `Common.InstantUpload` 가 드롭존 **바로 위**에 `.ds-upload-files` 를 만들어
      `previewsContainer` 로 넘긴다 → 올린 파일은 그 목록에 쌓이고 드롭존은 안내 문구만 남는다.
   🔑 **Dropzone 은 `previewsContainer` 가 자기 자신이 아니면 `dz-started` 를 붙이지 않는다** —
      그래서 안내 문구를 되살리는 규칙이 따로 필요 없고, 위 2026-07-23 규칙과도 부딪히지 않는다.
   🔑 자격증빙서류처럼 **서버가 그린 기존 목록**과 같은 자리·같은 모양이라
      한 화면에 두 가지 목록이 생기지 않는다.
   ⚠️ **아이템 이미지 갤러리는 대상이 아니다** — `TravelProduct.js` 가 만들며
      `previewsContainer` 를 쓰지 않아 미리보기가 드롭존 안 125px 격자로 남는다(그게 그 화면의 규격이다). */
.ds-upload-files { margin-bottom: 1rem; }
.ds-upload-files:empty { display: none; }

/* Dropzone 기본 CSS(`.dropzone .dz-preview …`)는 미리보기가 드롭존 **밖**이라
   하나도 걸리지 않는다 → 칩 모양을 여기서 전부 그린다.
   그래서 `!important` 가 필요 없다 — 다투는 상대가 없다. */
.ds-upload-files .dz-preview {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.35rem 1rem;
    margin-bottom: 0.5rem;
    border: 1px dashed var(--ds-line);
    border-radius: 0.475rem;
}

.ds-upload-files .dz-preview:last-child { margin-bottom: 0; }

.ds-upload-files .dz-image {
    width: 32px;
    height: 32px;
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
}

.ds-upload-files .dz-image img { max-width: 100%; max-height: 100%; object-fit: contain; }

/* 썸네일이 없는 파일(PDF 등)은 `<img>` 에 src 자체가 안 붙는다 → 파일 아이콘을 그린다.
   ⚠️ 기존 파일은 `displayExistingFile` 이 upload.svg 를 넘겨 src 가 있다 — 그쪽은 그대로 둔다.
   🔴 **깨진 그림도 이 자리로 떨어진다** (2026-09-08) — `instant-upload.js` 의 `_thumb` 이
      `onerror` 에서 `src` 와 `alt` 를 **걷어낸다**. 🔑 **새 모양을 만들지 않고 이 선례를 그대로 쓴다.**
      🔬 걷어내는 이유: 그림이 깨지면 브라우저가 `alt` 를 글자로 그려 **감춰 둔 파일명이 타일 한가운데
         튀어나온다**(사장님이 그 화면을 보셨다. 방아쇠는 88바이트짜리 손상된 PNG 였다). */
.ds-upload-files .dz-image:has(img:not([src])) { color: var(--ds-primary); }
.ds-upload-files .dz-image:has(img:not([src]))::before {
    content: "\f392";              /* bi-file-earmark */
    font-family: "bootstrap-icons";
    font-size: 1.1rem;
}
/* 🔴 **주소가 없는 `<img>` 는 접는다 — 안 접으면 브라우저가 「깨진 그림」 자리표시자를 그린다**
      (2026-09-08 실측). 🔬 접기 전 타일: 위쪽에 파일 아이콘, 아래쪽에 **125×125 회색 빈 상자 + 브라우저
         기본 깨진 그림 글리프**가 함께 서서 「무언가 잘못 그려진 화면」으로 보였다.
   🔑 접으면 위 `::before` 아이콘 하나만 가운데 남는다 — 문서 미리보기와 **같은 모양**이 된다.
   🔥 `visibility:hidden` 으로 바꾸지 말 것(자리를 그대로 먹어 아이콘이 한쪽으로 밀린다). */
.ds-upload-files .dz-image > img:not([src]) { display: none; }

.ds-upload-files .dz-details { flex: 1 1 auto; min-width: 0; }
.ds-upload-files .dz-filename { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ══════════════════════════════════════════════════════════════════════════════════════
   📏 **파일명 뒤에 「(용량)」을 붙인다** (2026-09-08 사장님 지적)
   ══════════════════════════════════════════════════════════════════════════════════════
   > *"문서 업로드 결과에 **파일명 뒤에 (용량) 부분은 어디로 가버린거야?**"*

   🕘 이 자리에 `.ds-upload-files .dz-size { display: none; }` 한 줄이 있었고 옆 주석은
      *"기존 파일은 크기를 안 받아 늘 `0 b` 다"* 였다 — **원인은 감추기가 아니라 「크기가 안 내려오는 것」**
      이었는데 증상만 덮은 것이다. 그래서 크기를 내려주도록 고치고 이 규칙을 걷었다.
   🔑 **크기를 모르면 칸 자체가 없다** — `instant-upload.js` 의 `_size` 가 `.dz-size` 를 **지운다**.
      그래서 여기서 괄호를 `content` 로 그려도 **「()」만 남는 일이 없다.**
      🔥 `display:none` 으로 되돌리지 말 것 — 그러면 크기를 내려주는 자리를 늘려도 아무 일도 안 일어난다.
   🔤 **괄호는 문장이 아니라 부호**라 CSS 에 적어도 번역 대상이 아니다(0절의 「CSS 에 글자를 박지 말 것」은
      **읽는 말**에 대한 규칙이다). 🔥 여기에 단위·낱말을 적지 말 것 — 그것은 Dropzone 이 만든다.
   📌 벤더 표기는 `<strong>340</strong> KB` 꼴이다(`Dropzone.prototype.filesize`) — **새로 올린 파일과
      기존 파일이 같은 함수를 탄다**(모듈이 `size` 바이트 수만 넘긴다). 🔥 화면에서 글자를 만들지 말 것.
   ══════════════════════════════════════════════════════════════════════════════════════ */
.ds-upload-files .dz-size::before { content: "("; }
.ds-upload-files .dz-size::after  { content: ")"; }

/* 진행 막대 — 다 올라가면 사라진다. 없으면 큰 파일에서 아무 반응이 없어 보인다. */
.ds-upload-files .dz-progress {
    display: block;
    width: 100%;
    height: 3px;
    margin-top: 0.35rem;
    background: var(--ds-fill);
    border-radius: 2px;
    overflow: hidden;
}

.ds-upload-files .dz-complete .dz-progress,
.ds-upload-files .dz-preview.dz-complete .dz-progress { display: none; }

.ds-upload-files .dz-upload { display: block; height: 100%; background: var(--ds-primary); }

/* 삭제 — 자격증빙서류 목록의 × 버튼과 같은 크기다(`--ds-badge-h`).
   ⚠️ `font-size: 0` 은 시각적 숨김일 뿐이라 스크린리더는 라벨을 그대로 읽는다. */
.ds-upload-files .dz-remove {
    flex: none;
    font-size: 0;
    text-decoration: none;
    width: var(--ds-badge-h);
    height: var(--ds-badge-h);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--ds-danger);
}

.ds-upload-files .dz-remove::before {
    content: "\f659";              /* bi-x-lg */
    font-family: "bootstrap-icons";
    font-size: 0.8rem;
}

/* Dropzone 기본 마커(체크·엑스)는 크기 규칙이 없어 54px 로 그려져 칩을 밀어 올린다 —
   상태는 진행 막대와 파일이 목록에 남는 것으로 충분하다.
   ⚠️ 오류는 Swal 이 띄우고 파일을 치우므로 오류 문구도 보여 줄 필요가 없다. */
.ds-upload-files .dz-success-mark,
.ds-upload-files .dz-error-mark,
.ds-upload-files .dz-error-message { display: none; }   /* 오류는 Swal 이 띄우고 파일을 치운다 */

/* ══════════════════════════════════════════════════════════════════════════════════════
   ⏳ **「올리는 중」 표시** — `.ds-upload-busy` (2026-09-08 사장님 지시)
   ══════════════════════════════════════════════════════════════════════════════════════
   🔑 **왜 필요한가** — 같은 날 미리보기를 **「서버에 올라간 것」**으로 바꾸면서 브라우저 썸네일을 껐다
      (`instant-upload.js` 의 `createImageThumbnails`). 그래서 **응답이 올 때까지 그 자리가 빈다** —
      표시가 없으면 사용자는 아무 일도 안 일어난 줄 알고 다시 고른다.
   🔥 **이 규칙을 지우고 브라우저 썸네일을 되살리는 쪽으로 가지 말 것** — 「그 자리를 비워 두지 않는다」가
      서버 미리보기 결정이 선 조건이었다.
   🔑 **자리 이름이 아니라 「상태」로 건다**(`.dz-preview.ds-uploading`) — 타일(이미지)·줄(문서)·
      아이템 이미지 갤러리가 **한 규칙**을 쓴다.
   ⚠️ **`.ds-upload-files` 로 스코프하지 말 것** — 아이템 이미지 갤러리(`previewsContainer:'self'`)의
      미리보기는 그 컨테이너 **밖**이라 스코프를 걸면 그 화면만 표시가 안 뜬다.
   ⚠️ **`.ds` 로도 스코프하지 말 것** — 가입 화면은 `.ds` 밖이다.
   🔤 문구는 `instant-upload.js` 가 `label.upload.uploading` 에서 넣는다 — 여기에 `content` 로 적지 말 것
      (CSS 에 글자를 박으면 영어 화면에도 한국어가 나간다).
   ══════════════════════════════════════════════════════════════════════════════════════ */
/* 🔴 **`.dz-preview` 로 좁히지 말 것** (2026-09-08 실측으로 바로잡음) — 아이템 문서 목록은 미리보기
      틀을 **마크업에서 뽑아 쓰므로** 그 요소의 클래스가 `dropzone-item` 이고 `dz-preview` 가 **없다.**
      좁혀 두면 그 화면에서만 기준 상자가 안 잡혀 「올리는 중」이 엉뚱한 자리에 뜬다.
   🔑 이 클래스는 `_busy` 가 미리보기 요소에만 붙인다 — 넓혀도 다른 곳에 닿지 않는다. */
.ds-uploading { position: relative; }

/* 🔑 **아바타 타일은 제 모서리를 따른다** (2026-09-23 여 H3-11 — 아바타에도 이 덮개를 쓰게 되면서 붙였다).
      🔬 아바타(`.image-input-wrapper`)는 둥근데 덮개만 `.475rem` 각이라 **모서리가 삐져나온다.**
      🔥 이 한 줄을 「아바타 전용 덮개」로 키우지 말 것 — 문구·배경·글자색은 위 규칙 한 벌이 정한다. */
.image-input-wrapper.ds-uploading .ds-upload-busy { border-radius: inherit; }

.ds-upload-busy {
    position: absolute;
    inset: 0;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--ds-sp-1);
    text-align: center;
    /* 🔑 **불투명하게 덮는다** — 반투명이면 아직 안 올라간 것이 비쳐 「올라갔다」로 읽힌다. */
    background-color: var(--ds-surface);
    /* 테두리는 inset 링이다 — border 면 상자가 2px 커져 125px 격자가 어긋난다(바로 아래 절과 같은 이유). */
    box-shadow: inset 0 0 0 1px var(--ds-line);
    border-radius: .475rem;
    color: var(--ds-text-faint);
    font-size: .8rem;
    line-height: 1.2;
    overflow: hidden;
}

/* ══════════════════════════════════════════════════════════════════════════════════════
   🔴 **미리보기는 드롭존 「안」이다 — 사장님 시안 규격** (2026-08-28~29 · 항목 5)
   ══════════════════════════════════════════════════════════════════════════════════════
   > *"여기도 파일을 업로드 하면 드롭존 밖으로 업로드 결과가 노출되고 있어. 드롭 존 안에 나와야 해."*
   > *"드롭존은 첨부한 이미지처럼 나오도록 했으면 좋겠어."*

   🕘 **2026-08-10 결정을 뒤집는다.** 위 절이 적은 두 이유(안내 문구와 섞인다 · 기존 목록과 자리가
      겹친다)는 **이 결정으로 대체됐다** — 🔥 그 문장을 근거로 되돌리지 말 것.
   🖼 **시안이 정한 것**: 상자·머리(안내 문구)는 지금 그대로 두고, 파일은 안내 문구 **아래** 같은 상자
      **안**에 **왼쪽부터 정사각 썸네일**로 나열한다. 지우기는 썸네일 **오른쪽 위 모서리에 원형 `×`**.
   ✅ **안내 문구는 남는다** — `common.js` 가 컨테이너를 드롭존의 **자식**으로 만들어 Dropzone 이
      `dz-started` 를 안 붙이기 때문이다. 🔥 `dz-started` 자체를 막지 말 것.
   ✅ 파일이 늘면 **상자가 세로로 늘어난다**. **상자 안 스크롤은 두지 않는다.**
   🔑 **규격은 여행상품 아이템 갤러리(`.dropzone-image-area`, 바로 아래)를 본떴다** — 125px 정사각.
      🔥 **갤러리의 규격을 건드리지 말 것**(그 화면의 규격이다).
   ⚠️ **여기서부터 Dropzone 기본 CSS 와 다툰다** — 위 절의 *"다투는 상대가 없다"* 는 전제가 깨졌다.
      🔑 선택자를 **`.dropzone > .ds-upload-files …`(0,3,0)** 로 한 단계 높여 `!important` 없이 이긴다.
   📌 이 앱의 즉시 업로드 드롭존은 **`trade/contractView` 2곳**이다.
   🔬 **375·430 을 반드시 볼 것** — 드롭존이 가장 좁아 상자가 세로로 늘어난다.
   ══════════════════════════════════════════════════════════════════════════════════════ */
.dropzone > .ds-upload-files {
    display: flex;
    flex-wrap: wrap;
    gap: var(--ds-sp-3);
    margin-top: var(--ds-sp-4);
    margin-bottom: 0;
}
.dropzone > .ds-upload-files .dz-preview {
    /* 🔑 `relative` 는 원형 `×` 의 기준 상자다 — 지우면 × 가 화면 구석으로 튄다. */
    position: relative;
    display: block;
    width: 125px;
    padding: 0;
    margin: 0;
    border: 0;
    min-height: 0;
}
.dropzone > .ds-upload-files .dz-image {
    width: 125px;
    height: 125px;
    border-radius: .475rem;
    background-color: var(--ds-surface);
    /* 🔑 테두리는 `border` 가 아니라 inset 링이다 — border 면 상자가 2px 커져 125px 격자가 어긋난다. */
    box-shadow: inset 0 0 0 1px var(--ds-line);
}
/* 🔴 **`contain` 이다 — `cover` 로 되돌리지 말 것** (2026-09-08 사장님 지적:
      *"미리보기로는 이미지를 알아볼 수가 없어"* · *"**이미지의 일부만 보여주기 때문에** 알아보기 힘들다는거야"*)
   🔬 `cover` 는 정사각 타일을 꽉 채우려고 **이미지를 잘라낸다.** 가로로 긴 그림(로고·배너·간판 사진)은
      **가운데 조각만 남아** 무엇인지 알 수 없다(실측: 5:1 로고가 가운데 글자 몇 자만 보였다).
   🔑 미리보기의 목적은 **「무엇을 올렸는지 알아보는 것」**이지 타일을 예쁘게 채우는 것이 아니다.
      `contain` 은 그림 전체를 넣고 남는 자리를 여백으로 둔다 — 여백은 `--ds-surface` 가 채운다.
   ⚠️ **얼굴 사진(`.ds-topbar-avatar`·프로필)의 `cover` 는 그대로 둘 것** — 그쪽은 원형으로 잘라내는
      것이 맞고, 그 자리에 「전체를 보여준다」는 요구가 없다. 🔥 이 규칙을 그리로 넓히지 마라. */
.dropzone > .ds-upload-files .dz-image img {
    width: 100%;
    height: 100%;
    object-fit: contain;
}
.dropzone > .ds-upload-files .dz-image:has(img:not([src]))::before { font-size: 2rem; }
/* ══════════════════════════════════════════════════════════════════════════════════════
   🔴 **이미지 타일에는 파일명을 그리지 않는다 — 「줄」에는 그린다** (2026-09-08 사장님 지적)
   ══════════════════════════════════════════════════════════════════════════════════════
   > *"이미지 미리보기 형식이 관리자는 잘못된것 같은데? **파일 명이 이미지 아래에 노출**되고 있어."*
   📌 사장님이 이 앱(브랜딩 이미지 · 업체 서류)에서 보고 지적하신 것이다.

   🕘 **2026-09-07 에 이 자리에 정반대 규칙이 있었다**(`opacity:1; position:static` 으로 파일명을
      타일 아래 보이게 했다). 그 근거는 *"PDF 는 썸네일이 없어 이름이 없으면 서류를 구별할 수 없다"* 였다.
   🔑 **그 근거는 「줄(문서)」에만 해당한다** — 이미지 타일은 **그림 자체가 무엇인지 말한다.**
      같은 날(2026-09-08) 문서 드롭존이 줄 형식(`.ds-upload-files--list`)으로 갈라져 나가면서
      그 근거가 이쪽에는 더 이상 성립하지 않게 됐다. 🔥 **낡은 주석을 근거로 되돌리지 말 것.**
   🔴 **`display:none` 이 아니라 `.visually-hidden` 방식이다**(절대 위치 + clip) — 낭독기에는
      파일명이 그대로 읽혀야 한다. 이 저장소가 이미 쓰는 선례다.
      🔥 `display:none` 으로 바꾸지 말 것 — 그 순간 타일이 낭독기에 「이미지」로만 읽힌다.
   🔑 **선택자에 `:not(.ds-upload-files--list)` 가 붙어 있는 것이 요점이다**(0,4,0) — 줄 형식은
      아래 목록 절이 자기 규격을 그대로 쓴다. 🔥 그것을 빼지 말 것(빼면 문서 이름까지 사라진다).
   ══════════════════════════════════════════════════════════════════════════════════════ */
/* 🔴 **벤더의 「감추고 겹쳐 놓기」를 되돌리는 이 두 줄은 타일·줄 「둘 다」에 걸린다 — 갈라 놓지 말 것.**
   🔬 Dropzone 기본 CSS 는 `.dropzone .dz-preview .dz-details` 를 `opacity:0` + `position:absolute;
      top:0; left:0; padding:26px 13px` 로 **썸네일 위에 겹쳐 감춰** 두고 마우스를 올릴 때만 보여 준다.
   🔥 이 절을 아래 타일 절 안으로 옮기지 말 것 — 2026-09-08 에 실제로 그렇게 했다가
      **줄 형식(문서)의 파일명이 투명해지고 아이콘 위에 겹쳤다**(실측: `position absolute`,
      상자 118×71.5 로 줄을 덮었다). 타일에서 이름을 걷는 것은 **아래 절 하나**의 몫이다. */
.dropzone > .ds-upload-files .dz-details {
    opacity: 1;
    position: static;
    padding: 0;
}

/* ══════════════════════════════════════════════════════════════════════════════════════
   📊 **진행 막대도 벤더를 이겨야 한다 — 위 `.dz-details` 와 똑같은 이유다** (2026-09-09)
   ══════════════════════════════════════════════════════════════════════════════════════
   🔬 위 15절의 `.ds-upload-files .dz-progress`(0,2,0)는 **벤더에게 진다.**
      Dropzone 기본 CSS 는 `.dropzone .dz-preview .dz-progress`(**0,3,0**)로
      `position:absolute; left:50%; top:50%; margin:-8px/-40px; width:80px; height:16px;
      border-radius:8px` 를 걸어 **썸네일 한가운데 떠 있는 80×16 알약**을 만든다.
      채움 막대도 `.dropzone .dz-preview .dz-progress .dz-upload`(**0,4,0**)가
      `position:absolute` + **짙은 회색 그라디언트**로 덮어 `--ds-primary` 가 안 먹었다.
   🔑 그래서 **선택자를 한 단계 높인다** — 바로 위 `.dz-details` 절과 같은 수법이고,
      `.ds-upload-files` 는 `instant-upload.js` 가 드롭존의 **직계 자식**으로 만들므로 `>` 가 성립한다.
      특이도가 벤더와 같아지고 `design-system.css` 가 `plugins.bundle.css` **뒤에 실려** 이긴다
      (실측 로드 순서: 1 plugins.bundle → 5 design-system). 🔥 `!important` 를 쓰지 말 것.
   🕘 **2026-09-09 이전에는 이 결함이 화면별 CSS 에 가려져 있었다** — `menuLevel/{Customer,Sign}.css`
      가 `display:none` 으로 막대를 통째로 감추고, `menuLevel/TravelProduct.css` 는 5px 흰 막대로
      덮어써서(둘 다 0,3,0) **벤더가 이기고 있다는 사실 자체가 안 보였다.** 그 12곳을 지우자 드러났다.
   🔥 **`margin-left` 를 지우지 말 것** — 벤더의 `-40px` 는 `position:static` 으로 돌려도
      **일반 흐름에서 그대로 먹어** 막대가 왼쪽으로 40px 삐져나간다.
   ══════════════════════════════════════════════════════════════════════════════════════ */
.dropzone > .ds-upload-files .dz-progress {
    position: static;
    width: 100%;
    height: 3px;
    margin-top: 0.35rem;
    margin-left: 0;
    background: var(--ds-fill);
    border-radius: 2px;
    overflow: hidden;
}
.dropzone > .ds-upload-files .dz-progress .dz-upload {
    position: static;
    display: block;
    height: 100%;
    background: var(--ds-primary);
}
.dropzone > .ds-upload-files:not(.ds-upload-files--list) .dz-details {
    position: absolute !important;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}
/* ══════════════════════════════════════════════════════════════════════════════════════
   🖱 **마우스를 올리면 파일명이 타일 「위에 겹쳐」 보인다** (2026-09-08 사장님 지시로 되살렸다)
   ══════════════════════════════════════════════════════════════════════════════════════
   > *"원래 있던 기능인데 없어진게 있어. **이미지 미리보기에 마우스 오버하면 이미지 파일명이
   >  미리보기 위에 보여지고** … 그게 없어졌네"*

   🕘 **벤더 기본이 원래 그랬다** — Dropzone 은 `.dz-details` 를 `opacity:0` 으로 썸네일 위에 겹쳐 두고
      **마우스를 올릴 때만** 보여 줬다. 2026-09-07 에 「항상 타일 **아래** 표시」로 바꾸면서 그 동작이
      사라졌고, 2026-09-08 에 「이미지 타일은 파일명을 감춘다」가 얹혀 아예 안 보이게 됐다.
   🔑 **바로 위 절과 충돌하지 않는다** — 위 절이 정한 것은 **「평소에 안 보인다」**이고,
      이 절은 **「마우스를 올렸을 때만 보인다」**다. 🔥 **타일 아래 상시 표시로 되돌리지 말 것**
      (그것이 2026-09-08 에 사장님이 물린 바로 그 모양이다).
   ⌨️ **`:focus-within` 을 빼지 말 것** — `:hover` 만 걸면 **키보드 사용자는 파일명을 볼 길이 없다.**
      타일 안에서 포커스를 받는 것은 `×`(`.dz-remove`, `<a>`)라 탭 이동만으로 이름이 뜬다.
   📱 **터치 기기에는 hover 가 없다** — 그 자리에서는 평소대로 이름이 안 보인다(낭독기는 그대로 읽는다).
      🔑 지금은 그대로 둔다. 바꿔야 하면 **「누르면 큰 그림」과 함께** 설계할 것(따로 만들면 또 갈린다).
   🎨 **바탕은 반투명이 아니라 `--ds-surface`(불투명)** 다 — 사진 위 글자는 배경이 비치면 대비를 보장할 수
      없다(밝은 사진 위 검은 글자 = 안 읽힘). 🔥 hex·`rgba()` 를 여기에 적지 말 것(0절: 토큰만).
   ══════════════════════════════════════════════════════════════════════════════════════ */
.dropzone > .ds-upload-files:not(.ds-upload-files--list) .dz-preview:hover .dz-details,
.dropzone > .ds-upload-files:not(.ds-upload-files--list) .dz-preview:focus-within .dz-details {
    top: auto;
    right: 0;
    bottom: 0;
    left: 0;
    width: auto;
    height: auto;
    margin: 0;
    padding: var(--ds-sp-1);
    clip: auto;
    /* 🔑 `×` 는 `z-index:1` 로 모서리 밖에 걸쳐 있다 — 그 위로 올라와야 이름이 안 가린다. */
    z-index: 2;
    background-color: var(--ds-surface);
    border-radius: 0 0 .475rem .475rem;
    line-height: 1.2;
}
.dropzone > .ds-upload-files:not(.ds-upload-files--list) .dz-preview:hover .dz-filename,
.dropzone > .ds-upload-files:not(.ds-upload-files--list) .dz-preview:focus-within .dz-filename {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--ds-text-strong);
    font-size: var(--ds-font-sub);
}
/* 📏 **타일에서는 용량을 띄우지 않는다** — 125px 안에 이름과 용량을 함께 넣으면 이름이 두 글자만 남는다.
   🔑 용량은 「줄(문서)」에서 보여 준다(그쪽은 가로가 넉넉하다). 🔥 여기서 되살리지 말 것. */
.dropzone > .ds-upload-files:not(.ds-upload-files--list) .dz-size { display: none; }
/* 벤더가 파일명 `<span>` 에 흰 반투명 알약을 씌운다(썸네일 위에 겹칠 때를 위한 것이다) —
   아래로 내려온 지금은 배경이 필요 없다. */
.dropzone > .ds-upload-files .dz-filename span {
    background: none;
    padding: 0;
}
/* 🗑 **원형 `×` 는 썸네일 오른쪽 위 모서리에 살짝 걸친다**(시안).
   🔑 크기는 **배지 높이**(`--ds-badge-h`)다 — 썸네일 위에 얹히는 표식이라 44px 이면 썸네일의
      3분의 1을 덮는다. ⚠️ 이 자리만은 27절의 44px 규격을 따르지 않고, 대신 **모서리 밖으로 내밀어**
      누를 면적이 썸네일과 겹치지 않게 했다.
   🔥 `font-size: 0` 을 지우지 말 것 — 라벨을 화면에서만 걷는 방식이고 낭독기는 그대로 읽는다. */
.dropzone > .ds-upload-files .dz-remove {
    position: absolute;
    top: calc(var(--ds-badge-h) / -3);
    right: calc(var(--ds-badge-h) / -3);
    z-index: 1;
    border-radius: 50%;
    background-color: var(--ds-surface);
    box-shadow: 0 0 0 1px var(--ds-line);
}
/* 🗑 **타일의 `×` 는 타일 「안쪽」이다 — 시안 ③**(2026-09-08 사장님이 화면에서 확정).
      전: 벤더(`.dropzone .dz-remove`)가 `top/right: -.825rem`(= **-13.2px**)이라 `×` 가 타일 **밖**으로
          나가 타일 간격 10px 을 넘어 **옆 타일을 침범**했다 → 어느 타일의 `×` 인지 알 수 없었다.
      후: **5px** 로 안쪽에 들이고 22px 정사각 + 그림자로 썸네일 위에서도 읽히게 한다.
   🕘 이 규칙은 원래 `.dropzone-image-area .dz-preview .dz-remove`(0,3,0)에 있었다 —
      그 클래스를 마크업에서 걷어내면서(위 상자 절) **자리만 옮겼다. 값은 그대로다.**
      🔥 「클래스를 지웠으니 이 규칙도 필요 없다」로 읽지 말 것 — 지우면 `×` 가 타일 밖으로 되돌아간다.
   🔴 **`background` shorthand 로 덮지 말 것 — `×` 아이콘이 벤더가 깐 배경 이미지(data URI SVG)다.**
      shorthand 를 쓰면 그 이미지가 **함께 지워져 검은 원만 남는다**(시안을 만들다 실제로 그렇게 됐다).
      → 여기서 손대는 것은 **위치 · 크기 · `background-size` · 그림자**뿐이다.
   ⚠️ **이 자리만은 px 가 맞다** — 125px 고정 타일 위에 얹히는 표식이라, rem 으로 두면 글자를 키웠을 때
      타일은 그대로인데 `×` 만 커져 썸네일을 덮는다. 「글자는 rem」 원칙의 대상이 아니다.
   📌 **줄 형식(`--list`)은 대상이 아니다** — 선택자에 `:not(.ds-upload-files--list)` 가 붙어 있다.
      이 앱에서 실제로 바뀌는 것은 **브랜딩 이미지 타일**이다. */
.dropzone > .ds-upload-files:not(.ds-upload-files--list) .dz-remove {
    top: 5px;
    right: 5px;
    left: auto;
    width: 22px;
    height: 22px;
    line-height: 22px;
    background-size: 12px 12px;
    box-shadow: 0 1px 4px rgba(0, 0, 0, .35);
    opacity: 1;
}

/* ══════════════════════════════════════════════════════════════════════════════════════
   🔴 **문서 드롭존은 타일이 아니라 「줄」이다** (2026-09-08 · 드롭존 구현 통합)
   ══════════════════════════════════════════════════════════════════════════════════════
   🔬 **고치기 전**: 위 절이 형식을 가리지 않고 **125px 정사각 타일**로 그렸다 → 썸네일이 없는 파일
      (사업자등록증 PDF·한글 문서·zip)은 타일 안에 그릴 것이 없어 **회색 네모**로 보였다.
      파일명은 타일 아래 12.8px 로 접혀 무엇이 붙어 있는지 읽기 어려웠다.
   🔑 **판정은 `data-accept` 다** — 이미지 전용이면 타일, 아니면 줄. 붙이는 것은 `common.js` 의
      `Common.InstantUpload` 한 곳이고(`.ds-upload-files--list`), 그 판정 함수는 `isImageOnly` 다.
      🔥 화면 CSS 에서 이 클래스를 직접 붙이지 말 것 — 그러면 문구(허용 형식 안내)와 갈린다.
   🔑 모양은 **2026-08-28 이전의 칩 모양을 되살린 것**이다(위 `.ds-upload-files` 기본 절과 같다).
      타일 절이 `.dropzone > …`(0,3,0)로 그것을 덮으므로, 여기서는 클래스를 하나 더 얹어
      **(0,4,0)** 으로 되돌린다 — `!important` 없이 이긴다.
   🔴 **`!important` 가 세 곳 붙는 이유** — 마이페이지·정산·가입의 서류 드롭존은 상자 규격 때문에
      `.dropzone-image-area` 를 **함께** 달고 있는데, 그 절의 `.dz-preview{width:125px !important}` ·
      `.dz-image{125px !important}` · `img{object-fit:cover !important}` 가 **특이도와 무관하게 이긴다.**
      🔬 그래서 2026-09-08 에 그 클래스를 아예 떼 봤더니 **상자 테두리·안여백이 Metronic 기본으로
         돌아가 그 화면들만 다른 모양**이 됐다 → 클래스는 되돌리고 여기에 `!important` 로 맞섰다.
      🔥 클래스를 다시 떼는 방식으로 되돌리지 말 것(상자가 바뀐다). 갤러리 절도 건드리지 말 것.
   ══════════════════════════════════════════════════════════════════════════════════════ */
.dropzone > .ds-upload-files.ds-upload-files--list {
    display: block;
    margin-top: var(--ds-sp-4);
}
.dropzone > .ds-upload-files.ds-upload-files--list .dz-preview {
    /* `relative` 는 남겨 둔다 — 진행 막대·오류 표시가 이 상자를 기준으로 잡힌다 */
    position: relative;
    display: flex;
    align-items: center;
    gap: 0.75rem;
    width: auto !important;
    padding: 0.35rem 1rem;
    margin: 0 0 0.5rem !important;
    /* 🔑 **드롭존 안의 줄은 실선이다** (2026-09-18 · 랜드사 1회차 P2-30 · 정본 P60ⓛ) — 드롭존 자신이 점선이라
          줄까지 점선이면 **점선이 두 겹**으로 보였다(단일·다중 파일 모두). 드롭존 밖 칩(`.ds-upload-files` 기본 절)은 그대로 점선이다. */
    border: 1px solid var(--ds-line);
    border-radius: 0.475rem;
}
.dropzone > .ds-upload-files.ds-upload-files--list .dz-preview:last-child { margin-bottom: 0 !important; }
.dropzone > .ds-upload-files.ds-upload-files--list .dz-image {
    width: 32px !important;
    height: 32px !important;
    border-radius: 0 !important;
    background-color: transparent;
    box-shadow: none;
}
.dropzone > .ds-upload-files.ds-upload-files--list .dz-image img {
    width: auto !important;
    height: auto !important;
    max-width: 100%;
    max-height: 100%;
    object-fit: contain !important;
}
.dropzone > .ds-upload-files.ds-upload-files--list .dz-image:has(img:not([src]))::before { font-size: 1.1rem; }
/* 📏 **줄에서는 「파일명 (용량)」이 한 줄에 선다** (2026-09-08 사장님 지적).
   🔬 벤더 기본 틀은 `.dz-details` 안에 **`.dz-size` 가 `.dz-filename` 보다 먼저** 오고 둘 다 블록이라,
      그대로 두면 **용량이 파일명 「위」에 두 줄로** 선다. 그래서 여기서 가로로 세우고 `order` 로 뒤집는다.
   🔑 **`order` 로 뒤집는 이유** — 미리보기 틀은 벤더 것 한 벌(`Dropzone.prototype.previewTemplate`)이라
      마크업 순서를 우리가 바꿀 수 없다(바꾸려면 틀을 다시 만들어야 하고, 그것이 2026-09-08 에
      「틀은 기본 하나뿐」으로 정리한 바로 그 자리다). 🔥 틀을 새로 만들어 순서를 고치지 말 것.
   ⚠️ **낭독기 순서는 마크업 그대로**(용량 → 파일명)다 — 뜻이 바뀌지 않는 부가 정보라 그대로 둔다. */
/* 🔴 **세로 가운데다 — `baseline` 으로 되돌리지 말 것** (2026-09-08 사장님 지적: *"파일명이 위로 가서
      붙어 버린다. 세로 중앙 정렬이 되어야지."*).
   🔬 실측(전): 줄(`.dz-preview`)은 `align-items:center` 인데 이 블록만 `baseline` 이라, 글자가
      기준선에 붙어 36px 짜리 블록 안에서 위로 쏠렸다 — 중심좌표 아이콘 1489 · 삭제 1489 인데
      **파일명 1482 · 용량 1481**(7px 위).
   🔑 **`baseline` 을 쓸 이유가 없다** — 파일명과 용량은 높이가 **둘 다 20px 로 같아서** `center` 로 바꿔도
      두 글자의 기준선은 그대로 맞는다(둘 사이 정렬이 목적이 아니라 줄 안에서의 위치가 문제였다).
   ⚠️ 타일(이미지) 쪽 `.dz-details` 는 hover 시 썸네일 위에 겹쳐 뜨는 **다른 자리**다 — 여기 값을
      그쪽으로 옮기지 말 것. */
.dropzone > .ds-upload-files.ds-upload-files--list .dz-details {
    width: auto;
    flex: 1 1 auto;
    min-width: 0;
    margin-top: 0;
    text-align: left;
    display: flex;
    align-items: center;
    gap: var(--ds-sp-1);
    /* 🔤 **줄의 글자 크기는 여기서 한 번만 정한다**(파일명·용량이 함께 물려받는다) — 아래 `.dz-size` 절 참고.
          벤더 기본은 `13px` 이고 `.85rem` 은 기본 글자(root 16)에서 **13.6px** 이라 거의 같으면서
          「글자 크기 3단계」 토글을 따라 함께 커진다. 🔥 px 로 되돌리지 말 것. */
    font-size: .85rem;
}
.dropzone > .ds-upload-files.ds-upload-files--list .dz-filename {
    order: 1;
    /* 🔑 `min-width: 0` 이 없으면 긴 파일명이 줄을 밀어내 용량이 `×` 밖으로 나간다(말줄임도 안 걸린다). */
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* 🔑 용량은 **줄어들지 않는다** — 파일명이 말줄임되고 「(용량)」은 끝까지 남는다.
   🔴 **`margin: 0` 을 지우지 말 것 — 이것이 「용량만 위로 뜨는」 두 번째 원인이다** (2026-09-08 실측).
      🔬 벤더가 `.dz-size` 에 **`margin-bottom: 1em`(=16px)** 을 걸어 둔다. 위 `.dz-details` 를
         `center` 로 바꾸면 플렉스는 **여백까지 포함한 상자**를 가운데 두므로 글자만 **8.8px 위**로 뜬다
         (실측 전: 아이콘 4657.4 · 삭제 4657.4 · 파일명 4657.1 인데 **용량 4648.6**).
      🔥 `.dz-details` 만 고치고 여기를 안 고치면 **지적이 절반만 해결된다** — 실제로 그랬다. */
/* 🔤 **용량은 파일명보다 크면 안 된다 — 같은 크기다** (2026-09-08 사장님 지적으로 바로잡음).
   🔬 실측(전): 파일명 **13px** / 용량 **16px**(줄높이는 둘 다 19.5px). 벤더가 `.dz-details` 를 13px 로
      두고 `.dz-size` 에만 **`font-size: 16px`** 을 따로 박아 둔 탓이다 — **보조 정보가 본 정보보다 컸다.**
   🔑 **크기를 더 낮추지 않고 「같게」 두는 이유** — 이 저장소 규칙은 *"보조 텍스트는 크기를 낮추기 전에
      색부터 낮춘다"* 이고, 용량은 **이미 색으로 물러나 있다**(바로 아래 `.dz-size span` =
      `--ds-text-label`, 파일명은 `--ds-text-strong`). 여기서 한 단계 더 줄이면 **두 번 물러난다.**
      📌 짝이 되는 규칙: *"비슷한 성격의 요소는 동일한 속성을 가진다"* — 파일명과 용량은 **한 줄에 나란히
      선 같은 층**이라 크기가 갈릴 이유가 없다.
   🔥 **px 로 되돌리지 말 것** — 「글자 크기 3단계」 토글이 root 기준 rem 이라 px 로 박으면 이 자리에서만
      토글이 아무 일도 하지 않는다. `.85rem` 은 기본 글자(root 16)에서 **13.6px** 이다.
   ⚠️ **`.dz-details` 와 `.dz-size` 를 함께 적어야 한다** — 벤더가 두 곳에 값을 박아 두어
      부모만 고치면 용량은 16px 그대로 남는다(한쪽만 고치면 지적이 절반만 해결된다). */
.dropzone > .ds-upload-files.ds-upload-files--list .dz-size {
    order: 2;
    flex: none;
    margin: 0;
    white-space: nowrap;
    font-size: inherit;
}
/* 🔴 **파일명 대비** — 즉시 업로드 줄에도 같은 색을 준다 (2026-09-08).
   🔬 실측: 흰 배경 위 `rgb(126,130,153)` = **3.79:1** 로 기준(4.5) 미달이었다 → `--ds-text-strong` 로 **16.79:1**.
   🕘 같은 뜻의 규칙이 `.dropzone.dropzone-queue .dropzone-item [data-dz-name]` 에 먼저 들어갔는데
      **그쪽은 아이템 화면(문서 드롭존)의 구조**다. 즉시 업로드 줄은 Dropzone 기본 템플릿이라
      `[data-dz-name]` 이 아니라 **`.dz-filename > span`** 이다 → **한쪽만 고쳐져 있었다.**
   🔥 **줄 형식은 두 구조가 있다는 것을 잊지 마라** — 한 곳만 고치면 화면 절반만 바뀐다.
   ⚠️ `background-color` 를 함께 지운다 — 벤더가 `.dz-filename span` 에 `rgba(255,255,255,.4)` 를
      깔아 둬서(썸네일 위에 얹힐 때를 위한 것) 흰 줄 위에서는 뿌옇게만 보인다.
   📌 관리자 앱에서는 업체 서류·계약 증빙·환불 첨부가 이 규칙을 탄다(업체 상세에서 실측했다). */
.dropzone > .ds-upload-files.ds-upload-files--list .dz-filename span {
    color: var(--ds-text-strong);
    font-weight: 500;
    background-color: transparent;
    padding: 0;
    border-radius: 0;
}
.dropzone > .ds-upload-files.ds-upload-files--list .dz-size span {
    color: var(--ds-text-label);
    background-color: transparent;
    padding: 0;
}
/* 🗑 줄 형식에서는 `×` 가 오른쪽 끝에 **자기 자리**로 선다 — 겹칠 썸네일이 없다.
   ⚠️ 아래 크기 세 줄은 **2026-09-08 시안 ③(타일 안쪽 `×`)이 이 줄까지 넘어오는 것을 막는 되돌림**이다.
      그 시안은 `.dropzone-image-area .dz-preview .dz-remove`(0,3,0)로 22px 정사각을 박는데,
      서류 드롭존은 상자 규격 때문에 `.dropzone-image-area` 를 **함께 달고 있어** 줄 형식에도 걸린다.
      이 블록은 (0,4,0)이라 이긴다. 🔥 지우면 줄의 `×` 만 27.2px → 22px 로 작아진다(타일과 성격이 다른 자리다).
   📌 관리자 앱에서 이 줄 형식이 서는 곳은 업체 서류 조각(`fragments/companyDocs`)이다. */
.dropzone > .ds-upload-files.ds-upload-files--list .dz-remove {
    position: static;
    border-radius: 0;
    background-color: transparent;
    box-shadow: none;
    width: var(--ds-badge-h);
    height: var(--ds-badge-h);
    background-size: 40%;
}

/* 🕘 **여기에 있던 `.dropzone-image-area .dz-preview{…!important}` 3줄은 지웠다** (2026-09-08).
      🔬 값이 위 `.dropzone > .ds-upload-files` 절과 **똑같은 사본**이었고(125px 정사각 · `contain`),
         `!important` 때문에 **줄(list) 형식에까지 걸려** 문서 드롭존이 화면마다 다르게 보였다
         (서류 드롭존은 상자 규격 때문에 `.dropzone-image-area` 를 함께 달고 있었다 — 이 앱의
          업체 상세 서류 절이 정확히 그 자리다).
      🔥 되살리지 말 것 — 타일 규격은 `.dropzone > .ds-upload-files .dz-image` 한 곳이 쥔다. */

/* ══════════════════════════════════════════════════════════════════════════════════════
   🎨 **드롭존 시안 — 2026-09-08 사장님이 브라우저에서 보고 확정** (① 글자 위계 ② 안내 문구 ③ `×` 위치 ④ 줄 대비)
   ══════════════════════════════════════════════════════════════════════════════════════
   🔑 아래 값은 전부 **실제 화면에 입혀 눈으로 확인한 것**이다. 임의로 바꾸지 말 것.
   🔑 **시안이 px 로 적은 글자 값은 rem 으로 옮겨 적었다** — 기본 글자(root 16px)에서 **한 픽셀도
      다르지 않으면서** 「글자 크기 3단계」 토글을 따라 함께 커진다
      (15.2px = `.95rem` · 13.6px = `.85rem` · 32px = `2rem`).
      🔥 **px 로 되돌리지 말 것** — 되돌리면 이 자리에서만 그 토글이 아무 일도 하지 않는다.
      📌 관리자 앱은 **초기 설정 마법사에도 글자 크기 토글이 있다**(헤더 없는 화면이라 따로 둔 것) —
         그 화면의 브랜딩 드롭존이 이 규칙을 그대로 탄다.
   ⚠️ **`!important` 를 쓴 이유** — 안내 문구 마크업이 Metronic 유틸리티(`fs-5`·`fs-7`·`fs-3x`·`mb-1`·`ms-4`)를
      달고 있고 그 유틸리티가 **전부 `!important`** 라 특이도만 올려서는 못 이긴다(바로 위 `dz-message` 절과 같다).
      🔬 실측: `.fs-5{1.15rem!important}`(=18.4px) · `.fs-7{.95rem!important}`(=15.2px) · `.fs-3x{3rem!important}`(=48px).
   📌 이 절은 **드롭존 구현 다섯 벌을 `Common.InstantUpload.attach()` 한 곳으로 합친 뒤**에 얹은 것이다 —
      그래서 CSS 한 벌만 고치면 초기 설정·플랫폼 설정의 브랜딩 이미지와 업체 서류 조각이 함께 바뀐다.
   ⚠️ **세 앱이 같은 한 벌이다 — 이 앱에만 고치지 말 것.** ④는 이 앱에 해당 마크업이 없지만
      (`.dropzone-queue` 는 랜드사 아이템 등록에만 있다) **규칙은 세 앱이 같아야 하므로 그대로 둔다.**
   ══════════════════════════════════════════════════════════════════════════════════════ */

/* ① **글자 위계** — 안내 문구가 항목 라벨(16.8px)보다 커서 **위계가 뒤집혀** 있었다.
      전: 제목 18.4px · 설명 15.2px · 아이콘 48px  →  후: 제목 15.2px · 설명 13.6px · 아이콘 32px */
.dropzone .dz-message h3 {
    font-size: .95rem !important;
    line-height: 1.35;
    margin-bottom: 2px !important;
}
.dropzone .dz-message span {
    font-size: .85rem !important;
    line-height: 1.4;
    font-weight: 500;
}
.dropzone .dz-message i { font-size: 2rem !important; }
.dropzone .dz-message .ms-4 { margin-left: .9rem !important; }

/* ② **안내 문구가 줄 전체를 차지한다.**
   🔑 문구가 **폭 100%** 를 잡아야 첫 줄을 통째로 쓰고 그 아래에 125px 타일이 깔린다.
      안 주면 문구가 타일과 **한 줄에 섞인다.**
   🕘 원래 선택자는 `.dropzone-image-area .dz-message` 였다 — 그 클래스를 마크업에서 걷어내면서
      **판정 클래스로 옮겼다**(위 상자 절과 같은 이유). 값은 그대로다. 🔥 지우지 말 것.
   🕘 짝이던 `.dropzone-image-area.dz-started .dz-message{margin-bottom:12px}` 는 **지웠다** —
      미리보기 컨테이너가 드롭존의 **자식**이라 `dz-started` 가 애초에 붙지 않는다(죽은 규칙이었다). */
.dropzone.js-instant-upload .dz-message { width: 100%; margin-bottom: 0; }

/* 🕘 **③(타일 안쪽 `×`)은 위 `.dropzone > .ds-upload-files:not(--list) .dz-remove` 로 옮겼다**
      (2026-09-08 · `.dropzone-image-area` 를 마크업에서 걷어내면서). 값은 그대로다.
   🕘 **④(줄 대비)의 `.dropzone.dropzone-queue .dropzone-item` 3줄은 지웠다** — 그 Metronic 큐
      마크업(`.dropzone-items`·`.dropzone-item`)은 아이템 등록 화면에서 걷어냈고 `dropzone-queue`
      클래스도 이번에 걷어냈다. **그리는 자리가 세 앱 어디에도 없는 규칙이었다.**
      🔑 지금 줄의 대비를 정하는 것은 `.dropzone > .ds-upload-files.ds-upload-files--list …` 절이다.
      🔥 그 이름으로 되살리지 말 것 — 되살려도 아무 요소에도 안 걸린다. */

/* ══════════════════════════════════════════════════════════════════════════════════════
   🖼 **누르면 무엇이 일어나는지 — 이미지는 「크게 보기」, 문서는 「내려받기」** (2026-09-08 사장님 확정)
   ══════════════════════════════════════════════════════════════════════════════════════
   > *"이미지의 경우 클릭하면 큰 이미지로 미리보기 (모달) / 문서의 경우 클릭하면 다운로드"*

   🔑 **동작을 거는 것은 `instant-upload.js` 한 곳**이고(`_bindTile`·`_linkPreview`), 여기는 **모양만**
      정한다. 🔥 화면 CSS 에서 이 커서·테두리를 다시 칠하지 말 것.
   🔑 `role="button"` 이 붙어 있을 때만 손 모양이 된다 — **눌러도 아무 일도 안 일어나는 자리**
      (그림이 깨져 모달에 띄울 것이 없는 타일)에는 커서가 바뀌지 않는다.
   ⌨️ **포커스 링을 지우지 말 것** — 키보드로 타일·줄을 열 수 있게 한 것이 이 변경의 절반이다. */
.dropzone > .ds-upload-files:not(.ds-upload-files--list) .dz-image[role="button"] { cursor: zoom-in; }
.dropzone > .ds-upload-files.ds-upload-files--list .dz-preview { cursor: pointer; }
.dropzone > .ds-upload-files .dz-image[role="button"]:focus-visible,
.dropzone > .ds-upload-files [data-dz-name][role="button"]:focus-visible,
.dropzone > .ds-upload-files .dz-filename span[role="button"]:focus-visible {
    outline: 2px solid var(--ds-primary);
    outline-offset: 2px;
    border-radius: .275rem;
}

/* ══════════════════════════════════════════════════════════════════════════════════════
   🔀 **끌어서 순서 바꾸기** — `.ds-upload-sortable` / `.ds-dragging` (2026-09-08 사장님 지시 *"만들어"*)
   ══════════════════════════════════════════════════════════════════════════════════════
   🔑 **동작은 `instant-upload.js` 한 곳**이고(`_initDnd`·`_prepDrag`·`_onDragMove`) 여기는 **모양만** 정한다.
      🔥 화면 CSS 에서 이 커서·테두리를 다시 칠하지 말 것.
   🔴 **여러 장 이미지 타일에만 걸린다** — 클래스는 JS 가 붙인다(`multiple && imageMode`).
      단일은 한 장이라 순서가 없고, 문서 줄은 아직 대상이 아니다.

   🔴 **`.dz-image[role="button"]` 의 `cursor: zoom-in` 을 덮지 않는다 — 일부러다** (위 절의 짝).
      🔬 그림이 타일을 거의 다 채우므로 `grab` 으로 덮으면 **「눌러서 크게 보기」의 유일한 예고가 사라진다**
         (사장님이 확정한 동작이다: *"이미지의 경우 클릭하면 큰 이미지로 미리보기"*).
      ✅ 그래서 `grab` 은 **타일의 테두리·여백 자리**에만 뜨고, 실제로 끌기 시작하면 `.ds-dragging` 이
         **판 전체를 `grabbing`** 으로 바꿔 「지금 옮기는 중」을 분명히 한다.
      🔥 `.dz-image` 에 `grab` 을 박아 「일관되게」 만들지 말 것 — 그 순간 클릭 예고를 잃는다.

   👁 **놓일 자리는 「끌고 있는 타일 자신」이 보여 준다** — JS 가 끄는 동안 DOM 에서 실제로 옮기므로
      점선 상자가 곧 놓일 자리다. 🔥 **빈 자리표시자를 따로 만들지 말 것**(요소가 둘이 되면
      「점선 자리」와 「실제로 들어갈 자리」가 어긋나는 순간이 생긴다).
   ⚠️ **`opacity` 만 주지 말 것** — 흐려지기만 하면 「지워지는 중」으로 읽힌다. 점선 테두리가 「자리」를 말한다.
   ══════════════════════════════════════════════════════════════════════════════════════ */
.dropzone > .ds-upload-files.ds-upload-sortable > .dz-preview { cursor: grab; }
.dropzone > .ds-upload-files.ds-upload-sortable > .dz-preview.ds-dragging {
    opacity: .45;
    outline: 2px dashed var(--ds-primary);
    outline-offset: 2px;
    cursor: grabbing;
}
/* 🔴 **끄는 동안 타일 「안」은 마우스를 받지 않는다** — 안 그러면 그림·`×` 가 `dragover` 의 대상이 되어
      포인터가 어느 타일 위인지 판정이 흔들린다(놓일 자리가 튄다). 🔥 지우지 말 것. */
.dropzone > .ds-upload-files.ds-upload-sortable > .dz-preview.ds-dragging * { pointer-events: none; }

/* ══════════════════════════════════════════════════════════════════════════════════════
   🖼 **크게 보기(라이트박스) — 세 앱 공용 부품** `.ds-lightbox` (2026-09-08)
   ══════════════════════════════════════════════════════════════════════════════════════
   🕘 그전에는 라이트박스가 **화면마다 제각각**이었다 — 랜드사만 마크업·핸들러·CSS 셋이 다 있었고,
      **이 앱은 핸들러만** 있어 눌러도 `null` 참조로 죽었으며, 여행사는 CSS 만 남아 있었다
      (셋 다 `#lightboxModal` 이라는 같은 id 를 쓰면서 그랬다).
   ✅ 이제 **모달 DOM 은 `instant-upload.js` 가 필요할 때 만들어 `body` 에 붙인다**(id `dsLightbox`).
      🔥 화면 템플릿에 모달 마크업을 넣지 말 것 — 넣는 순간 「넣은 화면과 안 넣은 화면」이 다시 갈린다.
   🔴 **선택자에 `.ds` 를 붙이지 말 것** — 이 모달은 `body` 직계라 `#kt_content.ds` **바깥**이다.
      붙이면 오류 없이 조용히 안 걸린다(이 파일의 다른 절이 여러 번 당한 자리다).
   🎨 **판 색은 `--ds-scrim` 이다** — 뒤의 막(`.modal-backdrop`, 같은 토큰) 위에 한 겹 더 얹혀
      사진 뒤가 충분히 어두워지고, **두 테마 모두 저절로 따라온다**.
      🔥 `rgba(0,0,0,.85)` 같은 생 색으로 되돌리지 말 것(0절 — 이 저장소에 그 값이 세 앱 CSS 에
      한 벌씩 박혀 있었고, 그중 둘은 걸릴 마크업조차 없었다).
   🔑 **제목 줄을 두지 않는다** — 막 위 글자에 쓸 역할 토큰이 없다(`--ds-text-*` 는 라이트에서
      어두운 색이라 안 보인다). 파일명은 그림의 `alt` 와 모달의 `aria-label` 로 남는다.
   ⚠️ `object-fit: contain` 이다 — 크게 보는 자리에서 **잘라 내면 볼 이유가 없다**(타일과 같은 이유).
   ══════════════════════════════════════════════════════════════════════════════════════ */
/* 📐 **판은 그림에 맞춰 줄어든다 — 고정 폭(`modal-xl` = 1140px)으로 되돌리지 말 것** (2026-09-08 사장님 지적).
   🔬 실측(전, 뷰포트 3840×1943): 400×260 그림을 열었더니 `.modal-content` 가 **1140 × 310** 이라
      회색 판이 그림 양옆에 **370px 씩** 남았다.
   🔑 **`width: fit-content` 만으로는 안 된다** — 그림에 `max-width: 100%` 를 주면 판의 최대 내용 폭이
      **그림의 원래 크기**로 먼저 잡히고, 그 뒤에 `max-height` 가 걸려 **그림만** 줄어든다
      → 큰 그림(세로가 긴 그림)에서 옆이 그대로 다시 빈다.
      ✅ 그래서 **그림의 두 상한을 모두 절대값(뷰포트 기준)으로** 준다. 그러면 그림의 최종 크기가
         먼저 정해지고 판이 그 크기를 그대로 감싼다. 🔥 `max-width: 100%` 로 되돌리지 말 것.
   🧮 빼는 값은 **판 밖 여백의 합**이다 — 대화상자 여백(벤더 `margin: 1.75rem auto`, 좌우 2번)과
      본문 안여백(`p-3` = .75rem, 좌우 2번) = 5rem. ⚠️ 좁은 화면은 대화상자 여백이 **.5rem** 이라 2.5rem 이다.
      🔥 px 로 적지 말 것 — 「글자 크기 3단계」 토글이 rem 이라 여백도 함께 커진다.
   📏 **그림의 위아래 여백이 같아야 한다**(C12-0 ②) — 그래서 닫기 버튼을 **판 위에 겹쳐** 띄운다.
      🔬 실측(전): 그림 위 **38** / 아래 **12**px. 닫기 버튼이 든 `.modal-header` 가 **흐름 안**에 있어
         위쪽만 그 높이만큼 밀려 있었다.
      🔥 **머리 줄(`.modal-header`)을 되살리지 말 것** — 되살리는 순간 그 차이가 그대로 돌아온다.
         (24절이 「닫기 전용 머리」를 예외로 남겨 두었지만, 이 자리는 그 예외를 쓰지 않는다.)
   ══ */
.ds-lightbox { --ds-lb-chrome: 5rem; }
@media (max-width: 575.98px) { .ds-lightbox { --ds-lb-chrome: 2.5rem; } }
.ds-lightbox .modal-dialog {
    width: fit-content;
    /* 🔥 벤더의 `.modal-dialog { max-width: 500px }` 를 푼다 — 폭은 아래 그림의 상한 하나가 정한다. */
    max-width: none;
}
.ds-lightbox .modal-content { background-color: var(--ds-scrim); }
.ds-lightbox .modal-header,
.ds-lightbox .modal-body { background-color: transparent; }
/* 🗑 닫기 버튼은 판 위에 겹쳐 뜬다(위 📏). `.modal-content` 는 벤더가 이미 `position: relative` 다. */
.ds-lightbox .ds-lightbox-close {
    position: absolute;
    top: var(--ds-sp-2);
    right: var(--ds-sp-2);
    z-index: 1;
}
.ds-lightbox .ds-lightbox-img {
    /* 🔑 `block` 이어야 한다 — 인라인이면 글꼴의 **기준선 아래 여백**이 그림 밑에 붙어
       위아래 여백이 다시 어긋난다(가운데 정렬은 `text-center` 대신 `margin-inline: auto` 가 맡는다). */
    display: block;
    margin-inline: auto;
    max-width: calc(100vw - var(--ds-lb-chrome));
    max-height: 85vh;
    object-fit: contain;
    border-radius: 0.475rem;
}

/* ══ 16) 요약 지표 카드 — `.ds-kpi` (2026-07-23, Trade.css 의 `.ff-kpi` 에서 이관) ═════════

   목록·상세 상단에 얹는 **요약 지표 카드**의 규격이다. 값 하나를 크게 보여주는 자리.

   🔑 **라벨은 좌측, 값은 우측**이다. 가운데 정렬을 쓰지 않는다 —
      · 값이 숫자·금액이라 프로젝트 규칙("천단위 콤마 + 우측정렬")을 따라야 하고,
      · 카드가 여러 장 나란히 놓일 때 **값의 오른쪽 끝선이 맞아야** 크기 비교가 된다.
      이행 상세가 2026-07-20 사용자 요청으로 가운데 → 좌/우로 바꿨고, 통계 리포트 KPI 도 같다.

   🔑 **목록·상세 위에 얹는 요약 카드는 배경색을 쓰지 않는다.** 요약 카드가 5장씩 깔리면 컬러 블록이
      반복돼 시끄럽고, 정작 강조해야 할 값보다 배경이 먼저 눈에 들어온다(정산 목록 카드에서 세운 방향과 같다).
   🔴 **예외 — 메인 대시보드 맨 윗줄은 색을 유지한다** (2026-09-03 사장님 결정).
      🕘 그전에는 이 절이 *"배경색을 쓰지 않는다"* 한 줄이었는데 **여행사 메인이 이미 색을 입혀 쓰고 있어
         주석과 코드가 어긋나 있었다.** 그래서 두 갈래로 갈라 적는다.
      🔑 **왜 대시보드만 예외인가** — 그 줄은 카드가 **넉 장뿐이고 색이 곧 뜻**이다(진행=파랑 ·
         대기·미입금=노랑 · 계약=초록 · 매출=빨강 · 회원·이행=보라). 위 금지는 **한 화면에 요약 카드가
         여럿 깔릴 때의 소음**을 막으려는 것이라 넉 장짜리 한 줄에는 그 근거가 서지 않는다.
      🔑 색은 **화면 전용 클래스**로 각 앱 `menuLevel/Main.css` 가 입힌다
         (`.main-kpi-*` → `var(--ds-*-soft)`). 🔥 **여기(16절)에 배경색을 넣지 말 것** —
         넣으면 목록·상세의 요약 카드까지 전부 색이 든다.
      🔥 **마크업에 `bg-light-*` 를 붙여 색을 주지 말 것** — 그 유틸리티는 `!important` 라
         (실측 `.bg-light-primary{background-color:#f1faff!important}`) CSS 규칙으로 덮을 수 없고,
         색이 마크업으로 새면 「색은 뜻이다」가 화면마다 갈린다.

   ⚠️ 원래 `.ff-kpi` 라는 이름으로 **Trade.css(메뉴 전용)** 에 있었다. 이행 상세 말고
      매입매출 대조표도 쓰게 되면서 전역으로 올렸다 — "전 메뉴 공통 CSS 는 메뉴 CSS 가 아니라
      전역 CSS 에 둔다"(2026-07-22 사용자 원칙). 요약 카드를 쓰는 화면이 늘면 그냥 이 클래스를 쓸 것. */
/* 카드를 늘어놓는 줄. **열 수를 고정하지 않는다** — 최소 폭만 정하고 나머지는 브라우저가 센다.
   🔥 `row-cols-2 / md-3 / xl-5` 로 열 수를 박았더니 **1200~1439px 구간에서 카드가 158~174px** 이 되어
      억 단위 금액(`1,234,567,890 KRW`)이 줄바꿈됐다(실측). 375px 2열(161px)도 같았다.
      필요 폭은 값 + 좌우 패딩 ≈ 206px 이다.
   🔑 `auto-fit` 은 **남는 트랙을 접는다** → 카드가 4장이든 5장이든 그 수만큼만 열이 생기고 폭을 나눠 갖는다.
      열 수를 상수로 관리할 필요가 없다(정산 목록 카드 그리드에서 쓴 것과 같은 방식).
   ⚠️ `min(100%, 210px)` 로 감쌀 것 — 없으면 컨테이너가 210px 보다 좁을 때 **그대로 넘친다**. */
.ds .ds-kpi-row {
    display: grid;
    gap: 1rem;
    /* 🔴 **트랙 최소 = 값 자리 11rem + 카드 좌우 여백 둘** (2026-09-18 · 랜드사 1회차 V3-06 · 정본 P56).
       🔬 `210px` 은 카드 여백이 `1.1rem` 이던 때 값이다. 지금 카드 본문 여백은 `--ds-card-x`(넓은 화면 30px)라
          768px 에서 카드 214px · 값 자리 **154px** 로 「22,100,000 KRW」가 **두 줄**이 됐다.
       🔑 여백을 식에 넣어 값 자리를 늘 11rem(기본 글자 176px) 이상으로 지킨다 — 좁은 화면(여백 17px)은 종전과 같다. */
    grid-template-columns: repeat(auto-fit, minmax(min(100%, calc(11rem + 2 * var(--ds-card-x))), 1fr));
    align-items: stretch;
}
.ds .ds-kpi-row > .card { height: 100%; }

.ds .ds-kpi { padding: .85rem 1.1rem; }
.ds .ds-kpi-label {
    display: flex; align-items: baseline; gap: .35rem;
    font-size: .95rem; font-weight: 600;
    color: var(--ds-text-faint);
    margin-bottom: .15rem;
}
/* 비율(수익률 등)은 **라벨 옆**이다. 금액 줄에 붙이면 폭이 좁을 때 금액이 먼저 잘린다. */
.ds .ds-kpi-rate { font-size: .85rem; font-weight: 700; }
/* 🔴 **여기가 굵고 큰 것은 위반이 아니다 — 22절(상세 항목 그리드)과 정반대가 정상이다.**
   상세 그리드   항목명 .875rem/600/gray-700  ▸ 데이터 1.0625rem/400/gray-900   ← **이름**이 먼저
   지표 카드     라벨   .95rem /600/gray-600  ▸ 값     1.25rem  /700/(기본)     ← **값**이 먼저
   🔑 **왜 반대인가** — 상세 그리드는 항목이 10~20개라 **먼저 이름을 훑어 자리를 찾는** 화면이고,
      지표 카드는 항목이 3~5개로 정해져 있어 **찾을 것이 없고 답(숫자)만** 보면 되는 화면이다.
      「총 회원 10」에서 「총 회원」이 먼저 걸리면 그 카드는 아무 일도 안 하는 것이다.
   🔥 **22절 기준으로 이 값들을 얇게 고치지 말 것**(반대로 22절을 이 기준으로 굵히지도 말 것).
      두 규격이 「갈렸다」고 보고 한쪽으로 맞추는 일이 이 저장소에서 반복됐다.
      통계 화면·차트도 이쪽 편이다. ❓ 판정 기준은 **「눈이 먼저 하는 일이 찾기인가, 읽기인가」** 하나다. */
.ds .ds-kpi-value {
    text-align: right;
    font-size: 1.25rem; font-weight: 700;
    line-height: 1.3;
    word-break: keep-all;
}
/* 🔢 **값 아래 보조 줄** — 「전체 11」·「이번달 신규 계약 6」·「발송 견적 22」처럼
   **값을 읽는 데 필요한 배경 수치**의 자리다 (2026-09-03 사장님 결정 ㉯ 로 규격에 넣었다).
   🕘 그전에는 이 규격에 **라벨·비율·값 셋뿐**이라 담을 자리가 없어, 관리자 메인이
      `fs-8 text-gray-500 text-end mt-1` 유틸리티 조합을 화면에 직접 적고 있었다.
   🔬 크기·색·여백은 **그 관리자 화면에서 실측한 값 그대로**다(새로 정하지 않았다):
      `fs-8` = `.85rem`(root 16 에서 **13.6px**) · `text-gray-500` → 0-0 절이 `--ds-text-faint` 로
      교정하므로 실측 **`rgb(126,130,153)`** · `mt-1` = `.25rem`(**4px**) · `text-end`.
   🔥 **화면에 `fs-*`·`text-*`·`mt-*` 를 다시 적지 말 것** — 그렇게 흩어져 있어서 같은 자리가 앱마다 갈렸다.
   🔥 **값을 여러 개 담는 자리로 쓰지 말 것** — 이 줄은 **보조**다. 통화별 금액처럼 값이 여럿이면
      `.ds-kpi-value` 를 **여러 줄** 쓴다(그쪽이 값이다).
   ⚠️ 색을 `--ds-text-label`(gray-700)로 올리지 말 것 — 라벨과 같은 층이 되어
      「라벨 ▸ 값 ▸ 보조」 세 층이 두 층으로 무너진다. */
.ds .ds-kpi-sub {
    text-align: right;
    font-size: .85rem;
    color: var(--ds-text-faint);
    margin-top: .25rem;
}
/* 🌑 **차트 판은 카드 면을 그대로 쓴다 — 배경을 칠하지 않는다** (2026-09-18 · 랜드사 1회차 B1-26 · 정본 P61ⓓ).
   🔬 ApexCharts 는 `theme.mode: 'dark'` 이면 svg 에 **`background: rgb(66,66,66)`** 을 인라인으로 박는다 →
      다크 카드(`rgb(30,30,45)`) 위에 **회색 판이 따로 뜬 상자**로 보였다(랜드사 메인 두 차트 · 다섯 폭 전부).
   🔑 인라인 선언이라 `!important` 로만 이긴다. 라이트에서는 원래 투명이라 아무것도 안 바뀐다.
   📌 세 앱 **모든 ApexCharts**(메인·통계)가 한 번에 따른다 — 화면 JS 에서 `chart.background` 를 따로 적지 말 것. */
.apexcharts-canvas > .apexcharts-svg { background: transparent !important; }

/* ══════════════════════════════════════════════════════════════════════════
   17) 프레임 없는 단독 화면 — 세로 가운데 정렬 (2026-07-23 사용자 요청)
   ══════════════════════════════════════════════════════════════════════════
   약관 재동의(`sign/terms`)·계정 잠금 해제(`sign/unlock`) 처럼 **헤더·사이드바 없이
   콘텐츠 카드 한 장만** 있는 화면은 `fullWidthLayout` 의 body 가 그냥 블록이라
   카드가 **화면 맨 위에 붙고 아래가 통째로 비었다.**
   → 이 클래스 하나로 화면 높이를 채우고 가운데에 놓는다.

   ⚠️ **`.ds` 로 스코프하지 않는다** — 이 화면들은 `#kt_content` 자체가 없다.
   ⚠️ `min-height` 다(`height` 가 아니다). 내용이 화면보다 길어지면 **늘어나야 한다** —
      `height:100vh` 로 박으면 약관이 여러 건일 때 카드 아래가 잘린다.
   ⚠️ 좌우 여백을 `padding` 으로 준다. 좁은 화면에서 카드가 화면 끝에 붙으면 안 된다.
   📌 로그인·비번재설정·2차 인증은 **자기 골격**(`flex-root` + `flex-center`)을 이미 갖고 있어
      대상이 아니다. 새 단독 화면을 만들 때만 이 클래스를 쓸 것. */
.ds-standalone-page {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    padding: 2rem 1rem;
}
/* 자식(카드)은 자기 폭 유틸리티(`w-lg-500px` 등)를 그대로 쓰되,
   좁은 화면에서 넘치지 않도록 상한만 건다. */
.ds-standalone-page > * { max-width: 100%; }

/* ══════════════════════════════════════════════════════════════════════════
   17-1) 로그인 계열 화면(비로그인 카드 한 장) — 좁은 화면 (2026-08-18 사용자 실기기 지적)
   ══════════════════════════════════════════════════════════════════════════
   대상은 **같은 골격**을 쓰는 여섯이다 — `sign/signin` · `sign/changePwd` · `sign/resetPwd` ·
   `sign/unlock` · `sign/terms`(약관 재동의) · `sign/signup`(가입).
   🔥 한 화면만 고치지 말 것 — 이 저장소에서 가장 자주 반복된 실패다.

   🐛 세 가지가 한꺼번에 났다. 전부 **375px 보다 좁은 실기기**에서 드러난다.
   ① **언어 선택기가 로고 위에 겹친다.** 선택기는 화면 우상단에 띄워 두는데(absolute) 본문은
      세로 가운데 정렬이라, 카드가 화면보다 길어지면 **위로 밀려 선택기 밑으로 들어간다.**
      🔬 실측(360×780): 선택기 y 0~124.3 · x 157.2~345 / 로고 y 37.5~82.5 · x 52.5~292.5 — 겹쳤다.
      → 좁은 화면에서는 **흐름 안에 놓는다**(줄 하나를 차지하고 우측 정렬). 겹칠 자리가 사라진다.
   ② **좌우 여백이 폭의 절반을 먹었다.** 바깥 `p-10`(2.5rem) + 카드 `p-10`(2.5rem) = **한쪽 75px**.
      🔬 360px 에서 글이 쓸 수 있는 폭이 **195px** 뿐이었다 → 마크업의 유틸리티를
         `p-4 p-md-10` / `p-6 p-md-10` 로 낮춰 **한쪽 37.5px · 쓸 수 있는 폭 285px** 가 됐다.
      🔥 **`md`(768px) 위는 2.5rem 그대로다** — 넓은 화면은 바뀌지 않는다.
   ③ **한글이 글자 단위로 쪼개진다**(「비밀번 / 호」 · 「계정 생성하 / 기」). 원인이 **둘**이다.
      (가) 라벨과 옆 링크가 `flex-stack`(nowrap)으로 한 줄을 다툰다
           — 실측 360px: 둘이 필요로 한 폭 219.4px(59.3 + 160.1) / 가진 폭 195px.
      (나) 기본 `word-break` 는 한글을 **아무 데서나** 끊는다.
      → (가) 안 들어가면 줄을 바꾸게 하고, (나) **단어 단위로만** 끊게 한다.
      🔑 ②만으로도 285px ≥ 219.4px 라 한 줄에 들어간다 — 여기 규칙은 **더 좁은 기기의 안전장치**다.
   🔥 **글자 크기를 줄여서 풀지 말 것** — 크기는 사용자가 3단계 토글(0-0 절)로 정하는 값이라
      여기서 손대면 그 토글과 어긋난다.
   ⚠️ **`.ds` 로 스코프하지 않는다** — 이 화면들에는 `#kt_content` 가 없다(17절과 같은 이유). */

/* 언어 선택기 — **모든 폭에서 흐름(static)으로 선다.** (2026-08-24 사장님 승인)
   🕘 예전에는 조각(`fragments/fullWidthHeader`)에 `position-absolute top-0 end-0 p-10` +
      인라인 `z-index` 로 적혀 있었다.
      🔑 클래스 하나로 옮긴 이유는 **좁은 화면에서 흐름으로 되돌리려면 `!important` 가 필요**했기
         때문이다(부트스트랩 `.position-absolute` 가 `!important` 다). 규격을 여기로 가져오면 그럴 일이 없다.
      🔥 마크업에 `position-absolute` 를 되살리지 말 것 — 되살리는 순간 이 규칙이 조용히 진다.

   🕘 **2026-08-24 에 「`.ds-auth-page` 안에서만 흐름으로 돌린다」는 제한을 풀었다.**
      🔑 그 제한의 근거는 *"같은 조각을 쓰는 약관 팝업은 자기 높이 사슬(`Term.css` 의 `height:100vh`
         + flex)이 있어 줄이 하나 늘면 카드 아래가 잘린다"* 였다. **관리자 앱에서는 그 근거가 없어졌다** —
         🔬 실측(2026-08-24, `term/termDetail`): 흐름으로 돌려도 카드 아래가 **잘리지 않는다**
            (문서 1049 / 창 942 — 세로 스크롤로 닿는다). 관리자 약관 화면에는 `100vh` 사슬 자체가 없다.
      🐛 풀어야 했던 이유: absolute 로 두면 본문이 세로 가운데 정렬이라 **좁은 화면에서 로고 위로 겹쳤다.**
         🔬 실측(375px): 언어 버튼 y 0~60 / 로고 y 102~ → 흐름으로 돌린 뒤 **안 겹침**.

   🔴 **주의 — 랜드사·여행사 약관 팝업(`term/termDetail`)에는 그 높이 사슬이 아직 살아 있다.**
      그 두 앱의 `Term.css` 는 `.term-page { height:100vh; overflow:hidden }` 사슬을 쓴다.
      🔑 **잘리지는 않는다** — `.term-shell`·`.term-card`·`.term-content-area` 가 전부
         `flex:1 1 auto; min-height:0` 이라 늘어난 줄만큼 **본문 스크롤 영역이 줄어 흡수**한다.
      ⚠️ 다만 그 두 앱 `Term.css` 의 `@media (max-width:767.98px)` 에
         `.term-page .term-shell { padding: 3.25rem .75rem .75rem }` 이 **절대배치였던 언어 버튼 자리를
         비워 두려고** 넣은 값이라, 흐름으로 돌린 지금은 **그만큼이 빈 공간으로 남는다.**
         🔥 그 값을 손보려면 **두 앱을 함께** 고치고 실물로 다시 잴 것(2026-08-24 시점 미조치).

   🔑 넓은 화면 좌우 여백(2.5rem)은 **예전 `p-10` 그대로**다 — 세로만 1rem 으로 낮춘다
      (흐름으로 서면서 줄 높이가 그대로 화면을 먹기 때문이다). */
.ds-auth-lang { position: static !important; display: flex; justify-content: flex-end; padding: 1rem 2.5rem; z-index: auto; }
/* 🕘 2026-09-07: 문턱을 **575.98 → 767.98** 로 합쳤다(사장님 확정 「구간 수를 줄인다」 — 0-4 절 `--ds-card-x` 주석). */
@media (max-width: 767.98px) { .ds-auth-lang { padding: .75rem 1rem; } }
@media (max-width: 767.98px) {
    /* 🕘 **2026-08-24: 이 블록에서 `position`·`display`·`justify-content` 를 지웠다** —
       바로 위 기본 규칙이 이미 모든 폭에서 같은 값을 주므로 **완전한 중복**이었다.
       🔑 **남긴 것은 `padding` 하나뿐**이고, 그것이 이 블록이 지금도 하는 일의 전부다:
          로그인 계열 화면(`.ds-auth-page`)은 2026-08-18 에 실기기로 검증해 **더 좁은 여백**으로
          맞춰 둔 값이라 그대로 지킨다(카드가 화면 폭을 최대한 쓰게 하려던 그때의 처방이다).
       ⚠️ **특이도로 이긴다** — `.ds-auth-page .ds-auth-lang`(0,2,0)이 위의 `.ds-auth-lang`(0,1,0)을
          이기므로, 로그인 계열 화면은 ≤767.98px 에서 **위의 `.75rem 1rem` 이 아니라 이 값**을 쓴다.
          🔥 「왜 좁은 화면에서 새 여백이 안 먹지」로 이 블록을 지우지 말 것 — **의도된 것**이다.
          🔬 그래서 2026-08-24 변경 뒤에도 로그인·비밀번호 재설정·비밀번호 변경 화면의
             좁은 화면 모습은 **전과 같다**. */
    .ds-auth-page .ds-auth-lang {
        padding: .5rem .75rem;
    }
}

/* 카드 안 글자는 **단어 단위로만** 끊는다(위 ③-나). */
.ds-auth-card { word-break: keep-all; }
/* 🔗 **링크 결과 안내 문장은 줄 길이를 고르게** 나눈다 (2026-09-17 여행사 1회차 A4-13·A5-03).
   🔬 비밀번호 변경·잠금 해제의 「만료·사용된 링크」 안내가 좁은 카드에서 `…has already been / used.` 처럼
      **끝줄에 한 낱말만** 남았다. 문장 단위 줄바꿈(`Fmt.br`)은 그대로 두고 **한 문장 안의 줄만** 고르게 한다.
   📌 대상은 표식 클래스 `.ds-auth-msg` 하나다(세 앱 `sign/changePwd`·`sign/unlock`). */
.ds-auth-card .ds-auth-msg { text-wrap: balance; }
/* 라벨과 그 옆 링크(「비밀번호」 / 「비밀번호를 잊으셨나요?」)가 한 줄을 다투면 **줄을 바꾼다**(③-가).
   ⚠️ Metronic `.flex-stack` 은 `flex-wrap` 을 정하지 않아 기본값 `nowrap` 이다 —
      (0,2,0) 으로 맞받으므로 `!important` 가 필요 없다. */
.ds-auth-card .flex-stack { flex-wrap: wrap; }
/* 🏷 **약관 줄은 그 예외다 — 제목이 길어도 「필수」 배지는 오른쪽에 붙어 있어야 한다** (2026-09-22 사장님 지적).
   🔬 무엇이 문제였나 — 약관 재동의 화면(`sign/terms.html`)의 줄도 `.ds-auth-card` 안의 `.flex-stack` 이라
      바로 위 규칙을 물려받는다. 제목 묶음이 줄어들지 못한 채(`min-width: auto`) 폭을 다 먹으면
      **배지가 아래 줄로 떨어져 왼쪽에 홀로 선다.** 실측(2026-09-22 · 랜드사 화면):
        · 영문 768·1200px — 「Electronic Financial Transactions…」 줄에서 배지가 **408px·368px 왼쪽**으로 밀림
        · **360px 에서는 국문도 5줄 중 3줄**이 같은 모양이었다(216px) — 영문만의 문제가 아니다.
   🔑 바로 위 규칙은 **로그인 화면의 「비밀번호」 라벨과 「비밀번호를 잊으셨나요?」 링크**를 위한 것이다
      (한 줄을 다투면 줄을 바꾼다). 🔥 **그 규칙을 지우거나 좁히지 말 것** — 여기서 필요한 것은 예외 하나다.
   🔑 처방은 세 줄이다: 줄바꿈을 끄고 · 제목 묶음에 **`min-width: 0`** 을 줘서 자기 칸 안에서 접히게 하고 ·
      배지는 **안 줄어들게** 한다. 🔥 **`min-width: 0` 을 빼지 말 것** — 이 저장소가 반복해 밟은 함정이다
      (`screen-layout.md` ⑥ · 2026-09-22 P91 · `Sign.css` 의 `.term-head-wrap` 은 가입 화면에서 같은 처방이다).
   ⚠️ **`.flex-stack` 자체를 고치지 말 것** — 세 앱 수십 곳이 쓰는 Metronic 유틸리티다. 표식 클래스로 좁힌다.
   📌 여백은 `gap` 이 준다(`ms-*` 로 주면 줄이 바뀌었을 때 왼쪽이 어색해진다 — `Sign.css` 와 같은 이유).
   📌 **이 앱에는 재동의 화면이 없어 닿는 자리가 0곳이다. 안 쓴다고 지우지 말 것** — 세 앱의
      이 파일은 규칙 단위로 같아야 한다(대조법은 워크스페이스 `CLAUDE.md`). */
.ds-auth-card .ds-term-row { flex-wrap: nowrap; gap: 1rem; }
.ds-auth-card .ds-term-row > .d-flex.flex-column { min-width: 0; }
.ds-auth-card .ds-term-row > .badge { flex-shrink: 0; }

/* 🪟 **다크에서 인증 카드가 배경과 구분되지 않는다** (2026-08-18 사용자 지적 — 폰 실기기)

   > *"다크모드에서 로그인 카드의 경계가 안 보여 입력칸들이 허공에 떠 있는 것처럼 보인다."*

   🔬 실측(관리자 로그인, 다크): 카드 배경 `rgb(30,30,45)` · 페이지 배경 `rgb(30,30,45)` — **같은 색**이라
      대비 **1.000** 이다. 라이트도 `#FFFFFF` 위 `#FFFFFF` 로 **똑같이 1.000** 인데 문제가 안 되는 이유는
      `shadow-sm`(`rgba(0,0,0,.05)`)이 **흰 바탕 위에서는 보이고** 검정 바탕 위에서는 안 보이기 때문이다.
   🔑 그래서 10절의 **모달·드로워와 완전히 같은 처방**을 쓴다 — `--ds-panel-edge`(라이트 투명 / 다크 gray-400).
      🔥 **여기만 다른 방법으로 고치지 말 것.** 「판과 배경이 둘 다 검정이라 그림자가 죽는다」는 한 가지 원인이고
         처방도 한 가지여야 한다(값을 새로 정하면 모달과 로그인 카드의 테두리가 갈린다).
   ⚠️ **라이트는 렌더 결과가 그대로다** — 링이 `transparent` 라 뒤에 이어 붙인 `shadow-sm` 기본값만 남는다.
      📌 뒤 값은 Metronic `.shadow-sm` 의 실측 기본값 그대로다(`rgba(0,0,0,.05) 0 1.2px 12px 3px`).
   ⚠️ **`border` 로 주지 말 것** — 라이트에서 투명 1px 이 자리를 차지해 카드가 2px 커진다(10절과 같은 이유).
   ⚠️ **선택자에 `.shadow-sm` 을 함께 물린 이유** — 같은 `ds-auth-card` 를 쓰지만 **카드가 아닌 화면**이 있다
      (DMC·Agency 회원가입은 스테퍼 안이라 `bg-body rounded shadow-sm` 이 없다). 거기까지 링을 그리면
      **없던 상자가 생긴다.** 🔬 실측: 세 앱 16개 파일 중 **14개가 카드**(로그인·비밀번호 변경·재설정·
      잠금 해제·약관), **2개(회원가입)가 카드 아님**이다.
   ⚠️ Bootstrap `.shadow-sm` 이 `!important` 라 여기도 `!important` 가 필요하다. */
.ds-auth-card.shadow-sm {
    box-shadow: 0 0 0 1px var(--ds-panel-edge), 0 1.2px 12px 3px rgba(0, 0, 0, .05) !important;
}
/* 📏 **인증 카드는 좁은 폭에서도 넓은 화면의 폭을 넘지 않는다** (2026-09-17 여행사 1회차 A3-03·A4-13)
   🔬 `w-lg-500px` 는 **992px 이상에서만** 폭을 준다 → 그 아래는 카드가 내용 폭(max-content)대로 늘어
      768px 에서 비밀번호 재설정 **673px** · 비밀번호 변경 **660px**(992px 에서는 500px) — 좁은 화면이 더 넓었다.
      로그인은 문구가 짧아 392px 이라 드러나지 않았다.
   ✅ 같은 값을 **상한으로만** 준다(`max-width`). 내용이 짧은 카드는 지금처럼 내용 폭이다.
   ⚠️ **px 인 이유** — 짝이 되는 Metronic `w-lg-500px`·`w-lg-600px` 가 px 다. rem 으로 주면 글자 「아주 크게」에서
      768px 상한(625)이 992px 폭(500)보다 커져 **같은 뒤집힘이 돌아온다.** 🔥 클래스와 값을 짝으로 고칠 것. */
@media (max-width: 991.98px) {
    .ds-auth-card.w-lg-500px { max-width: 500px; }
    .ds-auth-card.w-lg-600px { max-width: 600px; }
}

/* ══════════════════════════════════════════════════════════════════════════
   18) 상단 바 아바타 (2026-07-23 사용자 지적)
   ══════════════════════════════════════════════════════════════════════════
   🕘 예전엔 `symbol-30px symbol-md-40px` 였다. 옆 아이콘 버튼도 40px 상자지만
      그 안의 글리프는 `svg-icon-1`(24px)이라, **아바타만 40px 원판으로 꽉 차** 혼자 튀었다.
   → 글리프(24px)와 버튼 상자(40px) 사이인 **32px**(모바일 28px)로 맞춘다.
   ⚠️ Metronic 의 `symbol-*` 은 20/25/30/35/40… 눈금이라 32px 이 없다. 그래서 전용 클래스다.
      `symbol-md-35px` 로 되돌리지 말 것 — 눈금에 맞추려다 다시 어긋난다.
   ⚠️ `.symbol` 은 자식 `img` 의 크기를 **부모에서** 정한다 → width/height 를 함께 준다. */
.ds-topbar-avatar { width: 28px; height: 28px; }
.ds-topbar-avatar > img {
    width: 100%; height: 100%;
    object-fit: cover;
    border-radius: .475rem;
}
@media (min-width: 768px) {
    .ds-topbar-avatar { width: 32px; height: 32px; }
}

/* ══════════════════════════════════════════════════════════════════════════
   19) 상단 바 아이콘 색 (2026-07-23 사용자 지적: "다크모드에서 아이콘들이 잘 보이지 않아")
   ══════════════════════════════════════════════════════════════════════════
   원인이 **둘**이었고, 하나만 고치면 여전히 안 보인다.

   ① 아이콘 SVG 가 색을 **검정으로 박고** 있었다(`fill="#000000"`).
      → 템플릿 40곳은 `fill="currentColor"` 로 치환했고, `iconUtil.getIcon()` 이 읽어 오는
        duotone 646개는 **읽는 순간** 치환한다(`IconUtil.BLACK_FILL`).
   ② 그러고 나니 Metronic 의 `.svg-icon` 색(다크 `#565674`)을 따라가는데 **대비가 2.33** 이다.
      우리 등급표 기준(3:1 미만은 텍스트 금지)에 미달이라 여전히 흐리다.
      → `--ds-text-label`(gray-700, 다크 대비 **5.34**)로 올린다.

   ⚠️ **`.ds` 로 스코프하지 않는다** — 헤더·사이드바는 `#kt_content` 바깥이라 `.ds` 가 안 닿는다.
   ⚠️ 색을 hex 로 박지 말 것. 토큰은 테마별로 이미 반전돼 있어 라이트에서는 진한 회색이 된다.
   ⚠️ **`fill` 이 아니라 `color` 를 준다.** SVG 가 `currentColor` 를 쓰므로 색 하나로 끝난다 —
      `fill` 을 직접 주면 `fill="none"`(외곽선용 path)까지 칠해져 아이콘이 뭉개진다. */
#kt_header .svg-icon {
    color: var(--ds-text-label);
}
/* 🔥 **사이드바는 라이트 테마에서도 배경이 어둡다**(실측 `#1B1B28`). 그래서 테마를 따라 뒤집히는
      `--ds-text-label` 을 그대로 쓰면 **라이트에서 대비가 2.73 으로 떨어진다**(어두운 바탕에 어두운 글자).
      → 사이드바 아이콘만은 **항상 다크용 계조**를 쓴다. 이건 `--ds-surface` 와 같은 종류의 예외다
        (Metronic 이 그 값을 변수로 노출하지 않아 담아 갈 토큰이 없다).
   ⚠️ 이 값을 `--ds-text-label` 로 되돌리지 말 것 — 다크만 보면 맞는 것처럼 보인다. */
:root { --ds-aside-icon: #92929F; }   /* = 다크 테마의 gray-700. 사이드바 바탕 대비 5.34 */
#kt_aside .menu-icon .svg-icon {
    color: var(--ds-aside-icon);
}
/* 🔥 `color` 만으로는 안 된다 — Metronic 이 `.svg-icon svg [fill]:not(…)` 로 **fill 을 직접** 지정하고,
      CSS 선언은 `fill="currentColor"` **표시 속성(presentation attribute)을 항상 이긴다.**
      그래서 span 의 color 는 바뀌는데 아이콘 색은 그대로였다(실측: color 146,146,159 / fill 86,86,116).
   ⚠️ 대상은 **`[fill="currentColor"]` 뿐**이다. 그냥 `[fill]` 에 걸면 외곽선용 `fill="none"` 까지
      칠해져 아이콘이 검은 덩어리로 뭉개진다. */
#kt_header .svg-icon svg [fill="currentColor"],
#kt_aside .menu-icon .svg-icon svg [fill="currentColor"] {
    fill: currentColor;
}
/* 🔴 **활성 메뉴의 아이콘은 글자와 같은 색이어야 한다 — 이 세 줄을 지우지 말 것**
   (2026-08-25 사장님 지적 · 구현 #49: *"사이드 바에 현재 메뉴 활성화 되어 있는데 아이콘은 왜 활성화가
   안되어 있을까?"*)

   🕘 **바로 위에 「활성·호버는 Metronic 에 맡긴다」고 적혀 있었는데 사실이 아니었다.**
   🔬 위 규칙 `#kt_aside .menu-icon .svg-icon` 은 **id 를 물고 있어 특이도가 (1,2,0)** 이고,
      Metronic 의 활성 아이콘 규칙은 **클래스뿐(0,6,0)** 이라 **우리 쪽이 조용히 이긴다.**
      그래서 활성 메뉴는 **글자만 흰색이 되고 아이콘은 회색(`#92929F`)으로 남았다**
      (실측: 링크 `rgb(255,255,255)` / 아이콘 `rgb(146,146,159)`).
   🔑 **색을 새로 정하지 않고 `inherit` 로 되돌린다** — 그래야 활성·호버의 색을 Metronic 이 정한다는
      원래 의도가 실제로 성립한다(값을 박으면 테마·상태가 늘 때마다 또 갈린다).
   🔑 `.here` 는 **자식이 열려 있는 부모 아코디언**이다(그 자신에게 `.active` 가 없는 경우가 있다).
   ⚠️ 특이도가 (1,4,0) 이라 위 (1,2,0) 을 이긴다 — 선택자에서 `#kt_aside` 를 빼지 말 것(지면 아무 일도 안 난다). */
#kt_aside .menu-link.active .menu-icon .svg-icon,
#kt_aside .menu-item.here > .menu-link .menu-icon .svg-icon,
#kt_aside .menu-link:hover .menu-icon .svg-icon {
    color: inherit;
}


/* ══════════════════════════════════════════════════════════════════════
   🎨 `badge-light-*` 대비 교정 (2026-08-05, 대기목록 #64)

   🔥 Metronic 기본값은 **읽을 수 없는 수준**이었다(라이트 실측):
        warning 1.47 · success 1.92 · primary 2.75 · danger 3.44   (AA 기준 4.5)
      상태 배지는 거래·정산 화면에서 **상태를 알려 주는 유일한 표식**이라
      "장식이 흐리다" 가 아니라 **정보가 안 읽힌다** 는 문제다.

   🔑 **배경은 건드리지 않는다.** 옅은 배경이 이 배지의 정체성이고, 배경을 바꾸면
      카드·표 안에서 배지가 덩어리로 튄다. **글자만** 어둡게(다크에선 밝게) 한다.
   🔑 **색조(hue)와 채도는 유지한다.** HSL 의 명도(L)만 옮겨 4.6 을 처음 넘는 지점을 골랐다 —
      경고가 갑자기 회색이 되면 색으로 전달하던 의미가 사라진다.

   🔑 **라이트와 다크는 교정 방향이 반대다**(어둡게 vs 밝게). 그래서 규칙을 두 벌 쓰지 않고
      **토큰 하나에 두 값**을 담는다 — 이 파일의 원칙 그대로다("다크 계조는 값이 아니라 이름으로").
      규칙은 한 벌이라 나중에 배지 규칙이 늘어도 테마 분기가 번지지 않는다.
      `data-theme` 은 `fragments/head.html` 이 **인라인 스크립트로 즉시** 넣으므로 깜빡임이 없다.
   ⚠️ **이미 충족하는 변형은 그 테마의 기본값을 그대로 토큰에 담았다** — 값을 옮기면
      팔레트 균형만 흐트러진다(다크 primary 4.62 · success 5.19 · warning 6.74 / 라이트 info 5.59).
   📌 여기서 고치는 것은 **`badge-light-*` 뿐**이다. 흰 글자 위 `btn-primary` 파랑(2.93)은
      버튼·배지·링크가 다 걸리는 별개 사안이라 함께 판단해야 한다.
   ══════════════════════════════════════════════════════════════════════ */

/* 라이트 — 어둡게 교정한 값(또는 이미 충족하는 기본값) */
:root {
    --ds-badge-primary-fg: #0076B9;   /* 2.75 → 4.63 */
    --ds-badge-success-fg: #25814F;   /* 1.92 → 4.62 */
    --ds-badge-warning-fg: #8B6D00;   /* 1.47 → 4.60 */
    --ds-badge-danger-fg:  #DE1043;   /* 3.44 → 4.62 */
    --ds-badge-info-fg:    #7239EA;   /* 5.59 — 기본값 유지 */
}

/* 다크 — 밝게 교정한 값(또는 이미 충족하는 기본값) */
:root[data-theme="dark"] {
    --ds-badge-primary-fg: #3699FF;   /* 4.62 — 기본값 유지 */
    --ds-badge-success-fg: #0BB783;   /* 5.19 — 기본값 유지 */
    --ds-badge-warning-fg: #FFA800;   /* 6.74 — 기본값 유지 */
    --ds-badge-danger-fg:  #F75F6F;   /* 4.17 → 4.60 */
    --ds-badge-info-fg:    #A77DFD;   /* 3.06 → 4.63 */
}

.badge-light-primary { color: var(--ds-badge-primary-fg); }
.badge-light-success { color: var(--ds-badge-success-fg); }
.badge-light-warning { color: var(--ds-badge-warning-fg); }
.badge-light-danger  { color: var(--ds-badge-danger-fg); }
.badge-light-info    { color: var(--ds-badge-info-fg); }
/* `dark` 변형은 라이트 14.94 · 다크 13.79 라 손대지 않는다 */

/* ── `btn-light-*` 도 같은 토큰을 쓴다 (2026-08-09)
   🔑 배지와 **배경색이 완전히 같다**(실측: 5개 변형 전부 일치) — 그래서 배지용으로 이미
      AA 를 맞춰 둔 토큰을 그대로 재사용한다. 값을 따로 만들면 같은 색이 둘로 갈린다.
   🔥 라벨이 안 읽히면 배지보다 나쁘다 — 배지는 상태를 알려 줄 뿐이지만 버튼은
      **눌러서 무슨 일이 일어나는지**를 그 글자로만 알린다(실측 warning 1.47 · success 1.92).
   ⚠️ **hover·active 는 건드리지 않는다.** Metronic 이 그때 solid 배경 + 흰 글자로 바꾸는데
      그 규칙은 `.btn.btn-light-*:hover`(0,3,0) 라 여기(0,2,0)를 이긴다 — `!important` 를
      붙이면 그 뒤집힘까지 덮어써 **hover 에서 글자가 사라진다.**
   📌 흰 글자 위 solid `btn-primary` 파랑(2.93)은 **여전히 별개 사안**이다(위 주석 그대로).
      여기는 '색 위의 흰 글자' 가 아니라 '연한 배경 위의 색 글자' 라 배지와 같은 축이다. */
.btn.btn-light-primary { color: var(--ds-badge-primary-fg); }
.btn.btn-light-success { color: var(--ds-badge-success-fg); }
.btn.btn-light-warning { color: var(--ds-badge-warning-fg); }
.btn.btn-light-danger  { color: var(--ds-badge-danger-fg); }
.btn.btn-light-info    { color: var(--ds-badge-info-fg); }

/* ── 🪟 **`btn-light-*` 는 다크에서 「배경이 없는 것처럼」 보인다 → 가장자리를 준다** (2026-08-18 사용자 지적 — 폰 실기기)

   > *"회원 상세의 「가입 승인」·「가입 반려」 중 반려 쪽 배경이 거의 안 보인다(글자만 떠 보인다)."*

   🔬 **실측(회원 상세, 다크. 카드 표면 `rgb(30,30,45)` 대비):**
        btn-light          `#1B1B29`  **1.035**  ← 최악. **표면보다 오히려 어둡다**(L* −1.5)
        btn-light-danger   `#3A2434`  **1.159**  ← 사용자가 지적한 「가입 반려」
        btn-light-info     `#2F264F`  1.182
        btn-light-dark     `#2B2B40`  1.190
        btn-light-primary  `#212E48`  **1.211**  ← 같은 자리의 「가입 승인」
        btn-light-success  `#1C3238`  1.222
        btn-light-warning  `#392F28`  1.260
      🔑 **승인과 반려가 다르게 보인 이유**는 danger 가 primary 보다 **밝기 차이가 작아서**다
         (L* +5.6 vs +7.1). 다크 화면의 L*≈12~20 구간에서는 **채도가 거의 안 읽히고 밝기만 남는다** —
         danger 는 ΔE 로는 primary 보다 큰데도 더 안 보인다.
      📌 라이트도 사실 같은 처지다(흰 카드 대비 primary 1.057 · danger 1.068). 다만 **그림자가 없는 대신
         화면 전체가 밝아** 눈이 견딘다.

   🔥 **배경을 밝히는 방식은 버렸다 — AA 를 깬다.** 🔬 배경에 자기 색을 섞어 밝히면 글자와 배경이 함께
      가까워져 **위 「badge-light-* 대비 교정」 절이 힘들게 맞춰 둔 글자 대비 4.6 이 무너진다**:
      12% 섞으면 4.60 → **3.85**, 18% 면 **3.44** 다(전 변형 같은 경향). 게다가 `badge-light-*` 와
      **배경색이 같다**는 전제(바로 위 절)가 깨져 같은 색이 둘로 갈린다. 🔥 **되살리지 말 것.**

   🔑 **그래서 「판의 경계」와 같은 처방을 쓴다**(10절 `--ds-panel-edge` · 로그인 카드와 같은 계열).
      다만 여기서는 **테마별로 값을 가를 필요가 없다** — 버튼은 **자기 의미색(`currentColor`)** 을 이미
      들고 있고 그 색은 라이트/다크에서 이미 뒤집혀 있다(위 두 `:root` 블록). 🔑 **한 값으로 양쪽이 산다.**
      🔬 30% 로 정한 근거: **가장 약한 danger 의 링이 다크에서 표면 대비 1.834** 로,
         사용자가 승인한 모달 테두리(`--ds-panel-edge` = gray-400, **판 대비 1.83**)와 **같은 급**이다.
         나머지는 1.90~3.13 이다(라이트 1.47~2.13).
   🔑 **`box-shadow` 를 쓰는 이유** — 실측상 이 버튼들의 `border-width` 는 **0px** 이라(Metronic 이 죽여 놨다)
      `border-color` 만 줘서는 **아무것도 안 그려지고**, `border-width` 를 살리면 버튼이 2px 커져 줄이 밀린다.
      `inset` 이라 **버튼 크기가 한 픽셀도 안 변한다**(실측: 39.8×45.88 그대로).
   ⚠️ **`.btn-icon` 은 제외한다** — 4-2 절이 그 배경을 일부러 지웠는데 링을 그리면 **지운 상자가 되살아난다.**
   ⚠️ **hover·active 는 제외한다** — 그때 Metronic 이 solid 배경 + 흰 글자로 바꾸므로 경계가 이미 뚜렷하다
      (바로 위 절이 hover 를 건드리지 않는 것과 같은 판단).
   🔥 **`!important` 를 빼지 말 것 — 빼면 조용히 아무 일도 안 일어난다.**
      🔬 벤더 번들에 `.btn:not(.btn-shadow):not(.shadow):not(.shadow-sm):not(.shadow-lg){box-shadow:none !important}`
         가 있다. 특이도는 (0,5,0) 으로 여기(0,6,0)보다 **낮은데도** `!important` 라서 이긴다 —
         2026-08-18 에 실제로 이것에 걸려 규칙을 넣고도 `box-shadow` 가 `none` 으로 계산됐다.
      ⚠️ 그래서 **hover 는 `:not(:hover)` 로 빼 두는 것이 곧 「벤더 기본으로 되돌리는 장치」**다
         (hover 에서는 벤더의 `none !important` 가 그대로 산다). */
.btn.btn-light:not(.btn-icon):not(:hover):not(.active):not(.show),
.btn.btn-light-primary:not(.btn-icon):not(:hover):not(.active):not(.show),
.btn.btn-light-success:not(.btn-icon):not(:hover):not(.active):not(.show),
.btn.btn-light-warning:not(.btn-icon):not(:hover):not(.active):not(.show),
.btn.btn-light-danger:not(.btn-icon):not(:hover):not(.active):not(.show),
.btn.btn-light-info:not(.btn-icon):not(:hover):not(.active):not(.show),
.btn.btn-light-dark:not(.btn-icon):not(:hover):not(.active):not(.show) {
    box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 30%, transparent) !important;
}


/* ── 🔘 **표 안의 행위 버튼은 「채움」이 아니라 「테두리」다** (2026-09-01 · 항목 79 ⑩)

   > 사장님 지적: *"「동의 철회」가 배지처럼 보인다."*

   🔬 **원인은 그 화면이 아니라 규격이다.** 바로 위 두 절이 `btn-light-*` 와 `ds-badge-*` 에
      **같은 처방**(연한 채움 + `currentColor` 30% 안쪽 링)을 줬다 → 표 한 줄에 배지와 버튼이 나란히 서면
      **바탕·테두리·글자색이 모두 같은 계열**이라 구별할 단서가 크기밖에 안 남는다.
      📌 이 저장소에 같은 지적 이력이 있다(구현 #58: *"버튼이 상태 배지로 읽혔다 —
         **배지는 상태, 버튼은 행위**라 같아 보이면 안 된다"*).

   🔑 **가르는 축은 「면(面)」이다** — 배지는 **채운 알약**, 버튼은 **빈 상자 + 또렷한 테두리**.
      색(의미)은 그대로 두므로 「무엇에 대한 행위인가」는 잃지 않는다.
      🔥 **화면마다 클래스를 갈아 끼우지 말 것**(`btn-light-danger` → `btn-outline-danger` 식).
         그러면 표마다 다른 클래스가 쓰이고, 다음에 만드는 표가 또 배지처럼 보인다.
         선택자를 **성격으로** 쓴다 — 「표 본문 칸 안에 있는 버튼」(`table tbody td > .btn`).

   ⚠️ **`.btn-icon` 은 제외한다** — 4-2 절이 그 배경을 일부러 지웠고, 아이콘 하나짜리는 애초에
      배지로 읽히지 않는다(테두리를 그리면 지운 상자가 되살아난다).
   ⚠️ **hover·active 도 제외한다** — 그때 Metronic 이 solid 배경 + 흰 글자로 바꿔 경계가 이미 뚜렷하다
      (바로 위 두 절과 같은 판단이고, 벤더의 `box-shadow:none !important` 로 자연히 되돌아간다).
   🔥 **`!important` 를 빼지 말 것** — 벤더에
      `.btn:not(.btn-shadow):not(.shadow):not(.shadow-sm):not(.shadow-lg){box-shadow:none !important}`
      가 있어 특이도만으로는 진다(2026-08-18 에 바로 위 절이 실제로 걸렸다).
   🔬 링을 30% → **55%** 로 올린 이유: 바탕을 지우면 30% 링은 판 위에 거의 안 보인다.
      55% 는 글자색(의미색)의 절반 남짓이라 **글자보다 약하고 판보다 진하다.**
      ⚠️ `currentColor` 라 **다크용 값을 따로 가를 필요가 없다**(위 두 절과 같은 이유). */
:is(.ds, .quote-content) table tbody td > .btn:not(.btn-icon):not(:hover):not(.active):not(.show) {
    background-color: transparent !important;
    box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 55%, transparent) !important;
}


/* ── 🪟 **배지에도 같은 가장자리를 준다 — 「배경과 같은 색이지만 배경보다 진한 테두리」** (2026-08-26 사용자 지시)

   > *"배지들은 색상에 따라 경계가 잘 보이지 않는 경우가 많네
   >  그래서 배경과 같은 색이지만 배경보다는 진한 테두리가 있었으면 좋겠어."*

   🔬 **실측(업체 목록 `/admin/company/list`. 배지 바탕 대 카드 표면):**
        라이트 `ds-badge-ok` **1.048** · `ds-badge-info` 1.057 · `ds-badge-wait` 1.065 · `ds-badge-stop` 1.068
        다크   `ds-badge-mute` **1.035** · `ds-badge-stop` 1.159 · `ds-badge-info` 1.211 · `ds-badge-wait` 1.260
      🔑 **1.0 은 「면이 없다」는 뜻이다.** 색만 어렴풋이 다를 뿐 경계가 눈에 잡히지 않는다 —
         사용자가 지적한 것이 정확히 이 값이고, 그래서 **바탕은 건드리지 않고 테두리만** 더한다.

   🔑 **처방은 바로 위 `btn-light-*` 절과 같다** — 같은 증상을 같은 방법으로 푼다.
      **글자색이 곧 「같은 계열의 더 진한 색」**이라 사용자의 말이 그대로 성립하고,
      `currentColor` 라서 **다크용 값을 따로 가를 필요가 없다**(위 두 `:root` 블록이 이미 뒤집어 놨다).
   🔥 **바탕을 진하게 섞는 방식은 쓰지 않는다** — 바로 위 절이 재 보고 버린 길이다
      (12% 섞으면 글자 대비 4.60 → **3.85**). 테두리는 글자·바탕 어느 쪽도 건드리지 않는다.
      🔬 실측으로 확인했다: 적용 뒤에도 글자 대비가 **라이트·다크 모두 4.60~4.63 그대로**다.

   🔴 **`border` 가 아니라 `inset box-shadow` 인 이유 — 상자 크기를 한 픽셀도 바꾸면 안 된다.**
      🔬 이 저장소는 배지 높이를 `--ds-badge-h`(1.7rem)로 못 박고 **그 값에 맞춘 자리가 여럿**이다
         (0-3 절 「인접 규칙」의 *배지 옆 버튼* 과, 그 토큰을 그대로 쓰는 아이콘 버튼들).
         `border:1px` 은 상자를 **2px 키워** 그 정렬을 전부 어긋낸다.
         🔬 실측: `inset` 은 적용 전후 높이가 **27.19px · 29.42px 그대로**다.
      📌 Metronic 이 `.badge` 의 `border-width` 를 **0** 으로 죽여 놔서 `border-color` 만 줘서는
         **아무것도 안 그려진다** — 위 버튼 절과 같은 사정이다.
   ⚠️ **`!important` 는 「지금 필요해서」가 아니라 「보험으로」 붙였다 — 재 보고 적는다.**
      🔬 실측(2026-08-26): `.badge` 를 잡으면서 `box-shadow: none` 을 강제하는 벤더 규칙은 **0개**이고,
         `!important` 없이도 링이 그대로 그려졌다. **`.btn` 과는 달랐다** — 그쪽은 실제로 졌다
         (`.btn:not(.btn-shadow)…{box-shadow:none !important}` 가 있다).
      🔑 그래도 붙여 둔다: 화면별 CSS(`menuLevel/*.css`)나 번들 갱신이 나중에 `.badge` 의 `box-shadow` 를
         덮으면 **조용히 아무 일도 안 일어나는** 쪽으로 무너지기 때문이다(그때 원인을 찾기가 가장 어렵다).
      🔥 **오늘은 빼도 동작한다 — 그래서 「빼도 되네」 하고 지우지 말 것.**

   ── 🔴 **거는 대상은 「클래스 이름」이 아니라 「글자가 바탕보다 진한가」로 가른다** ──
   🔥 **`badge-` 로 시작한다고 싸잡아 걸지 말 것.** solid 배지(`badge-primary` 등)는
      **진한 바탕 + 흰 글자**라 `currentColor` 가 흰색이다 → 링이 바탕보다 **밝아진다.**
      사용자가 요구한 것은 「배경보다 **진한**」 테두리이므로 **정반대**가 된다.
      🔬 게다가 필요도 없다 — 실측 바탕 대 표면이 라이트에서 `badge-warning` 1.565 · `badge-success` 2.012 ·
         `badge-primary` 2.903 · `badge-danger` 3.676 · `badge-info` 6.019 · `badge-dark` **16.787** 로
         **이미 면이 뚜렷하다**(위 1.05 와 비교). → **이 여섯 변형은 뺀다.**
   🔑 **반대로 `badge-secondary` 는 이름만 solid 이고 성격은 연한 배지다** — 실측 바탕 `#E4E6EF` +
      글자 `#3F4254`(대비 7.96)로 **글자가 더 진하다.** 바탕 대 표면도 **1.245**(라이트)·1.318(다크)로
      약한 축이어서 링이 실제로 도움이 된다. → **건다.**
      📌 solid 계열은 화면 마크업에 **0곳**이고 전부 공통코드 의미색(`SEMANTIC_TYPE`)으로만 나온다 —
         `SECONDARY` 는 정산 조정 사유 ETC · 수수료 부담 NOT_WAIVED · 문의 유형 ETC 3개 코드가 쓴다.
   ⚠️ **`badge-light-secondary` 는 뺀다 — 바탕이 아예 없다**(실측 `rgba(0, 0, 0, 0)`).
      「배경과 같은 색」이라는 전제부터 성립하지 않고, 이 저장소는 그 클래스를
      *"흰 카드 위에서 거의 안 보인다"* 며 이미 쓰지 말라고 적어 두었다.

   ── ⚠️ **중립 배지는 20% 로 낮춘다(의미색은 30%)** ──
   🔬 중립(`ds-badge-mute`·`badge-light`·`badge-light-dark`·`badge-secondary`)은 글자가 의미색이 아니라
      **무채색**(라이트에선 거의 검정, 다크에선 거의 흰색)이라 **같은 30% 가 훨씬 세게 나온다.**
      실측 링 대 바탕(30%): 의미색이 라이트 1.47~1.65 · 다크 1.58~1.87 인데
      중립은 라이트 1.38~**1.89** · 다크 **2.05~2.62** 로 혼자 튄다.
      → 나란히 서면 **부가 표식이 상태 배지보다 세 보인다.**
   🔑 **20% 면 두 무리가 같은 급이 된다** — 다크 1.61~1.90(의미색 1.58~1.87 과 겹친다) · 라이트 1.24~1.50.
      🔬 18% 는 다크가 1.54~1.78 로 살짝 약하고, 22% 는 `badge-light-dark` 가 다크에서 2.03 으로 다시 튄다.
      🔥 **이 값을 30% 로 되돌리지 말 것** — 위 실측이 그 결과다.
   📌 `text-hover-primary` 가 붙은 배지(목록 머리의 빠른 거르개)는 hover 에서 **글자색만** 바뀌고
      링이 함께 따라간다 — `currentColor` 를 쓴 덕분이며 그것이 맞는 동작이다.
      🔑 그래서 위 버튼 절과 달리 **hover 를 뺄 필요가 없다**(Metronic 은 `.badge` 에 hover 변화를 안 준다). */
.badge.ds-badge-info,
.badge.ds-badge-wait,
.badge.ds-badge-ok,
.badge.ds-badge-stop,
/* 🆕 `ds-badge-new` 는 **표식 배지**이므로 중립(20%)이 아니라 여기(30%)다 (2026-09-01).
      🔥 `border: 1px` 로 대신하지 말 것 — 배지 높이가 `--ds-badge-h`(1.7rem)로 못 박혀 있고
         **그 값에 맞춘 자리가 여럿**이라(배지 옆 버튼·아이콘 버튼) border 는 상자를 2px 키워
         그 정렬을 전부 어긋낸다. 🔬 넣은 뒤 실측: 높이 20/27px 로 「중요」 배지와 동일. */
.badge.ds-badge-new,
.badge.badge-light-primary,
.badge.badge-light-success,
.badge.badge-light-warning,
.badge.badge-light-danger,
.badge.badge-light-info {
    box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 30%, transparent) !important;
}
.badge.ds-badge-mute,
.badge.badge-light,
.badge.badge-light-dark,
.badge.badge-secondary {
    box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 20%, transparent) !important;
}


/* ═══════════════════════════════════════════════════════════════════════
   🔻 사이드바를 **접었을 때** — 심벌만 세우고 접기 화살표는 감춘다 (2026-09-07 사장님 지시)

   > *"축소된 상태에서는 `<<` 이게 아니고 **파비콘이 노출**되어야 할 것 같은데?
   >   사이드바가 **확장되었을 때에만** `<<` 아이콘이 보여야 할 것 같아."*

   🔬 **무엇이 문제였나** — 992~1229px 은 `common.js` 의 `AsideFold` 가 **늘 접어 두는 구간**이다
      (2026-08-21 결정: 그 폭에서 사이드바가 265px 을 먹으면 창을 넓혔는데 본문이 좁아진다).
      그런데 접힌 머리에 **접기 화살표만 홀로** 서 있었다 — 실측 992·1200px 에서
      **로고 0px · 화살표 28px**(1920px 은 로고 175.4px).
      원인은 마크업에 **`.logo-minimize`(심벌)가 없었던 것**이다. Metronic 은 `.logo` ↔ `.logo-minimize`
      **두 벌을 두고 갈아끼우는데** 우리는 한 벌만 두어, 접히면 그 자리가 통째로 빈다.
   ✅ 심벌은 `fragments/nav.html` 이 넣었다(세 앱). 이 절이 하는 일은 둘이다 —
      ① 펼친 상태에서 심벌이 로고와 **겹쳐 둘 다 보이지 않게** 하고
      ② 접힌 상태의 **화살표를 감춘다.**

   ⚠️ **①을 지우면 1920px 에서 로고와 심벌이 나란히 둘 다 보인다.** Metronic 에는
      `.logo-minimize` 를 **「펼친 상태에서 감추는」 기본 규칙이 없다**(접힌 상태 규칙만 있다).
   🔑 **접힌 채로도 다시 펼 수 있다** — 사이드바에 `aside-hoverable` 이 있어 **마우스를 올리면 265px 로
      펼쳐지고 그때 화살표가 돌아온다.** 그래서 판정을 Metronic 의 `:not(:hover)` 와 **같은 조건**으로 걸었다.
   🔴 **`(hover: hover)` 를 지우지 말 것 — 지우면 터치 기기에서 사이드바를 영영 못 편다.**
      이 구간(992~1229px)에는 **태블릿 가로 폭이 실제로 들어오는데**(iPad 1024) 거기에는 hover 가 없어,
      화살표를 감추면 **펼치는 수단이 하나도 남지 않는다.**
   ⚠️ `!important` 는 빼지 말 것 — 화살표에 Bootstrap 유틸리티 `d-lg-inline-flex`
      (`display: inline-flex !important`)가 붙어 있어 그것 없이는 **아무 일도 일어나지 않는다.**
   ═══════════════════════════════════════════════════════════════════════ */
.aside .aside-logo .logo-minimize { display: none; }

@media (hover: hover) {
    [data-kt-aside-minimize="on"] .aside:not(:hover) #kt_aside_toggle { display: none !important; }
}


/* ═══════════════════════════════════════════════════════════════════════
   📱 좁은 화면 — 햄버거 드로어 **안에도 로고를 보인다** (2026-08-07, 사용자 지시)

   🕘 그전에는 좁은 화면에서 **상단바에 전체 로고**가 들어가 폭을 171px 차지했다.
      이제 상단바는 **심벌만**(파비콘) 쓰고, 전체 로고는 사이드바(=햄버거 드로어)로 갔다.

   🔑 Metronic 은 `.aside .aside-logo` 를 **`≥992px` 에서만** 그린다(그 미디어쿼리 안에서
      `display` 뿐 아니라 **`height`·`padding` 까지 함께** 준다).
      마크업에 `d-flex` 를 붙여 보이게 했으므로 **여백·높이도 같이 살려야** 한다 —
      안 그러면 로고가 드로어 **좌상단 모서리에 딱 붙는다**(실측: padding 0 · x 0 · y 0).
   ⚠️ 값은 데스크톱 실측값과 **같게** 맞췄다(`height 65px` · `padding 0 25px`).
      한쪽만 바꾸면 폭을 넘나들 때 로고가 튄다.
   ⚠️ 축소 버튼(`#kt_aside_toggle`)은 마크업에서 `d-none d-lg-inline-flex` 로 가린다 —
      드로어에서 "사이드바 접기" 는 뜻이 없다.
   ═══════════════════════════════════════════════════════════════════════ */
@media (max-width: 991.98px) {
    .aside .aside-logo {
        height: 65px;
        padding: 0 25px;
        justify-content: space-between;
        align-items: center;
    }
}

/* ── 좁은 화면: 제목을 심벌 옆에 ──
   🔑 헤더가 고정이므로 여기 있으면 **스크롤해도 제목이 보인다**(사용자 지시).
   🔬 실측(375px): 심벌이 69에서 끝나고 툴바가 228에서 시작한다 → 제목이 쓸 폭 **약 150px**.
      메뉴명이 8글자를 넘는 것이 있어(「속성 정의 관리」) **크기를 줄이고 말줄임**을 함께 건다. */
@media (max-width: 991.98px) {
    /* ⚠️ flex 자식은 기본 `min-width: auto` 라 **내용보다 작아지지 않는다.**
          말줄임을 걸려면 **사슬의 모든 단계**에 `min-width: 0` 이 있어야 한다:
             `.container-fluid` → `#kt_header_wrapper` → `#kt_header_nav` → `h1`
          🔬 한 단계(래퍼)를 빠뜨렸더니 긴 메뉴명이 상단바를 **67px 넘치게** 하고
             툴바를 228 → 310 으로 밀어냈다. **네 줄이 세트다.** */
    #kt_header > .container-fluid,
    #kt_header_wrapper,
    #kt_header_nav,
    #kt_header_nav > .hdr-page-h1 { min-width: 0; }

    /* ⚠️ 제목은 **블록**이어야 한다 — `display:flex` 면 `text-overflow` 가 동작하지 않는다.
          🔑 그래서 마크업에서 `d-flex` 를 **뺐다**. 부트스트랩 유틸은 `!important` 라
             CSS 로 덮으려 하면 지고, `!important` 로 맞받으면 다음 사람이 이유를 모른다.
          ⚠️ `h1` 에 `d-flex` 를 다시 붙이지 말 것 — 붙는 순간 긴 메뉴명이 툴바를 밀어낸다.

          🔑 **글자 크기는 여기서 정하지 않는다.** `.fs-3` 가
             `calc(1.26rem + .12vw) !important` 라 뷰포트에 따라 저절로 줄어든다
             (🔬 375px → 15.57px · 2113px → 17.55px).
          🔥 처음엔 여기에 `font-size` 를 적었는데 `!important` 에 밀려 **한 번도 적용되지 않았다** —
             안 듣는 규칙을 남기면 다음 사람이 "여기서 크기를 정한다" 고 읽는다. 지웠다.
             크기를 정말 바꿔야 하면 **마크업의 `fs-3` 를 바꾸는 것**이 맞다. */
    #kt_header_nav > .hdr-page-h1 {
        /* 🔑 심벌은 파비콘 `<img>` 라 **안쪽 여백이 없다**(그림이 상자를 가득 채운다).
              햄버거는 아이콘 안쪽 여백 덕에 띄어 보이지만 심벌은 그렇지 않다 → 제목 쪽에서 띄운다.
              🔬 안 띄우면 간격 **0** 이라 붙어 보인다(2026-08-07 사용자 지적). */
        margin-left: 1.35rem;
        line-height: 1.35;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    /* 본문으로 내려간 블록에서 **맨 앞 구분선**은 뜻이 없다(제목이 헤더에 남았으므로). */
    #kt_content_container > .page-title > .hdr-title-sep { display: none; }
}

/* ===================== 비활성 버튼 (2026-08-09 사용자 지시) =====================
   🔥 Metronic 기본은 `opacity:.6` 하나뿐이라 **진한 solid 버튼은 60%로도 선명해** 활성처럼 보인다.
      실제로 검수 화면에서 "승인 상태인데 버튼이 왜 그대로냐" 는 지적을 받았다 —
      눌리지 않는데(`pointer-events:none`) 커서는 `pointer` 라 오히려 누를 수 있다고 말하고 있었다.
   🔑 색을 **죽이고**(회색 계열) 커서를 `not-allowed` 로 바꿔 "지금은 할 수 없다" 를 눈으로 알린다.
   ⚠️ `!important` 는 Metronic 의 `.btn-success` 등(같은 특이도)을 이기기 위해 필요하다.
   ⚠️ `pointer-events` 를 풀지 말 것 — 그러면 눌려서 서버까지 간다.
   📌 `.btn-link`·아이콘 버튼은 배경이 없어 이 규칙이 색을 덮어도 티가 나지 않는다. */
.btn:disabled,
.btn.disabled,
button:disabled.btn {
    background-color: var(--ds-fill) !important;
    border-color: var(--ds-line) !important;
    color: var(--ds-text-faint) !important;
    opacity: 1 !important;          /* 흐리게 하는 대신 색으로 말한다 */
    cursor: not-allowed !important;
    box-shadow: none !important;
}
/* 🔑 `pointer-events:none` 이면 커서 모양이 안 바뀐다 — 감싸는 쪽에서 알려 준다.
      (Bootstrap 이 `.btn:disabled` 에 `pointer-events:none` 을 건다) */
.btn:disabled, .btn.disabled { pointer-events: none; }

/* ===================== 비활성 체크박스·라디오 (2026-08-20) =====================
   🔑 **바로 위 버튼 규칙과 같은 원칙이다 — 흐리게 하는 대신 색으로 말한다.**
      벤더는 `.form-check-input:disabled` 에 **`opacity:.5` 하나**만 준다. 버튼은 이미 위에서
      `opacity:1` + 배경·글자색 교체로 바꿔 놓았는데 **체크박스만 그 예외로 남아 있었다.**
   🔴 **켠 채로 잠긴 칸과 끈 채로 잠긴 칸을 갈라서 칠해야 한다.**
      🔬 체크 표시는 **흰색 SVG 가 배경 이미지로** 박혀 있어(`fill='%23FFFFFF'` 가 data URI 안에 있다)
         `color` 도 `currentColor` 도 닿지 않는다 — `.btn-close` 와 같은 함정이다.
         그래서 **켠 칸까지 옅은 회색으로 칠하면 흰 체크가 흰 바탕에 얹혀 통째로 사라진다.**
         「잠겼다」를 알리려다 **「켜져 있다」를 지우는** 것이라 반대로 나쁘다.
      🔑 → 끈 칸은 **옅은 면**(`--ds-fill` + `--ds-line`), 켠 칸은 **중간 회색 면**(`--ds-text-faint`).
         🔬 흰 체크 대비 실측 근거: gray-600 은 카드 배경 대비 라이트 3.79 / 다크 3.24 인 계조라
            흰 글자와의 대비는 **라이트 ≈3.7 · 다크 ≈4.9** 로 그림 요소 기준(3:1)을 넘긴다.
      🔥 **`--ds-line-strong`(gray-400)으로 낮추지 말 것** — 흰 체크 대비가 **2.0** 이라 안 보인다.
      🔥 회색 체크 이미지를 새로 그려 넣는 방법으로 가지 말 것 — data URI 안에 **hex 를 박게 된다**(0 절).
   📌 **중간 상태(`indeterminate`)도 켠 칸과 같다** — 대시도 흰색 SVG 다.
   📌 라벨의 `opacity:.5` 도 같은 이유로 색으로 바꾼다(잠긴 항목은 대비 기준의 예외다).
   ⚠️ `!important` 를 쓰지 않는다 — 이 파일이 번들보다 **나중에** 로드돼 같은 특이도로 이긴다
      (0-0 절과 같은 근거). 🔥 순서가 바뀌면 조용히 무효가 되므로 `head.html` 을 옮기지 말 것.
   ⚠️ **끈 칸 규칙이 켠 칸 규칙보다 먼저 와야 한다** — 뒤집으면 켠 칸까지 옅은 면이 된다. */
.form-check-input:disabled {
    background-color: var(--ds-fill);
    border-color: var(--ds-line);
    opacity: 1;                     /* 흐리게 하는 대신 색으로 말한다 */
}
.form-check-input:checked:disabled,
.form-check-input[type="checkbox"]:indeterminate:disabled {
    background-color: var(--ds-text-faint);
    border-color: var(--ds-text-faint);
}
.form-check-input:disabled ~ .form-check-label,
.form-check-input[disabled] ~ .form-check-label {
    opacity: 1;
    color: var(--ds-text-faint);
}

/* ===================== 선택된 목록 항목 — `.list-group-item.active` (2026-08-12 사용자 지적) =====================
   🔥 Bootstrap 기본은 **진한 primary 배경 + 흰 글자**다. 그런데 이 저장소의 선택 목록에는
      항목마다 배지(`badge-light-*`)와 설명(`text-muted`)이 함께 들어가고, 그것들은
      **자기 색을 그대로 들고 있어서** 진한 배경 위에서 글자가 읽히지 않는다
      (실측: 역할·권한의 「슈퍼」 배지와 설명 줄이 파란 배경에 묻혔다).
   🔑 선택 표시는 **연한 배경 + 진한 글자 + 왼쪽 굵은 선**으로 한다 — 이 저장소가 이미 필터 칩
      (`.ds-filter-chip.active`)에서 쓰는 표현이라 화면마다 「선택됨」의 모양이 갈리지 않는다.
   ⚠️ 흰 글자로 되돌리지 말 것 — 배지·설명까지 덮는 규칙을 따로 써야 하고, 그러면 배지의
      의미색(위험=빨강 등)이 사라져 **무엇이 슈퍼 역할인지 목록에서 알 수 없게 된다.**
   ⚠️ 연한 배경만으로는 hover 와 구별되지 않는다 → 왼쪽 선을 함께 그린다.
   ⚠️ Bootstrap `.list-group-item.active` 는 (0,2,0) 이라 `.ds` 를 물면(0,3,0) 이긴다. */
/* 🐛 **다크에서 고른 것 말고는 아무것도 안 보였다**(2026-08-12 실측).
      Bootstrap `.list-group-item` 이 배경을 `#fff` 로 **박아** 두는데 Metronic 다크 번들이
      이 선택자를 덮지 않는다 → 다크 카드(#1E1E2D) 위에 **흰 줄**이 서고, 글자색은 다크용
      흰색이라 **흰 바탕에 흰 글자**(대비 1.0)가 된다. 역할 목록에서 실제로 그랬다.
   🔑 배경을 **투명**으로 돌려 카드 표면을 그대로 쓰게 한다 — `--ds-surface` 로 못을 박으면
      카드가 아닌 곳에 목록을 놓는 순간 다시 어긋난다. 글자색은 번들이 테마별로 이미 맞춘다. */
.ds .list-group-item { background-color: transparent; }

.ds .list-group-item.active {
    background-color: var(--ds-primary-soft);
    border-color: var(--ds-line-soft);
    color: var(--ds-text);
    box-shadow: inset 3px 0 0 var(--ds-primary);
}


/* 🕘 **「SweetAlert 안의 입력 폼」 규격(`.ds-swal-form` / `.ds-swal-form-popup`)은 2026-08-16 구현 #75 에 지웠다.**
   쓰는 곳이 개인정보 열람 사유 **모달** 하나뿐이었는데, 그 모달이 **화면**(`/admin/member/reason`)으로 바뀌어
   호출부가 0곳이 됐다 — 세 앱에 남겨 두면 다음 사람이 「있으니 쓰는 곳이 있겠지」로 읽는다.
   🔑 다시 필요해지면(Swal 본문에 폼을 넣는 화면이 생기면) **되살릴 것**: SweetAlert2 가 `.swal2-html-container` 에
      `max-height: 200px` 을 박아 **안내 한 줄 + 셀렉트 + textarea 만으로 세로 스크롤**이 생기고(실측 215px),
      좌우 여백까지 겹쳐 안내 문구가 두 줄로 접힌다(글자 폭 338.9px < 필요 434px). 처방은 `max-height: 60vh`
      (`none` 은 금지 — 낮은 창에서 버튼이 잘린 채 스크롤도 안 된다) + 좌우 여백 제거 + **팝업 폭 34em**(둘은 짝이다).
   📌 값의 근거는 작업기록 2026-08 「구현 #75」 및 그 이전 「사유 모달 폭」 절에 남아 있다. */


/* ══ 17) 상세 화면 뼈대 — `.ds-detail-*` (2026-08-12 사용자 기준) ═══════════════════

   > "상세 페이지를 DMC 계약 페이지처럼 정보 카드가 맨 위 가로 100% 차지하고 상세보기 버튼 달고.
   >  나머지 정보는 좌측 하단에, 미리보기가 있을경우 미리보기 카드에 처리 버튼, 없을 경우 처리 카드
   >  별도로 우측하단에. 최 하단에 목록 버튼 이런식으로 가자."

   자리가 정해져 있다 — 화면마다 다르게 놓지 말 것:

   ┌──────────────────────────────────────────────┐
   │ 정보 카드 (100%)              [상세 보기 ▾]  │  ← `.ds-detail-summary`
   ├───────────────────────────┬──────────────────┤
   │ 본문 카드들 (좌)          │ 처리 카드 (우)   │  ← `.row` + `col-xl-8` / `col-xl-4`
   │  · 미리보기가 있으면 그 카드에 처리 버튼을 얹고 이 칸은 두지 않는다 │
   ├───────────────────────────┴──────────────────┤
   │ 공통 버튼 (100%)   [목록]          [보조…]   │  ← `.ds-detail-actions`
   └──────────────────────────────────────────────┘

   🔑 **머리에 무엇을 보고 있는지, 발에 어디로 갈 수 있는지.** 정보는 위, 행위는 아래다.
   🔑 **목록 버튼은 맨 아래 카드에 둔다** — 헤더에도 두면 같은 일이 화면에 둘이 된다
      (DMC 계약 상세가 정본이고 거기도 아래에만 있다).
   ⚠️ 처리 버튼을 본문 카드 사이에 흩어 놓지 말 것 — 「어디서 처리하지」를 찾게 된다.
   📌 목록 화면의 규약(9절 `.ds-list-card`)과 짝이다: **목록은 찾고 고르는 곳, 상세는 처리하는 곳.**
   📌 DMC 계약·견적 상세는 `Trade.css` 의 `.summary-bar`/`.preview-col` 로 같은 모양을 이미 쓴다.
      그쪽은 미리보기 도킹·이력 드로어까지 얹힌 **거래 전용 작업대**라 여기로 합치지 않았다 —
      ⚠️ 두 벌이 된 것이 아니라 **범위가 다르다.** 이 절은 그 밖의 모든 상세 화면이 쓴다. */

/* ── 맨 위 정보 카드: 접힌 상태는 한 줄, 「상세 보기」로 펼친다 ──
      🔥 **값은 DMC `Trade.css` 의 `.summary-bar`/`.summary-toggle` 에서 그대로 옮겼다**
         (2026-08-12 사용자 지적: *"상세보기 버튼 디자인이 다르다. 똑같이 하랬는데 왜 다르게 하는거야"*).
         🕘 처음엔 「비슷하게」 새로 썼다가 **테두리 없는 회색 글자 버튼**이 되어 정본과 달라졌다.
      ⚠️ 여기 값을 손으로 고치지 말 것 — 고치면 두 화면이 갈린다. 바꿔야 하면 **양쪽을 함께.** */
/* 🔑 **스티키다** — 유틸바는 고정이라 스크롤해도 보이는데 이 카드가 흘러가 버리면
      긴 상세에서 「이게 무슨 상태였지」를 다시 맨 위로 올라가 확인해야 한다(2026-08-14 표준 2절).
   🔥 **값은 `Trade.css` 의 `.summary-bar` 에서 그대로 옮겼다** — 손으로 새로 짜지 말 것.
      떠 있는 동안 아래 카드가 이 카드 '밑'을 지나가는데, 하단 경계가 없으면 글자끼리 겹쳐 보인다
      → 그림자 + 경계선으로 「떠 있는 층」임을 드러낸다. `top` 은 고정 헤더 높이(PC 65 / 모바일 55)다. */
:is(.ds, .quote-content) .ds-detail-summary {
    margin-bottom: 1.5rem;
    position: sticky;
    top: 65px;
    z-index: 25;
    border-bottom: 1px solid var(--ds-line-soft);
    box-shadow: 0 6px 14px -6px rgba(0, 0, 0, .18);
}
html[data-theme="dark"] :is(.ds, .quote-content) .ds-detail-summary {
    border-bottom-color: var(--ds-line);
    box-shadow: 0 6px 14px -6px rgba(0, 0, 0, .5);
}
:is(.ds, .quote-content) .ds-ds-head {
    display: flex; flex-wrap: wrap; align-items: center;
    gap: .7rem;
    padding: .5rem 2.25rem;
}
/* 🔑 **우측 구분선은 `.ds-hist-row-title`(21절)과 같은 값**이다 (2026-08-15 사용자 지적:
      *"업체정보 카드 타이틀과 배지 사이에 구분선이 없네 관리 이력 카드에는 있는데..."*).
      두 카드가 상세 화면 맨 위에 **연달아** 놓이므로 제목 처리가 다르면 바로 눈에 띈다.
   ⚠️ 값을 손으로 새로 정하지 말 것 — 21절과 **같은 수치·같은 토큰**이어야 갈리지 않는다. */
/* 🔴 **크기·굵기·색도 `.ds-hist-row-title`(21절)과 같아야 한다** (2026-08-20 사용자 지적:
      *"카드 타이틀 폰트 사이즈 색상 패밀리 모두 맞춰야 해."*).
   🔬 실측(DMC 견적 상세, 다크): 이 제목만 **17.2px / 700 / gray-800** 이고, 같은 화면의 다른 카드 제목
      (`h3.card-title.fs-5.fw-bolder`)은 **18.4px / 600 / gray-900** 이었다 — 한 화면에 제목이 두 규격이었다.
   🔑 규격은 **`.card-title` + `fs-5` + `fw-bolder`** 한 가지다(`--ds-font-title` = 1.15rem = fs-5,
      `--ds-weight-title` = 600 = fw-bolder, `--ds-text-strong` = gray-900). 21절이 이미 그 값을 쓰고 있다.
   🔥 **`700`·`--ds-text` 로 되돌리지 말 것** — 되돌리면 카드 제목이 다시 두 규격으로 갈린다.
   ⚠️ **px 로 박지 말 것**(글자 크기 3단계 토글이 root 기준 rem 이다).
   🔑 `line-height` 를 21절과 같은 `1.2` 로 준다 — 이 제목은 **우측 구분선의 높이**를 정하는 자리라
      값이 다르면 연달아 놓인 두 카드의 **구분선 길이가 갈린다**(22px vs 27.6px). */
:is(.ds, .quote-content) .ds-ds-title {
    font-size: var(--ds-font-title);
    font-weight: var(--ds-weight-title);
    color: var(--ds-text-strong);
    line-height: 1.2;
    flex: none;
    padding-right: 1.15rem;
    border-right: 1px solid var(--ds-line-soft);
    margin-right: .45rem;
}
/* 접힌 줄에 얹는 요약 값 — DMC `.sc` 규격. 🔑 **식별에 필요한 것만** 올린다(다 올리면 접은 뜻이 없다).
   🔥 **높이는 배지와 같아야 한다**(`--ds-badge-h` = 1.7rem). 같은 줄에 배지와 나란히 서기 때문이다.
      🕘 예전엔 세로 패딩(`.32rem`)으로 높이를 만들어 **≈35px vs 배지 27.2px** 로 어긋나 있었다.
      ⚠️ **px 로 박지 말 것** — 배지 여백은 em 기반이라 글자 크기 토글(0-0 절)을 따라 함께 줄고,
         칩만 px 로 고정하면 그 순간 다시 어긋난다. `-2px` 는 위아래 **테두리 두께**뿐이다.
      ⚠️ `inline-flex` 로 바꾸지 말 것 — 좁은 화면의 `.is-key` 말줄임(`text-overflow`)이 죽는다
         (글자가 익명 flex 아이템이 되어 말줄임 대상이 아니게 된다). */
:is(.ds, .quote-content) .ds-ds-chip {
    display: inline-block;
    height: var(--ds-badge-h);
    line-height: calc(var(--ds-badge-h) - 2px);
    font-size: .95rem;
    background: var(--ds-fill);
    border: 1px solid var(--ds-line-soft);
    border-radius: .4rem;
    padding: 0 .75rem;
    color: var(--ds-text);
    font-weight: 600;
    white-space: nowrap;
    /* ✂️ **긴 값은 말줄임** (2026-09-18 여행사 2회차 C2-11·C5-15·C7-03·E2-30·E2-40·E4-56·E5-63)
       🔬 실측: 78자 상품명·100자 제목 칩이 `nowrap` 897px 로 카드를 뚫어 문서 가로 스크롤
          768px 214 · 992px 65 · 1200px 45px.
       🔑 상한은 **`min(100%, 30rem)`** — `100%` 만 주면 긴 칩이 제 줄로 접혀 **스티키 띠가 한 줄 두꺼워진다**
          (17절 머리 주석의 같은 이유). 30rem(16px root 480px)은 한국어 약 40자라 옛 짧은 값은 그대로다.
       ⚠️ 대가: 1920px 처럼 한 줄에 다 들어가던 긴 값도 30rem 에서 잘린다 — 전체는 펼친 상세에 있고,
          잘렸을 때만 `common.js` 가 `title` 을 붙인다.
       📌 `Trade.css` 의 `.summary-bar .sc` 도 같은 값이다(양쪽을 함께). */
    max-width: min(100%, 30rem);
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* 🔥 **접힌 줄의 배지는 칩과 같은 높이여야 한다**(2026-08-14 표준 3절) — 한 줄에 나란히 선다.
      Metronic `.badge` 는 마침 `--ds-badge-h`(1.7rem) 이지만, **화면마다 padding 을 덮은 배지 클래스**가
      있다(예: `Trade.css` 의 `.trade-status-badge` = 24.8px). 그런 것까지 여기서 한 번에 맞춘다.
   ⚠️ **직계 자식(`>`)만** 잡는다 — 칩 **안**에 든 배지(시스템 오류 상세의 HTTP 메서드)는
      칩 높이를 따라야 하므로 대상이 아니다.
   ⚠️ px 로 박지 말 것 — 배지 여백이 em 기반이라 글자 크기 토글을 따라 함께 줄어든다. */
:is(.ds, .quote-content) .ds-ds-head > .badge {
    display: inline-flex;
    align-items: center;
    height: var(--ds-badge-h);
}

/* 🔑 **테두리 있는 primary 글자 버튼**이다(DMC `.summary-toggle` 과 같은 값).
   ⚠️ `margin-left:auto` 로 오른쪽 끝에 붙인다 — `justify-content:space-between` 을 쓰면
      요약 값이 둘 이상일 때 가운데가 벌어져 줄이 흩어진다.
   📌 **좁은 화면(≤767.98px)에서는 이 `auto` 를 풀어 둔다** — 머리가 접힐 때 둘째 줄에 배지 하나와
      버튼만 남아 **사이가 텅 비기** 때문이다(2026-08-18 사용자 지적). 사유와 실측치는 아래
      `@media (max-width: 767.98px)` 블록의 `:is(.ds, .quote-content) .ds-ds-head > .ds-ds-toggle` 주석에 있다. */
/* 🔴 **화살표가 붙는 버튼은 이 한 가지 모양이다 — `.ds-head-btn`** (2026-08-20 사용자 지시 · 구현 #97)
   > *"상세보기 펼쳐보기 버튼등에 들어있는 화살표의 **세로 정렬이 버튼의 중간**이었으면 좋겠고
   >  **버튼의 디자인 및 색상도 하나로 통일**했으면 좋겠어 … 3앱 모두 통일해야 할 것 같아."*
   🔑 사용자가 첨부한 모양이 **바로 이 `.ds-ds-toggle`** 이다(테두리 + 흰 바탕 + primary 글자).
      그래서 새 디자인을 만들지 않고 **이것을 표준으로 승격**시켰다 — 같은 선언을 두 이름이 공유한다.
   🔑 **대상은 「화살표가 붙는 버튼」**이다: 상세 보기·펼쳐보기·드로워 열기·다른 화면으로 가는 링크.
   🔥 **행위 버튼(등록·다운로드·수정)에는 쓰지 말 것** — 그것들은 화살표도 안 붙는다.
      둘이 같은 모양이 되면 **「가는 것」과 「하는 것」이 구별되지 않는다.**
   ⚠️ `.ds-head-btn` 에는 `margin-left:auto` 가 **없다**(아래에서 `.ds-ds-toggle` 에만 따로 준다) —
      `.card-toolbar` 안에서는 툴바가 이미 오른쪽 끝에 놓기 때문이다. */
:is(.ds, .quote-content) .ds-ds-toggle,
:is(.ds, .quote-content) .ds-hist-row-all,
:is(.ds, .quote-content) .ds-head-btn {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    border: 1px solid var(--ds-line-soft);
    background: var(--ds-surface);
    color: var(--ds-primary);
    border-radius: .4rem;
    font-weight: 600;
    font-size: .95rem;
    line-height: 1.5;
    padding: .26rem .7rem;
    cursor: pointer;
    white-space: nowrap;
    flex: none;
}
/* 🔴 **`margin-left:auto` 는 요약 카드 토글에만 준다** (2026-08-20 — 주석과 코드가 어긋나 있던 것을 바로잡음).
   🔑 `.ds-ds-head` 는 배지와 버튼이 한 줄에 나열되는 flex 라 **버튼을 오른쪽 끝으로 밀 것이 필요**하다.
   🔥 `.ds-head-btn` 에는 주지 말 것 — 그 클래스는 `.card-toolbar` 안에서도 쓰는데,
      툴바가 **이미 오른쪽에 놓여 있어** 거기에 또 `auto` 가 걸리면 툴바 안에서 형제 버튼과 사이가 벌어진다.
   📌 좁은 화면(≤767.98px)에서 이 `auto` 를 푸는 규칙이 아래에 따로 있다(머리가 접힐 때 사이가 텅 빈다). */
:is(.ds, .quote-content) .ds-ds-toggle { margin-left: auto; }
:is(.ds, .quote-content) .ds-ds-toggle:hover,
:is(.ds, .quote-content) .ds-head-btn:hover {
    border-color: var(--ds-primary);
    background: var(--ds-primary-soft);
}
/* 🔴 **화살표는 버튼의 세로 한가운데** (2026-08-20 사용자 지적).
   🔬 `&rsaquo;` 는 **글자**라 글꼴의 기준선에 앉는다 — 아이콘(`<i>`)과 달리 그냥 두면 위로 뜬다.
   ✅ `.ds-arrow` 를 **flex 항목이자 그 자체로 flex 상자**로 만들어 `align-items:center` 로 앉힌다.
      `line-height:1` 이 함께 있어야 글자 상자의 여백이 중심을 밀지 않는다.
   🔥 화면에서 `vertical-align`·`mt-*` 로 맞추지 말 것 — 글꼴이 바뀌면 다시 어긋난다(본문 글꼴을
      2026-08-17 에 실제로 교체했다). */
:is(.ds, .quote-content) .ds-ds-toggle i,
:is(.ds, .quote-content) .ds-head-btn i { font-size: .8rem; line-height: 1; display: inline-block; transition: transform .2s; }
/* 🔴 **직계 자식으로 한정하지 말 것** (2026-08-20 — 처음에 그렇게 짰다가 구멍이 드러났다).
   🔬 「분쟁 확인」처럼 **강조색을 지켜야 해서 `.ds-head-btn` 을 안 쓰는 버튼**에도 화살표는 붙는다.
      직계 자식 선택자면 그런 자리에서 **정렬 규칙이 통째로 안 걸려** 글자가 기준선에 앉는다.
   🔑 `.ds-arrow` 는 **어디에 있든 세로 가운데**여야 한다 — 그것이 이 클래스의 존재 이유다. */
:is(.ds, .quote-content) .ds-arrow {
    display: inline-flex;
    align-items: center;
    line-height: 1;
    font-size: 1.15em;
}
:is(.ds, .quote-content) .ds-detail-summary.is-open .ds-ds-toggle i { transform: rotate(180deg); }

/* 🔑 기본은 **접힘**이다. 상세 화면에 들어서자마자 표가 화면을 밀어내면
      정작 할 일(처리 카드)이 아래로 밀린다.
   🔑 **여백은 다른 카드의 `card-body` 와 같은 값**(2rem 2.25rem)이다 (2026-08-15 사용자 지적:
      *"업체 정보 카드의 콘텐츠 상하 여백이 다른 카드와 다르다"*).
      🔬 그전에는 `.35rem … 1rem` 이라 실측 **위 5.6px / 아래 16px** 로, 다른 카드의 **32/32px** 과
         눈에 띄게 달랐다. 접힌 줄에 붙어 있던 시절의 값이 펼침 상태에 그대로 남아 있던 것이다.
      🔥 화면에서 px 로 다시 박지 말 것 — 이 값이 어긋나면 **이 카드만** 다른 규격으로 보인다. */
:is(.ds, .quote-content) .ds-ds-detail { display: none; padding: 2rem 2.25rem; }

/* 🔑 **펼쳤을 때만 머리 아래 구분선을 그린다** (2026-08-15 사용자 지적:
      *"업체 정보 카드는 「상세 보기」로 펼쳤을 때 카드 헤더 구분선이 없어서 어색하다"*).
      🔬 원인: 다른 카드의 머리는 `.card-header` 라 Metronic 이 아래 선을 들려 주는데,
         이 카드의 머리는 `.ds-ds-head`(제목·칩·배지·토글이 한 줄로 서는 자리)라 선이 없었다.
   🔥 **접힌 상태에는 그리지 말 것** — 접힌 카드는 그 줄 하나가 전부이고, 카드 자신이 이미
      `border-bottom`(스티키로 떠 있는 층의 경계)을 들고 있어 선이 **두 줄로 겹쳐 보인다.**
   🔑 색은 `.card-header` 와 **같은 `--ds-line-soft`** 다 — 값을 새로 정하면 두 카드가 갈린다.
   ⚠️ 선을 그으면 접힌 줄의 아래 여백(.5rem)이 그대로 선까지의 간격이 되어 답답하다 →
      펼쳤을 때만 머리 아래·본문 위 숨통을 함께 벌린다. */
:is(.ds, .quote-content) .ds-detail-summary.is-open .ds-ds-head {
    border-bottom: 1px solid var(--ds-line-soft);
    padding-bottom: .65rem;
}
:is(.ds, .quote-content) .ds-detail-summary.is-open .ds-ds-detail { display: block; }
/* 📌 **펼친 카드가 화면을 덮으면 고정을 푼다 — `is-unstuck`** (2026-09-18 여행사 2회차 · 상품 상세)
   🔬 펼친 카드(966·923px)가 폰 화면에 붙은 채 아래 「견적 요청」·「목록」을 덮어 누를 수 없었다.
   🔑 붙이는 판정은 `common.js` 「펼친 요약 카드가 화면을 덮으면」 블록(남는 높이의 60% 초과) 한 곳이다.
      🔥 여기서 폭 미디어 쿼리로 풀지 말 것 — 덮는지는 **창 높이**가 정한다(1200×957 은 안 덮었다).
   📌 `Trade.css` 의 `.summary-bar`(거래 작업대)도 같은 클래스를 받는다 — 두 골격을 이 한 줄이 함께 푼다. */
:is(.ds, .quote-content) .ds-detail-summary.is-unstuck,
.summary-bar.is-unstuck { position: relative; top: auto; }

/* 📝 **접힌 줄 아래의 「요청사항」 한 줄** — `.ds-ds-note` (2026-08-19 구현 #97 2차, 사용자 결정)
   🔑 **접혀 있어도 요청사항은 바로 보여야 한다**는 사용자 요구다. 한 번 없앴다가 되돌린 자리다.
   🔥 **한 줄이다** — `nowrap` + 말줄임(`…`). 여러 줄로 펼쳐지면 이 카드는 스티키라
      **고정 띠가 그만큼 두꺼워져 스크롤 내내 화면을 먹는다**(17절 머리 주석의 같은 이유).
   🔑 **펼치면 사라진다** — 펼침 격자에 같은 항목이 있어 그대로 두면 한 카드에 같은 값이 둘이 된다.
   🔑 값은 랜드사·여행사 `Trade.css` 의 옛 `.summary-note` 에서 **그대로 옮겼다**. 새로 정하지 말 것.
   📌 **관리자 앱에는 이 줄이 없다** — 랜드사·여행사의 거래 상세 전용이다. 세 앱 파일을 **한 벌로**
      유지하려고 여기에도 같은 규칙을 둔다. 🔥 관리자 화면에 이 줄을 만들지 말 것. */
:is(.ds, .quote-content) .ds-ds-note {
    display: flex; align-items: baseline; gap: .6rem;
    padding: .55rem 2.25rem;
    font-size: .95rem; line-height: 1.55;
    border-top: 1px dashed var(--ds-line-soft);
}
:is(.ds, .quote-content) .ds-ds-note-k { color: var(--ds-text-label); font-weight: 600; flex: none; }
:is(.ds, .quote-content) .ds-ds-note-v {
    color: var(--ds-text); flex: 1; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
:is(.ds, .quote-content) .ds-detail-summary.is-open .ds-ds-note { display: none; }

/* 🔓 **이 카드의 처리 푸터 — 펼쳤을 때만 보인다** (2026-08-15 사용자 지시, 구현 #58:
   *"업체 정보 수정 버튼을 … 푸터로 옮기자 … 대신 **펼침 상태가 아니면 보이지 않도록** 해 줘"*).
   🔑 **접혀 있으면 푸터가 없다 → 스티키 고정 띠가 원래 높이(≈50px)로 유지된다.**
      🔬 푸터를 늘 보이게 두었을 때는 띠가 **122px**(모바일 147px)까지 두꺼워졌다 —
         이 카드는 스크롤 내내 화면에 붙어 있으므로 그 띠가 곧 잃는 화면이다.
      🔑 그래도 되는 이유: **승인·수정은 정보를 펼쳐 보고 하는 일**이라 접힌 채로 누를 일이 없다.
   🔧 **마크업이 아니라 여기서 여닫는다** — 푸터는 `.ds-ds-detail` **안이 아니라 카드의 직계 자식**이어야 한다.
      🔥 상자 안에 넣지 말 것: 푸터 여백 규격(2-1 절 `.9rem`)의 선택자가 **`.ds .card > .card-footer`**
         (직계 자식)라 상자 안에서는 **안 먹어 Metronic 기본 `2rem 2.25rem`** 으로 돌아가고,
         상자 자신의 좌우 패딩과 겹쳐 **여백이 이중**이 되며 `border-top` 이 카드 폭을 다 못 긋는다.
   ⚠️ `display: flex` 로 되살린다 — 이 푸터는 `d-flex`(유틸리티)로 버튼을 늘어놓는데
      `display: none` 을 `block` 으로 되돌리면 `me-auto` 가 죽어 좌우 갈라짐이 사라진다. */
:is(.ds, .quote-content) .ds-detail-summary > .card-footer { display: none; }
/* 🔥 **줄 배치를 마크업의 `d-flex`·`gap-*` 유틸리티로 주지 말 것** — 유틸리티는
   `display: flex !important` 라 위의 `display: none` 을 **이겨서 접어도 푸터가 남는다.**
   🔬 실측(2026-08-15): 유틸리티를 붙였더니 접힘 상태의 스티키 띠가 **122px** 그대로였다.
   📌 `me-auto`(margin) 는 함께 써도 된다 — `display` 와 다투지 않는다.
      왼쪽 = 일상 작업(수정) / 오른쪽 = 심사 행위(승인·반려·취소) 를 그것이 갈라 놓는다. */
:is(.ds, .quote-content) .ds-detail-summary.is-open > .card-footer {
    display: flex;
    align-items: center;
    /* 🔑 **처리 버튼은 오른쪽**이다 (2026-08-16 구현 #71, 사용자 지적: *"승인·반려 버튼이 왼쪽에 있다"*).
       업체 상세는 왼쪽 버튼(「업체 정보 수정」)에 `me-auto` 가 붙어 있어 우연히 오른쪽으로 밀려 있었고,
       왼쪽 버튼이 없는 화면(회원 상세)에서는 처리 버튼이 그대로 왼쪽에 섰다.
       🔥 `me-auto` 에 기대지 말 것 — 그건 「일상 작업 / 심사 행위」를 **가르는** 장치이지 정렬 장치가 아니다.
       📌 `me-auto` 가 있는 화면에서는 그 margin 이 남는 공간을 다 먹으므로 이 값은 아무 일도 하지 않는다
          (업체 상세의 배치는 그대로다 — 실측으로 확인). */
    justify-content: flex-end;
    gap: .5rem;
}
/* 📏 **상세 화면의 카드 푸터 버튼은 전부 같은 크기**다 — `--ds-btn-h`(단독 버튼 2.2rem)
   (2026-08-16 구현 #74 사용자 지시: *"카드 푸터에 버튼들은 이용 정지 버튼과 사이즈를 통일해 줘.
   버튼 사이즈는 업체 상세에도 적용해 줘."*).
   🕘 그전에는 이 규칙이 **접이식 요약 카드의 푸터에만** 걸려 있어, 같은 화면의 다른 카드 푸터
   (회원 상세 「로그인 기록」·업체 상세 「확인함」·여신 한도 「저장」)는 **Metronic 기본 크기**로 커 보였다.
   🔑 선택자를 **`.ds .card > .card-footer`(모든 카드 푸터)** 로 넓혀 한 곳에서 정한다 —
      화면마다 `btn-sm` 을 붙이거나 px 를 칠하지 말 것.
   ⚠️ **px 로 박지 말 것** — 글자 크기 3단계 토글이 root 기준 rem 으로 동작한다(박으면 글자만 커진다).
   ⚠️ **배지 높이에 맞추지 말 것** — 버튼이 상태 배지처럼 보인다는 지적을 이미 한 번 받았다(구현 #58).
   🔎 **직계 자식(`>`)이 아니라 후손 선택자다** — 카드 푸터가 `<form>` 안에 들어 있는 화면이 있다
      (마이페이지 프로필 수정: 실측 52.4px 로 혼자 컸다). 여백 규칙(2-1 절)은 직계여야 하지만
      **버튼 크기는 어디 있든 같아야** 하므로 이 규칙만 후손으로 잡는다.
   📌 맨 아래 공통 버튼 카드(`.ds-detail-actions`)와 목록 카드 발(`.ds-lc-foot`)도 같은 토큰을 쓴다 —
      상세 화면에서 「혼자 서는 버튼」의 크기가 한 가지로 보인다. */
.ds .card .card-footer .btn {
    height: var(--ds-btn-h);
    padding: 0 1rem !important;   /* Metronic `.btn:not(…)×6` 이 (0,7,0) 이라 !important 가 필요하다 */
    display: inline-flex;
    align-items: center;
    font-size: var(--ds-font-body);
    line-height: 1;
}

/* ── 🔗 앵커 링크로 **방금 도착한 카드** — 잠깐 테두리를 밝힌다 (2026-08-15) ──
   🔑 **없으면 「아무 일도 안 일어났다」로 읽힌다.** 같은 줄에 나란히 놓인 카드끼리 오갈 때
      세로 위치가 거의 같아 화면이 움직이지 않기 때문이다(업체 상세의 거래 준비 ↔ 정산 준비).
   🔑 클래스는 스크립트가 잠깐 붙였다 뗀다 — 상태가 아니라 **신호**다.
   ⚠️ `outline` 이 아니라 `box-shadow` 다 — 카드의 둥근 모서리를 따라가고 자리를 차지하지 않는다
      (`outline` 은 모서리를 무시해 각진 사각형이 덧그려진다). */
.ds .ds-anchor-hit {
    box-shadow: 0 0 0 2px var(--ds-primary);
    transition: box-shadow .25s ease-out;
}

/* 좁은 화면 — DMC 와 같은 처리: 여백을 줄이고 보조 칩은 감춘다(핵심 하나만 남긴다). */
@media (max-width: 991.98px) {
    /* 모바일 고정 헤더가 55px 이라 스티키 기준선도 함께 내린다(`.summary-bar` 와 같은 값). */
    :is(.ds, .quote-content) .ds-detail-summary { top: 55px; }
}
@media (max-width: 767.98px) {
    /* 🔥 **`flex-wrap: nowrap` 으로 되돌리지 말 것** (2026-08-15 실측).
       🔬 업체 상세의 머리 배지가 셋(승인 상태 · 거래 준비 · 유형)이 되자 375px 에서
          「상세 보기」 버튼이 화면 밖으로 **73px** 밀려나 **가로 스크롤이 생겼다**
          (`clientWidth 375 / scrollWidth 448`). 칩은 이미 감춰져 있어 더 줄일 것이 없었다.
       🔑 `wrap` 은 **넘칠 때만** 두 줄이 된다 — 배지가 둘뿐인 화면(계약 상세 등)은 그대로 한 줄이다.
          ⚠️ 그래서 배지를 감추는 처방을 쓰지 않았다: 머리 배지는 「그 화면이 재촉하는 일 전부」라
             좁은 화면에서 사라지면 모바일에서만 할 일을 못 보게 된다.
       📌 **버튼은 접힌 줄에서도 오른쪽 끝에 선다**(17절 `margin-left: auto`). 그래도 사이가 안 벌어지는 것은
          아래 `.is-key` 칩이 **늘어나지 않기** 때문이다 — 배지들이 왼쪽에 모이고 버튼만 오른쪽에 남는다. */
    :is(.ds, .quote-content) .ds-ds-head { flex-wrap: wrap; row-gap: .35rem; padding: .55rem 1rem; }
    /* 🕘 **2026-08-18 에 여기서 한 번 헛짚었다 — 같은 증상을 두 곳에서 고치려다 새 결함을 만들었다.**
       🔬 사용자 지적은 「접힌 둘째 줄에서 배지와 「상세 보기」 사이가 텅 빈다」(375px 에서 **189.4px**)였다.
          범인을 17절의 `margin-left: auto` 로 보고 이 구간에서 `margin-left: 0` 으로 풀었는데,
          그러자 이번엔 **버튼이 배지 바로 옆 왼쪽에 붙어** 다시 지적받았다.
       🔑 **진짜 원인은 아래 `.is-key` 칩의 `flex-grow` 하나였다.** 칩이 첫 줄의 남는 폭을 전부 먹어
          ① **배지 안 오른쪽이 텅 빈 채 늘어나고** ② 밀려난 배지들이 둘째 줄로 접히면서
          ③ 버튼과의 사이가 벌어졌다. **세 증상이 한 원인**이다.
       🔥 그래서 `margin-left: 0` 은 **다시 없앴다** — 버튼은 17절대로 오른쪽 끝에 선다.
          ⚠️ 되돌아오기 쉬운 자리다. **「간격이 벌어진다」를 보면 먼저 `flex-grow` 를 의심할 것.**
       🔥 **버튼을 제 줄로 내려 전체 폭으로 두지 말 것** — 이 카드는 스티키라 줄이 하나 늘면 그만큼
          고정 띠가 두꺼워져 스크롤 내내 화면을 먹는다(17절 머리 주석의 같은 이유). */
    :is(.ds, .quote-content) .ds-ds-chip { display: none; }
    /* 🔴 **`flex-grow` 를 주지 말 것 — 칩이 남는 폭을 먹어 뒤가 텅 빈 채로 늘어난다**
          (2026-08-18 사용자 지적: 배지 안 오른쪽에 빈 공간이 생겼다).
       🕘 한때 `flex: 1 1 auto` 였다. 그러면 **첫 줄의 남는 폭을 이 칩이 전부 먹어**
          업체명이 짧아도 배지가 줄 끝까지 늘어나고, 밀려난 「상세 보기」가 **왼쪽에 붙는다**
          (사용자가 같은 화면에서 두 가지로 지적한 것이 실은 **한 원인**이었다).
       🔑 `0 1 auto` — **늘어나지는 않고 줄어들기는 한다.** 업체명이 길 때의 말줄임은 그대로 산다
          (`min-width: 0` 과 짝이다 — 이게 없으면 flex 항목이 min-content 아래로 못 줄어 넘친다). */
    :is(.ds, .quote-content) .ds-ds-chip.is-key {
        display: inline-block; flex: 0 1 auto; min-width: 0;
        overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }
    /* 🔴 **펼친 내용의 여백은 다른 카드의 `card-body` 와 같아야 한다** (2026-08-18 사용자 재지적).
       🕘 **2026-08-15 에 이미 한 번 고친 자리다** — 그때 넓은 화면(`2rem 2.25rem`)만 맞추고
          **이 좁은 화면 블록에 옛 값(`.35rem 1rem 1rem`)이 그대로 남았다.** 위 여백이 다른 카드의
          1/6 밖에 안 되어 폰에서 보면 이 카드만 내용이 머리에 붙어 보인다.
       🔑 **좌우만 1rem 으로 줄이고 상하는 그대로 둔다** — 다른 카드도 좁은 화면에서 좌우만 덮고
          상하는 넓은 화면 값을 쓴다(576행 `.ds .card > .card-body`). 같은 방식이어야 갈리지 않는다.
       ⚠️ **같은 규격을 고칠 때는 넓은/좁은 두 곳을 함께 볼 것.** 이 파일은 좁은 화면 블록이
          아래쪽에 따로 모여 있어 위만 고치면 폰에서만 옛 모습이 남는다. */
    :is(.ds, .quote-content) .ds-ds-detail { padding: 2rem 1rem; }
    /* 📝 접힌 줄의 요청사항도 **좌우만** 좁힌다(랜드사·여행사 전용 · 옛 `.summary-note` 와 같은 값). */
    :is(.ds, .quote-content) .ds-ds-note { padding: .35rem 1rem; }
}

/* ══════════════════════════════════════════════════════════════════════════
   🔗 **새 창으로 열리는 링크** — 표시는 아이콘 하나이고 **자동으로 붙는다**
      (2026-08-23 사용자 지시: *"새 창으로 열리는 링크의 텍스트 우측에는 새 창 열림을 표시하는
      아이콘을 무조건 붙여야 해. 3앱 모든 페이지에 모든 링크 전수조사 해."*)

   🕘 **어디서 왔나** — `.ds-go-link` 는 원래 **관리자 앱 `menuLevel/Trade.css` 에만** 있었다
      (2026-08-18 구현 #95). 여행사·랜드사에는 같은 자리(값이 링크인 항목)가 있는데 규격이 없어
      `text-primary text-hover-dark fw-bold` 로 각자 칠해져 있었다 → **세 앱 공용인 이 파일로 옮긴다.**
   🔥 **화면 CSS(`menuLevel/*.css`)로 되돌리지 말 것** — 사본이 생기고, 이 저장소에서 사본은 반드시 갈린다.
   🔥 **세 앱에 같은 문장이다 — 한쪽만 고치지 말 것.**
   ══════════════════════════════════════════════════════════════════════════ */

/* ── ↗ 값에 거는 「나가는 링크」 ──────────────────────────────────────────
   🔑 **값 자체가 링크**다(상호·상품명·계약번호). 카드 머리에 두면 반 폭 카드에서 폭이 없어
      **글자가 세로로 한 자씩** 무너진다(2026-08-18 실측으로 확인한 결함).
   🔴 **값 계조(`--ds-text-strong`)를 유지한다 — 파란 링크색으로 바꾸지 말 것.**
      항목 그리드에서 **그 값만 튀어** 세 층(항목명 ▸ 데이터 ▸ 설명)의 위계가 무너진다.
      밑줄도 hover 에서만 준다.
      🔬 2026-08-23 사용자 지적(*"상품명 링크 색상이 왜 파란색이며"*)의 실체가 이것이다 —
         여행사 견적 상세·랜드사 이행 상세·랜드사 정산 신청 상세 세 곳이 `text-primary`
         (흰 배경 대비 **2.90** — `screen-tokens.md` 가 AA 미달로 금지한 클래스)를 쓰고 있었다.
   🔥 `fw-bold` 를 함께 붙이지 말 것 — 22절 세 층에서 **데이터는 400** 이다. */
:is(.ds, .quote-content) .ds-go-link {
    color: var(--ds-text-strong);
    text-decoration: none;
    overflow-wrap: anywhere;
}
:is(.ds, .quote-content) .ds-go-link:hover { color: var(--ds-primary); text-decoration: underline; }
:is(.ds, .quote-content) .ds-go-link:focus-visible { outline: 2px solid var(--ds-primary); outline-offset: 2px; }
/* 🔽 마크업에 아이콘을 손으로 넣어 둔 옛 자리(관리자 `fragments/personMenu.html` 등)를 위한 규격.
      🔑 값은 **아래 자동 규칙과 같아야 한다** — 두 경로로 그려지는 같은 아이콘이 크기·색이 다르면
         한 화면에서 갈린다. 🔥 한쪽만 고치지 말 것.
      🔽 크기는 **이 저장소의 화살표 한 규격**과 같다(`.ds-ds-toggle i` · `.ds-hist-row-all i` = `.8rem`). */
:is(.ds, .quote-content) .ds-go-link > i {
    font-size: .8rem;
    line-height: 1;
    margin-left: .3rem;
    color: var(--ds-text-faint);
    vertical-align: baseline;
}
:is(.ds, .quote-content) .ds-go-link:hover > i { color: var(--ds-primary); }

/* ── ↗ **새 창 표시는 CSS 가 자동으로 붙인다** ────────────────────────────
   🔴 **마크업마다 손으로 넣지 않는다.** 사용자 지시가 「무조건·전 페이지」라서,
      손으로 넣는 방식은 **새로 만드는 링크에서 반드시 빠진다.** 속성 선택자면 새 링크에도 계속 걸린다.
   🔑 **조건부 `th:target` 도 함께 잡힌다** — 푸터 메뉴는 DB 값에 따라
      `th:target="… ? '_blank' : null"` 로 런타임에 갈리는데, CSS 는 **렌더된 속성**을 보므로
      팝업으로 뜨는 메뉴에만 아이콘이 붙는다(마크업 조건을 두 벌 적을 필요가 없다).
   🔑 **기호는 `::before/::after` 가 붙인다**는 이 저장소의 기존 관례 그대로다
      (i18n 문구에 기호를 넣지 않는다 — `.ds-note-warn` 의 `※`, `.ds-hint` 의 ⓘ 와 같은 방식).
   🔥 **글자로 `↗` 를 찍지 말 것**(관리자 `personMenu.html` 주석의 명시적 금지).
   ♿ **`content` 의 `/ ""` 는 「읽어 줄 대체 텍스트 없음」**이다 — 이것이 없으면 화면 낭독기가
      **사설 영역 글자(U+F1C5)를 그대로 읽어** 뜻 없는 소리가 난다.
      ⚠️ 그래서 이 아이콘은 **눈으로만 전달된다.** 낭독기에도 알리려면 링크마다
      `aria-label`(i18n 키 `label.common.openNewWindow`)이 필요하고 그건 마크업 작업이다.
   🚫 **뺀 자리와 이유**
      · `:has(.bi-box-arrow-up-right)` — **이미 같은 아이콘이 들어 있는 링크**(세 앱 14곳).
        안 빼면 **아이콘이 두 개**가 된다. 값이 링크 밖이고 아이콘만 링크인 옛 구조도 여기에 걸린다.
      · `:has(> img)` · `:has(> svg)` — **글자가 없는 그림 링크.** 「텍스트 우측」이 성립하지 않는다.
      · `.stretched-link` — **카드 전체를 덮는 투명 링크.** 아이콘을 붙이면 카드 왼쪽 위 구석에
        정체불명의 글리프가 뜬다(그 링크에는 글자가 없다).
      · `.ds-ext-icon-off` — **옵트아웃.** 위 셋으로 안 걸러지는 예외가 나오면 이 클래스를 붙인다.
        🔥 붙일 때는 **왜 뺐는지를 그 자리에 주석으로** 남길 것.
   ⚠️ **아이콘 폰트가 안 실리면 빈 네모가 뜬다** — `bootstrap-icons` 는 벤더 번들이 싣는다.
      🔥 폰트 이름·코드포인트(`\F1C5` = `bi-box-arrow-up-right`)를 바꾸지 말 것.
   ⚠️ **`.ds` 로 스코프하지 않는다** — 푸터·약관 팝업 링크는 `#kt_content.ds` **밖**이다.
      지시가 「모든 페이지의 모든 링크」이므로 스코프를 좁히면 그 자리가 통째로 빠진다. */
a[target="_blank"]:not(.ds-ext-icon-off):not(.stretched-link):not([href*="/file/download/"]):not([href*="/pdf?"]):not([href$="/pdf"]):not(:has(.bi-box-arrow-up-right)):not(:has(> img)):not(:has(> svg))::after {
    content: "\F1C5" / "";
    display: inline-block;
    font-family: "bootstrap-icons";
    font-style: normal;
    font-weight: 400;
    font-size: .8rem;
    line-height: 1;
    margin-left: .3rem;
    color: var(--ds-text-faint);
    vertical-align: baseline;
    text-decoration: none;
}
/* 값 링크는 hover 에서 아이콘도 함께 따라온다(위 `> i` 규칙과 같은 동작). */
:is(.ds, .quote-content) .ds-go-link:hover::after { color: var(--ds-primary); }

/* ── ⤓ **파일이 받아지는 링크에는 아이콘을 붙이지 않는다** ────────────────
   🔴 (2026-08-23 사용자 결정) 아이콘을 보면 **「새 창으로 이동한다」고 예상**하는데 실제로는
      **파일이 받아진다.** 탭이 열렸다 곧 닫히거나 아예 안 열린다 → **아이콘이 거짓말을 한다.**
   🔑 **판정은 경로다 — 화면마다 클래스를 손으로 붙이지 않는다**(그러면 새 링크에서 빠진다).
      경로가 곧 서버의 `Content-Disposition` 이라 **화면과 서버가 같은 근거**를 본다:
        · `/file/download/…` → `attachment` (`FileController`/`FileDownloadController` 의 `inline=false` 경로).
          🔑 마이페이지 사업자등록증·통장사본·자격증빙, 정산 증빙은 **매퍼가 이 경로를 만들어 내린다**
             (`CONCAT('/file/download/common/', …)`) — 그래서 서버가 만든 URL 도 자동으로 걸린다.
        · `/trade/{quote,reserve,contract}/pdf?…` → `attachment; filename="….pdf"` (실측: 세 앱 6개 매핑 전부).
   ✅ **반대로 `/file/view/…` 는 아이콘이 맞다** — 그쪽은 `inline` 이라 **새 탭에 그림이 뜬다**
      (업체 서류의 확장자 칩 미리보기·문의 첨부 이미지). 「받아진다」가 아니라 「보여준다」다.
      🔥 이 둘을 한 규칙으로 묶지 말 것 — 경로가 다른 이유가 바로 이것이다.
   ⚠️ **새 다운로드 경로를 만들면 위 선택자의 `:not()` 목록에 더할 것.** 안 더하면 그 링크만 거짓 아이콘을 단다.
   🔴 **별도 규칙으로 `content: none` 을 덮어쓰려 하지 말 것 — 실제로 해 봤고 졌다.**
      🔬 `a[target="_blank"][href*="/pdf?"]::after{content:none}` 은 (0,2,1) 인데 위 본 규칙은
         `:not()` 안의 클래스·`:has()` 가 각각 세어져 **(0,4,2)** 라 **본 규칙이 이긴다.**
         브라우저에서 다운로드 버튼에 아이콘이 그대로 남아 있는 것으로 드러났다.
      🔑 그래서 **덮어쓰지 않고 본 규칙의 조건에 합쳤다** — 규칙 두 개가 겹치지 않으면 싸울 일이 없다
         (`screen-layout.md`: *"`!important` + 선택자 늘리기로 이기려 하지 마라 — 겹치지 않을 규칙을 써라"*).
   📌 「파일이 받아진다」를 낭독기에 알리는 것은 **이번 범위 밖**이다(보고에 적었다). */

/* ── 맨 아래 공통 버튼 카드 ── */
:is(.ds, .quote-content) .ds-detail-actions { margin-top: 1.5rem; }

/* ── 17-1) 🔴 **상세 작업대의 카드 간격은 이 세 줄이 정한다** (2026-08-28 · 대기목록 P-2) ──
   > 사장님 지적: *"`/admin/trade/contract/view` 페이지 보면 카드간 세로 간격이 아예 없어서 카드끼리 붙어있어."*

   🔬 **실측(2026-08-27~28)**: 그 화면의 최상위 카드 15~16개 중 **세로로 이어지는 8~9쌍에서 5~7쌍이 `gap 0`**
      이었다. 여백을 가진 카드는 `.ds-detail-summary`(위 24px 규칙) **하나뿐**이었다.
   🔑 **원인은 「빠뜨린 여백」이 아니라 「여백을 줄 주체가 없다」는 것**이다 — 그 부모가 `display:block`,
      `row-gap:normal` 이라 간격이 **카드마다 붙인 `margin-bottom`** 에 달려 있었다.
   🔴 **카드마다 `margin-bottom` 을 붙여 메우지 말 것** — 새 카드를 넣는 사람이 또 빠뜨려 지금 상태가
      다시 온다(배치 규칙 ⑤ *"자식에 `margin-bottom` 으로 간격을 주지 마라 — 부모가 `gap` 으로 정한다"*).

   🕘 **2026-08-23 에는 이 세 줄이 `menuLevel/TravelProduct.css`(관리자) · `menuLevel/Trade.css`(랜드사·
      여행사)에 있었고, 그때 「공용 파일에 두지 말 것」이라고 적어 두었다. 그 판단을 뒤집은 것이다.**
   🔬 **왜 뒤집었나** — 그때 옮긴 것은 **뒤 두 줄(margin 되돌리기)뿐**이고 `gap` 은 menuLevel 에 남겨
      두었다. 그래서 `gap` 이 없는 화면(관리자 `trade/contractView.html` — `.quote-workspace` 마크업을
      쓰면서 `TravelProduct.css` 를 **`<link>` 로 안 부른다**)이 **간격을 통째로 잃었다.**
      🔑 **짝을 갈라 놓은 것이 문제였지 자리가 문제가 아니었다** → **셋을 함께** 이 파일로 옮긴다.
      그러면 `.quote-workspace` 마크업을 쓰는 순간 **어느 화면이든 gap 이 따라온다.**
   🔥 **다시 `menuLevel/*.css` 로 내리지 말 것** — 그러면 「어느 CSS 를 부르느냐」에 간격이 다시 매달린다.
      🔬 그 함정은 **주석으로는 안 잡힌다** — 그 템플릿의 주석에는 `TravelProduct.css` 가 적혀 있어서
      파일 이름을 검색하면 **불러오는 것처럼 보인다**(`<link>` 만 세야 한다).

   📐 **값은 `--ds-sp-5`(1.5rem = 기본 24px)** — 새로 고른 값이 아니라 **이미 그 화면에 살아 있던 24px**
      (`.ds-detail-summary` 의 `margin-bottom`)이고, `gap` 이 없는 상세 화면(업체·회원·정산·분쟁·문의·
      시스템 오류·고객센터)이 **전부 쓰고 있는 값**이다. 🕘 작업대만 `1.25rem`(20px)로 갈려 있었다 —
      척도에 없는 값이라 되돌리지 말 것(배치 규칙 ①: *"여백은 척도에서 고른다"*).
   ⚠️ **`>`(직계 자식)를 지우지 말 것** — 편집 열(`.editor-col`)이나 드로워 안에 같은 클래스가 오면
      그 부모는 `gap` 을 주지 않으므로 그때는 위 17절 값(24px)이 맞다. */
.quote-workspace { width: 100%; display: flex; flex-direction: column; gap: var(--ds-sp-5); }
.quote-workspace > .ds-detail-summary { margin-bottom: 0; }
.quote-workspace > .ds-detail-actions { margin-top: 0; }

/* ── 17-2) 🔴 **읽기 전용 항목 표의 숫자·금액 칸은 줄바꿈하지 않는다** (2026-09-01 · 항목 19) ──
   > 사장님 지적: *"테이블 안에서 항목명이 아닌 컬럼에서 줄바꿈이 일어난다"* ·
   > *"768px에서 수량과 단가 공급가액이 두줄로 표기된 큰 오류가 있어"*

   🔬 **실측(768px · 여행사 견적/계약 상세 · 표 611px)**: 「**수량**」 머리글이 안쪽 27.8px 에서
      「수 / 량」으로 갈렸고, 단가 「742,000 KRW」가 숫자와 통화로 갈렸다. 계약 표에서는 공급가액까지
      **세 칸이 전부 두 줄**이었다 — 견적 표에는 마지막 칸에만 `.trade-amount`(nowrap)가 붙어 있었고
      계약 표에는 그것조차 없었기 때문이다.
   🔴 **숫자와 통화 코드가 갈리면 다른 값처럼 읽힌다.** 「742,000」 / 「KRW」는 금액이 아니다.

   🔑 **폭을 숫자로 박지 않는다 — `nowrap` 이 곧 열의 바닥값이다.**
      이 표들은 `table-layout:auto` 라 칸의 min-content 폭을 지켜 준다 → `nowrap` 을 걸면 그 칸이
      **머리글과 값 중 긴 쪽만큼** 저절로 확보된다(항목 19 처방 ②③이 한 줄로 함께 풀린다).
      🔥 `min-width` 를 px 로 적지 말 것 — 편집 표의 값과 갈리고, 영문·글자 크기 3단계에서 또 깨진다.

   🎯 **범위를 가르는 세 조건**(전부 「성격」이지 화면 이름이 아니다):
      ① **`.text-end` 인 칸만** — 이 표들에서 우측 정렬은 곧 「숫자·금액 칸」이다. 글 칸(항목명·구분·
         사유)은 `text-end` 가 아니라 저절로 빠진다.
      ② 🔴 **첫 칸은 뺀다** — 첫 칸은 그 행의 **이름**이라 줄바꿈이 필요하다.
         🔬 취소 수수료 규정 표의 첫 칸이 `text-end` 인데 값이 「출발 30일 전 까지」라는 **문장**이다
            (영문은 더 길다). 여기에 `nowrap` 을 걸면 좁은 그릇에서 **가로 스크롤**이 생긴다.
      ③ 🔴 **편집 표(`--edit`)는 뺀다** — 그쪽은 `table-layout:fixed` 라 열 폭이 미리 정해져 있어
         `nowrap` 이 **칸을 넘쳐 잘린다**(폭이 늘지 않는다). 편집 표의 열 폭은 규칙 C11 이 따로 정한다.

   📌 `.trade-amount`(`menuLevel/Trade.css`)는 그대로 둔다 — 이 규칙과 값이 같아 충돌하지 않고,
      그쪽은 굵기·정렬까지 함께 주는 별개의 표식이다.
   🔑 **자리가 여기인 이유는 17-1 절과 같다** — 이 표는 세 앱 여러 화면에 있고 화면마다 부르는
      `menuLevel/*.css` 가 다르다. 🔥 **`menuLevel/*.css` 로 내리지 말 것**(그러면 같은 표가
      「어느 CSS 를 부르느냐」에 따라 다르게 접힌다). */
.quote-workspace .quote-items-table:not(.quote-items-table--edit) > thead > tr > th.text-end:not(:first-child),
.quote-workspace .quote-items-table:not(.quote-items-table--edit) > tbody > tr > td.text-end:not(:first-child),
.quote-workspace .quote-items-table:not(.quote-items-table--edit) > tfoot > tr > td.text-end:not(:first-child) {
    white-space: nowrap;
}

/* ── 17-3) 🔴 **요약 카드 펼침(`.ds-ds-detail`) 안의 「항목명 ↔ 값」 표** (2026-09-01 · 항목 63) ──
   > 사장님 지적: *"세로로 길게 쌓이는 것 보다 항목명 데이터 의 구조때문에 데이터 영역이 너무 좁다는
   >  게 더 큰 문제야."* · *"1920은 항목명 칸이 왜 이렇게 큰거야?"* · *"375px, 430px 에 데이터 좌측에 여백이 있다고"*

   🔬 **결함**: 첫 칸에 `w-150px` 이 박혀 **어느 폭에서나 150px 고정**이었다. 375px 에서 표 폭 302 중
      항목명 150 · 값 152 — **절반을 두 글자짜리 라벨이 먹고** 공지 제목이 152px 안에서 **4줄**로 접혔다.
      이 표들의 가장 긴 라벨은 「등록일」·「문의 유형」 서너 글자다.
   🔑 **숫자를 다시 박지 말 것** — 영문 라벨(`Registered`·`View count`)이 한글보다 길어 값을 고정하면
      그날로 다시 어긋난다. **표가 내용대로 재게** 둔다.
   🎯 **대상은 `.ds-ds-detail` 안의 표뿐**이다. 🔬 전수(2026-09-01 · 세 앱 템플릿): 그 안의 표 **11개가
      전부 2칸(항목명·값)** 짜리였다(여행사 5 · 랜드사 6 · 관리자 0). 그래서 아래 `1% / 99%` 가 안전하다.
      🔥 **3칸 이상인 표를 이 영역에 넣지 말 것** — 넣으려면 이 규칙의 선택자부터 좁혀야 한다.
      📌 이 앱에는 아직 해당 표가 **0개**다 — 그래도 **세 앱에 같은 문장**으로 둔다(안 쓴다고 지우면
         이 앱이 그 구조를 처음 쓰는 날 혼자 갈린다).
   🕘 **2026-09-01 에 여행사 `menuLevel/Customer.css` 에서 여기로 옮겼다.** 거기 두면 그 CSS 를
      `<link>` 로 부르는 화면만 고쳐지고 같은 구조를 쓰는 다른 앱은 그대로 남는다 —
      **17-1 절이 겪은 것과 같은 함정**이다. 🔥 **`menuLevel/*.css` 로 다시 내리지 말 것.**
   📌 **마크업에서 `w-150px` 을 함께 걷어내야 한다** — 그 유틸리티는 `width:150px !important` 라
      아래 규칙을 **이긴다**(CSS 만 고치면 아무 일도 일어나지 않는다). */

/* ① 항목명은 자기 글자만 · 남는 폭은 값이 전부 가져간다.
      🔬 **`width: max-content` 만으로는 부족하다** — 표가 남는 폭을 첫 칸에도 나눠 줘
         1920px 에서 항목명 칸이 **219px** 까지 자랐다.
      ✅ `1%` + 값 칸 `99%` 라야 첫 칸이 내용 폭으로 줄고 남는 폭을 값이 다 가져간다(실측 56 / 1444px).
      📌 새 수법이 아니다 — 대화상자 표의 마지막 열이 쓰던 `width:1%` + `nowrap` 과 같다.
      🔥 항목명에 `padding-right` 를 더하지 말 것 — 값 왼쪽에 여백이 생긴 것처럼 보인다
         (`1.5rem` 을 줬다가 사장님이 짚으셨다). 표의 원래 칸 여백으로 충분하다. */
.ds-ds-detail table td:first-child { width: 1%;  white-space: nowrap; }
/* 🔤 **머리 칸(`th`)도 같은 열이라 함께 한 줄로 둔다** (2026-09-17 여행사 1회차 B3-06)
   🔬 상품 상세 요금표만 이 영역에서 `<thead>` 를 갖는데, 열 폭이 위 `1%` 로 내용 폭까지 줄면서
      머리 「요금 유형」「Price Type」이 **768·992·1200·1920 전부 두 줄**(th 높이 63px)로 접혔다 — 본문 칸만 `nowrap` 이었다.
   📱 좁은 폭(≤767.98px)은 카드형이라 `thead` 가 감춰진다 — 이 줄은 넓은 화면에서만 일한다. */
.ds-ds-detail table th:first-child { white-space: nowrap; }
.ds-ds-detail table td:last-child  { width: 99%; }

/* ② 좁은 폭(≤767.98px)은 항목명을 값 **위로** 올려 값이 전폭을 쓰게 한다.
      🕘 2026-09-07: 문턱을 **575.98 → 767.98** 로 합쳤다(사장님 확정 「구간 수를 줄인다」).
      🔬 375·430 에서 값 칸이 152·207 → **299·353px 전폭**, 제목 줄 수 4 → 2.
      🔴 **칸 좌우 여백을 0 으로 빼는 줄을 지우지 말 것** — 안 빼면 값에 `padding-left: 9px`
         이 남아 **항목명보다 안쪽으로 들여쓰기**된다(사장님이 그 어긋남을 따로 짚으셨다). */
@media (max-width: 767.98px) {
    .ds-ds-detail table,
    .ds-ds-detail table tbody,
    .ds-ds-detail table tr        { display: block; }
    .ds-ds-detail table td        { display: block; width: auto;
                                    padding-left: 0; padding-right: 0; }
    .ds-ds-detail table td:first-child { padding-bottom: .1rem; }
    .ds-ds-detail table td:last-child  { padding-top: 0; }
}
:is(.ds, .quote-content) .ds-detail-actions .card-body {
    display: flex; align-items: center; justify-content: space-between;
    flex-wrap: wrap; gap: .75rem;
    padding: 1rem 1.5rem;
}
/* 오른쪽 보조 버튼 묶음. 없으면 목록 버튼만 왼쪽에 남는다(빈 자리를 만들지 않는다). */
:is(.ds, .quote-content) .ds-detail-actions .ds-da-end { display: flex; align-items: center; gap: .75rem; margin-left: auto; }
/* 📏 **이 카드의 버튼은 「단독 버튼 표준 높이」다** (2026-08-15 사용자 지적, 구현 #58:
   *"목록과 업체 정보 수정 버튼이 너무 뚱뚱하다"*).
   🔬 실측(수정 전): **높이 52.4px · 패딩 13px 25px · 글자 17.6px**. 원인은 카드 여백이 아니라
      **버튼에 크기 클래스가 없어 Metronic 기본 `.btn`(1.1rem 글자)이 그대로 나온 것**이다.
      ⚠️ 2-1 절의 `.9rem` 푸터 여백 규격은 여기에 **적용되지 않는다** — 이 카드는 `.card-footer` 가
         아니라 **`.card-body`** 를 쓴다(그 규격을 고쳐서 될 일이 아니었다).
   🔑 값은 **`--ds-btn-h`(단독 버튼 2.2rem)** 다 — 목록·수정처럼 **혼자 서는 버튼**의 표준이고
      9절 `.ds-lc-foot .btn` 이 이미 같은 토큰을 쓴다(새 값을 지어내지 않았다).
      ⚠️ **px 로 박지 말 것** — 글자 크기 3단계 토글이 root 기준 rem 이라 px 면 **글자가 상자를 뚫는다**.
   📌 영향 범위는 **`.ds-detail-actions` 를 쓰는 상세 화면 전부**다(관리자 앱 7화면 — 업체·회원·거래·
      정산·분쟁·문의·시스템 오류). 랜드사·여행사 앱에는 이 클래스를 쓰는 화면이 **없다**(거래 작업대는
      `Trade.css` 의 자기 규격을 쓴다). */
:is(.ds, .quote-content) .ds-detail-actions .btn {
    height: var(--ds-btn-h);
    padding: 0 1rem !important;   /* Metronic `.btn:not(…)×6` 이 (0,7,0) 이라 !important 가 필요하다 */
    display: inline-flex;
    align-items: center;
    font-size: var(--ds-font-body);
    line-height: 1;
}

/* ===================================================================
   18) 미리보기 본문 · 사용자 메뉴 — **글자를 키우자 드러난 어긋남** (2026-08-13 사용자 지적)

   🔑 셋 다 **원래 어긋나 있었는데 13px 에서는 티가 안 나던 것**이다. 글자 크기를 올리는 변경이
      「없던 결함을 만든 것」이 아니라 **있던 결함을 드러냈다** — 되돌리지 말고 여기서 고친다.
   =================================================================== */

/* ① 미리보기 카드: **헤더와 본문의 좌우 패딩이 달랐다.**
   🔬 실측(2026-08-13, DMC 상품 상세): 헤더 36px vs 본문 20px → 제목이 본문보다 16px 안쪽.
      「카드 헤더와 카드 본문의 좌우 패딩은 항상 같게」 규칙 위반이다.
   ⚠️ `!important` 는 Bootstrap 의 `.p-5{padding:1.25rem!important}` 를 이기려는 것이다.
   📌 `.quote-invoice`(견적서 미리보기)는 **일부러 제외**한다 — 카드 안에 놓인 '종이 한 장' 이라
      바깥 여백(margin:1rem)이 그 생김새의 일부다. */
.quote-preview-scroll-area > .p-5 {
    /* 🔑 `--ds-scrollbar-w` 만큼 빼는 이유 — 아래 ② 의 `scrollbar-gutter` 가 **본문에만** 걸려
       본문이 좌우로 그만큼 더 들어간다. 빼지 않으면 어긋남의 **방향만 뒤집힌다**
       (🔬 실측: 제목 1485 / 본문 1495 — 고치기 전과 크기가 같고 부호만 반대였다).
       값은 `common.js` 가 부팅 때 재서 넣는다(브라우저·OS 마다 다르고 Mac 오버레이는 0 이다).
       ⚠️ 폴백을 쓰지 않는다 — 기본값은 0-3 절에 **선언**돼 있다. */
    /* 🔴 **2026-08-24 두 값을 `--ds-card-x`(17 / 30px)로 바꿨다** — H5 확정.
       🔑 머리·본문의 좌우 끝선은 하나여야 한다 — 머리가 `--ds-card-x` 로 갔으므로 여기도 같은 값을 쓴다.
       🔥 `2.25rem` 으로 되돌리지 말 것 — 그날로 미리보기 본문만 45px 로 밀린다. */
    padding-left: calc(var(--ds-card-x) - var(--ds-scrollbar-w)) !important;
    padding-right: calc(var(--ds-card-x) - var(--ds-scrollbar-w)) !important;
}

/* ② 세로 스크롤바가 **오른쪽만** 먹어 좌우가 어긋났다(🔬 실측 좌 20px / 우 20+10px).
   양쪽에 같은 자리를 예약해 대칭으로 만든다.
   ⚠️ `stable` 만 쓰면 오른쪽에만 예약돼 지금과 똑같다 — **`both-edges` 가 핵심**이다.

   🔥 **`overflow-y: auto` 를 함께 줘야 한다 — 빼면 위 ① 의 보정이 거꾸로 작동한다.**
      `scrollbar-gutter` 는 **스크롤 컨테이너에만** 걸린다. DMC 는 `Trade.css` 가 `overflow-y:auto`
      를 주지만 **Admin 상품 검수의 미리보기는 `visible` 이라 거터가 0 이었다** — 그런데 ① 은
      거터만큼 패딩을 빼므로 본문이 헤더보다 10px 밖으로 나갔다(🔬 실측: 제목 341 / 본문 331).
      ⚠️ 높이 제한이 없어도 `stable` 은 자리를 예약한다 — 그래서 스크롤이 안 생기는 화면에서도 값이 같다.
      ⚠️ 이 줄을 지우면 **거터가 걸리는 화면과 안 걸리는 화면이 갈린다.** */
.quote-preview-scroll-area {
    overflow-y: auto;
    scrollbar-gutter: stable both-edges;
}

/* 📏 **PC 의 미리보기 스크롤바를 6px 로 줄인다** (2026-08-21 구현 #48 사용자 결정).
   🔬 실측: 기본 **15px** · `scrollbar-width:thin` 10px · `::-webkit-scrollbar{width:6px}` **6px**.
   🔥 **진짜 오버레이는 CSS 로 안 된다** — `overflow-y: overlay` 는 크롬이 폐기해서 계산값이
      `auto` 로 바뀌고 **자리를 15px 그대로 먹는다**(실측). Metronic 의 `hover-scroll-overlay-y` 도
      평소엔 `overflow:hidden`(스크롤 자체가 막힘)이라 이 자리엔 못 쓴다.
      📌 외부 라이브러리(OverlayScrollbars·SimpleBar)라야 자리 0 이 되는데, 이 판은 펼침·접힘으로
         **높이가 계속 바뀌어** 재계산을 걸어야 하고 세 앱에 얹어야 한다 — 6px 로 충분하다고 판단했다.
   🔑 **`--ds-scrollbar-w` 를 판 안에서만 6px 로 다시 정의**하면 머리·본문 여백 식이 **그대로 따라온다**
      (둘 다 이 변수를 쓴다). 값을 두 곳에 적지 않는 것이 요점이다.
   🔴 **`@media (hover:hover) and (pointer:fine)` 로 감싼다 — 터치 기기는 건드리지 않는다.**
      폰은 스크롤바가 원래 자리를 안 먹는데(오버레이), 거기에 `::-webkit-scrollbar` 를 지정하면
      **자리를 먹기 시작해** 지금 맞는 여백이 도로 어긋난다. */
@media (hover: hover) and (pointer: fine) {
    .preview-col { --ds-scrollbar-w: 6px; }
    .quote-preview-scroll-area::-webkit-scrollbar { width: 6px; }
    .quote-preview-scroll-area::-webkit-scrollbar-track { background: transparent; }
    .quote-preview-scroll-area::-webkit-scrollbar-thumb {
        background: var(--ds-line);
        border-radius: 3px;
    }
}

/* -------------------------------------------------------------
   div 스크롤바를 **전 폭에서 같게** 한다 (2026-08-24 사용자 지시:
   *"div 에 사용되는 스크롤바는 1920 에 사용하는걸로 통일해"*).

   🔬 Metronic 번들은 이 규격을 `@media (min-width:992px)` 안에만 둔다 —
      그래서 **991px 이하에서는 브라우저 기본 스크롤바**(굵고 진하다 · 자리 15~17px)가 나와
      폭에 따라 스크롤바 모양이 달라졌다(약관·회원가입 두 화면에서 확인했고, 전 앱 공통이다).
   🔑 값은 **2026-08-21 구현 #48 의 미리보기 스크롤바 규격 그대로**다(위 `.quote-preview-scroll-area`).
      폭 6px · `var(--ds-line)`(테마 인식 — 다크에서 자동으로 뒤집힌다) · 반경 3px · 트랙 투명.
      **새로 고른 값이 없다.** Metronic 의 `rgb(239,242,245)` 는 하드코딩 라이트라 다크에서 굳는다.
   🔴 **`@media (hover:hover) and (pointer:fine)` 로 감싼다 — 터치 기기는 건드리지 않는다.**
      폰은 스크롤바가 원래 자리를 안 먹는데(오버레이), 거기에 `::-webkit-scrollbar` 를 지정하면
      **자리를 먹기 시작해** 여백이 도로 어긋난다. 이것이 위 미리보기 규칙과 같은 이유다.
   🔬 선택자 `div,ol,pre,span,ul` 은 **Metronic 번들이 992px 위에서 이미 쓰는 것 그대로**다 —
      이 저장소에서 이 넓이의 선택자는 검증된 것이다(여기서는 그 넓이를 전 폭으로 넓히기만 한다).
   📌 위 `.quote-preview-scroll-area` 규칙의 `::-webkit-scrollbar` 부분은 이제 이 규칙과 겹치지만,
      그 규칙의 `.preview-col{--ds-scrollbar-w:6px}`(여백 계산용 변수)는 여기 없으므로 지우지 말 것. */
@media (hover: hover) and (pointer: fine) {
    div, ol, pre, span, ul {
        scrollbar-width: thin;
        scrollbar-color: var(--ds-line) transparent;
    }
    div::-webkit-scrollbar, ol::-webkit-scrollbar, pre::-webkit-scrollbar,
    span::-webkit-scrollbar, ul::-webkit-scrollbar { width: 6px; height: 6px; }
    div::-webkit-scrollbar-track, ol::-webkit-scrollbar-track, pre::-webkit-scrollbar-track,
    span::-webkit-scrollbar-track, ul::-webkit-scrollbar-track { background: transparent; }
    div::-webkit-scrollbar-thumb, ol::-webkit-scrollbar-thumb, pre::-webkit-scrollbar-thumb,
    span::-webkit-scrollbar-thumb, ul::-webkit-scrollbar-thumb {
        background: var(--ds-line);
        border-radius: 3px;
    }
}

/* ③ 사용자 메뉴 — **이메일 길이에 따라 가로가 늘어난다.**
   🔥 예전에는 `w-275px` **고정**이었다(min-width 0). 안쪽 폭이 195px 밖에 안 남아
      글자 크기 버튼(198px)이 **3줄로 접혔다**(🔬 실측). 긴 이메일은 잘렸다.
   ⚠️ 최대치를 두는 이유: 이메일은 얼마든지 길어질 수 있어 상한이 없으면 메뉴가 화면을 덮는다.
      상한을 넘으면 말줄임으로 끊는다. */
.ds-user-menu {
    width: auto !important;
    min-width: 275px;
    max-width: min(32rem, 92vw);
}
/* ⚠️ **`min-width: 0` 이 핵심이다.** flex 항목의 기본값은 `min-width:auto` 라 내용보다 작아지지 않아
   말줄임이 **아무 일도 하지 않는다** — 상한(32rem)을 넘는 긴 이메일이 메뉴 밖으로 삐져나온다
   (🔬 실측: 102자 이메일에서 메뉴 512px 안에 824px 짜리 링크가 들어 있었다). */
.ds-user-ident { min-width: 0; }
.ds-user-email {
    display: block;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ④ 글자 크기 3단계 — **줄바꿈 금지** + 각 버튼은 **그 크기가 적용됐을 때의 크기**로 보인다.
   🔑 **여기만 px 를 쓴다.** 0-0 절의 「px 를 박지 말 것」에 대한 의도된 예외다 —
      이 숫자들은 레이아웃 치수가 아니라 **절대 크기 견본**이다. `rem` 으로 쓰면 「크게」를 고른
      상태에서 세 버튼이 전부 한 단계씩 커져 **견본이 거짓말을 한다.** */
.ds-font-scale { flex-wrap: nowrap !important; }
.ds-font-scale > .btn { white-space: nowrap; line-height: 1.25; }
.ds-font-scale > .btn[data-font-scale="normal"] { font-size: 16px; }
.ds-font-scale > .btn[data-font-scale="large"]  { font-size: 18px; }
.ds-font-scale > .btn[data-font-scale="xlarge"] { font-size: 20px; }
@media (max-width: 767.98px) {
    /* 모바일은 root 가 1px 작다(0-0 절) — 견본도 그 값이어야 거짓말이 아니다 */
    .ds-font-scale > .btn[data-font-scale="normal"] { font-size: 15px; }
    .ds-font-scale > .btn[data-font-scale="large"]  { font-size: 17px; }
    .ds-font-scale > .btn[data-font-scale="xlarge"] { font-size: 19px; }
}


/* ══ 18) 탭 — **밑줄형 하나로 통일** (2026-08-13 사용자 결정, 구현 #42) ══════════════
   🔥 정본은 `nav nav-tabs nav-line-tabs nav-line-tabs-2x mb-0 border-0` + 링크 `px-3 py-2` 다
      (「🎚 선택된 목록 항목」 절의 탭 규칙과 같은 것이다). 그런데 세 앱 템플릿에
      **7가지 변종 23곳**이 흩어져 같은 성격의 탭이 화면마다 다르게 보였다.
   🔬 실측(2026-08-13, Admin 거래 목록 = 정본):
        · 글자가 **13px 고정**이다 — Metronic 본문 13px 을 물려받아
          root 16px 상향(0-0 절)의 효과를 **탭만 못 받았다.**
        · 활성·비활성 **글자색이 같다**(둘 다 `#a1a5b7`). 구분은 파란 밑줄 하나뿐이고,
          그 회색은 흰 카드 위 대비가 **2.45**(AA 기준 4.5 미달)다.
   🔑 그래서 **마크업을 정본으로 맞추는 것만으로는 부족하다** — 정본 자체를 여기서 보강한다:
        ① 글자를 `rem` 로 — root(기본 16px, 사용자 토글 18·20px)를 따라간다.
        ② **활성 탭은 글자도 파랑**(밑줄과 같은 색) — 사용자 결정. 밑줄 하나에만 기대지 않는다.
   ⚠️ **`.nav-stretch` 는 제외한다** — 마이페이지의 화면 대제목급 탭(18.4px/600)은
      성격이 달라 **예외로 둔다**(사용자 결정). 여기 규칙이 닿으면 그 위계가 사라진다.
   ⚠️ **`.ds` 로 스코프하지 않는다** — DMC·Agency 의 거래 화면은 `#kt_content` 에 `ds` 가 없고
      (`quote-content`), 정본 탭이 거기에 있다. `.ds` 를 붙이면 **그 화면들만 옛 모습으로 남는다.**
      이 파일의 유일한 비`.ds` 규칙이다 — 선택자를 `nav-line-tabs` 로 좁혀 범위를 막았다.
   ⚠️ **굵기(`font-weight`)는 건드리지 않는다** — 활성 탭만 굵어지면 탭을 옮길 때마다
      **글자 폭이 변해 옆 탭이 밀린다.** 색만으로 충분히 구분된다. */
.nav-line-tabs:not(.nav-stretch) .nav-link {
    font-size: .95rem;
}
.nav-line-tabs:not(.nav-stretch) .nav-link.active {
    color: var(--ds-primary);
}

/* 🔴 **탭 사이 간격은 부모의 `column-gap` 하나가 정한다 — 자식에 `margin` 으로 주지 않는다**
   (2026-08-23 사용자 지적: *"768, 900에서 카테고리가 2줄로 나올 때 2번째 줄 왼쪽 첫 번째에
   좌측 여백이 있어서 줄이 맞지 않아."*)
   🔬 실측(랜드사 견적 목록, 탭 9개): 900px 에서 첫 줄 첫 칸 `left` **76** / 둘째 줄 첫 칸 **92** —
      **16px 밀렸다.** 768px 도 같다. `<li>` 는 양쪽 다 76 이고 밀린 것은 그 안의 `<a>` 다.
   🔥 범인은 벤더 규칙 두 줄이다 — `.nav-line-tabs .nav-item .nav-link{margin:0 1rem}` 으로 간격을 주고
      `.nav-item:first-child .nav-link{margin-left:0}` 로 **첫 칸만** 지운다. `:first-child` 는
      **목록 전체의 첫 칸**이라 **줄바꿈된 둘째 줄 첫 칸에는 안 걸린다.**
   🔑 그래서 margin 을 걷고 `column-gap` 으로 옮긴다 — **몇 줄로 접히든 왼쪽 끝선이 하나**다.
      배치 규칙 ⑤(`.claude/rules/screen-layout.md`)의 *"자식에 margin 으로 간격을 주지 마라 —
      부모가 gap 으로 정한다"* 그대로이고, 2026-08-18 배지 간격에서 **이미 같은 결론**을 냈다
      (*"margin 은 접힌 줄까지 들여쓴다 → column-gap 이 맞았다"* — `silent-traps.md`).
   📐 값은 척도의 **`--ds-sp-6`(2rem = 32px)** — 새로 고른 값이 아니라 **벤더가 주던 간격(16+16)과
      같은 값**이라 첫 줄 모양이 그대로다. 🔥 생 숫자로 되돌리지 말 것.
   ⚠️ **`gap` 단축을 쓰지 말 것 — `column-gap` 이어야 한다.** 단축을 쓰면 접힌 줄 사이가 32px 로 벌어지고,
      `.ds-filter-bar`(.25rem)와 `.pv-langtabs`(.25rem)가 각자 정해 둔 **세로 간격까지 덮는다.**
   ⚠️ **`.nav-stretch` 는 여기서도 제외**다(마이페이지 대제목급 탭 — 위 규칙과 같은 이유).
   🔢 특이도: 벤더의 `.nav-line-tabs .nav-item .nav-link` 는 (0,3,0), `:first-child`/`:last-child` 쪽은
      (0,4,0) 이다. 아래 링크 규칙은 **(0,4,0)** 이라 (0,3,0) 을 이기고, (0,4,0) 인 두 규칙과는
      **값이 같아**(margin-left/right: 0) 겨룰 일이 없다 → **`!important` 가 필요 없다**(배치 규칙 ④).
      컨테이너 쪽은 `.ds .ds-filter-bar .nav{gap:.25rem}`(0,3,0)와 겨루므로 **(0,4,0) 짜리를 함께** 적는다
      — 로드 순서에 기대지 않기 위해서다. */
.nav-line-tabs:not(.nav-stretch),
.ds .ds-filter-bar .nav-line-tabs:not(.nav-stretch) {
    column-gap: var(--ds-sp-6);
}
.nav-line-tabs:not(.nav-stretch) .nav-item .nav-link {
    margin-left: 0;
    margin-right: 0;
}


/* ══ 19) 경고성 안내 문구 — `.ds-note-warn` (2026-08-14 사용자 지시, **전 시스템 공통**) ══

   > "경고성 메세지 같은 경우에는 말 머리에 당구장 표시라던지 따로 표시를 하고
   >  텍스트 컬러를 따로 줘야 할것 같은데? (이건 전체 시스템 공통이야.)"

   🔥 그전에는 경고가 **보조 설명과 같은 회색**(`text-muted fs-8`)이었다. 「아직 설정되지 않았습니다.
      설정 전에는 최종 승인에서 경고만 표시됩니다.」처럼 **지금 손을 봐야 한다는 문장**이 단위·경과일
      같은 곁다리 설명과 **같은 무게**로 보였다 — 그러면 읽고 그냥 지나간다.
   🔑 표식은 **당구장 표시(※) + 색** 두 겹이다. 색만으로는 색각 이상·흑백 인쇄에서 사라지고,
      기호만으로는 화면을 훑을 때 눈에 안 걸린다.
   ⚠️ **마크업이나 i18n 문구에 `※` 를 직접 적지 말 것** — 언어 파일마다 빠뜨리게 되고 번역 과정에서
      지워진다. 기호는 **렌더 시점에** 붙인다(`.ds-lc-key::after` 의 콜론에서 이미 세운 규칙이다).
   ⚠️ **`alert` 배너를 대체하는 것이 아니다.** 배너는 화면 전체에 걸리는 상태를 말하고, 이것은
      **어떤 항목 바로 아래 붙는 한 줄**이다.
   ⚠️ **아무 설명에나 붙이지 말 것.** 대상은 **지금 조치하지 않으면 나중에 막히거나 손해가 되는 문장**
      뿐이다. 단순 보충 설명은 `text-muted` 그대로 둔다 — **전부 경고면 아무것도 경고가 아니다.** */
:root {
    /* 🔥 **`--bs-warning`(#FFC700)을 글자색으로 쓰지 말 것** — 흰 카드 위 대비가 **1.7** 이라
       배지 바탕으로는 되지만 글자로는 거의 안 보인다(0-1 절이 `.text-muted` 를 올린 것과 같은 이유).
       이 값은 대비 **4.8**(AA)로 잡은 **글자 전용** 경고색이다. 배지 바탕에 쓰지 말 것. */
    --ds-warning-text: #9A6700;

    /* 📐 **말머리 기호가 차지하는 칸의 폭** — 행잉 인덴트의 기준값 (2026-08-16 구현 #75 사용자 지적:
       *"들여쓰기 맞춰야지."*). 🔬 구현 #72(문장 줄바꿈 훅)가 마침표마다 줄을 나누면서 드러났다 —
       그전에는 안내가 대개 한 줄이라 **둘째 줄이 없어서** 어긋남이 보이지 않았다.
       🔥 **px 로 박지 말 것 · `em` 이어야 한다** — 기호도 글자라 크기 3단계 토글을 따라 함께 늘고 줄며,
          `.ds-hint` 는 본문보다 작은 글꼴(`--ds-font-sub`)이라 **그 자리의 글자 크기**를 따라야 한다.
       🔬 값의 근거: 「※」(전각 1em) · bi 아이콘(약 1em) + 뒤 여백 약 .35em. */
    --ds-note-indent: 1.35em;
}
/* 다크 카드(#1E1E2D) 대비 **9.4**. ⚠️ 라이트 값을 그대로 쓰면 대비 1.4 로 사라진다. */
html[data-theme="dark"] { --ds-warning-text: #F1BC44; }

.ds-note-warn {
    /* ⚠️ **`display: block` 을 여기 넣지 말 것** — 이 클래스는 문장 끝 인라인 `<span>` 에도 붙는다
       (금액 줄 옆 주의 문구 등). 줄을 바꿔야 하는 자리에서는 마크업이 `d-block` 을 함께 준다.
       ⚠️ `!important` 가 필요하다 — `.text-muted` 는 Metronic 과 0-1 절 **양쪽에서 `!important`** 라,
       마크업에 그것이 남아 있으면 조용히 이겨서 **경고가 다시 회색**이 된다
       (고쳤는데 안 고쳐진 것처럼 보이는, 이 저장소에서 가장 찾기 어려운 형태의 어긋남이다). */
    color: var(--ds-warning-text) !important;
}
.ds-note-warn::before {
    content: "※";
    font-weight: 600;
    /* 🔑 **기호는 고정 폭 칸**이다 — 이 폭이 곧 아래 행잉 인덴트의 들여쓰기 값이다.
       ⚠️ `content` 뒤의 공백을 지운 것은 실수가 아니다: 간격은 이제 이 칸의 남는 폭이 만든다
          (공백을 함께 두면 둘째 줄부터 그만큼 어긋난다). */
    display: inline-block;
    width: var(--ds-note-indent);
    /* 아래 음수 들여쓰기가 이 칸 **안쪽까지** 따라 들어오지 않게 막는다(inline-block 은 상속받는다). */
    text-indent: 0;
}

/* 📐 **말머리 기호가 붙는 안내는 행잉 인덴트다** — 둘째 줄부터 **첫 줄의 글자 시작점**에 맞춘다
   (2026-08-16 구현 #75 사용자 지적). 🔥 **화면에서 다시 칠하지 말 것 · 규격은 여기 한 곳**이다.

   수법은 `padding-inline-start` + **같은 크기의 음수 `text-indent`** 다:
   첫 줄은 그 둘이 상쇄되어 기호가 왼쪽 끝에서 시작하고, 둘째 줄부터는 padding 만 남아
   **기호 칸만큼 들여쓰인다.** 🔑 줄이 몇 줄이든 무너지지 않는다(둘째 줄 이후는 전부 같은 값이다).
   🌏 **논리 속성(`inline-start`)이라 RTL 에서도 반대편으로 따라간다** — `padding-left` 로 바꾸지 말 것.
      `text-indent` 도 쓰기 방향의 시작 변을 기준으로 동작한다.

   ⚠️ **`text-indent` 는 블록 상자에만 걸린다** → 이 규칙은 **블록으로 쓰이는 자리에만** 준다.
      🔬 실측(세 앱 전수): `.ds-note-warn` 22곳 중 **인라인 `<span>` 은 2곳뿐**이고(문장 끝에 덧붙는
         주의 문구다) 나머지는 `div`·`p`·`span.d-block` 이다. 인라인에 padding 만 걸리면
         **기호 앞이 벌어져** 오히려 나빠진다 — 그래서 선택자를 나열한다.
      🔑 새 자리를 만들 때 **여러 줄이 될 안내라면 블록으로** 쓸 것(`d-block` 또는 `div`/`p`). */
p.ds-note-warn,
div.ds-note-warn,
.ds-note-warn.d-block,
:is(.ds, .quote-content) .ds-hint {
    padding-inline-start: var(--ds-note-indent);
    text-indent: calc(-1 * var(--ds-note-indent));
}


/* ══ 20) 첨부 서류 목록 — `.ds-file-*` (2026-08-15 사용자 지시 — **간단 리스트**) ══

   > "첨부 파일은 간단 리스트 — 미리보기 타일 금지. `[확장자][이름][날짜][⤓]` 한 줄."

   **파일 하나 = 줄 하나**다. 왼쪽부터 확장자 칩 · 파일명 · 등록일 · 받기 아이콘.

       [PDF] 사업자등록증.pdf ················· 2026-08-12  [⤓]
       [JPG] 여행업등록증.jpg ················· 2026-08-12  [⤓]

   🔥 **타일 그리드(4:3 썸네일)로 되돌리지 말 것** (2026-08-15 사용자 결정).
      🕘 하루 전에는 썸네일 타일이었다. 서류 절에서 먼저 읽어야 하는 것은 **무엇이 몇 장 있는가**이고
         내용은 열어서 판단한다 — 미리보기가 절의 세로를 다 먹으면 그 「몇 장인가」가 뒤로 밀린다.
      📌 열어 보는 길이 사라진 것이 아니다 — **확장자 칩이 미리보기 링크**다(이미지일 때만).
   🔑 **파일명 자체는 링크가 아니다.** 이름을 눌렀을 때 미리보기인지 받기인지 알 수 없어서다 —
      두 행위를 각각 **눈에 보이는 자리**로 갈랐다(미리보기 = 확장자 칩, 받기 = 아이콘 버튼).
   ⚠️ 이미지가 아닌 파일을 미리보기 경로로 열지 말 것 — 그 경로는 이미지가 아니면 **첨부로
      내려보내므로** 미리보기 자리에서 다운로드가 시작된다(판정은 각 앱의 `support/FileKind`).
   🔒 **파일명은 업체가 올린 값이다** — 말줄임하고 전체 이름은 `title` 속성으로만 보인다.
      **`data-bs-toggle="tooltip"` 에 `data-bs-html` 을 켜지 말 것**(저장형 XSS 자리가 된다). */
.ds-file-list {
    display: flex;
    flex-direction: column;
    /* ⚠️ 카드 모드(≤767px)에서 이 목록이 든 칸은 **flex** 다(4-1 절) — 폭을 못 박으면
       내용 폭으로 줄어 오른쪽이 텅 빈다. */
    width: 100%;
    min-width: 0;
    flex: 1 1 auto;
}
/* 🕘 옛 이름은 `.ds-file-tile`(타일)이었다. ⚠️ 옛 이름을 **남겨 두지 않았다** —
   두 이름이 공존하면 어느 쪽이 정본인지 알 수 없어진다. */
.ds-file-line {
    display: flex;
    align-items: center;
    gap: .65rem;
    min-width: 0;
    padding: .3rem 0;
}
/* 확장자 칩 — **폭을 고정**해야 파일명 시작선이 줄마다 맞는다. 들쭉날쭉하면 목록으로 안 읽힌다.
   ⚠️ px 로 박지 말 것 — 글자 크기 토글(0-0 절)을 따라 함께 움직여야 한다. */
.ds-file-ext {
    flex: 0 0 auto;
    min-width: 3.2rem;
    text-align: center;
    font-size: var(--ds-font-sub);
    font-weight: var(--ds-weight-label);
    text-transform: uppercase;
    color: var(--ds-text-label);
    background: var(--ds-fill);
    border-radius: .375rem;
    padding: .15rem .35rem;
    line-height: 1.45;
    text-decoration: none;
}
a.ds-file-ext:hover,
a.ds-file-ext:focus-visible { color: var(--ds-primary); }
.ds-file-name {
    flex: 1 1 auto;
    min-width: 0;
    color: var(--ds-text-strong);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.ds-file-date {
    flex: 0 0 auto;
    font-size: var(--ds-font-sub);
    color: var(--ds-text-faint);
}
/* ⤓ 받기는 **아이콘 버튼**이다(2026-08-15 사용자 지시) — 마크업은 `btn btn-sm btn-icon`.
   🔒 글자가 없으므로 **`aria-label`(i18n `common.download`) 이 유일한 이름**이다 — 빼지 말 것. */
.ds-file-act { flex: 0 0 auto; }
/* 좁은 화면 — 등록일을 감춘다. 네 값이 한 줄에 그대로 남으면 파일명이 두세 글자로 잘린다.
   ⚠️ 확장자 칩을 감추지 말 것 — 그것이 미리보기로 가는 유일한 길이다. */
/* 🕘 2026-09-07: 문턱을 **575.98 → 767.98** 로 합쳤다(사장님 확정 「구간 수를 줄인다」). */
@media (max-width: 767.98px) {
    .ds-file-date { display: none; }
}

/* 🕐 **「한 항목을 한 줄로 세우는 목록」 규격(`.ds-statelist-*`, 20-1절)은 2026-08-16 구현 #79 에 지웠다.**
   쓰는 곳이 **관리자 회원 상세의 「약관 동의」 카드 하나뿐**이었는데, 그 카드가 그날 **카드 격자**
   (9절 `.ds-list-grid` / `.ds-list-card`)로 바뀌어 호출부가 0곳이 됐다 — 세 앱에 남겨 두면
   다음 사람이 「있으니 쓰는 곳이 있겠지」로 읽는다(`.ds-swal-form` 과 같은 이유다).
   🔥 **그 모양으로 되돌리지 말 것** — 사용자가 하루 만에 명시적으로 거절했다:
      *"가로 100%로 보여달라는게 아니었는데 … **목록이 늘어나면 가운데만 쭉 늘어나니까**"*.
      🔑 그것이 **줄 목록과 카드 목록을 가르는 기준**이다 — 항목마다 칸이
      세 개뿐(이름·상태·일시)이면 넘치는 폭은 **빈 가운데로** 가고, 그것이 위 지적이다.
      카드 격자는 남는 폭을 **칸 수를 늘려** 쓴다.
   📌 경위는 작업기록 2026-08 「구현 #78」·「구현 #79」 에 남아 있다. */

/* ══ 21) 이력 드로워 — `.ds-drawer` (2026-08-14 사용자 지시) ═══════════════════════════

   > "관리 이력 카드(최상단 가로 100%, 우측 드로워)가 필요할 것 같아."

   오른쪽에서 밀려 들어오는 판 하나 + 뒤를 덮는 막(scrim). 여는 것은 `<body>` 의 클래스다.

   🔑 **값은 DMC `Trade.css` 의 `.history-col`/`.hist-*` 에서 그대로 옮겼다** — 같은 물건이
      두 가지 두께·속도로 열리면 앱을 오갈 때 다른 화면처럼 보인다.
   🔥 **이름을 `.history-col` 로 맞추지 말 것.** 그 이름은 DMC·여행사의 `Trade.css` 에 이미 있고,
      그 파일은 거래 작업대(미리보기 도킹·요약 바)에 얽혀 있다 — 여기에 같은 이름을 두면
      **두 정의가 같은 화면에서 겹쳐** 거래 화면의 이력 드로워가 조용히 달라진다.
      17절(`.ds-detail-*`)이 `.summary-bar` 와 이름을 나눈 것과 **같은 이유**다.
   ⚠️ **본문에 두지 말고 콘텐츠 컨테이너 안**에 둘 것 — `position: fixed` 라 자리는 상관없지만,
      `.ds` 스코프를 벗어나면 카드 규격(헤더 48px·입력창 32px)이 Metronic 기본으로 돌아간다.
   ⚠️ `z-index` 는 헤더(100)보다 커야 한다 — 작으면 드로워 상단이 헤더에 가려 닫기 버튼을 못 누른다. */
.ds .ds-drawer {
    position: fixed;
    top: 0;
    right: 0;
    height: 100vh;
    width: min(440px, 92vw);
    z-index: 108;
    transform: translateX(100%);
    transition: transform .28s ease;
}
/* 🔓 **여는 방법이 둘이다 — 옛것과 새것이 함께 산다** (2026-08-18 구현 #94 3차).
   ① `<body>.ds-drawer-open` … **화면에 드로워가 하나뿐일 때.** 기존 화면 전부가 이 방식이다
      (관리자 랜드사별 마진율·업체 상세·상품 검수, 랜드사 계약 상세 …). 🔥 이 줄을 지우지 말 것.
   ② `.ds-drawer.is-open` … **한 화면에 드로워가 여럿일 때.** 자기 자신만 열린다.
   🔴 **둘을 섞어 쓰지 말 것.** 드로워가 여럿인 화면에서 `<body>` 클래스를 붙이면
      **전부 동시에 슬라이드해 겹친다**(그래서 ②를 만들었다).
   📌 한 번에 하나만 열리게 하는 것은 **화면 JS 의 몫**이다(CSS 는 상태만 그린다) —
      폰에서 둘이 겹치면 뒤엣것을 닫을 방법이 모호해진다. */
body.ds-drawer-open .ds .ds-drawer { transform: none; }
.ds .ds-drawer.is-open { transform: none; }
/* 📐 **넓은 변형** — `.ds-drawer.is-wide` (2026-08-18 사용자 지적, 구현 #94 4차)

   > *"여행상품 드로워, 견적서 드로워의 가로 사이즈가 DMC의 드로워보다 좀 작은데?"*

   🔬 실측: 랜드사 앱 미리보기 슬라이드오버는 **`min(560px, 96vw)`**(`menuLevel/Trade.css`),
      이 골격의 기본은 **`min(440px, 92vw)`** — **120px 좁았다.**
   🔑 값은 **랜드사 앱과 같게** 맞췄다 — 두 앱이 **같은 문서**(견적서·상품 일정)를 보여주는데
      폭이 다르면 줄바꿈 자리가 달라져 **같은 문서가 다르게 보인다.**
   🔑 **기본값(440px)은 그대로 둔다.** 이력 드로워는 한 줄짜리 목록이라 440px 로 충분하고,
      마진 이력·업체 이력·검수 이력·랜드사 계약 상세가 이미 그 폭을 쓰고 있다.
      🔥 기본값을 넓히지 말 것 — 그 화면들이 한꺼번에 넓어진다(아무도 요청하지 않은 변화다).
   ⚠️ **문서를 담는 판에만** 붙인다(여행상품·견적서). 목록형 판에는 붙이지 말 것.
   📌 표가 넘치는 문제는 없다 — `.ds-drawer-body` 가 `overflow-y: auto` 라 CSS 규칙상
      `overflow-x` 도 `auto` 로 계산된다(한쪽이 `visible` 이 아니면 다른 쪽도 `visible` 일 수 없다)
      → 표는 **자기 판 안에서 가로로 스크롤**되고 본문이 좌우로 밀리지 않는다. */
/* 🔴 **폭은 `rem` 이다 — `560px` 로 되돌리지 말 것** (2026-08-28 · D2 ①).
   🔬 판은 px 상수인데 안에 담기는 문서는 전부 rem 이라, 글자 「아주 크게」(root 20px)에서
      **내용만 25% 커지고 판은 그대로**였다 → 견적서 항목 표의 상품명 칸이 **48.4px 로 눌려
      한 글자씩 세로로 쌓였다**(실측: 줄 수 14 · 낱말 수 5. 기본 글자에서는 146.3px 로 멀쩡했다).
   ✅ `35rem` = root 16 에서 **560px(종전과 완전히 같다)** · 18 → 630 · 20 → **700px**.
   🔥 **랜드사·여행사 `menuLevel/Trade.css` 의 `.preview-col`, 관리자 `TravelProduct.css` 와
      같은 값이어야 한다**(위 절의 요지 그대로 — 같은 문서를 다른 폭으로 그리면 줄바꿈이 갈린다). */
.ds .ds-drawer.is-wide { width: min(35rem, 96vw); }
.ds .ds-drawer > .card {
    height: 100%;
    border-radius: 0;
    display: flex;
    flex-direction: column;
    /* 🌫 앞의 링이 **다크에서의 판 경계**다(10절 참고 — 다크에서 그림자는 안 보인다).
       라이트에서는 `--ds-panel-edge` 가 투명이라 **종전 그림자 그대로**다. */
    box-shadow: 0 0 0 1px var(--ds-panel-edge), -8px 0 30px -12px rgba(20, 28, 45, .35);
}
/* 📐 **헤더 좌우 패딩은 본문에 맞춘다**(2026-08-15 사용자 지적: *"드로워 카드 헤더에 좌우 여백이
      또 이전처럼 크게 들어갔어"*).
   🔬 실측: 헤더 36px(2.25rem) vs 본문 24px(1.5rem) — 제목이 내용보다 12px 안쪽이었다.
   🔑 드로워는 폭이 좁다(440px). 넓은 카드 기준(2.25rem)을 그대로 쓰면 양쪽에서 72px 를 먹는다
      → **본문 쪽 값에 맞춘다.** 「헤더와 본문의 좌우 패딩은 항상 같게」 규칙은 값이 아니라
      **같아야 한다**는 뜻이다.
   🕐 **「또」 인 이유** — 같은 결함을 2026-08-13 에 `.history-col`(상품 검수 드로워)에서 이미 고쳤는데,
      21절을 새로 만들면서 그 규칙을 물려받지 못해 **되살아났다.** 드로워를 새로 만들 때는 이 줄을 함께 볼 것. */
.ds .ds-drawer > .card > .card-header {
    /* ⚠️ **`!important` 를 빼지 말 것** — 2절의 기본 규칙(`.ds .card > .card-header`)이
          `padding: .5rem 2.25rem !important` 라 그것 없이는 **조용히 진다**(실측: 36px 그대로). */
    /* 🔴 **2026-08-24 (H5) 값을 `--ds-card-x`(17 / 30px)로 바꿨다** — 2-0-2 절의 카드 머리 규칙이
          `#kt_content` 스코프 (1,2,0) 라 이 줄(0,4,0)을 이긴다. 같은 값을 쓰면 겨룰 일이 없다.
       🔑 넓은 화면에서는 값이 안 바뀐다(`1.5rem` = root 20 에서 30px = `--ds-card-x`).
       🔥 이 절의 요지(**드로워는 넓은 카드 기준 2.25rem 을 쓰지 않는다**)는 그대로다. */
    padding-left: var(--ds-card-x) !important;
    padding-right: var(--ds-card-x) !important;
}
/* 🔑 목록만 스크롤한다 — 머리(제목·닫기)는 붙어 있어야 어디서든 닫을 수 있다. */
.ds .ds-drawer-body {
    flex: 1;
    overflow-y: auto;
    /* 🔑 좌우는 **머리와 같은 값**이어야 한다(H5, 2026-08-24) — `--ds-card-x`(17 / 30px). */
    padding: 1.25rem var(--ds-card-x);
}
.ds .ds-drawer-close {
    border: 0;
    background: none;
    font-size: 1.4rem;
    line-height: 1;
    color: var(--ds-text-label);
    cursor: pointer;
    margin-left: auto;
}
.ds .ds-drawer-close:hover { color: var(--ds-text); }

/* ── 닫기 **아이콘은 버튼의 글자색을 따른다** (2026-09-03 — 닫기 부품 「B안」 적용)
   🔬 Metronic 번들(`style.bundle.css`)이 **맨 `i` 요소에 색을 박아 둔다**
      (`i { line-height: 1; font-size: 1rem; color: rgb(161,165,183) }`). 특이도는 (0,0,1) 이지만
      **자기 선언이라 상속을 이긴다** → 닫기 버튼 안에 `<i class="bi bi-x-lg">` 를 넣으면
      버튼이 정한 `--ds-text-label`(rgb(94,98,120)) 이 아니라 **gray-500** 으로 그려진다.
   🔬 랜드사 앱만 멀쩡했던 까닭은 **그 앱 `common.css` 에만 `i { color: inherit }` 가 있기 때문**이다
      (2026-07-16 에 초록 배경 위 아이콘 대비 문제로 넣은 줄이고, 다른 두 앱에는 없다).
      🔥 **`common.css`·`common.js` 는 세 앱이 서로 다르다 — 파일을 복사하지 말 것**
      → 그래서 세 앱 공용인 이 파일에 **자리를 좁혀** 둔다(전역 `i` 를 건드리지 않는다).
   🔑 **크기는 건드리지 않는다** — 위 `i { font-size: 1rem }` 이 정하는 **16px** 이 이미 이 저장소의
      기존 규격이다(실측 2026-09-03, 랜드사 계약 상세 `.hist-close`·`.pv-close`:
      아이콘 16×24 · 버튼 28×33.11 · 색 rgb(94,98,120)).
      🔥 **버튼의 `font-size`(1.4rem)를 줄여 맞추지 말 것** — 그 값은 글리프가 아니라
      **누르는 영역의 높이**를 만든다(줄이면 손가락이 닿는 칸이 함께 작아진다).
   ⚠️ `.pv-close`·`.hist-close` 에는 `.ds` 를 붙이지 말 것 — 그 둘은 `.ds` 스코프 밖에서도 쓰이고,
      이 파일의 좁은 화면 절도 이미 바깥 이름으로 적고 있다. */
.ds .ds-help-close > .bi,
.ds .ds-drawer-close > .bi,
.ds .cc-parent-del > .bi,
.pv-close > .bi,
.hist-close > .bi {
    color: inherit;
}
/* 🔑 **칩 안의 지우기는 「닫기 버튼」이 아니다** — 배지 높이(`--ds-badge-h`, 1.7rem) 안에 들어가야 한다.
      📌 같은 판정의 선례가 `.ds-upload-files .dz-remove::before`(bi-x-lg, `.8rem`) 다.
      🔥 「닫기와 같은 16px 로 맞추자」로 키우지 말 것 — 칩 높이를 밀어 올린다. */
.ds .cc-parent-del > .bi { font-size: .8rem; }

/* 막(scrim) — 뒤가 눌리지 않게 막고, 아무 데나 눌러 닫는 자리이기도 하다.
   ⚠️ `display:none` 으로 감추지 말 것 — 열고 닫을 때 서서히 나타나는 것이 사라진다. */
.ds .ds-drawer-scrim {
    position: fixed;
    inset: 0;
    background: var(--ds-scrim);
    opacity: 0;
    pointer-events: none;
    transition: opacity .25s;
    z-index: 107;
}
body.ds-drawer-open .ds .ds-drawer-scrim { opacity: 1; pointer-events: auto; }
/* 🔑 막은 **하나를 여럿이 함께 쓴다** — 드로워마다 막을 두면 겹칠 때 어두움이 두 겹이 된다. */
.ds .ds-drawer-scrim.is-open { opacity: 1; pointer-events: auto; }
@media (max-width: 767.98px) {
    /* 카드 본문 좌우 패딩은 머리와 같아야 한다(기준: 헤더와 본문의 끝선은 늘 같다). */
    /* 🔴 **드로워도 머리와 본문의 좌우 끝선이 같아야 한다** (2026-08-18).
       🕘 예전에는 이 줄이 **본문뿐**이라, 넓은 화면용 머리 값(1.5rem `!important`)이 폰에서도 남아
          **머리만 8px 안쪽**으로 들어가 있었다. 카드 세 층을 함께 고치라는 위 규칙(2절)과 같은 이유다.
       ⚠️ **`!important` 를 빼지 말 것** — 3445행의 머리 규칙이 `!important` 라 그것 없이는 조용히 진다.
       📌 본문 쪽은 2절의 좁은 화면 기본이 이미 `1rem !important` 로 주고 있어 값이 같다(중복이지만
          **머리와 나란히 적어 두는 것이 이 어긋남을 다시 만들지 않는 길**이라 남긴다). */
    /* 🔴 **2026-08-24 (H5) `--ds-card-x` 로 바꿨다** — 좁은 화면에서도 머리·본문 끝선이 하나여야 한다. */
    .ds .ds-drawer > .card > .card-header,
    .ds .ds-drawer > .card > .card-body { padding-left: var(--ds-card-x) !important; padding-right: var(--ds-card-x) !important; }
}

/* ── 📄 **문서 드로워의 머리** — 문서 종류(펼침 메뉴) + 버전(선택 상자) (2026-08-18, 구현 #95)

      🔴 **자리가 둘로 갈려 있다**(2026-08-18 사용자 확정):
         · **왼쪽(제목 자리)** = **문서 종류**. 「예약확인증 / 견적서 / 계약서(매입·매출)」를 고른다.
           텍스트 + 아래 화살표이고 **선택 상자 모양이 아니다** — 그 자리는 원래 카드 제목이라
           상자를 두르면 제목이 사라진 것처럼 보인다.
         · **오른쪽(닫기 왼쪽)** = **버전**. 평범한 선택 상자(`.form-select`)다.
           🔑 **버전이 있는 문서에서만 그린다**(견적서뿐) — 빈 칸이 남으면
              「고를 게 있는데 비어 있다」로 읽힌다.
      🔑 **고를 것이 하나뿐이면 펼침 메뉴를 만들지 않는다** — 여는 동작이 헛되기 때문이다.
         그때는 평범한 카드 제목(`h3.card-title`)을 그대로 쓴다.
      🔽 화살표는 **`bi-chevron-down` · `.8rem` 한 규격**이다(17절 `.ds-ds-toggle i` 와 같은 값).
         🔥 `▾`·`▼` 같은 글자를 쓰지 말 것 — 화면마다 크기·모양이 갈린다.
      ⚠️ 드로워가 좁다(560px) → 메뉴 폭에 **상한**을 두고 길면 안에서 스크롤한다.
         🔥 메뉴를 `position: fixed` 로 띄우지 말 것 — 드로워가 스크롤될 때 따라오지 않는다.
      📌 **글씨 크기·굵기를 여기서 정하지 않는다** — 껍데기가 `h3.card-title.fs-5.fw-bolder` 라
         버튼이 `font: inherit` 로 그대로 물려받는다. 🔥 크기를 박으면 카드 제목과 갈린다. */
.ds .ds-doc-switch { position: relative; display: inline-flex; align-items: center; min-width: 0; }
.ds .ds-doc-switch-btn {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    border: 0;
    background: none;
    padding: 0;
    font: inherit;
    color: inherit;
    cursor: pointer;
    min-width: 0;
}
.ds .ds-doc-switch-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds .ds-doc-switch-btn > i {
    font-size: .8rem;
    line-height: 1;
    color: var(--ds-text-faint);
    flex: none;
}
.ds .ds-doc-switch-btn:hover,
.ds .ds-doc-switch-btn[aria-expanded="true"] { color: var(--ds-primary); }
.ds .ds-doc-switch-btn:hover > i,
.ds .ds-doc-switch-btn[aria-expanded="true"] > i { color: var(--ds-primary); }
.ds .ds-doc-switch-btn:focus-visible { outline: 2px solid var(--ds-primary); outline-offset: 2px; }

/* 🔽 펼침 메뉴. 🔑 **닫힘이 기본**이고 여는 것은 JS 가 `.is-open` 으로 한다
      (17절 접기 토글과 같은 방식 — CSS `:hover` 로 여는 규칙을 만들지 말 것). */
.ds .ds-doc-menu {
    display: none;
    position: absolute;
    top: calc(100% + .35rem);
    left: 0;
    z-index: 10;
    min-width: 15rem;
    max-width: min(24rem, calc(100vw - 2rem));
    max-height: 60vh;
    overflow-y: auto;
    padding: .4rem;
    border: 1px solid var(--ds-line);
    border-radius: .6rem;
    background: var(--ds-surface);
    box-shadow: 0 0 0 1px var(--ds-panel-edge), 0 12px 30px -12px rgba(20, 28, 45, .35);
}
.ds .ds-doc-switch.is-open > .ds-doc-menu { display: block; }
.ds .ds-doc-menu-item {
    display: block;
    width: 100%;
    text-align: left;
    border: 0;
    background: transparent;
    padding: .45rem .65rem;
    border-radius: .4rem;
    cursor: pointer;
    font: inherit;
}
.ds .ds-doc-menu-item:hover { background: var(--ds-fill); }
/* 🔵 지금 보고 있는 장. 🔑 낭독기에는 `aria-current` 가 말하고, 눈에는 이 배경이 말한다. */
.ds .ds-doc-menu-item[aria-current="true"] { background: var(--ds-fill); }
.ds .ds-doc-menu-item[aria-current="true"] .ds-doc-menu-name { color: var(--ds-primary); }
/* 🚫 **아직 없는 문서** — 누르기 전에 알 수 있어야 헛걸음이 없다(줄이 흐리고 이유가 적혀 있다).
      🔴 **`disabled`·`aria-disabled` 를 쓰지 않는다** — 이 줄은 **여전히 눌린다.** 누르면 그 판이
         「왜 못 보는지」를 다시 말하므로 「동작하지 않는다」고 알리는 것은 사실과 다르고,
         낭독기로 옮겨 온 사람에게 **아무 반응이 없는** 편이 더 나쁘다.
      🔑 이유는 줄 안의 글(`.ds-doc-menu-note`)이 말한다 — 버튼 안이라 낭독기가 함께 읽는다. */
.ds .ds-doc-menu-item.is-muted .ds-doc-menu-name { color: var(--ds-text-label); font-weight: 400; }
.ds .ds-doc-menu-item.is-muted .ds-doc-menu-note { font-style: italic; }
.ds .ds-doc-menu-name {
    display: block;
    font-size: var(--ds-font-field-v);
    font-weight: var(--ds-weight-label);
    color: var(--ds-text-strong);
}
.ds .ds-doc-menu-note {
    display: block;
    margin-top: .1rem;
    font-size: var(--ds-font-field-sub);
    color: var(--ds-text-faint);
}
/* 📄 버전 선택 상자 — 좁은 드로워에서 제목을 밀어내지 않게 상한을 둔다. */
.ds .ds-doc-version { max-width: 11rem; }


/* ── 이력 목록 — 점과 선으로 이은 세로 흐름 (`.ds-hist-*`)
      🔑 **최신이 위**다. 그래서 연결선은 「점과 점 사이」에만 그린다 — 컨테이너 전체에 그리면
         마지막 점 아래로 선이 남아 「아래로 더 있다」는 인상을 준다(DMC 정본과 같은 처방).
      📌 값은 `Trade.css` 의 `.trade-timeline-*` 에서 옮겼다. */
:is(.ds, .quote-content) .ds-hist-list {
    position: relative;
    padding-left: 2rem;
}
:is(.ds, .quote-content) .ds-hist-item {
    position: relative;
    padding-bottom: 1.5rem;
}
:is(.ds, .quote-content) .ds-hist-item:last-child { padding-bottom: 0; }
:is(.ds, .quote-content) .ds-hist-item::after {
    content: '';
    position: absolute;
    left: calc(-1.625rem + 4px);
    top: calc(.25rem + 10px);
    bottom: -.25rem;
    width: 2px;
    background-color: var(--ds-line-soft);
}
:is(.ds, .quote-content) .ds-hist-item:last-child::after { content: none; }
:is(.ds, .quote-content) .ds-hist-item::before {
    content: '';
    position: absolute;
    left: -1.625rem;
    top: .25rem;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background-color: var(--ds-primary);
    border: 2px solid var(--ds-surface);
    box-shadow: 0 0 0 2px var(--ds-primary);
}
:is(.ds, .quote-content) .ds-hist-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
    flex-wrap: wrap;
    margin-bottom: .25rem;
}
:is(.ds, .quote-content) .ds-hist-time {
    font-size: .8rem;
    color: var(--ds-text-label);
}
/* 🔑 **바뀐 값은 「이전 → 이후」 한 줄**로 읽는다. 두 줄로 나누면 무엇이 무엇으로 바뀌었는지
      눈이 짝을 다시 맞춰야 한다. ⚠️ 긴 값은 줄바꿈으로 접는다(잘라 내면 무엇이 바뀌었는지 사라진다). */
:is(.ds, .quote-content) .ds-hist-diff {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: .25rem .5rem;
    font-size: .9rem;
    padding: .5rem .75rem;
    background-color: var(--ds-fill);
    border-radius: .375rem;
    word-break: keep-all;
    overflow-wrap: anywhere;
}
/* 이전 값은 「지나간 값」이다 — 취소선까지 긋지 않고 색만 낮춘다(취소선은 삭제로 읽힌다). */
:is(.ds, .quote-content) .ds-hist-from { color: var(--ds-text-faint); }
:is(.ds, .quote-content) .ds-hist-arrow { color: var(--ds-text-label); }
:is(.ds, .quote-content) .ds-hist-to {
    color: var(--ds-text-strong);
    font-weight: 600;
}
/* 값이 없던/없어진 자리 — 빈칸으로 두면 무엇이 사라졌는지 알 수 없다. */
:is(.ds, .quote-content) .ds-hist-empty { color: var(--ds-text-label); font-style: italic; }

/* 🎛 **스코프가 `.ds` 하나가 아니다 — `:is(.ds, .quote-content)` 다** (2026-08-19 구현 #97)
   🔑 아래 세 벌(`.ds-hist-*` 21절 · `.ds-detail-summary`/`.ds-ds-*` 17절 · `.ds-field*` 22절)은
      랜드사·여행사의 **거래 상세 6화면**(견적·계약·예약)도 쓴다. 그 화면들은 편집 화면이라
      **`.ds` 밖**(`.quote-content` + `Trade.css`)이다 — 구현 #74 가 그렇게 두었다.
   🔥 **`#kt_content` 에 `ds` 를 붙여 해결하지 말 것** — 그러면 입력창 높이(32px)·카드 머리/발 여백·
      푸터 버튼 크기 같은 `.ds` 전역 규칙이 그 편집 화면에 통째로 걸린다.
   🔥 **두 카드만 `<div class="ds">` 로 감싸는 것도 안 된다** — `.ds-detail-summary` 는 `position: sticky`
      라 **감싼 상자의 높이만큼만** 움직인다(상자가 카드 두 장 높이면 붙어 있을 자리가 없다).
   🔑 그래서 **선택자 쪽을 넓혔다.** `:is()` 는 특이도가 `.ds` 와 같아(0,1,0) 기존 우선순위가 안 바뀐다.
   ⚠️ **관리자 앱에는 `.quote-content` 화면이 없다** — 여기서는 아무 일도 하지 않지만
      세 앱의 이 파일은 **한 벌로 같아야 한다**(갈리면 다음 사람이 어느 쪽이 맞는지 모른다). */

/* ── 이력 **한 줄 요약** 카드 — `.ds-hist-card` / `.ds-hist-row` (2026-08-14 사용자 지시)

   > "업체 상세 관리이력카드를 상품 검수 상세 검수 이력과 동일하게 형식을 맞춰 줘"

   카드 하나에 **줄 하나**다. 순서는 **제목 → 배지 → 본문 → 시각 → 「전체 이력」 버튼**이고,
   나머지 전부는 위의 드로워(`.ds-drawer`)가 맡는다.
   🔑 **최신 1건만 올린다** — 「지금 왜 이 상태인가」의 답이 한 줄이면 카드가 화면을 밀어내지 않는다.

   🔑 **값은 관리자 `menuLevel/TravelProduct.css` 의 `.activity-row`(원본은 DMC `Trade.css`)에서
      그대로 옮겼다** — 상품 검수 상세가 이 규격의 정본이고, 두 화면이 눈으로 달라 보이면 안 된다.
      옮긴 정의는 그 파일에서 **지웠다**(복사가 아니라 이동이다).
   🔥 **이름을 `.activity-row` 로 맞추지 말 것.** 그 이름은 DMC·여행사 `Trade.css` 의 거래 작업대에
      이미 있어, 같은 이름을 여기 두면 그 화면의 이력 줄이 조용히 달라진다 —
      `.ds-drawer` 를 `.history-col` 로 맞추지 않은 것과 **같은 이유**다.
   ⚠️ **카드 헤더가 없는 카드**라 좌우 패딩(2.25rem)을 줄이 직접 든다(카드 본문과 같은 끝선).
      그래서 카드에 `.card-header`·`.card-body` 를 덧대지 말 것 — 덧대면 패딩이 두 번 들어간다.
   ⚠️ 좁은 화면(≤767.98px)에서는 **본문·시각을 감춘다** — 넷을 다 밀어 넣으면 버튼이 두 줄로 접힌다. */
/* 🔴 **2026-08-24 (H2/H3) — 바닥값을 카드 머리와 같은 식으로 바꿨다**(사장님 마감 조건:
      *"카드 머리 높이가 전부 같을 것 — 전체 이력 줄과도 일치"*).
   🔬 `48px` 고정이라 **root 14(글자 작게·좁은 화면)에서만 이 줄이 48, 카드 머리는 44.2** 로 갈렸다.
      두 값 모두 `--ds-header-ctl-h` + 세로 패딩으로 정해지므로 식을 같게 하면 저절로 맞는다.
   🔑 **이 줄이 그 토큰의 출처다** — 「전체 이력」 버튼 높이가 `--ds-header-ctl-h`,
      이 줄의 세로 패딩이 `--ds-header-ctl-pad` 다. 식을 쓰는 것이 원래 뜻에 맞다.
   🔥 `48px` 로 되돌리지 말 것 — 좁은 화면에서 카드 머리와 3.8px 어긋난다. */
:is(.ds, .quote-content) .ds-hist-card > .ds-hist-row { min-height: calc(var(--ds-header-ctl-h) + var(--ds-header-ctl-pad) * 2 + 1px); }
/* 🔴 **칸이 좁으면 줄을 바꾸지 말고 항목을 감춘다** (2026-08-18 사용자 폰 실기기 지적 — 구현 #94 3차).
   > *"견적이력 카드에 칸이 좁으면 텍스트를 줄바꿈 하지 말고 항목을 숨겨"*
   🔬 실측: 작성자(`테스트 AGENCY` / `전수영`)가 **두 줄로 꺾여** 카드 높이가 늘고 시각이 밀려났다.
   🔑 원인은 둘이었다 — ⓐ 이 줄이 `flex-wrap: wrap` 이었고 ⓑ `.ds-hist-row-actor` **정의가 아예 없어서**
      기본값(`white-space: normal`)으로 제 안에서 접혔다. 둘 다 고친다.
   🔥 **말줄임(`…`)으로 자르는 것과 다르다** — 잘린 이름은 자리는 그대로 먹으면서 읽히지도 않는다.
      감출 것은 **통째로** 감춘다. */
:is(.ds, .quote-content) .ds-hist-row {
    display: flex;
    align-items: center;
    flex-wrap: nowrap;
    gap: .65rem;
    padding: .5rem 2.25rem;
    font-size: .95rem;
    line-height: 1.55;
}
/* 제목 — 카드 헤더가 없는 줄의 인라인 타이틀. `.card-title`(fs-5 fw-bolder)과 같은 규격이다
   (1.15rem / 600 / gray-900). 우측 구분선은 **바로 옆에 데이터가 붙기** 때문이다 —
   헤더(`.hdr-quote-meta`)와 같은 처리이고, 좌우 간격도 그쪽과 맞춘 값이다. */
/* 🔴 **`flex: none` 으로 되돌리지 말 것 — 그것이 L-5/A-2(영문 배지 잘림)의 원인이었다** (2026-09-04).
   🔬 실측(375px · 영문 · 줄 안쪽 317px): 관리자 업체 상세는 제목 「Management history」가
      **152.2px(줄의 48%)** 를 `flex:none` 으로 못 박아, 줄에서 줄어들 수 있는 것이 **배지와 버튼뿐**이
      됐다 → 배지가 **70.7px** 로 눌려 「Bankbook copy file」(111px)이 **40px 잘렸고**,
      「Settlement bank」(15자)마저 **31px** 잘렸다. 랜드사 계약 상세도 같은 구조로 19~68px 잘렸다.
   🔑 **국문은 한 번도 안 잘린다** — 「관리 이력」 69px · 「계약 이력」 64px 이라 애초에 넘치지 않는다.
      **넘치지 않으면 `flex-shrink` 는 아무 일도 하지 않으므로 국문 화면은 그대로다.**
   🔑 **줄이는 순서를 「뜻이 없는 것부터」로 바꾼 것**이 이 처방이다 — 제목과 「전체 이력」 버튼은
      **어느 줄에나 같은 글자가 박히는 고정 라벨**이고, 배지만이 **그 줄에서만 다른 값**(무슨 일이
      있었나)을 담는다. 그래서 **제목·버튼이 먼저 말줄임되고 배지는 마지막에** 양보한다.
      🔬 확인(375px): 제목·버튼에 `flex-shrink: 1000` 을 준 뒤 관리자 여섯 문구 전부 **잘림 0**
      (제목 152.2→80~104 · 버튼 73.1→64~81), 랜드사 다섯 문구도 **잘림 0**, 문서 가로 스크롤 0.
   🔥 **배지에 `flex: none` 을 주어 푸는 것으로 바꾸지 말 것** — 21절이 정한 것은 「제목·배지는 끝까지
      남긴다」이고, 배지를 안 줄이면 **옆 칸이 대신 눌려** 문제가 자리만 옮긴다(B-1 ④).
   ⚠️ **`min-width` 바닥값을 지우지 말 것** — 지우면 극단적으로 긴 배지에서 제목이 0 까지 줄어
      **제목이 화면에서 사라진다**(21절은 제목을 끝까지 남기라고 정했다). */
:is(.ds, .quote-content) .ds-hist-row-title {
    font-size: 1.15rem;
    font-weight: 600;
    color: var(--ds-text-strong);
    line-height: 1.2;
    white-space: nowrap;
    flex: 0 1000 auto;
    min-width: 4.5rem;
    overflow: hidden;
    text-overflow: ellipsis;
    padding-right: 1.15rem;
    border-right: 1px solid var(--ds-line-soft);
    margin-right: .45rem;
}
/* 🔴 **`flex-basis` 는 `auto` 가 아니라 `0` 이다 — 되돌리지 말 것** (2026-08-27 B-1 후속).
   🔬 `auto` 면 이 칸의 **기준 크기가 본문 글자 폭**(실측 653px)이라, 그것만으로 줄이 넘쳐
      **줄어들 수 있는 형제가 전부 함께 줄어든다.** 그래서 아래 배지에 `min-width: 0` 을 준 순간
      **1440px·기본 글자에서도 상태 배지가 말줄임**됐다(「Quote Request…」 98px / 내용 136px).
   🔑 `0` 이면 이 칸은 **남는 자리만** 차지하므로 넘침에 기여하지 않는다 → 배지는 **정말 자리가
      없을 때만** 줄어든다. 넘치지 않는 경우의 최종 폭은 `auto` 일 때와 **같다**(계산상 동일). */
:is(.ds, .quote-content) .ds-hist-row-text {
    color: var(--ds-text-label);
    flex: 1 1 0;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* 본문(`.ds-hist-row-text`)이 없을 때도 시각·버튼이 우측 끝에 붙도록 auto 마진으로 밀어낸다. */
:is(.ds, .quote-content) .ds-hist-row-time {
    color: var(--ds-text-label);
    flex: none;
    margin-left: auto;
}
/* 🕘 **모양 선언은 위 `.ds-head-btn` 블록으로 합쳤다** (2026-08-20 구현 #97).
   🔬 이 클래스는 테두리·배경·글자색·여백이 `.ds-head-btn` 과 **글자 그대로 같은** 셋째 사본이었다.
      화살표 버튼을 한 모양으로 통일하라는 지시에 따라 선택자 목록에 넣고, 여기에는 **자리만** 남긴다.
   🔥 모양을 여기에 다시 적지 말 것 — 그러면 또 갈린다. */
:is(.ds, .quote-content) .ds-hist-row-all { margin-left: .5rem; }
:is(.ds, .quote-content) .ds-hist-row-all i { font-size: .8rem; line-height: 1; }
/* 🔴 **줄이 좁아지면 이 버튼이 「먼저」 줄어든다** (2026-08-27 B-1 — 가로 넘침 8화면의 단일 원인).
   🔬 실측(관리자 · **영문** · `scrollWidth − clientWidth`): 기본 글자에서도 375px 에서
      업체 상세 26 · 견적 상세 53 · 예약 상세 39 · 계약 상세 54px 넘쳤고,
      `xlarge` 에서는 375/430/768/1100 이 각각 86/31/106/39(업체) · 116/61/101/212(견적) ·
      100/45/78/71(예약) · 117/62/103/96(계약) 이었다.
   🔑 **원인은 위 `.ds-head-btn` 묶음의 `flex: none`** 이다 — 버튼이 글자 폭 그대로 자리를 요구해
      **줄 전체를 컨테이너 밖으로 끌고 나간다.** 넘치는 자리를 실제로 짚어 보면 남는 것은
      **이 버튼의 글자 `<span>` 과 화살표 `<i>` 둘뿐**이다(`.ds-hist-row-text` 는 이미
      `min-width:0` + 말줄임이라 0 까지 줄어든다).
   🔥 **국문에서는 안 보인다** — 「전체 이력」·「변경 이력」보다 영문
      「Quotation history」·「Change history」가 훨씬 길다. **한국어 화면만 보고 「0건」이라고 하지 말 것.**
   🔥 `.ds-head-btn`·`.ds-ds-toggle` 의 `flex: none` 은 그대로 둔다 — 그쪽은 카드 툴바·요약 바라
      옆에 줄어들 것(말줄임되는 본문)이 없다. **이 줄에서만** 예외를 만든다.
   ⚠️ `position: relative` 는 아래 ≤430px 블록에서 라벨을 화면 밖으로 보낼 때 **기준 상자**가 된다 —
      없으면 그 라벨이 페이지 전체를 기준으로 잡혀 **가로 스크롤이 다시 생긴다.** */
/* 🔴 **`flex-shrink` 는 `1` 이 아니라 `1000` 이다 — 되돌리지 말 것** (2026-09-04 L-5/A-2).
   🔑 위 제목과 **같은 이유**다: 이 버튼의 글자(「전체 이력」·「Change history」)는 어느 줄에나 같은
      고정 라벨이라, 자리가 모자라면 **상태 배지보다 먼저** 양보해야 한다.
   ⚠️ **`min-width: 0` 을 그대로 두지 말 것** — 줄어드는 힘을 키운 만큼 바닥이 없으면 버튼이
      화살표보다 더 줄어 **누를 것이 없는 조각**이 된다(B-1 ④가 겪은 자리 그대로다).
      바닥값은 머리 컨트롤 높이와 같은 토큰이라 **정사각 버튼**에서 멈춘다. */
:is(.ds, .quote-content) .ds-hist-row { min-width: 0; }
:is(.ds, .quote-content) .ds-hist-row-all {
    flex: 0 1000 auto;
    min-width: var(--ds-header-ctl-h);
    overflow: hidden;
    position: relative;
}
:is(.ds, .quote-content) .ds-hist-row-all > span {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
:is(.ds, .quote-content) .ds-hist-row-all > i { flex: none; }
/* 🔴 **상태 배지도 줄어들 수 있어야 한다** (2026-08-27 B-1 2단계).
   🔬 이 배지는 원래 `flex: 0 1 auto`(= 줄어도 된다)인데 **`min-width` 의 초기값이 `auto`(= min-content)**
      라 실제로는 한 글자도 안 줄었다 — `silent-traps.md` 의 「`min-width: 0` 을 빠뜨리지 말 것」 그대로다.
   🔬 실측(관리자 계약 상세 · 영문 · `xlarge` · 375px): 줄 안쪽 302px 에 제목 142.8 +
      배지 「Rejected by platform」 140.2 + 버튼 32.8 + 간격 18.2 = **334px**. 배지가 108px 까지만
      줄어 주면 들어간다.
   🔑 **여기서는 감추지 않고 줄인다** — 21절이 「제목·배지는 끝까지 남긴다」로 정해 놓았기 때문이다.
      감추는 순서(작성자 → 시각 → 본문)에 배지를 끼워 넣지 말 것. */
:is(.ds, .quote-content) .ds-hist-row > .badge {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* 🖊 **작성자** — 「누가 했나」. 정의가 없어 제 안에서 접히던 것을 여기서 잡는다(2026-08-18).
   ⚠️ `flex: none` 이라 줄어들지 않는다 → 좁아지면 **줄이 아니라 이 항목이 사라진다**(아래 단계). */
:is(.ds, .quote-content) .ds-hist-row-actor {
    color: var(--ds-text-faint);
    font-size: var(--ds-font-sub);
    white-space: nowrap;
    flex: none;
}
/* 📉 **감추는 순서는 「좁은 화면에서 이 줄이 답해야 하는 물음」이 정한다.**
      끝까지 남는 것 = **무슨 일이 있었나(제목·배지)** 와 **더 볼 수 있나(버튼)**.
      먼저 사라지는 것 = 작성자(누가) → 시각(언제) → 본문(무엇을).
   🔑 「누가」가 먼저인 이유: 이력 카드는 **한 줄 요약**이고 행위자는 드로워를 열면 전부 보인다. */
@media (max-width: 991.98px) {
    :is(.ds, .quote-content) .ds-hist-row-actor { display: none; }
}
@media (max-width: 767.98px) {
    /* 🔴 **2026-08-24 세로 패딩을 `--ds-header-ctl-pad` 로 맞췄다**(H2/H3 마감 조건).
       🔬 `.55rem` 이라 좁은 화면에서 줄 높이가 카드 머리보다 **0.4px** 높았다 — 같은 토큰을 쓰면 0 이 된다.
       📌 좌우는 이제 2-0-2 절의 `--ds-card-x` 가 쥔다(여기 값은 폴백으로만 남는다). */
    :is(.ds, .quote-content) .ds-hist-row { padding: var(--ds-header-ctl-pad) 1rem; }
    :is(.ds, .quote-content) .ds-hist-row-text, :is(.ds, .quote-content) .ds-hist-row-time { display: none; }
    /* 우측으로 미는 auto 마진을 시각에 걸어 뒀는데 여기선 그 요소를 숨기므로,
       버튼이 직접 auto 마진을 갖게 해서 우측 정렬을 유지한다. */
    :is(.ds, .quote-content) .ds-hist-row-all { margin-left: auto; }
}
/* 📱 **줄이 좁으면 감춘다 — 문턱은 「뷰포트」가 아니라 「컨테이너 폭(rem)」이다** (2026-08-27 B-1 3·4단계).
   🔬 위 미디어 쿼리(991.98 / 767.98)는 **뷰포트**만 본다. 그런데 이 줄이 넘치는 진짜 조건은
      「줄에 든 글자가 줄 폭보다 넓은가」이고 그것은 **글자 크기 3단계**를 따라 달라진다.
      실측(관리자 견적 상세 · 영문 · 1100px): 줄은 **720px** 인데 제목 219 + 배지 170 + 작성자 165 +
      시각 101 + 버튼 + 간격 65 = **750px** 이라 `xlarge` 에서만 넘쳤다(기본 글자에서는 들어간다).
   🔑 **문턱을 `rem` 으로 적으면 문턱 자체가 글자 크기를 따라 커진다** — 컨테이너 폭(px)은 그대로인데
      문턱만 커지므로 글자를 키운 사용자에게서 **한 단계 먼저** 항목이 사라진다. 그것이 이 줄의
      기존 규칙(작성자 → 시각 → 본문 순으로 감춘다)과 **정확히 같은 동작**이다.
   🔬 **문턱은 실측한 카드 폭 표를 보고 정했다**(관리자 업체 상세, 일곱 폭):
      기본  375:28.0 · 430:32.6 · 768:42.1 · 900:50.3 · 1100:46.3 · 1440:67.5 · 1920:97.5rem
      xlarge 375:24.0 · 430:27.9 · 768:32.6 · 900:39.3 · 1100:36.0 · 1440:53.0 · 1920:77.0rem
      → **기본 글자에서 문턱에 걸리는 폭은 375·430 뿐**이고 그 둘은 원래 `≤767.98px` 규칙이 이미
        감추던 자리다 → **기본 글자 화면은 하나도 바뀌지 않는다.**
   🔴 **버튼 글자는 `display:none` 이 아니라 화면에서만 걷는다** — 낭독기용 이름은 남긴다.
      🔥 `display:none`·`font-size:0` 으로 바꾸지 말 것(버튼에 이름이 없어져 「버튼」이라고만 읽힌다).
   🔑 **글자를 걷은 뒤에는 `flex: none`** 이다 — 그러지 않으면 버튼이 화살표보다 더 줄어
      **누를 것이 없는 조각**이 된다(실측: `xlarge` 768px 에서 화살표 16px 이 22px 상자에 갇혔다).
   ⚠️ `container-type: inline-size` 는 **절대 위치 자식의 기준 상자**가 된다 — 이 카드 안에 있는 것은
      아래 버튼 라벨뿐이고 그것은 버튼(`position: relative`)에 매여 있어 안전하다.
      🔥 드로워·모달을 이 카드 안으로 옮기지 말 것.
   📌 세 앱 템플릿 **13곳 전부** `.ds-hist-row` 가 `.ds-hist-card` 안에 있다(2026-08-27 전수 확인) —
      그래서 뷰포트 문턱을 따로 두지 않았다. 🔥 카드 밖에 이 줄을 새로 놓지 말 것(조건이 안 걸린다). */
:is(.ds, .quote-content) .ds-hist-card {
    container-type: inline-size;
    container-name: dshistrow;
}
/* 1단계 — 작성자(누가)를 감춘다. 🔬 문턱이 **48rem** 인 이유: 기본 글자 1100px 에서 카드가
   46.3rem 인데 그 폭에서 제목+배지+작성자+시각+버튼이 실제로 안 들어간다(고치기 전 실측 넘침 12px).
   작성자를 감추지 않으면 그 자리를 **상태 배지가 말줄임으로** 메꾼다(「Quote Requ…」 100px / 내용 136px) —
   🔥 21절이 정한 순서는 **작성자가 먼저**다. 배지로 떠넘기지 말 것. */
@container dshistrow (max-width: 48rem) {
    :is(.ds, .quote-content) .ds-hist-row-actor { display: none; }
}
/* 2단계 — 버튼 글자를 걷고 화살표만 남긴다.
   🔴 **문턱이 「구간」인 것에 유의** — `34rem` 아래에서는 다시 글자를 되살린다 (2026-08-29 사장님 지적:
      *"전체 이력 버튼 텍스트 없음"*).
   🔬 **왜** — 375·430 에서 그릇이 **336px = 28rem** 이라 2·3단이 **함께** 걸렸다. 그런데 3단이
      본문·시각을 지우고 나면 줄 안쪽 **304px** 에 남는 것은 제목 69.5 + 배지 54.9 + 버튼 28.4 +
      간격 23.4 = **176.2px** 이고 **남는 자리가 127.8px** 이다. 「전체 이력」은 **45px** 이라 넉넉히 들어간다.
      🔑 **2단은 「3단이 아직 안 걸린 구간」(34~40rem)을 위한 규칙**인데 그보다 좁아져도 계속 걸려 있었다
      → 자리는 남는데 글자만 사라져 **화살표뿐인 버튼**이 됐다(실측 글자 `<span>` 폭 **1px**).
   ⚠️ **`min-width` 를 `34rem` 으로 적지 말 것** — 3단(`max-width: 34rem`)과 **정확히 34rem 에서 겹쳐**
      그 한 점에서 둘 다 걸린다. `34.01rem` 으로 한 칸 띄운다.
   🔤 **영문 문구를 `History` 로 줄였다**(2026-08-29 사장님 확정 — *"영문은 history로 변경"*).
      🔬 되살린 글자가 영문에서 다시 넘치지 않게 하려는 것이다. **한국어는 「전체 이력」 그대로**다.
      📌 키는 `label.trade.quote.history.viewAll` 이고 **세 앱 11곳이 전부 이 버튼 라벨로만** 쓴다
         (전수 확인) — 이 앱에서는 상품 검수 상세 2곳이다.
      🔴 **이 앱의 `*.section.history`(「Quotation history」·「Contract history」 등)는 바꾸지 않았다** —
         그 키들은 **이력 줄의 제목(`.ds-hist-row-title`)과 드로워 제목까지 함께** 쓰므로 「History」로
         줄이면 무슨 이력인지 화면에서 사라진다. 🔥 뒤늦게 줄이지 말 것.
   🔥 `display:none`·`font-size:0` 으로 바꾸지 말 것(버튼 이름이 사라져 낭독기가 「버튼」이라고만 읽는다). */
@container dshistrow (max-width: 40rem) and (min-width: 34.01rem) {
    :is(.ds, .quote-content) .ds-hist-row-all { gap: 0; flex: none; }
    :is(.ds, .quote-content) .ds-hist-row-all > span {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
        white-space: nowrap;
        border: 0;
    }
}
@container dshistrow (max-width: 34rem) {
    :is(.ds, .quote-content) .ds-hist-row-text,
    :is(.ds, .quote-content) .ds-hist-row-time { display: none; }
}

/* 🕘 **「드로워를 여는 손잡이 띠」(`.ds-drawer-rail` / `.ds-drawer-rail-btn`)는 없앴다**
      (2026-08-18 구현 #94 5차 — 사용자 지시로 화면에서 지웠다).
   🔑 드로워를 여는 자리는 **그 판의 내용이 실린 카드의 머리**다 — 화면 가장자리를 늘 차지하는
      띠는 같은 일을 하는 두 번째 진입점이라 소음이 된다.
   🔥 되살리지 말 것. 되살려야 할 이유가 생기면 **그때 근거와 함께** 다시 만든다
      (쓰는 곳이 0곳인 규격을 미리 남겨 두지 않는다 — 이 저장소에서 실제로 몇 달 방치된 적이 있다). */


/* ══ 22) 상세 화면의 **항목 그리드** — `.ds-fields` (2026-08-15 사용자 지시) ═══════════

   > "항목명은 값 **위**에 써. 항목 구분은 **간격만으로** — 배경 블록도 구분선도 두지 마.
   >  항목명과 값의 대비를 확실히 하고, 항목별 바로가기는 **화살표 하나**만."

       사업자번호                진행 중 계약            평점
       123-45-67890             12건  ›                 4.5 / 5.0  ›
                                4,000,000 KRW

   🔑 **라벨을 값 옆에 두지 않는다.** 옆에 두면 라벨 열의 폭을 정해야 하는데(`w-150px`),
      그 폭은 카드 폭마다 맞는 값이 달라 절반 폭 카드에서 값 칸이 300px 밖에 안 남는다.
      위에 두면 폭을 정할 일이 없어지고 **한 줄에 몇 항목이 들어갈지는 브라우저가 센다.**
   🔥 **배경 블록·구분선을 넣지 말 것** (디자인 표준). 항목이 10개면 상자가 10개가 되어
      정작 봐야 할 값보다 상자가 먼저 눈에 들어온다 — 그리드 **안**의 구분은 **간격**이 한다.
   🔑 **묶음이 여럿이면 절(`.ds-fields-sec`)이 아니라 「카드」로 가른다** (2026-08-15 사용자 결정:
      *"카드를 3장으로 나누되 모두 가로 100%를 사용하자"*).
      🕘 그전 규칙은 *"묶음의 경계는 간격이 만든다"* 였고, 절 사이를 2.6rem 까지 벌려 놓았다.
         **그것으로 부족했다** — 업체 상세의 「실적」 카드 한 장이 업무 현황·누적·계약 총액·평가
         **네 묶음**을 품자 카드 안이 다시 「어디까지가 한 덩어리인지」 알 수 없는 화면이 됐다.
      🔑 그래서 **묶음 = 카드 한 장(가로 100%)** 으로 올린다. 절 제목은 **카드 제목**이 되고,
         `.ds-fields-sec` 는 **카드 안에 하위 묶음이 남을 때만**(「누적」 안의 「계약 총액」) 쓴다.
      ⚠️ 절 간격(2.6rem)·`is-sub`(1.5rem) 규칙을 **지우지 말 것** — 하위 묶음이 그것을 쓴다.
      🔥 카드로 가를 때 **카드 제목과 같은 말을 카드 안에 또 그리지 말 것**(한 화면에 같은 말이 둘이 된다).
   🔥 **열 수를 고정하지 말 것**(`auto-fit` + `minmax`). 이 그리드가 들어가는 칸은
      100% 카드 · 절반 폭 카드 · 좁은 화면 세 가지라, 열을 박으면 그중 하나는 반드시 무너진다.
      📌 항목 수를 **3의 배수**로 맞추면 100% 카드에서 줄 끝이 가지런해진다 — 그것은 **항목을
         고르는 쪽**이 할 일이지 열 수를 박을 이유가 아니다(박으면 억 단위 금액이 줄바꿈된다).
   ⚠️ 라벨을 **더 흐리게 하지 말 것** — 0-1 절의 계조 표대로 작은 글씨는 `700`(gray-700) 이상이라야
      대비를 지킨다. 대비는 **크기·굵기·색 셋으로** 벌린다(.875rem/600/gray-700 ↔ 1rem/600/gray-900).
   📌 라벨 크기를 `fs-9`(.75rem) 아래로 내리지 말 것 — 0-2 절이 금지한 계조다.
   🔥 **`.8rem` 으로 되돌리지 말 것** (2026-08-15 사용자 지적: *"모든 카드의 항목명 폰트가 너무 작다"*).
      🔬 실측: root 16px 에서 .8rem = **12.8px** 이라 값(16px)보다 3.2px 작았고, 같은 역할인
         `.ds-label`(절 제목·표 라벨 칸 — 크기 지정이 없어 1rem)과도 **한 화면 안에서 갈려 있었다.**
         .875rem(14px)이면 값과의 층은 남기면서 `.ds-label` 과의 벌어짐이 절반으로 줄어든다.
      ⚠️ **px 로 박지 말 것** — 글자 크기 3단계 토글(0-0 절)이 root 기준 `rem` 으로 동작한다. */
/* 🔥 **한 줄에 최대 3개**다 — 4개 이상이면 다음 줄로 넘긴다 (2026-08-15 사용자 결정, 구현 #53).
   🔬 그전에는 상한이 없어 **1440px 창의 100% 카드에서 한 줄에 5개**가 들어갔다(실측). 그래서 항목을
      6/6/3 으로 맞춰 놓아도 줄 끝이 5+1 로 어긋났다 — 「3의 배수로 맞춘다」가 화면에서 성립하지 않았다.
   🔑 **열 수를 박지 않는다.** 최소폭을 「3열이 되는 폭」으로 끌어올리는 방식이고, 이것은 22-2 절
      (추이 타일)이 이미 쓰는 것과 **같은 수법**이다 — 넓으면 그 값이 커져 4열이 될 수 없고,
      좁으면 `11rem` 이 이겨 2 → 1 로 자연스럽게 줄어든다.
      🔥 `repeat(3, 1fr)` 로 박지 말 것 — 반 폭 카드(신용·공급 운영)에서 값 칸이 손톱만 해지고
         억 단위 금액이 글자마다 줄바꿈된다.
   ⚠️ **가로 gap 을 바꾸면 아래 `3rem`(gap 2개분)도 함께** 바꿀 것 — 어긋나면 3열이 2열로 주저앉는다. */
:is(.ds, .quote-content) .ds-fields {
    display: grid;
    /* 🔑 `min(100%, …)` 가 없으면 컨테이너가 최소폭보다 좁을 때 **그대로 넘친다**(9절과 같은 함정). */
    grid-template-columns: repeat(auto-fit, minmax(min(100%, max(11rem, (100% - 3rem) / 3)), 1fr));
    /* 🔑 **세로 2rem · 가로 1.5rem** (2026-08-15 사용자 지적: *"세로 간격은 업무 현황 카드 정도가
       좋은 것 같아. 세로 간격이 넉넉하고 세로 줄이 맞으니까 눈에 들어온다"*).
       🔬 실측(1440px, 업무 현황 카드): 보조값(`.ds-field-sub`) 두 줄이 달린 「진행 중 계약」이 86px 이라
          그 줄 전체가 높아져, **보조값이 없는 이웃 항목은 아래로 59px** 이 비었다 — 사용자가 좋다고 한
          그 여백이다. 보조값이 없는 카드(누적·평가)는 gap 인 **18.4px** 뿐이라 훨씬 빽빽했다.
       🔑 그 둘 사이를 **2rem(32px)** 으로 잡았다 — `card-body` 좌우·상하 패딩과 **같은 값**이라
          카드 안팎의 리듬이 한 값으로 모인다. 59px 을 그대로 쓰면 항목이 서로 남처럼 떨어진다.
       🔥 **가로 gap(1.5rem)을 건드리지 말 것** — 위 최소폭 식이 `(100% - 3rem)`으로 **그 값의 2개분을
          박고 있어** 가로를 바꾸면 식도 함께 고쳐야 하고, 안 고치면 3열 상한이 조용히 깨진다.
       🔥 **보조값을 지워서 간격을 벌지 말 것** — 사용자가 *"부가 정보는 간격 안에 표시해도 거슬리지
          않는다"* 고 했다. 벌리는 것은 gap 의 일이다.
       ⚠️ px 로 박지 말 것(글자 크기 3단계 토글이 root 기준 rem 이다). */
    gap: 2rem 1.5rem;
    /* 🔑 **`align-items: start` 를 지우지 말 것** — 「세로 줄이 맞는다」가 이 지적의 절반이다.
       `stretch`(기본값)로 두거나 항목을 flex 로 바꾸면 줄이 어긋난다. */
    align-items: start;
}
/* 🔴 **정보 격자 다음에 오는 블록은 선으로 끊는다** (2026-08-21 구현 #48 사용자 지적:
      *"Booking Deadline, Product Description 사이에 구분선"*).
   🔬 실측(1440px, 여행사 상품 상세): 격자(`.ds-fields`)가 끝나고 상품 설명 블록이 **간격 0px** 로
      붙어 있었다 — 경계선도 위 여백도 없다. 그래서 격자의 **마지막 항목**(예약 마감일)과
      **다음 절의 제목**(상품 설명)이 한 덩어리로 읽혔다. 🔑 **한국어·영어 모두 같다**(구조 문제다).
   🔑 값은 척도에서 고른다 — `--ds-sp-5`(24px, 「구역과 구역 사이」). 격자 자체의 세로 gap 이 2rem 이라
      그보다 작게 두면 **항목 사이보다 절 사이가 좁아** 오히려 한 덩어리로 보인다.
   ⚠️ **`.separator` 는 제외한다** — 이미 선을 그리는 부품이라 두 겹이 된다.
   ⚠️ **카드 푸터도 제외한다** — 푸터는 자기 경계선을 갖고 있다.
   🔥 화면 CSS 에서 각자 긋지 말 것 — 그러면 화면마다 선 색·여백이 갈린다(이 규칙이 생긴 이유다). */
:is(.ds, .quote-content) .ds-fields + *:not(.separator):not(.card-footer) {
    margin-top: var(--ds-sp-5);
    padding-top: var(--ds-sp-5);
    border-top: 1px solid var(--ds-line-soft);
}
/* 값이 긴 항목(주소·소개·계좌)이 섞인 그리드 — 최소폭을 넓혀 두 글자마다 줄바꿈되지 않게 한다.
   📌 상한(3열)은 위와 같다. 바닥만 올린 것이라 **넓은 화면에서 둘의 결과가 같다** —
      좁아질 때 이쪽이 먼저 2 → 1 로 내려가는 것이 차이다.
   🔥 **바닥은 13rem 이다. 16rem 으로 되돌리지 말 것** (2026-08-15 사용자 지적:
      *"한 줄에 표시하는 데이터 수를 통일해서 줄을 좀 맞춰 달라"*).
      🔬 실측: `.ds-half-row` 의 반 폭 카드(528px, 안쪽 456px)에서 16rem 은 **1열**, 기본(11rem)은 **2열**이라
         나란히 놓인 두 카드가 **한쪽은 1개씩, 한쪽은 2개씩** 떨어져 줄이 어긋났다
         (거래 자격 | 정산 준비). 13rem 이면 둘 다 **2열**이 되어 줄이 맞는다.
      ⚠️ **열 수를 박아서 맞추지 말 것** — 박으면 좁은 폭에서 값이 글자마다 줄바꿈된다.
      📌 100% 카드에서는 `(100% - 3rem)/3` 이 이기므로 **3열 그대로**다(업체 정보 카드 실측 3+3+2). */
:is(.ds, .quote-content) .ds-fields.is-wide {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, max(13rem, (100% - 3rem) / 3)), 1fr));
}
/* 📱 **좁은 화면에서도 2열을 지킨다** (2026-08-15 사용자 지적, 구현 #58:
   *"모바일에서는 회사 소개랑 관계없이 **업체정보만으로도** 다른 내용을 확인할 수가 없네"*).
   🔬 실측(375px): 바닥값 11rem·13rem 이 이겨 **1열**로 떨어지면서 업체 정보 12항목이
      **세로 12줄**이 되어 카드 하나가 화면 몇 배 길이가 됐다 — 아래 카드에 닿으려면 한참 스크롤해야 한다.
   🔑 **바닥값만 낮춘다(9.5rem)** — 375px 기준 안쪽 폭 ≈343px 이라 `2×152 + 24(gap) = 328` 로 **2열**이 되고,
      항목 12개가 **6줄**로 줄어 카드 높이가 대략 절반이 된다.
      📌 `is-wide`(값이 긴 항목용)도 **함께** 내린다 — 업체 정보·정산 준비가 그 갈래라
         한쪽만 내리면 같은 화면에서 카드마다 열 수가 달라진다.
   🔥 **상한 식(`(100% - 3rem)/3`)과 가로 gap(1.5rem)은 건드리지 않았다** — 그 둘이 맞물려
      「한 줄 최대 3개」를 만들므로, 한쪽만 바꾸면 **넓은 화면의 3열 상한이 조용히 깨진다.**
      좁은 화면에서는 어차피 `(100% - 3rem)/3` 이 작아 바닥값이 이긴다(= 여기서 바뀌는 것은 바닥값뿐이다).
   ⚠️ **더 내리지 말 것** — 8rem 이면 375px 에서 3열이 되어 「사업자 종류」·날짜가 글자마다 줄바꿈된다.
   ⚠️ 값이 긴 항목은 `is-full` 로 **줄 전체**를 쓰므로 이 변경의 영향을 받지 않는다(주소·소개). */
@media (max-width: 767.98px) {
    :is(.ds, .quote-content) .ds-fields,
    :is(.ds, .quote-content) .ds-fields.is-wide {
        grid-template-columns: repeat(auto-fit, minmax(min(100%, max(9.5rem, (100% - 3rem) / 3)), 1fr));
    }
}
/* ✕ **모달 닫기 버튼 — 다크에서 안 보이던 것** (2026-08-18 사용자 지적 — 폰 실기기)

   > *"다크모드에서 모달 닫기 x 버튼 색상때문에 잘 안보여"*

   🔬 벤더의 `.btn-close` 는 **검은색 X 를 그린 SVG 를 배경 이미지로** 깔고 `opacity: .5` 를 준다
      (`fill='%23000000'` 이 data URI 안에 **박혀 있다**). 다크 테마 블록에는 이것을 다루는 규칙이
      **아예 없어서**, 검은 판(`#1E1E2D`) 위에 **반투명 검정 X** 가 놓였다.
   🔑 **색을 바꿀 수가 없다** — SVG 가 data URI 안에 있어 `color` 도 `currentColor` 도 닿지 않는다.
      그래서 **`filter` 로 뒤집는다.** 토큰 한 곳에서 정하므로 화면마다 다시 손댈 일이 없다.
   ⚠️ **`invert(1)` 은 요소 전체를 뒤집는다** — `.btn-close` 는 배경 이미지 하나뿐이라 안전하다.
      🔥 배경색·테두리가 있는 버튼에 이 방식을 쓰지 말 것.
   📌 **라이트는 `none`** 이라 렌더 결과가 그대로다. `opacity` 도 함께 올려 다크에서 또렷하게 한다
      (벤더 기본 .5 는 검은 판 위에서 너무 묻힌다). */
:root { --ds-close-filter: none; --ds-close-opacity: .5; }
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) { --ds-close-filter: invert(1); --ds-close-opacity: .75; }
}
:root[data-theme="dark"] { --ds-close-filter: invert(1); --ds-close-opacity: .75; }
.modal .btn-close,
.ds .btn-close {
    filter: var(--ds-close-filter);
    opacity: var(--ds-close-opacity);
}
.modal .btn-close:hover,
.ds .btn-close:hover { opacity: 1; }

/* 한 항목이 줄 전체를 쓴다(회사 소개처럼 문장이 들어오는 자리). */
:is(.ds, .quote-content) .ds-field.is-full { grid-column: 1 / -1; }

/* 📱 **좁은 화면에서만 줄 전체를 쓴다** — `is-full-narrow` (2026-08-18 사용자 지적 — 폰 실기기)

   > *"「megamost.kr@gmail.co / m」 처럼 마지막 한 글자가 다음 줄로 넘어간다. 저럴거면 컬럼 1개로 줄여야 할듯."*

   🔬 실측(375px, 회원 상세): 이메일 칸은 **147px** 인데 `megamost.kr@gmail.com` 은 **152.9px** 이라
      **5.9px 모자라** 마지막 한 글자만 둘째 줄로 떨어졌다(`.ds-field-v` 가 `word-break: break-word` 다).
   🔑 **「지금 넘치는가」로 고르지 않는다** — 글자 크기 토글·언어·데이터에 따라 그 판정은 매번 달라진다.
      **값의 성격**으로 가른다:
        · **길이를 우리가 정할 수 없는 값**(이메일·주소·회사명·URL·자유 입력) → 이 클래스.
        · **개수가 유동적인 값**(배지·칩 목록) → 여기도 `is-full-narrow` 다.
          🔴 **한때 「폭과 무관하게 `is-full`」로 적었다가 바로잡았다** — 넓은 화면에서는 배지 서너 개가
             한 칸에 잘 들어가고, 거기서 줄 전체를 쓰면 **2026-08-16 구현 #77 에서 지적받은
             「그리드 맨 끝에 홀로 뜬 띠」**가 그대로 되살아난다.
          🔑 **같은 항목에 대한 두 지적이 서로 다른 화면이었다** — #77 은 PC, 이번은 폰이다.
             🔥 「전에 반대로 지적받았다」로 판단하기 전에 **어느 폭에서 본 것인지** 먼저 볼 것.
          ⚠️ 배지가 크게 늘어 넓은 화면에서도 접히기 시작하면 그때 다시 본다.
        · **길이가 사실상 정해진 값**(이름·전화·사업자번호·날짜·코드값) → 그대로 2열.
   🔑 **`is-full` 과 가르는 이유** — 이 값들은 **넓은 화면에서는 한 칸에 잘 들어간다.** 거기서도 줄 전체를
      쓰면 옆 칸이 통째로 비어 카드가 길어지기만 한다(🔬 1280px 실측: 이메일 칸 **340px** vs 값 152.9px).
   ⚠️ **끊는 폭은 항목 그리드가 2열로 내려앉는 지점(767.98px)과 같아야 한다** — 다르면 「2열인데 안 펴지는」
      구간이 생긴다. 🔥 여기만 다른 숫자를 쓰지 말 것.
   ⚠️ 대가는 **세로 길이**다(한 항목이 한 줄을 통째로 쓴다). 그래서 **성격상 필요한 항목에만** 붙인다. */
@media (max-width: 767.98px) {
    :is(.ds, .quote-content) .ds-field.is-full-narrow { grid-column: 1 / -1; }
}

/* 📜 **길이 제한 없는 자유 텍스트 — 상자 안에서만 세로로 스크롤한다** (2026-08-15 사용자 지시, 구현 #58:
   *"회사소개가 너무 긴 경우 다른 화면을 볼 수 없는 상황이 생기는데 … 세로 스크롤 되는 박스 안에 넣어주던지
   해 줘. 필요하다면 전문보기 버튼과 모달을 추가해도 돼"*).
   🔬 업체 상세 실측: **회사 소개 한 항목이 수십 줄**을 차지해 아래 카드가 전부 화면 밖으로 밀렸다 —
      한 항목이 화면을 삼킨다. 대상은 **업체가 직접 쓰는 글**(회사 소개·반려 사유·검수 의견 같은 것)이다.
   🔑 **상한은 8줄**이다 — 두세 문단짜리 소개는 그대로 다 보이고(짧은 글은 잘리지 않는다),
      그 이상은 카드 하나가 화면을 먹기 시작하는 지점이다. 계산은 `1.45em`(= `.ds-field-v` 의 줄높이) × 8.
      ⚠️ **px 로 박지 말 것** — `em` 이라 글자 크기 3단계 토글(0-0 절)을 따라 함께 늘고 준다.
   🔑 **`display: block` 으로 되돌린다** — `.ds-field-v` 는 flex 라 그대로 두면 글이 익명 flex 항목이
      되어 `white-space` · 스크롤이 뜻대로 걸리지 않는다. 이 자리는 값이 아니라 **글 한 덩어리**다.
   🔥 **가로 스크롤은 만들지 않는다**(`overflow-x: hidden` + `overflow-wrap: anywhere`) —
      긴 URL·공백 없는 문자열이 들어와도 줄을 바꿔서 담는다.
   🔒 줄바꿈은 **CSS 가 살린다**(`pre-wrap`) — 🔥 `<br>` 을 만들어 넣지 말 것.
      그 글은 사용자가 넣은 값이라 태그를 섞는 순간 **저장형 XSS** 자리가 된다(마크업은 `th:text` 로만).
   📌 쓰는 법: 값 `<span>` 에 `ds-longtext js-longtext`, 그 **다음 형제**로 `hidden` 인
      `<button class="ds-longtext-more js-longtext-more">전문 보기</button>` 를 둔다.
      화면 어딘가에 모달 하나(`js-longtext-modal` · 제목 `js-longtext-title` · 본문 `js-longtext-body`).
      🔴 **여닫기와 넘침 판정은 세 앱 `common.js` 한 곳이 한다**(2026-09-18 P44) —
      `bindGlobalEvents` 의 전역 위임 + `Common.measureLongText()`. 🔥 화면 스크립트에 복제하지 말 것
      (그전에는 관리자 `Company.js`·`Member.js`·`ProductReview.js` 에 **세 벌**로 갈려 있었다).
      ⚠️ 값을 JS 가 채우는 화면은 채운 자리에서 `Common.measureLongText()` 를 직접 부른다. */
:is(.ds, .quote-content) .ds-field-v.ds-longtext {
    display: block;
    max-height: calc(1.45em * var(--ds-longtext-lines));
    overflow-y: auto;
    overflow-x: hidden;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    border: 1px dashed var(--ds-line);
    border-radius: .475rem;
    padding: .5rem .75rem;
}
/* 📖 **펼친 상태 — `.is-expanded`** (2026-09-22 사장님 지시).
   > *"긴 텍스트는 테두리가 있는 div 안에 넣고 기본 3-4줄만 보이는 세로길이에 스크롤을 제공하고
   >  펼쳐보기 버튼을 제공해서 전체 세로 길이로 확장되도록 하는게 좋을 듯"*
   🔑 **`max-height: none` 하나로 끝난다** — 🔥 펼친 높이를 JS 가 재서 px 로 박지 말 것.
      글자 크기 3단계 토글(0-0 절)·창 폭이 바뀌면 그 px 가 곧바로 틀린 값이 된다.
   🔑 `overflow-y: visible` 을 함께 푼다 — 안 풀면 상한만 사라지고 **스크롤 막대가 남는다.**
   🕘 **이 자리는 원래 「전문 보기」 모달이었다**(2026-09-18 P44). 🔥 모달로 되돌리지 말 것 —
      모달은 **원문을 화면 밖으로 옮기는 것**이라 앞뒤 항목과 대조하며 읽을 수 없었다. */
:is(.ds, .quote-content) .ds-field-v.ds-longtext.is-expanded,
.ds-longtext-box.is-expanded {
    max-height: none;
    overflow-y: visible;
}
/* 🔘 **펼쳐보기/접기 버튼 — `.ds-longtext-more`**
   ⚠️ **`.ds` 스코프를 쓰지 않는다**(2026-09-22 에 풀었다) — 이 버튼은 이제 `common.js` 가
      상자 바로 뒤에 만들어 붙이고, 상자는 **`.ds` 가 없는 상세 화면**에도 있다(예약·계약·문의 상세).
      🔥 `.ds` 를 다시 붙이면 그 화면들에서 버튼이 **민짜 링크처럼** 보인다.
   🔑 `min-height` 다(`height` 가 아니다) — 좁은 화면에서 글자가 두 줄이 되어도 눌리는 칸이 남는다.
      📌 좁은 화면 44px 바닥값은 `--ds-btn-h` 가 이미 쥐고 있다(0-2 절 `max(36px, …)` 절 참고). */
.ds-longtext-more.btn {
    margin-top: .4rem;
    min-height: var(--ds-btn-h);
    padding: 0 .75rem !important;   /* Metronic `.btn:not(…)×6` 이 (0,7,0) 이라 !important 가 필요하다 */
    display: inline-flex;
    align-items: center;
    font-size: var(--ds-font-sub);
    line-height: 1;
}
/* 🔥 **`[hidden]` 규칙을 지우지 말 것** — 바로 위 `display: inline-flex` 가 브라우저 기본
   `[hidden]{display:none}` 보다 세서, 이 줄이 없으면 **짧은 글에도 버튼이 늘 보인다.**
   🔬 **`.btn` 규칙 앞에 두면 안 된다**(2026-09-22 바로잡음) — 두 선택자의 명시도가 **같아서**
      뒤에 온 쪽이 이긴다. 전에는 이 줄이 앞에 있어 사실상 **아무 일도 하지 않았다.**
      `!important` 는 다시 순서가 뒤집혀도 버티라고 붙인 것이다. */
.ds-longtext-more[hidden] { display: none !important; }
/* 👆 **좁은 화면에서는 손가락이 닿는 칸의 바닥값이 44px 다.**
   ⚠️ `--ds-btn-h` 를 건드리지 말 것 — 그 토큰은 **모든 단독 버튼**의 높이라, 44 로 올리면
      화면 전체의 버튼 줄 높이가 함께 바뀐다. 이 부품에만 건다. */
@media (max-width: 767.98px) {
    .ds-longtext-more.btn { min-height: 44px; }
}
/* 🗑 **`.ds-longtext-full` 은 지웠다** (2026-09-24 P93).
   🕘 「전문 보기 모달의 본문」 규격이었는데, 2026-09-23 「펼쳐보기」 개편으로 그 모달이 없어지고
      쓰던 자리가 전부 **`.ds-longtext-box`** 로 옮겨 가 **참조가 0곳**이 됐다
      (전수 실측 2026-09-24: 세 앱 `templates/**`·`static/**` 에서 실사용 0 · 남은 언급 2곳은
       이 앱 `product/reviewDetail.html`·`trade/quoteView.html` 의 **주석 속 과거 기록**이다).
   🔥 **되살리지 말 것** — 규격이 둘이면 다음 사람이 새 화면에 옛 것을 쓴다.
      이 저장소가 `.ds-statelist-*` 에서 이미 겪은 모양이다. 긴 글은 `.ds-longtext-box` 를 쓴다. */

/* 📜 **줄 수로 상한을 둔 세로 스크롤 상자 — `.ds-scrollbox`** (2026-08-17 사용자 지시).
   길이를 알 수 없는 **목록**(몇 건일지 모르는 미리보기 줄들)을 상자 안에 가둔다.
   🕘 `.ds-longtext`(바로 위)와 **형제 규격**이다 — 그쪽은 「글 한 덩어리」, 이쪽은 「줄 여럿」이다.
      🔥 둘을 합치지 말 것: `.ds-longtext` 는 `.ds-field-v` 에만 붙는 값 자리 규격이고
      `white-space: pre-wrap` 으로 원문의 줄바꿈을 살리지만, 이 상자는 **자식 요소가 곧 줄**이다.
   🔑 **상한은 줄 수(`em`)로 잡는다** — px 로 박으면 글자 크기 3단계 토글(0-0 절)에서
      상자만 그대로라 6줄이 4줄이 된다. `line-height` 를 여기서 못 박아야 `1.6em × 6` 이 정확히 6줄이다.
   🔥 **가로 스크롤을 만들지 않는다** — 긴 업체명은 `overflow-wrap` 으로 줄을 바꿔서 담는다.
      가로로 흘리면 오른쪽 끝의 「무엇으로 바뀌는지」가 화면 밖으로 밀린다(그것이 이 상자의 요점이다).
   🔑 **제목(건수)은 이 상자 밖에** 둔다 — 안에 넣으면 목록을 내리는 순간 「몇 건인지」가 사라진다.
   📌 쓰는 법: 제목 한 줄 + 그 아래 `<div class="ds-scrollbox">` 안에 줄 요소를 나열한다. */
.ds .ds-scrollbox {
    max-height: calc(1.6em * 6);
    line-height: 1.6;
    overflow-y: auto;
    overflow-x: hidden;
    overflow-wrap: anywhere;
    border: 1px dashed var(--ds-line);
    border-radius: .475rem;
    padding: .5rem .75rem;
}
.ds .ds-scrollbox > * + * { margin-top: .25rem; }

/* 📜 **카드 본문에 단독으로 서는 자유 텍스트 — `.ds-longtext-box`** (2026-08-18 사용자 지시, 구현 #94 4차)

   > *"요청 사항 항목 같은 사용자가 자유롭게 입력하는 항목의 경우에는 사이즈가 정해진 div 안에 출력하고
   >  길어지면 세로 스크롤되도록 하는 게 맞을 것 같아. 엄청 길게 입력하면 화면이 엄청 길어질 테니 말야."*

   🔑 **새로 만든 값이 하나도 없다** — 위 두 규격에서 그대로 가져와 조합했다:
      · 글 규칙(8줄 상한·`pre-wrap`·가로 스크롤 없음) = `.ds-longtext`
      · 상자 규칙(점선 테두리·모서리·안쪽 여백) = `.ds-scrollbox`
   🕘 **왜 그 둘을 그냥 쓰지 못하나**: `.ds-longtext` 는 선택자가 `.ds-field-v` 에 묶여 있어
      **항목 그리드 밖에서는 아무 일도 하지 않고**(카드 본문의 문단이 그렇다), `.ds-scrollbox` 는
      *"자식 요소가 곧 줄"* 인 **목록용**이라 원문의 줄바꿈을 살리지 않는다.
      🔥 그 둘의 선택자를 넓혀 합치지 말 것 — 그쪽 주석이 *"둘을 합치지 말 것"* 이라고 못박아 두었다.
   🔑 **`max-height` 다(고정 높이가 아니다)** — 짧은 글에 **빈 상자가 남지 않는다.**
      상한 8줄은 `.ds-longtext` 와 같은 값이고, `em` 이라 글자 크기 3단계 토글을 따라 함께 는다
      (폰 12px 에서도 8줄이다). ⚠️ px 로 박지 말 것.
   👁 **잘렸다는 것이 보인다** — 점선 상자가 경계를 그려 스크롤 막대가 그 안에 선다.
      🔥 테두리를 지우지 말 것: 경계가 없으면 **아래에 글이 더 있는지 알 수 없다**(이 규격의 요점이다).
   🔒 줄바꿈은 **CSS 가 살린다**(`pre-wrap`) — 🔥 `<br>` 을 만들어 넣지 말 것(저장형 XSS 자리다).
   📌 **문서(발행된 견적서·계약서) 안의 자유 텍스트에는 쓰지 않는다** — 문서는 전문이 보여야 한다. */
/* 🔴 **2026-09-22 에 이 규격이 「긴 자유 텍스트」의 정본이 됐다**(사장님 지시).
   ⚠️ **`.ds` 스코프를 풀었다** — 예약·계약·견적·문의·분쟁 상세는 `.ds` 가 없는 화면이라
      `.ds .ds-longtext-box` 로는 **아무 일도 일어나지 않았다.** 🔥 `.ds` 를 다시 붙이지 말 것.
   🔑 **줄 수는 `--ds-longtext-lines`(0-2 절) 한 곳**이 정한다 — 🔥 여기에 숫자를 다시 적지 말 것.
   🔑 **넘칠 때만 「펼쳐보기」 버튼이 붙는다** — 붙이는 것도 넘침 판정도 `common.js`
      `Common.measureLongText()` 한 곳이 한다. 🔥 화면 마크업에 버튼을 적지 말 것(갈린다).
   🔑 **`.is-plain`** 은 **제 상자를 이미 가진 자리**(말풍선 `.cs-chat-bubble` · 타임라인 본문
      `.trade-timeline-body` · 경고 띠 `.alert` · 메모 카드 `.memo-card-body`)에 쓴다.
      상자 안에 상자를 겹쳐 그리지 않으면서 **줄 상한·스크롤·펼쳐보기만** 가져간다.
      🔥 `.is-plain` 없이 그 자리에 붙이지 말 것 — 테두리가 이중으로 그려진다. */
.ds-longtext-box {
    display: block;
    max-height: calc(1.45em * var(--ds-longtext-lines));
    overflow-y: auto;
    overflow-x: hidden;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}
/* 🔥 **`:not(.is-plain)` 이어야 한다 — `.is-plain` 에서 `padding: 0` 으로 「지우지」 말 것.**
   🔬 지우는 방식이면 (0,2,0) 이라 `.cs-chat-bubble`·`.trade-timeline-body`(0,1,0)의 **제 안쪽 여백까지
      함께 없앤다**(말풍선이 납작해진다). 아예 **선언하지 않는 것**만이 host 의 값을 남긴다. */
.ds-longtext-box:not(.is-plain) {
    border: 1px dashed var(--ds-line);
    border-radius: .475rem;
    padding: .5rem .75rem;
    color: var(--ds-text-strong);
}

:is(.ds, .quote-content) .ds-field { min-width: 0; }
/* ── 세 층의 규격은 **0-2 절의 토큰 한 곳**이 정한다(크기·자간). 여기서 숫자를 다시 적지 말 것.
      🔑 세 층이 **서로의 값에 기대어** 순서를 만들기 때문에 한 층만 따로 고치면 순서가 뒤집힌다. ── */
/* 🏷 ① 항목명 — **가장 먼저 읽혀야 하는 층**이다.
   색은 `.ds-label` 과 **같은 토큰**이다(0-2 절). 화면마다 손으로 칠하지 말 것.
   🔥 **`.8rem` 으로 되돌리지 말 것** — 22절 머리의 계조 설명 참고(2026-08-15 사용자 지적).
   🔑 **자간(`--ds-track-field-k`)이 이 층의 세 번째 축**이다 — 굵기·색만으로는 바로 아래 붙은
      값 덩어리와 잘 안 갈렸다. 벌어진 글자는 「띠」처럼 읽혀 훑을 때 먼저 걸린다.
      ⚠️ **`em` 이라 글자 크기 토글을 따라간다** — px 로 박지 말 것. */
:is(.ds, .quote-content) .ds-field-k {
    display: block;
    font-size: var(--ds-font-field-k);
    font-weight: var(--ds-weight-label);
    letter-spacing: var(--ds-track-field-k);
    color: var(--ds-text-label);
    line-height: 1.35;
    margin-bottom: .15rem;
}
/* 📊 ② 데이터 — **크고 진하되 얇다.**
   🔥 **굵기를 `--ds-weight-label`(600)로 되돌리지 말 것.** 그것이 2026-08-17 이전 상태였고,
      항목명과 굵기가 같아 **상세 화면이 통째로 굵어** 둘이 구분되지 않았다
      (사용자 지적: *"데이터가 bold 처리된 항목들이 엄청 많이 보인다"*).
      🔑 **굵기를 내주는 대신 크기와 색을 받았다** — 1rem→1.0625rem, gray-900 그대로.
         그래서 값은 **여전히 화면에서 가장 진한 글자**이고 「데이터가 항목명보다 중요하다」가 지켜진다.
   🔢 **`tabular-nums` 는 장식이 아니다** — 금액·건수·요율이 세로로 늘어서는 그리드라 숫자 폭이
      들쭉날쭉하면 자릿수를 눈으로 못 센다. 폭을 고정하면 **자리끼리 세로로 맞는다.**
      ⚠️ 굵기를 400 으로 내리면서 숫자가 가늘어졌으므로 이 정렬이 더 필요해졌다.
   🔥 화면에서 `fw-*`·`font-weight`·`fs-*` 로 덮지 말 것 — 그 화면만 갈린다. */
:is(.ds, .quote-content) .ds-field-v {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: .35rem;
    font-size: var(--ds-font-field-v);
    font-weight: var(--ds-weight-body);
    font-variant-numeric: tabular-nums;
    color: var(--ds-text-strong);
    line-height: 1.45;
    min-width: 0;
    word-break: keep-all;
    overflow-wrap: anywhere;
}
/* 🕘 **`.ds-field-v.is-text` 는 2026-08-17 에 없앴다 — 되살리지 말 것.**
   🔑 **왜 있었나**(지우면 안 되는 기록이다): 2026-08-15 사용자 지적 *"알림 문구가 볼드로 나온다"*.
      그때 `.ds-field-v` 의 굵기가 **600** 이라, 알림 카드처럼 **문장 한 줄**이 그 자리에 오면
      본문이 화면에서 가장 굵어져 옆의 유형 배지보다 먼저 읽혔다. 그래서 「값이 아니라 읽는 문장인
      자리」에만 **굵기를 400 으로 내리는** 예외 클래스를 뒀다.
   🔑 **왜 없앴나**: 2026-08-17 에 값의 기본 굵기가 **그 예외와 같은 400** 이 되면서
      이 클래스가 **아무것도 바꾸지 않는 이름**이 됐다. 남겨 두면 다음 사람이 「이게 왜 필요하지」 하며
      기본 굵기를 되돌리는 근거로 삼는다(예외가 있다는 것은 기본이 다르다는 뜻이므로).
      🔬 실측: 전 저장소 사용처 **10곳(전부 Admin 템플릿)** — 업체 상세 3 · 회원 상세 4 ·
         상품 검수 상세 1 · 약관 관리 1(+주석 1). DMC·Agency 는 **0곳**이었다.
         10곳 전부에서 클래스 이름을 걷어냈다(`ds-longtext` 와 겹친 3곳은 그쪽만 남겼다).
   🔥 **문장이 굵어 보인다는 지적이 다시 오면 이 클래스를 되살리지 말고 「값의 기본 굵기가 400 인가」를
      먼저 재라** — 되살아났다는 것은 누군가 기본을 600 으로 되돌렸다는 뜻이다. */

/* 📝 ③ 설명 — **물러나는 층**이다(값 아래 한 줄 보조: 예금주 · 경과일 · 최근 성립일).
   🔥 **색을 더 낮추지 말 것.** `--ds-text-faint`(gray-600)가 바닥이고 라이트 대비 **3.79** 라
      이미 4.5 에 못 미친다 — 여기서 더 낮추면 읽을 수 없는 글자가 된다.
      더 물러나게 해야 하면 **크기(.8rem)와 위 여백(.25rem)** 으로 가른다.
   🔑 **위 여백이 .1rem → .25rem 으로 늘었다**(2026-08-17) — 값이 1.0625rem 으로 커지면서
      바로 아래 붙은 설명이 값의 두 번째 줄처럼 읽혔다. 여백이 「딸린 글」임을 만든다. */
:is(.ds, .quote-content) .ds-field-sub {
    display: block;
    font-size: var(--ds-font-field-sub);
    font-weight: var(--ds-weight-body);
    color: var(--ds-text-faint);
    margin-top: .25rem;
    line-height: 1.4;
}
/* ▸ 항목별 바로가기 — **화살표 하나뿐**이다 (2026-08-15 사용자 지시:
   *"항목마다 「통계」·「거래」 같은 글자를 반복하지 마"*).
   🔒 글자가 없으므로 **`aria-label` 이 유일한 이름**이다 — 어디로 가는지 반드시 남길 것.
   ⚠️ 링크가 없는 값(`notificationUrl` 은 서버가 null 로 준다)에는
      **화살표 자체를 그리지 않는다** — 없는 주소를 지어 넣으면 404 가 아니라 흰 화면이다. */
:is(.ds, .quote-content) .ds-field-go {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 1.35rem;
    height: 1.35rem;
    border-radius: .3rem;
    line-height: 1;
    font-size: 1.05rem;
    color: var(--ds-text-label);
    text-decoration: none;
}
:is(.ds, .quote-content) .ds-field-go:hover,
:is(.ds, .quote-content) .ds-field-go:focus-visible {
    color: var(--ds-primary);
    background: var(--ds-fill);
}

/* 카드 안의 **절 제목** — 「업무 현황」·「계약 총액」처럼 항목 묶음에 이름을 준다.
   ⚠️ 이것도 배경·선이 아니라 **글자와 간격**뿐이다. 제목 자체는 `.ds-label` 이 색·굵기를 정한다.

   🔥 **묶음의 경계는 「간격」이 만든다** (2026-08-15 사용자 지적: *"알아보기가 힘들어 …
      아무래도 위아래 여백이 같아서 소속감을 못느끼는듯하네"*).
      절 사이(2.6rem)가 **절 안의 행 간격(1.15rem)의 두 배 이상**이어야 한 묶음으로 읽힌다 —
      1.85rem 은 1.6배라 항목 사이 여백과 구분이 안 됐다.
   ⚠️ **배경 블록·구분선으로 가르지 말 것**(22절 머리와 같은 이유). */
:is(.ds, .quote-content) .ds-fields-sec + .ds-fields-sec { margin-top: 2.6rem; }
:is(.ds, .quote-content) .ds-fields-head {
    display: flex;
    align-items: center;
    gap: .5rem;
    margin-bottom: .85rem;
}
/* 절 **안**의 작은 묶음 — 「누적」 안의 「계약 총액」처럼 한 절을 다시 가르는 자리.
   🔑 절 사이(2.6rem)보다 **좁아야** 「같은 절 안」으로 읽힌다.
   ⚠️ 앞 규칙과 같은 (0,3,0) 이라 **뒤에 두어야** 이긴다 — 순서를 바꾸지 말 것. */
:is(.ds, .quote-content) .ds-fields-sec.is-sub { margin-top: 1.5rem; }

/* 📝 **묶음에 딸린 주석** — 「계약 총액 기준입니다」처럼 **바로 위 묶음**을 설명하는 한 줄.
   🔥 **위 여백은 좁게, 아래 여백은 절 간격이 벌려 준다** (2026-08-15 사용자 지적:
      *"이 문구는 대체 어디에 속한건지 이해가 잘 안가 … 위아래 여백이 같아서"*).
      위아래가 같으면 어느 묶음의 것인지 눈이 정하지 못한다.
   ⚠️ `mt-3`(.75rem)+`text-muted fs-8` 조합을 화면에 손으로 적지 말 것 — 화면마다 값이 갈린다. */
:is(.ds, .quote-content) .ds-fields-note {
    display: block;
    margin-top: .4rem;
    font-size: var(--ds-font-sub);
    color: var(--ds-text-faint);
    line-height: 1.45;
}

/* 🕒 **처리 일시 메타 줄** — 카드 맨 아래 **우측**에 붙는 「등록 · 승인 · 최종 수정」 한 줄
   (2026-08-16 구현 #77 사용자 지시: *"등록일시, 최종 승인 일시 등 … **우측에 표시**해 줘"*).

   🕘 그전에는 `fs-8 text-muted mt-6` 을 **화면마다 손으로 적어** 왼쪽 아래에 두었다 — 회원 상세와
      업체 상세가 **같은 자리에 같은 것**을 그리는데 규격이 화면에 흩어져 있었다(그래서 함께 옮겼다).
   🔑 **왜 우측인가** — 이 줄은 **결정에 쓰는 값이 아니라 출처 표시**다. 좌측 시작선은 항목 이름들이
      쓰는 자리라, 거기에 두면 마지막 항목의 값처럼 읽힌다. 우측 끝은 「이 카드의 꼬리표」로 읽힌다.
   🔑 **한 덩어리로 모은다** — 등록·승인·정지·최종 수정이 흩어져 있으면 「언제 무슨 처리가 있었나」를
      눈으로 조립해야 한다. 🔥 이 줄의 값을 항목 그리드로 되돌리지 말 것.
   ⚠️ **`flex-wrap` 이 있어야** 좁은 화면에서 넘칠 때 줄을 바꾼다 — 없으면 가로 스크롤이 생긴다.
      🔑 좁은 화면에서도 **우측 정렬을 유지**한다(왼쪽으로 되돌리면 두 화면이 폭에 따라 달라진다).
   📌 색·크기는 보조 텍스트 규칙 그대로다(크기를 낮추기 전에 **색부터** 낮춘다). */
.ds .ds-meta-line {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    align-items: baseline;
    gap: .15rem .9rem;
    margin-top: 1.5rem;
    font-size: var(--ds-font-sub);
    color: var(--ds-text-faint);
    line-height: 1.6;
}
/* 🔑 한 처리(이름표 + 처리자 + 일시)는 **함께 줄바꿈**돼야 한다 — 낱말이 따로 넘어가면
   「등록」과 그 일시가 다른 줄에 서서 어느 값인지 알 수 없어진다. */
.ds .ds-meta-line > span {
    display: inline-flex;
    flex-wrap: nowrap;
    gap: .3rem;
    white-space: nowrap;
}

/* ══ 22-1) 반 폭 두 장 줄 — `.ds-half-row` ═══════════════════════════════════════════

   「거래 자격 | 정산 준비」·「공급 운영 | 신용」처럼 **성격이 짝인 카드 두 장**의 줄이다.

   🔑 `auto-fit` 이라 **카드가 한 장뿐인 유형에서 옆이 비지 않는다.**
      공급 운영은 랜드사에만, 신용(여신 한도)은 여행사에만 있어 그 줄은 실제로 한 장이 된다 —
      `col-xl-6` 이면 나머지 절반이 **회색 벌판**으로 남는다.
   🔥 **`align-items: stretch` 를 지우지 말 것** — 두 장일 때 짧은 쪽 아래로 바탕이 드러난다.
   ⚠️ 최소폭 26rem 은 `col-xl-6` 이 주던 실측 폭(1200px 뷰포트에서 약 427px)과 같은 자리다 —
      더 좁히면 992~1199px 구간에서 두 장이 서고 그 안의 값이 글자마다 줄바꿈된다. */
.ds .ds-half-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 26rem), 1fr));
    gap: var(--ds-gap);
    align-items: stretch;
}
.ds .ds-half-row > .card { height: 100%; margin-bottom: 0; }

/* ══ 22-2) 추이 타일 — `.ds-trend-*` (2026-08-15) ═════════════════════════════════════

   축 하나가 타일 하나다. **한 줄에 최대 3개**이고 좁아지면 스스로 2 → 1 로 줄어든다.

   🔥 **열 수를 「고정」하지도, 「상한 없이」 두지도 말 것** (2026-08-15 사용자 지적:
      *"한줄에 3개만 노출해달라 했는데 6개가 나왔음"*).
      🔬 예전 값 `minmax(15rem, 1fr)` 은 **상한이 없어** 폭이 넓으면 6개가 한 줄에 다 들어갔다
         (1920px 실측: 한 줄 6개). 반대로 `repeat(3, 1fr)` 로 박으면 375px 에서 꺾은선이 손톱만 해진다.
      🔑 그래서 **최소폭을 「3열이 되는 폭」으로 끌어올린다**:
         `max(12rem, (100% - 가로 gap 2개) / 3)` — 넓으면 그 값이 커져 **4열이 될 수 없고**,
         좁으면 12rem 이 이겨 2 → 1 로 자연스럽게 줄어든다. 가로 gap 이 1.5rem 이므로 2개분 = 3rem.
      ⚠️ **gap 을 바꾸면 아래 `3rem` 도 함께 바꿀 것** — 어긋나면 마지막 열이 밀려 3열이 2열이 된다.
   📐 **반 폭 카드(추이 | 신용·공급 운영)에서도 이 그리드가 산다** → 최소폭이 12rem 이다
      (15rem 이면 반 폭에서 1열로 주저앉는다). 실측은 `docs/작업기록/2026-08.md`.
   🔑 꺾은선은 **`Company.js` 가 그린다**(값 목록 → SVG). 서버가 좌표를 내리지 않는 이유는
      점 개수가 축마다 다르고 폭이 반응형이라 좌표를 미리 정할 수 없기 때문이다.
   ⚠️ 자료가 모자란 축(`enough=false`)은 **선 대신 안내 문구**다 — 점 하나로 그린 선은
      「변화가 없다」로 읽힌다. */
.ds .ds-trend-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, max(12rem, (100% - 3rem) / 3)), 1fr));
    gap: 1.6rem 1.5rem;
}
.ds .ds-trend { min-width: 0; }
.ds .ds-trend-val {
    font-size: var(--ds-font-lead);
    font-weight: 700;
    color: var(--ds-text-strong);
    line-height: 1.3;
}
/* 전월 대비 — **좋아졌나 나빠졌나는 축마다 다르다**(분쟁·미납은 줄어드는 것이 좋다).
   그 판정은 서버(`CompanyTrends.deltaGood`)가 하고 여기서는 색만 준다. */
.ds .ds-trend-delta { font-size: var(--ds-font-sub); font-weight: 700; }
.ds .ds-trend-delta.is-good { color: var(--ds-success); }
.ds .ds-trend-delta.is-bad  { color: var(--ds-danger); }
.ds .ds-trend-spark {
    display: block;
    width: 100%;
    height: 3.4rem;
    margin-top: .4rem;
    overflow: visible;
}
/* ⚠️ SVG 는 `fill`·`stroke` 가 `color` 를 자동으로 따르지 않는다 — 토큰을 직접 준다. */
.ds .ds-trend-spark .ds-spark-area { fill: var(--ds-primary-soft); stroke: none; }
.ds .ds-trend-spark .ds-spark-line {
    fill: none;
    stroke: var(--ds-primary);
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
    vector-effect: non-scaling-stroke;
}
.ds .ds-trend-spark .ds-spark-dot { fill: var(--ds-primary); }
.ds .ds-trend-empty {
    display: flex;
    align-items: center;
    height: 3.4rem;
    margin-top: .4rem;
    font-size: var(--ds-font-sub);
    color: var(--ds-text-faint);
}

/* ══ 22-3) 이니셜 아바타 — `.ds-avatar` (2026-08-15) ═══════════════════════════════════

   소속 계정 카드의 얼굴 자리다. **사진이 있으면 사진, 없으면 이름 첫 글자**를 그린다(2026-08-15 보강).
   🕐 예전 주석은 「계정에는 프로필 사진 컬럼이 없다」 였는데 **틀렸다** — 사진은 `TB_ACCOUNT` 의 칸이
      아니라 **`TB_ATTACH_FILE` 의 `PROFILE` 첨부**(소유자 = 계정 SEQ)다.
   🔥 **이니셜을 폴백으로 남길 것** — 사진이 없는 것이 정상이고, 빈 회색 원으로 두면
      「사진을 못 불러왔다」로 읽힌다. */
.ds .ds-avatar {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.2rem;
    height: 2.2rem;
    border-radius: 50%;
    /* ⚠️ 안에 들어오는 사진이 원 밖으로 삐져나오지 않게 자른다(사진 비율이 제각각이다). */
    overflow: hidden;
    background: var(--ds-primary-soft);
    color: var(--ds-primary);
    font-weight: 700;
    font-size: var(--ds-font-sub);
    line-height: 1;
    text-transform: uppercase;
}

/* 🖼 「얼굴 자리」 안에 들어가는 사진. ⚠️ **`object-fit: cover` 를 빼지 말 것** —
   프로필 사진은 정사각형이 아니라 원 안에서 가로세로가 늘어난다(회사 로고는 비율을 지키려고
   반대로 **높이만 제한**한다. 두 규칙을 같게 만들지 말 것 — 자리가 다르다). */
.ds .ds-avatar > .ds-avatar-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 50%;
}

/* ══ 22-4) 회사 로고 — `.ds-company-logo` (2026-08-15) ═══════════════════════════════════

   🔑 자리는 **요약 카드 머리가 아니라 「상세 보기」 안**이다(2026-08-15 설계) — 머리에 올리면
      접힌 줄의 높이를 로고가 정하게 되어, **스크롤 내내 붙어 있는 스티키 띠가 두꺼워진다.**
   ⚠️ 로고 파일마다 비율이 제각각이다 → **높이만 제한하고 폭은 비율대로** 둔다.
      폭을 고정하면 가로로 긴 로고가 찌그러진다. */
.ds .ds-company-logo {
    max-height: 3.6rem;
    max-width: 100%;
    width: auto;
    object-fit: contain;
}


/* ══ 23) 설명(도움말) — `.ds-help-*`(말풍선) · `.ds-hint`(입력 아래 고정 안내) ══════

   > "관리자가 설정하는 항목에 대해서는 어디에 어떻게 사용이 되는 항목인지 어떻게 입력해야
   >  하는지를 설명을 꼭 달아줘야 할 것 같아." (2026-08-15 사용자 지시 — **전 시스템 공통**)

   **자리가 다르면 형태도 다르다.** 두 가지뿐이고, 그 밖의 형태를 새로 만들지 말 것:

   (가) **읽기 전용 항목** → 항목명 오른쪽에 `?`. 마우스를 올리면 말풍선, **누르면 고정**되고
        말풍선의 닫기(×)로 닫는다.
            한도 사용액 (KRW) [?]
            4,000,000
   (나) **입력 항목** → 입력칸 **바로 아래 고정 설명**(`.ds-hint`). 값을 넣기 **전에** 읽어야 하는
        글이라 가려 두면 안 된다 — 말풍선은 「궁금하면 찾아 읽는」 것이고, 이건 「먼저 읽는」 것이다.

   🔑 **(나)의 색은 경고(19절 `.ds-note-warn`)와 반드시 달라야 한다.** 경고가 아닌 안내인데
      경고색을 쓰면 19절이 세운 **「전부 경고면 아무것도 경고가 아니다」**가 그날로 무너진다.
      ⚠️ 반대로 `text-muted`(gray-600)로 두는 것은 이번 지적("설명이 안 보인다")의 **정반대**다
         → 읽히는 계조의 **파랑**으로 따로 세운다(아래 `--ds-hint-text`).
   🔒 **말풍선 문구는 텍스트로만 넣는다.** Bootstrap tooltip 의 `data-bs-html` 같은 HTML 허용
      옵션을 켜지 말 것 — 설명이 DB·운영자 입력에서 오는 순간 **저장형 XSS** 자리가 된다.
      (같은 이유로 이 컴포넌트는 Bootstrap tooltip/popover 를 **쓰지 않는다** — 그쪽에는
       닫기 버튼이 없어 「눌러서 고정」 요구를 만족시킬 수도 없다.)
   🔑 **마크업은 조각 하나**로 쓴다 — `fragments/dsHelp.html :: help(helpId, text)`.
        <span th:replace="~{fragments/dsHelp :: help('helpCreditLimit', #{어떤.키})}"></span>
      화면마다 손으로 적으면 `aria-describedby` 나 닫기 버튼이 한두 곳씩 빠진다.
      ⚠️ `helpId` 는 **화면 안에서 유일**해야 한다(반복문 안이면 통화코드 등을 이어 붙일 것).
      📌 이 조각은 지금 관리자 앱에만 있다 — 다른 앱에서 처음 쓸 때 **같은 내용으로** 만들 것.
   ⌨️ **키보드로도 열린다** — `?` 가 `<button>` 이라 탭으로 닿고 포커스만으로 열린다.
      🔥 `:hover` 만으로 만들지 말 것(마우스가 없는 사람에게는 없는 기능이 된다).
   📌 여닫기(고정·닫기·Esc·바깥 클릭·좌우 뒤집기)는 **`common.js` 의 전역 위임 한 곳**이다.
      CSS 는 모양만 정한다 — 여기서 `:hover` 로 여는 규칙을 만들면 JS 와 두 벌이 된다. */
:root {
    /* 🔑 흰 카드 대비 **6.4**(AA 이상). `--ds-primary` 를 그대로 쓰지 않는 이유는 19절과 같다 —
       그 값은 버튼·링크의 색이라 **작은 본문 글씨의 대비 기준**으로 잡힌 값이 아니다
       (`text-primary` 는 흰 배경 대비 2.90 으로 AA 미달이다 — 폭 유틸리티 절 참고). */
    --ds-hint-text: #2A5DB0;
}
/* 다크 카드(#1E1E2D) 대비 **6.5**. ⚠️ 라이트 값을 그대로 쓰면 대비 1.8 로 읽히지 않는다. */
html[data-theme="dark"] { --ds-hint-text: #7EA6E0; }

/* ── (나) 입력칸 아래 고정 설명 ────────────────────────────
   ⚠️ **`.form-text`(Bootstrap)와 섞어 쓰지 말 것** — 그쪽은 gray-600 이라 이번 지적으로 돌아간다.
   🔑 말머리 기호는 **렌더 시점에** 붙인다(19절 `※` 와 같은 규칙) — i18n 문구에 적으면
      언어 파일마다 빠뜨리고 번역 과정에서 지워진다. */
/* 🔴 **`.ds` 밖(편집 화면 `.quote-content`)도 함께 묶는다** (2026-08-21 구현 #47).
   🔬 실측: 랜드사 견적 상세의 `.ds-hint` **3개가 스타일을 하나도 못 받고 있었다** —
      색은 상속(흰색) · `ⓘ` 없음 · 행잉 인덴트 없음. 그 화면 뿌리가 `#kt_content.quote-content` 라
      `.ds` 선택자가 닿지 않기 때문이다.
   🔑 **27절이 카드 푸터 버튼에 쓴 것과 같은 처방**이다(`.ds` 안팎을 한 규칙으로 묶는다).
      🔥 화면 CSS(`menuLevel/*.css`)에서 `.ds-hint` 를 다시 칠하지 말 것 — 그러면 화면마다 갈린다.
   ⚠️ 이 한 줄로 그 화면들의 기존 안내(통화·유효기한 등)도 **함께 파랑·`ⓘ`·들여쓰기를 얻는다.**
      그것이 맞다 — 「입력칸 아래 고정 설명」은 화면과 무관하게 한 모양이어야 한다(23절).
   📌 이 앱에는 `.quote-content` 화면이 없다 — 그래도 두는 이유는 이 파일이 **세 앱 한 벌**이기 때문이다.
      🔥 「안 쓰니까」로 지우지 말 것. */
:is(.ds, .quote-content) .ds-hint {
    display: block;
    margin-top: .35rem;
    font-size: var(--ds-font-sub);
    line-height: 1.5;
    color: var(--ds-hint-text);
}
:is(.ds, .quote-content) .ds-hint::before {
    content: "\f431";                 /* bi-info-circle */
    font-family: "bootstrap-icons";
    font-style: normal;
    /* 🔑 **행잉 인덴트의 기호 칸**이다 — 규격과 이유는 19절 `.ds-note-warn::before` 아래에 있다.
       🕘 예전에는 `margin-right: .3rem` 이었다. **되돌리지 말 것** — 여백을 margin 으로 주면
          기호 칸의 폭이 `--ds-note-indent` 와 달라져 **둘째 줄이 그만큼 어긋난다**
          (그 어긋남이 2026-08-16 지적의 실체다). 간격은 이 칸의 남는 폭이 만든다. */
    display: inline-block;
    width: var(--ds-note-indent);
    text-indent: 0;
}

/* ── (가) 항목명 옆 `?` 와 말풍선 ───────────────────────────
   ⚠️ `.ds-field-k` 는 `display:block` 이라 그 안에서 `?` 는 글자 옆에 그냥 선다.
      🔑 **`inline-flex` 로 둔다** — `inline-block` 이면 아이콘의 baseline 때문에 라벨 줄이 내려앉는다. */
.ds .ds-help-wrap {
    position: relative;
    display: inline-flex;
    align-items: center;
    margin-left: .25rem;
    vertical-align: middle;
}
.ds .ds-help {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.1rem;
    height: 1.1rem;
    padding: 0;
    border: 0;
    background: none;
    line-height: 1;
    color: var(--ds-text-faint);
    cursor: pointer;
}
.ds .ds-help > i { font-size: 1rem; line-height: 1; }
.ds .ds-help:hover,
.ds .ds-help:focus-visible,
.ds .ds-help-wrap.is-open > .ds-help { color: var(--ds-primary); }

/* 🔑 **말풍선은 카드 밖으로 삐져나가도 된다** — 떠 있는 층이다. 다만 화면 오른쪽 끝에서는
      가로 스크롤이 생기므로 `common.js` 가 오른쪽 정렬(`is-flip`)로 뒤집는다.
   ⚠️ `white-space` 를 `nowrap` 으로 두지 말 것 — 설명은 문장이라 반드시 접혀야 한다. */
.ds .ds-help-pop {
    display: none;
    position: absolute;
    z-index: 40;
    top: calc(100% + .4rem);
    left: 0;
    width: max-content;
    max-width: min(20rem, 70vw);
    padding: .6rem 1.9rem .6rem .75rem;   /* 오른쪽은 닫기 버튼 자리다 */
    border: 1px solid var(--ds-line);
    border-radius: .4rem;
    background: var(--ds-surface);
    box-shadow: 0 8px 20px -8px rgba(0, 0, 0, .3);
    color: var(--ds-text);
    font-size: var(--ds-font-sub);
    font-weight: var(--ds-weight-body);
    line-height: 1.55;
    text-align: left;
    white-space: normal;
}
.ds .ds-help-pop.is-flip { left: auto; right: 0; }
.ds .ds-help-wrap.is-open > .ds-help-pop { display: block; }
/* 🔑 **항목이 여럿인 설명은 한 항목이 한 줄**이다 (2026-08-21 구현 #48 사용자 지시).
      「성인 만 12세 이상 · 아동 만 2~11세 · 유아 만 1세 이하」처럼 가운뎃점으로 이으면
      **어디까지가 한 항목인지** 눈으로 끊어야 한다 — 항목 수가 늘수록 나빠진다.
   ⚠️ `<br>` 로 줄을 나누지 말 것 — 줄바꿈이 **문구 안에** 들어가면 언어마다 항목 수가
      다를 때 대응이 안 되고, 문구 파일에 마크업이 섞인다. 줄은 **배치가 정한다.** */
.ds .ds-help-line { display: block; }
/* ⚠️ 닫기는 **고정된 뒤에만** 뜻이 있지만 늘 그린다 — 마우스를 올려 읽다가 그대로 누르는
   사람이 있다(누르면 `common.js` 가 고정 여부와 상관없이 닫는다). */
.ds .ds-help-close {
    position: absolute;
    top: .3rem;
    right: .35rem;
    width: 1.3rem;
    height: 1.3rem;
    padding: 0;
    border: 0;
    background: none;
    line-height: 1;
    font-size: 1.05rem;
    color: var(--ds-text-faint);
    cursor: pointer;
}
.ds .ds-help-close:hover,
.ds .ds-help-close:focus-visible { color: var(--ds-text-strong); }

/* ══ 24) 헤더 ▸ 알림 보기 창 — `#kt_notification_menu` (2026-08-15 사용자 지적) ════════
   ═══════════════════════════════════════════════════════════════════════════════════
   > *"알림 보기 창 헤더에 여백이 너무 크고 위아래가 달라."*

   ⚠️ **이 창은 `.ds` 바깥이다** — 상단 바(`#kt_header`) 안이라 `.ds` 가 닿지 않는다(19절과 같다).
      `.ds …` 로 적으면 규칙이 **조용히 무시된다.**

   ① 파란 머리의 위아래 여백이 **40px / 24px** 로 달랐다. 원인은 Metronic 여백 유틸리티를
      **위아래 서로 다른 눈금**으로 준 것이다 — `mt-10`(2.5rem)·`mb-6`(1.5rem).
      🔑 화면에 px 를 덧발라 덮지 않고 **그 유틸리티를 마크업에서 걷어내고** 여기서 상하 같은
         값(`1.25rem`)으로 준다. 되돌려 붙이면 다시 갈린다.
      🔑 좌우는 **2rem** — 아래 목록(`px-8` = 2rem)과 같아야 창 제목과 알림 제목의 왼쪽 끝이 맞는다
         (예전엔 `px-9` = 2.25rem 이라 4px 어긋나 있었다).

   ② 제목은 `<h3>` 였다. **창 폭을 타지는 않는다** — Bootstrap 기본 `calc(1.3rem + .6vw)` 는
      Metronic 번들이 `1.35rem` 으로 덮어 죽어 있다(실측 21.6px 고정, 뷰포트 2113px).
      다만 375px 짜리 드롭다운의 제목으로는 커서 여백이 더 커 보였다 → **1.15rem**.
      ⚠️ **px 로 박지 말 것** — 글자 크기 3단계 토글(0-0 절)이 root 기준 `rem` 으로 동작한다.

   🔑 배경 무늬는 예전에 템플릿의 `style="background-image:…"` **인라인**이었다.
      「css 는 html 에 직접 쓰지 않는다」는 규약대로 여기로 옮겼다.
      ⚠️ 경로는 **문서 기준 절대경로**로 적는다(CSS 상대경로면 `assets/css/` 기준이 되어 깨진다).

   📌 하단 버튼 줄(`#notificationFooter`)은 여기서 손대지 않는다 — **여닫는 것은 `common.js` 의
      `_updateBadge`** 한 곳이다. 버튼이 없을 때 `border-top` 만 남는 빈 띠를 없애려고 CSS 에
      `:empty` 같은 규칙을 새로 만들지 말 것(여는 조건이 두 벌이 된다).
   ═══════════════════════════════════════════════════════════════════════════════════ */
#kt_notification_menu .ds-noti-head {
    display: flex;
    flex-direction: column;
    padding: 1.25rem 2rem;
    background-image: url("/assets/media/misc/pattern-1.jpg");
    background-repeat: no-repeat;
}
#kt_notification_menu .ds-noti-title {
    margin: 0;
    font-size: 1.15rem;
    line-height: 1.4;
}

/* ═══════════════════════════════════════════════════════════════════════════════════
   📅 25) 날짜·시간 선택기 — **Air Datepicker 3.6.0** (2026-08-17 구현 #83)

   > 데이트타임 피커에 **적용 버튼이 없어서 불편**한데 이 기능 넣어줄 수 있을까?
   > 년과 월의 **순서를 바꿔** 주고, 년과 월을 **클릭해서 선택하는 화면도 div 기반**이었으면 좋겠다.
   > **다른 피커를 사용하는 것도 괜찮아** — 모든 기능을 지원하고 디자인이 깔끔한 것으로.

   🕘 **2026-08-17 오전까지 이 자리는 flatpickr 규격이었다.** 바꾼 이유는 취향이 아니라 **기능**이다:
      flatpickr 는 ⓐ 확인 버튼이 없고(플러그인이 번들에 없다) ⓑ 머리의 **년·월이 DOM 순서로 고정**이며
      ⓒ **년은 숫자 입력칸, 월은 화살표**라 「눌러서 고르는 판」이 아예 없다. 셋 다 **직접 만들어야**
      했고, 그 손코드는 **나중에 96곳으로 전부 따라간다.**
      → Air Datepicker 는 **판(년/월/일 세 화면)·확인 버튼·모바일 가운데 정렬이 전부 기본 기능**이다.
      🔥 **flatpickr 로 되돌리지 말 것** — 되돌리는 순간 위 세 가지를 다시 손으로 만들어야 한다.
      📌 flatpickr 자체는 Metronic 번들 안에 그대로 살아 있다(다른 화면이 쓴다). 지운 것은 **이 규격**뿐이다.

   🔑 **벤더 파일은 `plugins/custom/air-datepicker/`** (MIT, 저자 Timofey Marochkin · `LICENSE` 동봉).
      JS 48,971B + CSS 19,619B, **의존성 0**(jQuery·moment 를 쓰지 않는다).
      🔥 **벤더 파일을 손으로 고치지 말 것** — 갱신하면 덮인다. 겉모습은 **전부 이 절에서** 바꾼다.
   🔑 **이 라이브러리는 겉모습을 CSS 변수(`--adp-*`)로 다 열어 두었다** → 우리는 그 변수에
      **역할 토큰(`--ds-*`)을 꽂기만** 한다. 🔥 그래서 hex 도, 다크용 분기도 필요 없다
      (`--ds-*` 가 이미 테마를 따라 뒤집힌다). **여기에 색을 직접 적지 말 것.**
   🔥 **`air-datepicker.css` 는 이 파일보다 먼저 실려야 한다**(`fragments/head.html`) —
      순서로만 이기는 규칙이 아래에 있다(글꼴·버튼·시각 칸). 뒤로 옮기면 조용히 무효가 된다.

   📌 **적용 범위는 두 묶음이다**(2026-08-17 확대):
      ① 관리자 「마진율 변경 예약 추가」 모달 두 칸 — **날짜+시각**(`.js-datetime`, 시범 적용).
      ② **세 앱 목록 화면의 검색 기간 칸 72개**(36화면) — **날짜만**(`.js-date`).
      🔥 **아직 안 바꾼 곳이 남아 있다** — 등록·수정 폼의 날짜(계약일·여행일 등)·상세 화면·모달 안의 날짜.
         값 형식과 화면 골격이 달라 **화면 종류별로 끊어서** 넓힌다. 여기서 한꺼번에 늘리지 말 것.
   🔴 **넓힐 때 가장 위험한 것은 값 형식이다** — 검색 날짜는 서버가 `yyyy-MM-dd` 로 받는데
      (문자열 파라미터는 매퍼가 `CONCAT(#{dateFrom},' 00:00:00')` 로 쓰고, 통계는 `LocalDate`),
      형식이 어긋나면 **오류가 아니라 검색이 0건**이 되고 **화면은 멀쩡해 보인다.**
      🔑 그래서 칸마다 **서버가 무엇으로 받는지 먼저 확인**하고 **클래스로 형식을 못 박는다**
         (`.js-date` = `yyyy-MM-dd` / `.js-datetime` = `yyyy-MM-dd HH:mm`). 🔥 「다 날짜겠지」로 넘기지 말 것.
   ⚠️ 세 앱 공용 파일이다 — 한 앱만 고치지 말 것. ✅ **벤더 파일·`<link>`·`<script>` 는 세 앱에 다 있다**
      (2026-08-17 — 그전에는 Admin 에만 있어 다른 두 앱에서 이 절이 닿는 요소가 없었다).
   ═══════════════════════════════════════════════════════════════════════════════════ */

/* ── 25-1) 토큰 꽂기 — 색·크기를 전부 `--ds-*` 로 받는다 ────────────────────────────
   ⚠️ 이 블록은 **변수만** 정의한다(선언 순서로 이길 필요가 없다). 실제 그림은 벤더 CSS 가 그린다. */
.air-datepicker {
    /* 크기 — 🔥 px 로 박지 말 것(글자 크기 3단계 토글이 root 기준 rem 이다) */
    --adp-font-size:      var(--ds-font-body);
    --adp-width:          17.5rem;
    --adp-padding:        .5rem;
    --adp-nav-height:     2.4rem;
    --adp-nav-action-size: 2.4rem;
    --adp-day-cell-height:   2.2rem;
    --adp-month-cell-height: 2.8rem;
    --adp-year-cell-height:  2.8rem;
    --adp-btn-height:     var(--ds-btn-h);
    --adp-border-radius:      .475rem;
    --adp-cell-border-radius: .475rem;

    /* 🔴 모달(1055·1060) 위에 떠야 한다 — 이 피커는 **모달 안에서** 쓰인다.
          🔥 값을 낮추지 말 것: 달력이 모달 뒤로 들어가면 **아무 일도 안 일어나는 것처럼** 보인다. */
    --adp-z-index: 1075;

    /* 면·선·글자 */
    --adp-background-color:        var(--ds-surface);
    --adp-background-color-hover:  var(--ds-fill);
    --adp-background-color-active: var(--ds-line-soft);
    --adp-color:           var(--ds-text);
    --adp-color-secondary: var(--ds-text-faint);
    --adp-accent-color:    var(--ds-primary);
    --adp-border-color:        var(--ds-line);
    --adp-border-color-inner:  var(--ds-line-soft);
    --adp-border-color-inline: var(--ds-line);

    /* 요일 머리글 — 벤더 기본은 주황이다(우리 색이 아니다) */
    --adp-day-name-color:       var(--ds-text-label);
    --adp-day-name-color-hover: var(--ds-primary);

    /* 이번 달이 아닌 날 · 다른 10년대의 연도 */
    --adp-color-other-month:       var(--ds-text-faint);
    --adp-color-other-month-hover: var(--ds-text-label);

    /* 🔴 **고를 수 없는 칸**(`minDate`·`maxDate` 밖의 날·월·연) — 여기만 본문 대비 아래로 내린다.
          🔑 「400·500 은 글자에 쓰지 않는다」의 **의도된 예외**다: 이 글자는 읽으라고 있는 것이 아니라
             **누를 수 없다는 표시**다. `--ds-text-faint`(보조 텍스트)로 올리면 고를 수 있는 날과
             구별이 안 되어 **눌렀는데 아무 일도 안 일어나는** 화면이 된다(그것이 고장으로 읽힌다). */
    --adp-color-disabled:          var(--ds-line-strong);
    --adp-color-disabled-in-range: var(--ds-line-strong);

    /* 고른 칸 · 기간(범위) 안의 칸 — 기간 선택은 아직 안 쓰지만 **한 라이브러리로 덮을 것**이라 함께 정한다 */
    --adp-cell-background-color-selected:          var(--ds-primary);
    --adp-cell-background-color-selected-hover:    var(--ds-primary);
    --adp-cell-background-color-in-range:          var(--ds-primary-soft);
    --adp-cell-background-color-in-range-hover:    var(--ds-primary-soft);
    --adp-cell-border-color-in-range:              var(--ds-primary);
    --adp-background-color-in-range:               var(--ds-primary-soft);
    --adp-background-color-in-range-focused:       var(--ds-primary-soft);
    --adp-background-color-selected-other-month:         var(--ds-primary);
    --adp-background-color-selected-other-month-focused: var(--ds-primary);

    /* 시각 칸(슬라이더) */
    --adp-time-track-color:       var(--ds-line);
    --adp-time-track-color-hover: var(--ds-line-strong);
    --adp-time-day-period-color:  var(--ds-text-faint);

    /* 📱 좁은 화면 — 값은 JS 가 `isMobile` 을 켤 때만 쓰인다(25-4 절 참고) */
    /* 🔴 **여기만 px 다 — rem 으로 되돌리지 말 것** (2026-08-20 사용자 실측 확정).
          🔬 좁은 화면의 root 는 **12px**(0-0 절: normal 12 / large 13 / xlarge 14) 이라 rem 으로 적으면
             `2.6rem` 이 **31.2px**, `2.4rem` 이 **28.8px** 이 된다 → 실측한 터치 목표가 **전부 44px 미만**이었다
             (날짜칸 32.6×31.2 · 월이동 33.6×33.6 · 버튼 111.8×26.4 · 달력 전체 240×311 —
              화면 437px 중 197px 을 놀리고 있었다).
          🔑 **손가락 크기는 글자 크기 설정과 무관한 물리적 값**이다. 「글자를 키웠으니 버튼도 커진다」는
             맞지만 **거꾸로 「글자가 작으니 버튼도 작아도 된다」는 성립하지 않는다** → 터치 목표는 절대값.
          📌 저장소의 「px 고정폭 금지」(screen-tokens.md)는 **글자를 담는 칸**에 대한 규칙이다 —
             그 규칙의 근거가 「글자 크기 토글을 따라 함께 커져야 한다」인데, 여기는 반대로
             **글자 크기와 무관하게 일정해야 하는 값**이라 같은 근거가 닿지 않는다.
          🔥 **글자 크기(`--adp-mobile-font-size`)는 px 로 박지 않는다** — 그것은 읽는 값이라
             글자 크기 토글을 따라야 한다. 대신 **바닥값 16px** 을 둬서 12px 로 내려가지 않게만 막는다.
          🔬 적용 후 실측: 날짜칸 **48×46** · 월칸 **112×52** · 연칸 **84×52** · 월이동 **52×52** ·
             버튼 높이 **48** · 달력 **348×472**(화면 500×715 안). 전부 터치 통과. */
    --adp-mobile-font-size:        max(16px, 1.25rem);
    --adp-mobile-width:            min(348px, calc(100vw - 32px));
    --adp-mobile-nav-height:       52px;
    --adp-mobile-day-cell-height:  46px;
    --adp-mobile-month-cell-height: 52px;
    --adp-mobile-year-cell-height:  52px;
}

/* 뒤를 덮는 막 — 값은 **이력 드로워(21절)와 같은 것**을 쓴다.
   ⚠️ 막은 달력보다 **한 칸 아래**여야 한다(같으면 막이 달력을 먹는다). */
.air-datepicker-overlay {
    --adp-overlay-background-color: var(--ds-scrim);
    --adp-overlay-z-index: 1074;
}

/* ── 25-2) 벤더가 박아 둔 글꼴을 걷어낸다 ──────────────────────────────────────────
   🔬 벤더는 달력에 `-apple-system…`, 시:분 숫자에 `Century Gothic` 을 박는다 → 화면의 다른 숫자와
      **글꼴이 갈린다**(본문은 Pretendard 다). 🔥 이 두 줄을 지우지 말 것.
   ⚠️ **순서로만 이긴다**(특이도가 같다) — `air-datepicker.css` 를 이 파일 뒤로 옮기면 되살아난다. */
.air-datepicker,
.air-datepicker-time--current-hours,
.air-datepicker-time--current-minutes {
    font-family: inherit;
}

/* ── 25-3) 머리(년·월) — **눌러서 판을 여는 자리**라는 것이 보여야 한다 ──────────────
   🔑 년·월의 **순서는 CSS 가 정하지 않는다** — `navTitles` 형식 문자열을 `Intl` 이 만든다
      (한국어면 「2026년 8월」, 영어면 「August 2026」). 🔥 여기서 `order`·`direction` 으로 뒤집지 말 것:
      **보이는 순서와 탭 순서가 어긋나** 키보드로 쓰는 사람에게 화면이 거꾸로 읽힌다. */
.air-datepicker-nav--title {
    font-size: var(--ds-font-strong);
    font-weight: var(--ds-weight-title);
    color: var(--ds-text-strong);
}
/* 일→월→연으로 올라갈 자리가 없을 때(최상위 판)는 벤더가 `-disabled-` 를 준다 —
   그때는 **누를 수 있는 것처럼 보이지 않아야** 한다. */
.air-datepicker-nav--title.-disabled- { color: var(--ds-text-faint); }

/* ── 25-3-1) ◀▶ **못 가는 방향의 화살표는 자리를 지키고 흐려진다** (2026-08-17 사용자 지적)
   > *"이전 달로 갈 수 없는데 왼쪽 화살표가 통째로 사라져 머리가 한쪽으로 쏠려 보인다."*

   🔬 벤더가 `.air-datepicker-nav--action.-disabled- { visibility: hidden }` 으로 **통째로 감춘다.**
      그런데 그 자리는 **머리의 좌·우 양 끝**이라, 한쪽이 사라지면 가운데 년·월 글자가 남은 쪽으로
      밀려 **달력이 기울어 보인다**(관리자 예약 모달은 「지금+24시간」 하한이 있어 첫 달에서 늘 그렇다).
   🔑 **감추는 대신 「지금은 갈 수 없다」고 말한다** — 자리는 그대로, 화살표 색만 내린다.
      색은 25-1 절이 고를 수 없는 날에 쓰는 **`--ds-line-strong` 과 같은 토큰**이다(한 화면에서
      「누를 수 없음」이 두 가지 색으로 갈리지 않게). 🔥 `opacity` 로 흐리지 말 것 —
      비활성 버튼 규칙(위 「비활성 버튼」 절)이 **흐리게 하는 대신 색으로 말한다**로 이미 정해져 있다.
   🔴 **눌러도 아무 일이 없어야 한다** — 흐리기만 하고 동작이 남으면 감춰 두는 것보다 나쁘다.
      `pointer-events: none` 이 클릭도 hover 배경도 함께 막는다(벤더의 `:hover` 배경 규칙이 그 자리에 있다).
   🔥 `visibility: visible` 을 지우지 말 것 — 벤더 규칙과 **특이도가 같아 순서로만 이긴다**.
      `air-datepicker.css` 를 이 파일 뒤로 옮기면 그날로 다시 사라진다. */
.air-datepicker-nav--action.-disabled- {
    visibility: visible;
    cursor: default;
    pointer-events: none;
}
.air-datepicker-nav--action.-disabled- path { stroke: var(--ds-line-strong); }
/* 🔤 **머리 안의 `<i>` 는 글자 모양을 물려받는다** (2026-08-17 사용자 지적: *"년자만 폰트가 이상해"*)
   🔬 실측: 「2026」·「8월」은 **16px** 인데 그 사이의 「년」만 **17.2px** 이었다 —
      벤더 CSS 가 `<i>` 에 자기 크기를 주는데, **「년」은 `<i>` 밖의 맨 글자**라 머리 크기를 그대로 쓴다.
      한 낱말이 **두 크기로 갈려** 붙어 보이던 것이 이 지적의 실체다(기울임은 이미 잡혀 있었다).
   🔑 `<i>` 는 **토큰을 감싸려고 쓴 그릇일 뿐**이다 — 라이브러리가 `yyyy`·`MMMM` 을 치환할 때
      토큰 앞뒤가 「끝·공백·구두점」 중 하나여야 해서, 한글이 바로 붙는 「yyyy년」은 **치환이 안 된다.**
      그래서 `<i>` 로 감싸 경계를 만들었다. 🔥 뜻이 있는 태그가 아니므로 **모양을 남기면 안 된다.**
   🔥 `<i>` 를 걷어내지 말 것 — 걷어내면 치환이 깨져 화면에 **`yyyy년 8월`** 이 그대로 나간다. */
.air-datepicker-nav--title i {
    font-size: inherit;
    font-weight: inherit;
    font-style: inherit;
    color: inherit;
    /* 🔴 **줄 높이까지 물려받아야 밑선이 맞는다** (2026-08-17 사용자 지적:
          *"년보다 월이라는 글자가 더 아래로 처진 것 같다"*).
       🔬 실측: 머리는 **flex** 배치라 `<i>` 는 flex 항목이 되고, 그 사이의 맨 글자 「년 」은
          **익명 flex 항목**이 된다. 그런데 상자 높이가 갈렸다 —
          `<i>` 17.2px(벤더가 준 줄 높이) vs 「년 」 25px(머리의 줄 높이 25.8 을 그대로 씀).
          크기가 다른 두 상자를 가운데로 맞추니 **글자 밑선이 어긋나** 한쪽이 처져 보였다.
       🔑 `line-height` 를 물려받으면 두 상자가 같은 키가 되어 밑선이 맞는다.
       🔥 머리에 `align-items: baseline` 을 주는 방식으로 고치지 말 것 —
          그 줄에는 **좌우 화살표 버튼도 flex 항목**이라 그것들까지 함께 어긋난다. */
    line-height: inherit;
}

/* 월·연 판의 칸은 날짜 칸보다 크다 — 글자도 본문 크기로 맞춘다(벤더 기본은 상대값이라 작아 보인다) */
.air-datepicker-cell.-month-,
.air-datepicker-cell.-year- { font-size: var(--ds-font-body); }

/* ── 25-4) 판 아래 버튼 줄 — **지우기 · 확인** ─────────────────────────────────────
   🔑 버튼 자체는 라이브러리 기능(`buttons` 옵션)이라 **마크업을 만들지 않는다.**
      문구는 화면이 아니라 **i18n 사전**(`label.common.confirm` · `label.common.clear`)에서 온다.
   🔑 **채움(solid) primary 로 만들지 않는다** — 이 모달에는 이미 solid 「저장」이 있고
      (「solid btn-primary 는 화면당 하나」), 달력의 확인은 **저장이 아니라 「고르기를 끝낸다」**다.
      🔥 흰 글자를 쓰려고 `#fff` 를 적지 말 것 — soft 조합이면 토큰만으로 두 테마가 다 성립한다. */
.air-datepicker-button.ds-adp-confirm {
    background: var(--ds-primary-soft);
    /* 🔒 **`--ds-primary` 가 아니라 `--ds-badge-primary-fg`** 다 — 연한 배경 위에서
       원색 primary 는 대비 **2.74**(AA 미달)라 22-1 절이 이미 교정해 둔 값이 있다
       (`.btn.btn-light-primary` 와 **같은 토큰**을 쓴다 — 라이트 4.63 / 다크 4.62).
       🔥 `--ds-primary` 로 되돌리지 말 것 — 같은 화면의 다른 연한 버튼보다 혼자 흐려진다. */
    color: var(--ds-badge-primary-fg);
    font-weight: var(--ds-weight-strong);
    border-radius: var(--adp-btn-border-radius);
}
.air-datepicker-button.ds-adp-confirm:hover,
.air-datepicker-button.ds-adp-confirm:focus,
.air-datepicker-button.ds-adp-confirm:active {
    background: var(--ds-primary);
    color: var(--ds-surface);
}

/* 🗑 **「지우기」 — 고른 값을 비운다** (2026-08-19 구현 #97 사용자 지시:
      *"모든 선택기에는 입력된(선택된) 값을 지우는 버튼이 있어야 해."*)
   🔑 **왼쪽에 선다** — 왼쪽이 「되돌리는 일」, 오른쪽이 「끝내는 일」이다(모달 푸터와 같은 순서).
      순서는 `common.js` 가 배열에 담는 순서로 정해진다(CSS `order` 로 뒤집지 말 것 —
      **보이는 순서와 탭 순서가 어긋난다**. 25-3 절의 년·월과 같은 이유다).
   🔑 **중립색이다** — 이 판에서 primary 를 입는 것은 「확인」 하나뿐이어야 한다
      (한 자리에 강조가 둘이면 무엇이 주 행위인지 사라진다).
   🔴 **값이 없으면 감추지 않고 흐린다** — 버튼 줄이 `grid-auto-flow: column` 이라 하나가 사라지면
      남은 「확인」이 **줄 전체로 늘어나** 판을 열 때마다 모양이 달라진다.
      🔑 흐리는 색은 25-1 절이 **고를 수 없는 날**에 쓰는 `--ds-line-strong` 과 같은 토큰이다 —
         한 화면에서 「누를 수 없음」이 두 가지 색으로 갈리지 않게. 🔥 `opacity` 로 흐리지 말 것.
   ⚠️ **특이도로 이긴다** — 벤더의 `.air-datepicker-button:hover`(0,2,0)보다 높아야 하므로
      선택자에 `.ds-adp-clear` 를 반드시 함께 적는다. */
.air-datepicker-buttons {
    /* 두 버튼이 맞붙지 않게. 벤더 규칙과 특이도가 같아 **순서로만 이긴다**(이 파일이 뒤에 실린다). */
    gap: .375rem;
}
.air-datepicker-button.ds-adp-clear {
    background: var(--ds-fill);
    color: var(--ds-text-label);
    font-weight: var(--ds-weight-strong);
    border-radius: var(--adp-btn-border-radius);
}
.air-datepicker-button.ds-adp-clear:hover,
.air-datepicker-button.ds-adp-clear:focus,
.air-datepicker-button.ds-adp-clear:active {
    background: var(--ds-line-soft);
    color: var(--ds-text-strong);
}
.air-datepicker-button.ds-adp-clear:disabled,
.air-datepicker-button.ds-adp-clear:disabled:hover,
.air-datepicker-button.ds-adp-clear:disabled:focus,
.air-datepicker-button.ds-adp-clear:disabled:active {
    background: transparent;
    color: var(--ds-line-strong);
    cursor: default;
}

/* ── 25-5) 📱 좁은 화면(≤767.98px) ────────────────────────────────────────────────
   🔑 **가운데 띄우기·막·큰 칸은 라이브러리가 한다**(`isMobile` 옵션 → `.-is-mobile-`).
      🕘 flatpickr 때는 이 자리에 `position: fixed !important` 와 `::before` 막을 **우리가** 적어 두었다.
         🔥 그 블록을 되살리지 말 것 — 라이브러리가 같은 일을 하는데 두 벌이면 좌표가 싸운다.
   ⚠️ `isMobile` 은 **미디어쿼리가 아니라 JS 옵션**이다 → 화면 폭 판정은 초기화 코드가 하고,
      **모달을 열 때마다 다시 잰다**(창을 줄인 뒤 여는 경우가 있다).
   🔑 여기서 더 할 일은 **폭이 화면을 넘지 않게 막는 것**뿐이다 — 20rem 이 root 를 따라 커지므로
      글자 크기를 키운 사용자에게는 화면을 넘길 수 있다. */
@media (max-width: 767.98px) {
    .air-datepicker.-is-mobile- {
        max-width: calc(100vw - 2rem);
        max-height: calc(100vh - 2rem);
        overflow: auto;

        /* 🖐 **버튼 줄도 터치 목표다** — 벤더에 `--adp-mobile-btn-height` 가 없어서 여기서 덮는다.
              🔬 그전에는 `--ds-btn-h`(2.2rem)이 좁은 화면 root 12px 을 타서 **26.4px** 이었다. */
        --adp-btn-height: 48px;
    }
    /* 🖐 **우리 규칙이 글자 크기를 따로 정한 두 곳**은 여기서 다시 올려야 한다 —
          위 `--adp-mobile-font-size` 는 `--adp-font-size`(달력 전체 기본값)만 갈아끼우므로
          **자기 `font-size` 를 가진 선택자는 그대로 남는다.** 🔥 빠뜨리면 그 글자만 12px 로 남는다. */
    .air-datepicker.-is-mobile- .air-datepicker-nav--title,
    .air-datepicker.-is-mobile- .air-datepicker-cell.-month-,
    .air-datepicker.-is-mobile- .air-datepicker-cell.-year- {
        font-size: max(16px, 1.25rem);
    }
    /* 요일 머리글 — 누르는 자리가 아니라 **읽는 자리**라 칸을 키우지 않는다(28px).
       🔑 대신 날짜 격자와 사이를 벌려 **어느 줄이 요일이고 어느 줄이 날짜인지** 한눈에 갈리게 한다
          (벤더 기본은 `margin: 8px 0 3px` 라 3px 밖에 안 떨어져 있었다). */
    .air-datepicker.-is-mobile- .air-datepicker-body--day-names { margin-bottom: 10px; }
    .air-datepicker.-is-mobile- .air-datepicker-body--day-name  {
        height: 28px;
        font-size: max(13px, 1.05rem);
    }
    /* 판 위아래 숨통 — 요일 줄과 마지막 날짜가 테두리에 붙지 않게(벤더 기본은 `--adp-padding` 뿐이다).
       ⚠️ 여기는 **여백**이라 rem 이 맞다 — 글자를 키우면 함께 벌어져야 한다. */
    .air-datepicker.-is-mobile- .air-datepicker--content {
        padding-top: 1rem;
        padding-bottom: 1rem;
    }
}

/* ══ 27) 🖐 **좁은 화면의 터치 목표 바닥값** — 누르는 칸은 44px 부터다 (세 앱 공통 — 2026-08-20) ══
   (파일 순서상 아래 26절보다 앞에 있다. 26절은 CSS 가 없는 규격 절이라 위치가 뜻을 갖지 않는다.)

   > 계기: 사용자 실주행 지적 — *"손가락으로 원하는 날짜나 버튼을 누르기엔 너무 작은 것 같아."*
     날짜 선택기는 25-5 절에서 이미 고쳤고, **같은 조사에서 함께 드러난 나머지**가 여기다.

   🔑 **뿌리는 개별 화면이 아니라 토큰 층 한 곳**이다 — 0-0 절이 좁은 화면 root 를 **12px** 로 내리므로
      `rem` 으로 적은 컨트롤이 폰에서 **25% 작아진다**(2.2rem=26.4px · 2.4rem=28.8px · 2.5rem=30px).
      화면을 하나씩 손대면 끝이 없고 서로 갈린다.
   🔑 **터치 목표만 px 이다** — `screen-tokens.md` 「손가락으로 누르는 칸의 최소 크기는 px」 절의 적용이다.
      손가락 크기는 글자 크기 설정과 무관한 물리값이라 rem 이 아니다.
   🔥 **글자 크기는 px 으로 박지 않는다 — 이 절에는 `font-size` 선언이 하나도 없다.** 상자만 건드린다.
      🔑 그래서 값이 전부 `max(44px, …rem)` 이다 — 「글자 크게」로 rem 이 44px 을 넘으면 **rem 이 이긴다.**
   ⚠️ **넓은 화면은 건드리지 않는다** — 마우스는 44px 이 필요 없고 지금 값이 규격이다(0-3 절).
   📌 **높이만 올리고 폭은 그대로 둔다.** 그래서 이 절이 가로 넘침을 만들 수 있는 자리는
      **폭까지 커지는 둘**(페이징 · 아이콘 버튼)뿐이다.

   🔑 **입력칸(`--ds-control-h`)을 함께 올린 이유** — 「글자를 넣는 칸이니 누르는 칸과 성격이 다르다」가
      처음 판단이었는데 두 가지가 그것을 막았다.
        ① **선택 상자와 날짜 칸은 눌러서 여는 자리**다(고르는 판이 뜬다). 그 둘만 골라 올릴 수는 있다.
        ② 그런데 0-3 절 **「인접 규칙」**이 *옆에 붙은 요소는 짝의 높이를 따른다* 고 못 박고 있다 →
           선택 상자만 44 로 올리면 같은 줄의 입력칸(28.8)과 **눈에 보이게 어긋난다.**
           검색 폼은 대부분 [입력칸][선택상자][버튼] 한 줄이라 **거의 모든 목록 화면이 그 모양**이 된다.
      → 갈라서 올릴 수 없으므로 토큰째 올린다.

   🔴 **토큰만 올려서는 안 닿는 자리가 있다 — 「토큰이 44 니까 됐다」고 믿지 말 것** (2026-08-20 실측 후 보강).
      🔬 크롬 실측에서 **두 곳이 토큰을 안 따랐다.** 원인은 둘 다 **화면별 CSS 가 더 세고 나중에 실린다**는 것이다
         (Layout Dialect 가 화면 `<head>` 를 `fragments/head` **뒤에** 붙이므로 `menuLevel` 의 CSS 가 늦게 실린다).
        ① **편집표 입력칸 30px** — `Trade.css` 의 `.quote-workspace .quote-items-table .form-control`(0,3,0).
           → **랜드사 `Trade.css` 안에서 좁은 화면만 풀었다**(거기 근거를 적었다). 여기서 덮지 않는다.
           📌 실측 보고에는 `height:30px !important` 로 적혔지만 **`!important` 가 붙은 것은 `min-height:0` 뿐**이고
              `height` 는 그냥 (0,3,0)+나중 로드로 이긴 것이다 — 덮을 때 `!important` 를 쓸 이유가 없다.
        ② **`.ds` 밖 카드 푸터 버튼 39.8px** — `#kt_content.quote-content`(랜드사·여행사 편집 화면)라
           17절의 `.ds .card .card-footer .btn` 이 닿지 않아 **벤더 기본**으로 남았다.
           → 아래에 **`.ds` 안팎을 함께 묶는 바닥값 한 줄**을 두었다.
      🔑 **다음에 토큰을 손볼 때는 화면별 CSS 의 `height`·`min-height` 를 함께 훑을 것.**
         전수 결과는 각 앱 `docs/작업기록/2026-08.md` 에 표로 남긴다(이 절의 근거).

   ⚠️ **대상에서 뺀 것 아홉 — 이유가 있다. 「빠뜨렸다」로 읽고 채워 넣지 말 것.**
      🔴 **이 목록 자체가 이 절의 값이다** (2026-08-21 보강). 🔬 목록이 셋뿐이던 동안 `.btn-sm` 이
         「일부러 뺀 것」이 아니라 **「빠뜨린 것」으로 읽혀 결함으로 보고됐다.** 뺄 때는 반드시 여기 적을 것 —
         **안 잰 것도 「안 쟀다」고 적는다**(「위반 0건」과 「검사 안 함」은 다르다).
     · 🔴 **「전체 이력」·「상세 보기」**(`.ds-ds-toggle`·`.ds-hist-row-all`·`.ds-head-btn`, 25px) —
       한 번 44px 로 올렸다가 **2026-08-21 사용자 지시로 되돌렸다.** 그 버튼이 사는 요약 카드 머리가
       **스티키**라 ≈40 → 58px 이 되어 폰에서 본문을 상시 가렸다. 실기기로 확인한 사용자 판단은
       **「누르기 불편하지 않다」**였다. 경위는 아래 그 자리에 남겼다. 🔥 다시 올리려면 먼저 물을 것.
     · 🕘 **폰 터치 목표 작업은 2026-08-21 에 사용자 지시로 종료됐다** —
       *"일단 모바일 버튼 사이즈 수정은 여기에서 종료하자. 차후 불편한 항목이 있으면 따로 수정요청을 할게."*
       🔬 그때 전 화면 훑기로 **미달 120건**을 찾아 갈래로 묶어 두었다(FAQ 질문 줄 65 · 탭 24 ·
       `.btn-sm` 9 · 모달/저장 13 · 미리보기 도구 6 · 약관 언어 버튼 3). 🔑 **목록은 남아 있으니
       요청이 오면 그 갈래만 꺼내 쓴다** — 다시 전수로 훑을 필요 없다. 🔥 **요청 없이 먼저 올리지 말 것.**
     · **다크모드 스위치**(`.form-switch`) — 정사각형 바닥값을 주면 **알약이 네모가 된다**
       (배경 이미지 위치가 `3.25rem` 폭에 맞춰져 있다). 아래에서 되돌린다.
     · **헤더(`#kt_header`)의 아이콘 버튼** — 거기는 `w-30px … w-md-40px` 로 **좁은 화면 값을 일부러
       따로 정해 둔 자리**이고 폰 헤더에 아이콘이 넷 이상 선다. 44 로 올리면 **페이지 제목이 그만큼
       잘린다**(제목이 `text-overflow: ellipsis` 다). 재 보고 정할 일이라 이번에는 두었다.
     · **말풍선 안의 닫기(`.ds-help-close`, 1.3rem)** — 그 판은 **바깥을 누르거나 Esc 로도 닫힌다**
       (23절 · `common.js` 한 곳). 닫는 행위에 **화면만큼 큰 대체 목표**가 이미 있다.
     · **미리보기 세로 탭(`.pv-tab`, 실측 23×68)** — 폭을 44 로 올리면 화면 오른쪽 가장자리에
       **44px 슬래브가 상시 얹힌다**(375px 화면의 11.7%). 그 탭은 `position:absolute; top:40vh` 로
       본문 위에 떠 있어 넓힌 만큼 그대로 내용을 가린다.
       🔑 **같은 행위에 전폭 대체 목표가 있다** — 하단 액션 바의 「미리보기」(`#btnPreviewBottom`)를
          이번에 44 로 올렸다. 게다가 세로 68px 이라 **주 방향 목표는 이미 충분히 크다.**
     · **`.btn-sm` 에 일괄 바닥값을 주지 말 것** (2026-08-21 — 실제로 한 번 「결함」으로 오독됐다).
       🔬 이 파일이 **이미 다섯 자리에서 `btn-sm` 높이를 옆 요소에 맞춰** 놓았다(정산 이력 카드의
          28.6px 정렬 · 배지 옆 아이콘 버튼의 `!important` · 목록 발 버튼 등). `#kt_content .btn-sm
          { min-height: 44px }` **한 줄이면 그 정렬이 한꺼번에 깨진다.**
       🔬 9화면 실측에서 `.btn-sm` 위반은 **로그인 화면의 비밀번호 보기 토글 하나**뿐이었고,
          그것은 아래 `.ds-auth-page` 규칙이 잡는다.
       🔴 **남은 진짜 자리는 「카드 본문에 홀로 선 `btn-sm` 행위 버튼」**이다(관리자 문의 상세
          「처리중으로」·「완료 처리」 83×**31.8**). 🔑 그것의 답은 **규칙이 아니라 그 화면에서
          `btn-sm` 을 떼는 것**이다 — 되돌릴 수 없는 행위에 「작게」를 붙인 것 자체가 잘못이다.
          ⚠️ 세 앱 `btn-sm` 사용처는 **템플릿 214곳 · JS 53곳**이라 일괄 치환도 금지다.
     · **인증 화면 중 `.ds-auth-page` 를 안 단 것**(`sign/signup` · `sign/terms` · `sign/unlock`) —
       아래 로그인 규칙이 **원리적으로 안 닿는다.** 🔥 클래스를 달아서 끌어들이지 말 것
       (19절의 언어 선택 배치가 함께 걸려 그 화면들의 배치가 바뀐다).
       🔴 **「위반 없음」이 아니라 「아직 안 쟀다」**이다.
     · **언어 드롭다운의 항목(`.menu-link`)** — `.btn` 계열이 아니라 이번 실측의 대상이 아니었다.
       🔴 여기도 **「0건」이 아니라 「안 쟀다」**이다.

   🔎 **체크박스는 여기서 44px 에 못 간다** — `input` 은 대체 요소라 `::before`·`::after` 가 생기지 않아
      「겉모습은 그대로 두고 누르는 자리만 넓히는」 수법(아래 `?` 말풍선)을 쓸 수 없고, 12px 글자 옆에
      44px 정사각형을 두면 줄이 무너진다. → **24px**(WCAG 2.2 SC 2.5.8 최소 목표 크기)까지만 올린다.
      🔑 44px 로 가려면 **감싸는 라벨을 누르는 자리로 만드는 마크업 작업**이 필요하다(다음 회차).
   ══════════════════════════════════════════════════════════════════════════════════════════ */
@media (max-width: 767.98px) {
    /* ⚠️ 토큰은 `:root` 다(0 절과 같은 이유 — `.ds` 바깥의 모달·드로워·헤더에서도 해석돼야 한다). */
    :root {
        --ds-touch-min: 44px;
        /* 🔴 **글자가 든 컨트롤의 바닥값은 36px 이다** (2026-08-21 사용자 실기기 판단:
              *"모바일쪽 검색값 입력 요소 및 버튼이 너무 뚱뚱해 44px은 과한것 같아"* → **36px**).
           🔑 **`--ds-touch-min`(44px)과 갈라 둔 것이 이 값의 핵심이다** — 44 는 **글자가 없는 칸**
              (아이콘 버튼 · 페이징 · 닫기 ×)의 물리적 최소값이라 그대로 두고, 글자가 든 칸은
              **글자 자체가 조준 면적을 만들어** 낮춰도 누르는 데 지장이 없다.
           🔥 **둘을 다시 한 값으로 합치지 말 것** — 합치면 둘 중 하나는 반드시 틀린 크기가 된다
              (44 로 합치면 검색 줄이 뚱뚱하고, 36 으로 합치면 아이콘 버튼이 손가락보다 작다). */
        --ds-btn-h:     max(36px, 2.2rem);   /* 넓은 화면 2.2rem 은 그대로 */
        --ds-control-h: max(36px, 2.4rem);   /* 넓은 화면 2.4rem 은 그대로 */
    }

    /* 📄 **페이징 — 이 절에서 가로 넘침이 날 수 있는 유일한 자리다.**
       🔬 한 묶음이 최대 10쪽(`PaginationDto` 의 `pageCount` = 10)이라 이전·다음까지 **12칸**이고,
          30px 일 때 360px 이라 375px 화면에 겨우 들어가 있었다. 44px 이면 **528px** 이라 넘친다.
       🔑 그래서 **줄바꿈을 함께 켠다** — 12칸이 두 줄로 접힌다.
       🔥 `flex-wrap` 을 빼지 말 것 — 빼면 목록 화면 전체에 **가로 스크롤**이 생긴다. */
    .pagination { flex-wrap: wrap; row-gap: .25rem; }
    .page-item .page-link {
        min-height: max(var(--ds-touch-min), 2.5rem);
        min-width:  max(var(--ds-touch-min), 2.5rem);
    }

    /* 🔘 아이콘 버튼 — 본문·모달·드로워 안만(헤더는 위 주석의 이유로 뺐다).
       📌 `min-*` 는 벤더의 `height`·`width` 와 **다른 속성**이라 특이도 싸움 없이 이긴다. */
    #kt_content .btn.btn-icon,
    .modal .btn.btn-icon,
    .drawer .btn.btn-icon {
        min-width:  var(--ds-touch-min);
        min-height: var(--ds-touch-min);
    }
    /* ⚠️ `.ds-row-del`(5절)은 `min-width` 를 **`!important`** 로 못 박아 두었다 — 그쪽이 이겨
          **높이만 44 이고 폭은 20px 인 세로로 긴 버튼**이 된다. 여기서만 되받는다. */
    #kt_content .btn.btn-icon.ds-row-del { min-width: var(--ds-touch-min) !important; }

    /* 🔘 **카드 푸터의 행위 버튼 — `.ds` 안팎을 한 바닥값으로 묶는다** (2026-08-20 실측 후 추가)
       🔬 실측(랜드사 견적 상세 미리보기 「임시저장·견적 발송」, 500px): **39.797px**.
          그 화면 뿌리가 `#kt_content.quote-content` 라 **`.ds` 밖**이고, 17절의
          `.ds .card .card-footer .btn{height:var(--ds-btn-h)}` 가 닿지 않아 **벤더 기본**으로 남았다
          (`1.5em + .75rem×2 + 2px` = 39.8). 🔥 **토큰만 올려서는 여기 안 닿는다.**
       🔑 **높이를 박지 않고 바닥값만 준다** — `.ds` 안 푸터는 이미 44 라 아무 일도 안 하고,
          `.ds` 밖 푸터만 39.8 → 44 로 올라온다.
       🔑 **4px 이라 사소해 보이지만 고치는 이유는 크기 자체가 아니라 「갈림」이다** — 같은 폰 화면에서
          같은 성격의 버튼이 두 크기로 서는 것을 이 저장소는 구현 #74 에서 이미 한 번 고쳤다.
          그때 세 앱 카드 푸터 버튼 93개를 전수로 세어 **일부러 다르게 둔 곳이 0곳**임을 확인했으므로,
          여기에 예외를 남길 근거가 없다.
       📌 이 자리 버튼은 **저장·발송처럼 되돌리기 어려운 행위**다. 목표를 놓치면 손해가 큰 자리다.
       ⚠️ 모달 푸터는 `#kt_content` 밖이라 안 걸린다 — 그쪽은 3절이 이미 `--ds-btn-h` 로 잡는다.
       📌 이 앱에는 `.quote-content` 화면이 없어 실제로 걸릴 자리가 거의 없다 — 그래도 두는 이유는
          이 파일이 **세 앱 한 벌**이기 때문이다. 🔥 지우지 말 것. */
    /* 🕘 **2026-08-21: 바닥값(`min-height`)에서 지정 높이(`height`)로 바꿨다.** 글자 버튼의 값이
          44 → 36 으로 내려가면서 **바닥값만으로는 `.ds` 밖 푸터(벤더 기본 39.8px)를 못 내린다** —
          36 < 39.8 이라 규칙이 아무 일도 안 하고 이 절이 없애려던 **갈림이 그대로 돌아온다.**
          17절의 `.ds` 안 푸터가 이미 `height: var(--ds-btn-h)` 라 같은 수법이다. */
    #kt_content .card-footer .btn { height: var(--ds-btn-h); }

    /* 🔘 **카드 머리 툴바의 행위 버튼도 같은 바닥값** (2026-08-21 실측 후 추가)
       🔬 실측(관리자 1:1 문의 상세 「처리중으로」·「완료 처리」, 500px): **83×31.8 / 76×31.8**.
          `.card-toolbar` 는 푸터도 본문도 아니라 위 규칙들이 하나도 안 닿았다.
       🔥 **`btn-sm` 을 떼는 것으로 풀지 말 것 — 실제로 재 보고 배제했다.**
          떼면 좁은 화면에서 **39.8px**(벤더 기본)이라 여전히 미달이고, 넓은 화면에서는
          **50px** 이 되어 다른 툴바 버튼(`--ds-btn-h` = 35.2px)보다 **커진다.**
          🔑 즉 이 자리의 문제는 「작은 클래스를 썼다」가 아니라 **「바닥값이 없다」**이다.
       ⚠️ 이 규칙은 **위 11절의 카드 머리 세로 패딩이 0 이어야 성립한다** — 44 < 48(`min-height`)이라
          머리 높이가 안 변한다. 세로 패딩이 있으면 44 + 패딩이 48 을 넘어 **머리만 두꺼워진다**
          (실제로 그 상태였고 같은 날 11절에서 함께 고쳤다). 🔥 둘을 따로 되돌리지 말 것.
       📌 검색 폼 머리(`:has(form)`)의 「검색」 버튼은 위 `.input-group` 규칙이 이미 잡는다. */
    #kt_content .card-header .card-toolbar .btn { height: var(--ds-btn-h); }

    /* 🔎 **검색 줄의 버튼 — 높이는 맞는데 폭이 모자랐다** (2026-08-21 실측 후 추가)
       🔬 실측(500px): 「검색」 **41.5 × 44**. 높이는 위 토큰이 이미 잡았고 **폭만** 못 미쳤다 —
          `.input-group > .btn` 은 내용 폭으로 서는데 좌우 패딩 10.8px + 글자 두 자 19.9px = 41.5 다.
       🔑 **`width` 가 아니라 `min-width`** — 폭을 박으면 다른 언어에서 글자가 길어질 때(`Search`) 잘린다.
       🔬 **가로 넘침은 없다.** 저장소에서 가장 좁은 검색 그룹(랜드사 견적 상세 「대화 검색」, 246px)에서도
          옆 입력칸이 178.2 → 175.2px(**−1.7%**)일 뿐이고 `scrollWidth == clientWidth` 다.
          목록 화면의 검색 줄은 그보다 넓으므로 여유가 더 크다.
       📌 **「검색 취소」도 같은 패딩**이라 함께 걸리게 `.btn` 으로 잡는다 — 그 버튼은 검색어가 있을 때만
          나타나서 눈에 덜 띌 뿐 같은 결함이다. 🔥 「검색 버튼만」으로 좁히지 말 것.
       📌 `.ds` 의 좁은 화면 검색 줄은 3절이 **그리드로 스택**해 버튼이 이미 한 줄을 다 쓰므로
          이 바닥값이 아무 일도 하지 않는다 — 실제로 걸리는 것은 **`.ds` 밖**(편집 화면)의 검색 줄이다. */
    #kt_content .input-group > .btn,
    .modal .input-group > .btn,
    .drawer .input-group > .btn { min-width: var(--ds-touch-min); }

    /* 🔽 **화살표가 붙는 버튼** — 상세 보기 · 전체 이력 · 드로워 열기 (17절 규격 한 벌, 2026-08-21)
       🔬 실측(500px, 랜드사 견적 상세): 「전체 이력」 77×**25** · 「상세 보기」 77×**25**.
          17절의 `padding:.26rem .7rem` + `font-size:.95rem` 이 root 12px 을 타면 25.3px 이다.
       🔑 **뺄 수 없는 자리다** — 접힌 카드를 펴는 것도 이력 판을 여는 것도 **이 버튼 하나뿐**이다.
          아래 「대상에서 뺀 것」의 말풍선 닫기와 **정반대**다(그쪽은 바깥 클릭·Esc 라는 큰 대체 목표가 있다).
       ⚠️ **대가를 알고 올린다** — 이 버튼이 사는 `.ds-ds-head`(요약 카드 머리)는 **스티키**라
          머리가 그만큼 두꺼워진다(실측 전 25.3 → 44, 머리 높이 ≈40 → 58px). 17절이 「접힘 ≈50px」로
          아껴 둔 바로 그 띠다. 🔑 그래도 올린 이유는 **눌러야만 열리는 판**이라 목표를 놓치면
          그 내용에 닿을 방법이 아예 없기 때문이다.
       📌 폭은 안 건드린다(글자 버튼이라 이미 77px 이다).

       🔴🔴 **2026-08-21 되돌렸다 — 사용자 지시(폰 실기기 확인 뒤).**
          > *"이력카드와 정보요약 카드 헤드에 전체이력, 상세보기 버튼은 원래대로 되돌려 줘"*
          🔑 위에 적은 **대가가 실제로 컸다** — 요약 카드 머리가 스티키라 **≈40 → 58px** 이 되어
             폰에서 본문을 그만큼 상시 가린다. 17절이 「접힘 ≈50px」로 아껴 둔 띠를 되찾는다.
          🔑 사용자 판단: **누르기 불편하지 않다.** 이 버튼은 글자 77px 폭이라 가로로는 넉넉하고,
             세로 25px 이 실제 사용에서 문제가 되지 않았다.
          ⚠️ **되살리지 말 것.** 위 근거(「눌러야만 열리는 판이라 대체 목표가 없다」)는 여전히 맞지만,
             **실기기에서 재 본 사용자의 판단이 우선**이다. 다시 올리려면 사용자에게 먼저 물을 것.
          📌 규칙은 지우고 경위만 남긴다 — 이 절의 「대상에서 뺀 것」 목록에도 한 줄 넣었다. */

    /* ➕ **편집 화면의 행위 버튼** — 하단 액션 바(목록·다운로드·미리보기)와 「행 추가」 (2026-08-21)
       🔬 실측(500px, 랜드사 견적 상세): 「목록」 37×**27** · 「다운로드」 74×**27** · 「미리보기」 74×**27** ·
          「추가」·「상품 가격표에서 불러오기」 437×**29**.
          앞의 셋은 `menuLevel/Trade.css` 가 좁은 화면에서 **일부러 패딩을 줄인** 결과이고(그때 목적은
          세 버튼을 한 줄에 담는 것이었다), 「추가」는 점선 전폭 행이라 처음부터 29px 이다.
       🔑 **바닥값만 준다 — `Trade.css` 의 패딩 축소는 그대로 둔다.** 그 축소는 **폭** 문제를 푼 것이고
          이것은 **높이** 문제라 상충하지 않는다. 🔥 그 패딩을 되돌리지 말 것(글자가 두 줄로 뭉개진다).
       🔬 **가로 넘침 없음** — `Trade.css` 주석의 실측을 그대로 잇는다: 그 바는 375px 에서 쓸 수 있는 폭이
          **261px** 인데 지금 37+74+74+간격 6 = **191px** 이고, 「목록」이 44 로 올라도 **198px** 이다.
       📌 `.quote-add-row-btn` 은 랜드사에만, `.quote-action-bar` 는 랜드사·여행사에만 있다 —
          그래도 **이 파일 한 곳**에 둔다(세 앱 한 벌이라 갈리지 않는 것이 더 중요하다).
          🔥 「우리 앱엔 없다」로 지우지 말 것. */
    /* 🔴 **`min-height` 를 주면 글자를 가운데로 다시 세워야 한다** (2026-08-21 사용자 지적 — 폰 실기기:
          *"모바일에서 목록 버튼의 텍스트가 버튼 내 위쪽으로 올라가 붙었어"*).
       🔬 실측(500px, 랜드사 견적 상세 「목록」): 버튼 44px 인데 **글자 중심이 13.4px** — 가운데(22px)보다
          **8.6px 위**였다. 원인은 Bootstrap `.btn` 이 `display:inline-block` 이라 `min-height` 로 늘어난
          만큼이 **글자 아래에만** 붙기 때문이다(패딩으로 큰 것이 아니라 상자만 커진 것이다).
       🔬 같은 바의 「다운로드」·「미리보기」는 멀쩡했다 — 아이콘 때문에 마크업에 `d-flex` 가 이미 있었다.
          **그래서 한 버튼만 어긋나 더 눈에 띄었다.**
       🔑 **`min-height` 를 넣는 자리에는 이 세 줄을 함께 넣는다.** 자연 높이가 이미 44 이상인 버튼에는
          아무 일도 안 일어나므로 안전하다.
       🔥 패딩을 키워서 맞추지 말 것 — 그러면 글자가 긴 버튼의 **폭까지** 함께 늘어난다. */
    /* 🔴 **높이 바닥값은 `--ds-btn-h`(36px) 다 — `--ds-touch-min`(44px) 로 되돌리지 말 것** (2026-09-01)
       > 사장님 지적: *"375px, 430px 에서 맨 아래 목록 버튼이 너무 뚱뚱하다.
       >  그 이상 폭별에 있는 목록 버튼과 동일한 높이로 변경해야 겠어."*
       🔬 실측(여행사 견적 상세 「목록」): **375·430 에서 44.0px** 인데 768 이상은 **35.2px** —
          좁은 화면이 되레 9px 컸다. 원인은 root 가 12px 로 내려가 rem 높이(2.2rem)는 26.4px 이 되는데
          여기서 **px 44 를 박아** 그것을 덮었기 때문이다.
       🔑 **이 파일이 이미 두 값을 갈라 놓았다**(27절): `--ds-touch-min`(44px)은 **글자가 없는 칸**
          (아이콘 버튼·페이징·닫기 ×), `--ds-btn-h`(`max(36px, 2.2rem)`)는 **글자가 든 칸**이다.
          이 액션 바 버튼은 전부 글자가 든 버튼이라 **원래부터 `--ds-btn-h` 쪽**이었다.
          🔥 즉 이번 변경은 새 예외가 아니라 **2026-08-21 에 사장님이 정한 36px 규칙이 여기만 안 닿았던 것**이다.
       🔴 **`min-width` 는 44px 그대로 둔다** — 폭은 손가락이 빗나가는 축이고 지적은 **높이**였다.
       🔥 **`2.2rem` 만 남기고 px 바닥값을 지우지 말 것** — 좁은 화면 root 가 12px 이라 26.4px 이 되어
          넓은 화면(35.2)과 9px 어긋난다. 「동일한 높이」가 아니게 된다.
       📌 `.btn.quote-add-row-btn`(랜드사 견적 편집 표의 「행 추가」)도 **글자가 든 버튼**이라 함께 둔다 —
          성격이 같으므로 갈라 놓으면 그쪽만 44px 로 남아 같은 화면에서 두 높이가 생긴다. */
    .ds-detail-actions .card-body .btn,
    .btn.quote-add-row-btn {
        min-height: var(--ds-btn-h);
        min-width:  var(--ds-touch-min);
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }
    /* 📏 **좁은 화면 액션바의 좌우 여백은 14px 이 바닥 규격**이다 (2026-08-21 구현 #48).
       🔬 실측(375px, 여행사 상품 상세 영문): Metronic 기본 좌우 **19px** 로는 세 버튼이
          `List 60 + Schedule Info 143 + Request Quote 147 + 간격 9 = 359px` 인데 담을 폭은 **312px** —
          **두 줄**로 접혔다. 아이콘을 걷어 315px 이 돼도 3px 이 모자랐고, 14px 까지 줄여 **285px**(여유 27)로 한 줄이 됐다.
       ⚠️ **`!important` 가 필요하다** — Metronic `.btn` 의 좌우 패딩이 `!important` 라
          특이도(0,2,0 > 0,1,0)로는 안 이긴다. 실측: 규칙은 실렸는데 계산값이 19px 그대로였다.
          📌 19px 은 Metronic 의 rem 값이 **좁은 화면 root(12px)** 를 타고 줄어든 결과다.
       🔑 **높이는 건드리지 않는다** — 위 `--ds-touch-min`(44px)은 터치 목표라 성격이 다르다.
          여기서 푸는 것은 **폭** 문제다.
       🔥 8px 까지 줄이지 말 것 — 그러면 여유가 22px 뿐이라 문구가 조금만 길어져도 다시 두 줄이 된다. */
    .ds-detail-actions .card-body .btn {
        padding-left: 14px !important;
        padding-right: 14px !important;
    }

    /* 📐 **좁은 화면에서 액션바 버튼은 개수대로 폭을 나눠 갖는다** (2026-08-21 구현 #48 사용자 지시:
          *"모바일에서는 버튼 갯수대로 동일한 가로 너비에 중간 간격을 부여 하는걸로"*).
       🔑 **개수를 숫자로 박지 않는다 — `flex: 1 1 0` 이 브라우저에게 세게 한다.**
          🔬 전수 조사(26화면): 액션바 버튼이 1개 4화면 · 2개 8 · 3개 9 · 4개 4 · 5개 1 인데,
             그중 여럿이 **조건부(`th:if`)** 라 **같은 화면에서도 상태에 따라 개수가 달라진다**.
             그래서 「N개면 1/N」을 CSS 에 적으면 **그 순간 틀린다.**
       🔑 **묶음(`div`)도 같이 편다** — 액션바는 보통 `[목록] … [묶음]` 구조라 묶음을 안 펴면
          그 안의 버튼들이 한 칸을 나눠 갖고 목록만 커진다.
       📌 이 앱에는 `.quote-action-bar` 를 쓰는 화면이 아직 없다 — **세 앱 한 벌**이라 여기 둔다.
          🔥 「안 쓰니까」로 지우지 말 것. */
    /* 🔴 **묶음을 `display: contents` 로 펜다 — 이것이 없으면 균등이 되지 않는다.**
       🔬 실측(375px, 랜드사 상품 상세): 액션바가 `[목록] [묶음(복사·미리보기)]` 구조라
          `flex:1 1 0` 만 주면 **목록이 1/2(152px), 묶음 안 둘이 각 1/4(73px)** 이 됐다.
          🔑 `display: contents` 면 묶음 상자가 사라지고 **그 안의 버튼이 액션바의 직계 항목**이 되어
             셋이 정확히 1/3 씩 나눈다. 간격은 액션바의 `gap` 이 모든 버튼에 똑같이 준다.
       ⚠️ 묶음에 걸린 `ms-auto`(오른쪽 밀기)는 이때 무시된다 — 균등 분할에서는 밀 것이 없으므로 맞다.
       ⚠️ **넓은 화면에서는 이 규칙이 없다** — 거기서는 묶음이 살아 있어야 `ms-auto` 로 오른쪽에 붙는다. */
    /* ⚠️ **`!important` 가 필요하다** — 묶음에 Bootstrap `.d-flex`(= `display:flex !important`)가
          붙어 있어 평범한 선언으로는 진다. 실측: 규칙을 넣었는데 계산값이 `flex` 그대로였다.
       ⚠️ **구조가 화면마다 다르다** — 목록이 직계 `<a>` 인 화면(템플릿 상세)도 있고
          묶음 안에 든 화면(상품 상세)도 있다. 그래서 **둘 다 잡는 선택자**로 적는다. */
    .ds-detail-actions .card-body > div { display: contents !important; }
    .ds-detail-actions .card-body > .btn,
    .ds-detail-actions .card-body > div > .btn { flex: 1 1 0; }
    /* 🔴 **목록만 자기 크기로 둔다 — 나누는 쌍에서 뻐다** (2026-08-22 사용자 지시:
          *"목록버튼은 하단 액션바 좌측에 두되 너비를 n분의 1 하지 말고 작게 배치하자.
           액션바에서 목록 버튼이 차지한 나머지 영역을 남은 버튼들이 n 분의 1로 차지하도록"*).
       🔑 **판정은 `data-back-list` 로** 한다 — 「첫 번째 버튼」이 아니다.
          목록 버튼은 이미 그 표식을 달고 있고(보고 있던 페이지로 돌아가는 장치),
          순서는 화면마다 달라질 수 있다.
       🔑 **간격은 `gap` 하나가 준다** — 목록을 뺀 모든 버튼 사이가 같은 값이다
          (묶음을 `display:contents` 로 펴기 때문에 안쪽 버튼까지 한 줄로 서서 같은 gap 을 받는다).
       🔥 `margin` 으로 밀지 말 것 — 그러면 목록 옆만 간격이 달라진다. */
    .ds-detail-actions .card-body > [data-back-list],
    .ds-detail-actions .card-body > div > [data-back-list] { flex: 0 0 auto; }

    /* 🔑 **카드 푸터의 주 행위는 폰에서 전폭**이다 (같은 지시).
          이 자리는 버튼이 하나뿐이라 나눌 것이 없다 — 손가락이 닿기 쉬운 전폭이 정석이다. */
    .card-footer.quote-main-action > .btn { width: 100%; }

    /* ✕ **미리보기 판·이력 판의 닫기** — 아래 `.btn-close` 와 같은 물음, 다른 이름이다 (2026-08-21).
       🔬 실측(500px, 랜드사 견적 상세): `.pv-close` · `.hist-close` 가 **29 × 18.8**.
          둘 다 맨 `<button>` 에 `font-size:1.4rem; line-height:1` 뿐이라 **브라우저 기본 패딩**만 있다.
       ⚠️ **`.btn-close` 규칙에 얹을 수 없다** — 이 둘은 그 클래스를 쓰지 않는다.
       🕘 이 자리에 「글자 `×` 다」라고 적혀 있었다 — **더 이상 사실이 아니다.** 2026-09-01·09-03 에
          세 앱의 닫기가 전부 **`<i class="bi bi-x-lg">`** 가 됐다(위 「닫기 아이콘은 버튼의 글자색을
          따른다」 절). 위 실측 **29 × 18.8** 도 글자 시절 값이다 — 지금은 **28 × 33.11**(1440px).
          🔥 그래도 마크업을 `.btn-close` 로 바꾸지 말 것: 그 클래스는 배경 이미지 X 라
          `filter: invert(1)` 같은 다크 처방이 딸려 오고, 이 판들은 글자 색을 토큰으로 따라간다.
          🔥 마크업을 `.btn-close` 로 바꾸는 쪽으로 고치지 말 것: 그 클래스는 배경 이미지 X 라
          `filter: invert(1)` 같은 다크 처방이 딸려 오고, 이 판들은 글자 색을 토큰으로 따라간다.
       📌 관리자 앱에는 이 둘의 **CSS 가 없지만 템플릿에는 있다** — 이 파일이 세 앱 한 벌이라 여기 둔다.
          🔥 「안 쓰니까」로 지우지 말 것. */
    .pv-close,
    .hist-close {
        box-sizing: border-box;
        min-width:  var(--ds-touch-min);
        min-height: var(--ds-touch-min);
        /* 🔴 **터치 영역을 키웠으면 글자를 끝선으로 다시 밀어야 한다**
              (2026-08-21 구현 #48 사용자 지적: *"닫기 버튼과 콘텐츠 영역의 우측 정렬이 맞지 않아"* /
               *"실기기에서는 X 닫기 버튼만 우측으로 붙여도 될것 같긴 해"*).
           🔬 실기기 실측: 콘텐츠 카드의 오른쪽 끝보다 **닫기 `×` 글자가 40px 안쪽**이었다.
              상자(44px)의 끝은 맞는데 **글자가 상자 한가운데** 있어 `(44 − 글자)/2` 만큼 들어간 것이다.
           🔑 **스크롤바 보정은 여기서 하지 않는다** — 머리의 좌우 여백이 이미
              `+ var(--ds-scrollbar-w)` 로 그 몫을 갖고 있다(`menuLevel/Trade.css` 좁은 화면 절).
              여기서 또 더하면 **두 번 보정**되어 글자가 안쪽으로 밀린다.
           🔥 터치 영역 44px 은 그대로다 — **버튼을 작게 만들어 맞추지 말 것.** */
        margin-right: calc((var(--ds-touch-min) - 1em) / -2);
    }

    /* 🔐 **로그인·비밀번호 화면** — 이 절의 다른 규칙이 **원리적으로 안 닿던 자리** (2026-08-21)
       🔴 위 규칙들은 전부 `#kt_content` · `.modal` · `.drawer` · `.ds` 로 한정돼 있는데 이 화면들은
          **그 어느 것도 아니다**(뿌리가 `.d-flex.flex-column.flex-root.ds-auth-page` 다).
          🔥 **가장 먼저 만나는 화면인데 통째로 사각지대였다.**
       🔬 실측(500px): 「계속하기」 230×**42**(2px 모자람) · 언어 「한국어」 94×**40** ·
          비밀번호 보기 토글 **31.8×31.8**.
       🔴 **여기까지다 — 더 넓히지 말 것.** 대상은 `.ds-auth-page` 를 단 화면 셋
          (`sign/signin` · `sign/changePwd` · `sign/resetPwd`)뿐이고, 그 안의 `.btn` 은 실측상
          **제출 버튼 · 언어 버튼 · 비밀번호 보기 토글**뿐이다(언어 드롭다운 항목은 `.menu-link` 라 안 걸린다).
          🔥 **`#kt_header` 로 넓히지 말 것** — 그쪽은 이 절이 **일부러** 뺐고 이유가 위에 적혀 있다.
       📌 **회원가입(`sign/signup`)은 이 클래스를 안 달고 있어 대상이 아니다.**
          🔥 달아서 끌어들이지 말 것 — 그 클래스에는 언어 선택 배치 규칙(19절)이 함께 걸려 있어
          **가입 화면 배치가 같이 바뀐다.** 필요하면 그 화면을 따로 재고 정할 일이다. */
    .ds-auth-page .btn { min-height: var(--ds-touch-min); }
    /* 🔑 **입력칸도 함께 올린다** — 이 절 머리말의 「인접 규칙」 그대로다. 칸이 39.8 인데 위에 얹힌
          토글만 44 면 토글이 칸 밖으로 2.1px 씩 삐져나온다. 둘은 **한 벌로 움직여야** 한다. */
    .ds-auth-page .form-control { min-height: var(--ds-control-h); }
    /* 👁 **비밀번호 보기 토글** — 입력칸 **위에 떠 있다**(`position-absolute end-0 me-3`).
       🔑 넓히기 전에 「그 자리에 글자가 오는가」를 먼저 물었다 → **온다.** 칸에 오른쪽 여백이
          하나도 없어 지금(41px 점유)도 이미 덮고 있다. 그래서 **칸의 오른쪽을 먼저 비우고** 넓힌다
          — 고객센터 답변 상자(`menuLevel/Customer.css`)와 **같은 수법**이다.
       🔥 여백 없이 폭만 올리지 말 것(긴 비밀번호의 끝이 아이콘 밑으로 들어간다) ·
       🔥 반대로 여백만 주고 폭을 안 올리지도 말 것(빈 자리가 생길 뿐 목표는 그대로 작다). */
    .ds-auth-page .position-relative > .btn.btn-icon {
        min-width:  var(--ds-touch-min);
        min-height: var(--ds-touch-min);
    }
    .ds-auth-page .position-relative > .form-control {
        padding-right: calc(var(--ds-touch-min) + 1.5rem) !important;
    }

    /* ✕ 판 닫기 — `box-sizing` 을 함께 바꿔야 바닥값이 **바깥 상자**에 걸린다
          (벤더가 `content-box` 라 그대로 두면 안쪽 상자가 44 가 되고 여백이 그 위에 더해진다).
       📌 모달 머리에서는 벤더가 패딩만큼 **음수 마진**을 걸어 두어 머리 높이가 거의 안 늘어난다. */
    .modal .btn-close,
    .ds .btn-close {
        box-sizing: border-box;
        min-width:  var(--ds-touch-min);
        min-height: var(--ds-touch-min);
    }

    /* ❓ `?` 말풍선 — **겉모습은 그대로, 누르는 자리만 넓힌다.**
       🔑 상자를 실제로 키우면 이 `?` 가 얹힌 줄(항목명 · 카드 머리)의 높이가 통째로 44px 이 된다 —
          22절이 「말풍선은 배지보다 작아 카드 머리 높이를 올리지 않는다」로 잡아 둔 것이 그날로 깨진다.
       🔑 그래서 **패딩으로 넓히고 같은 값의 음수 마진으로 되돌린다** — 마진 박스는 1.1rem 그대로라
          배치가 하나도 안 움직이고, 손가락이 닿는 상자만 44px 이 된다(배경이 없어 넓힌 자리는 안 보인다).
       ⚠️ **`box-sizing: content-box` 가 이 수법의 핵심**이다 — 기본(border-box)이면 패딩이 폭을
          갉아먹어 상자가 **커지지 않는다**(오류가 아니라 아무 일도 안 일어난다).
       ⚠️ 글자를 키워 1.1rem 이 44px 을 넘으면 패딩이 음수가 되므로 `max(0px, …)` 로 막는다. */
    .ds .ds-help {
        box-sizing: content-box;
        padding: max(0px, calc((var(--ds-touch-min) - 1.1rem) / 2));
        margin:  calc(-1 * max(0px, calc((var(--ds-touch-min) - 1.1rem) / 2)));
    }

    /* ☑ 체크박스·라디오 — 위 주석의 이유로 **24px 까지만** 올린다. */
    .form-check-input {
        min-width:  max(24px, 1.75rem);
        min-height: max(24px, 1.75rem);
    }
    /* 🔥 스위치는 되돌린다 — 정사각형 바닥값을 주면 알약이 네모가 된다. */
    .form-switch .form-check-input { min-width: 0; min-height: 0; }
}

/* ══ 26) 📱 좁은 화면의 **검색 조건 — 라벨 줄을 없애고 자리표시자가 이름을 대신한다** ══════════
   (2026-08-17 사용자 지시 — 실기기 확인)

   > *"라벨형은 필요 없는 것 같아, 모바일에서는 플레이스홀더를 검색 조건 명으로 하는 건 어떨까?"*

   🔬 **왜** — 폰에서는 조건마다 라벨이 한 줄씩 붙어 **검색 카드만으로 화면이 찬다**(조건 6개 = 12줄).
   🔴 **이 절에는 CSS 가 없다. 규격만 있다.** 자리표시자 글자는 CSS 로 바꿀 수 없고
      (`input` 은 대체 요소라 `::before` 가 생성되지 않는다 — 오류가 아니라 아무 일도 안 일어난다),
      라벨을 숨기는 것도 **Bootstrap 의 `.visually-hidden`**(번들에 이미 있다)을 `common.js` 가
      껐다 켰다 하는 것으로 끝난다. 🔥 여기에 `display:none` 으로 라벨을 지우는 규칙을 만들지 말 것 —
      **화면 낭독기가 못 읽어** 그 칸이 무엇인지 알 수 없게 된다(`.visually-hidden` 은 읽힌다).

   🏷 **쓰는 법은 표식 두 개다**(꼴모습이 없는 표식이라 CSS 를 갖지 않는다 — `data-ds-daterange` 와 같은 취급):
      · `data-narrow-label="…"`  → **그 칸의 조건 이름.** 좁은 화면에서 자리표시자로 나간다.
        문구는 반드시 **i18n 키**로 준다(`th:data-narrow-label="#{…}"`). 🔥 화면에 한글을 박지 말 것.
      · `data-narrow-hide`       → **좁은 화면에서 눈에만 안 보이게 할 라벨.**
      두 표식을 읽는 곳은 `common.js` 의 「좁은 화면 — 검색 조건의 이름」 블록 **하나**다.

   🔴 **이름은 매퍼가 정한다 — 지어내지 말 것.** 기간 칸이 실제로 어느 컬럼을 비교하는지
      (`C_REG_DT` 인지 `C_TRAVEL_START_DT` 인지 `C_CONFIRM_DT` 인지)에 따라 화면마다 다르다.
      🔥 「기간」처럼 뭉뚱그리지 말 것 — 그러면 자리표시자가 이름 노릇을 못 해 이 규칙이 무의미해진다.
   ⚠️ **이미 라벨이 있는 화면은 그 라벨 글자를 그대로** 쓴다(새 i18n 키를 만들기 전에 기존 키를 볼 것).
      `data-narrow-label` 이 없으면 `common.js` 가 **짝지어진 `<label>` 의 지금 글자**를 읽는다 —
      그래서 라벨이 JS 로 바뀌는 자리(통계의 「조회 기간 (최대 N일)」)도 따라간다.

   📌 **선택 상자(`select`)는 자리표시자가 없다** → 손대지 않는다.
      🔑 첫 항목이 **「전체 상태」·「전체 통화」·「전체 유형」**처럼 이미 이름 노릇을 하고 있으면 그대로 두고
         라벨만 감춘다. 🔥 첫 항목이 **값**뿐인 선택 상자(통계의 「기간 단위」·「기준일」·「분해 기준」)는
         **라벨을 감추지 말 것** — 「일별」만 남으면 무엇을 고르는 칸인지 알 수 없다.
         그런 자리는 라벨을 **같은 줄로** 옮겨 줄 수를 줄인다(`menuLevel/Stats.css` 좁은 화면 절).
   🔥 **보이는 기간 칸에 `name` 을 주지 말 것**(4-4 절) — 이 절의 작업은 **보이는 것만** 바꾼다.
      서버 파라미터·매퍼·컨트롤러는 하나도 건드리지 않는다.
   ══════════════════════════════════════════════════════════════════════════════════════════ */

/* ══════════════════════════════════════════════════════════════════════════════════════════
   28) **배치 부품 — 새 화면·새 요소는 이것만 조합한다** (2026-08-21 구현 #47 사용자 지시)

   > *"이런건 애초에 니가 디자인 표준과 수정했던 내용을 확인하고 그 틀에 맞춰서 요소를 추가하는
   >  방식이 필요해."*  ·  *"뭐가 됐든 요소의 크기 간격 배치 등등으로 스트레스 받지 않게 해 줘"*

   🔬 **왜 부품인가 — 그날 나온 세 결함이 전부 「화면이 자기 배치를 직접 짠 것」이었다.**
      · 삭제 칸 `width:56px` 이 미디어 쿼리인데 표↔카드 전환은 컨테이너 쿼리 → 두 기준이 어긋났다
      · 표의 첫 열 여백이 카드형에도 살아 항목명만 24px 밀렸다
      · 「데이터 칸 대칭」 규칙이 행위 칸까지 잡아 버튼이 칸 안에서 좌3/우27 이 됐다
      🔑 셋 다 **화면 CSS 가 여백·폭을 직접 정했기 때문**이다. 부품이 정하면 어긋날 자리가 없다.

   🔴 **화면 CSS(`menuLevel/*.css`)에서 `padding`·`margin`·`gap` 을 새로 쓰지 마라.**
      부품으로 안 되는 배치를 만나면 **부품을 고치거나 여기 새 부품을 더한다** — 화면에서 때우지 않는다.
      🔑 그래야 같은 수정을 화면마다 반복하지 않는다(그것이 이 절을 만든 이유다).

   ⚠️ **기존 화면을 지금 일괄로 바꾸지 마라.** 손대는 화면만 옮긴다 — 옮길 때마다 그 화면의
      `menuLevel/*.css` 가 줄어드는 것이 정상이다.
   📌 검사: `tools/layout-audit.js` 를 화면에서 돌리면 끝선 어긋남·다닥다닥·넘침을 잡는다.
      🔥 **`LayoutAudit.selfTest()` 를 먼저 돌릴 것** — 이 저장소는 「0건」이 실은 「검사 안 함」이었던
      적이 여러 번 있다.
   ══════════════════════════════════════════════════════════════════════════════════════════ */

/* ── 28-1) 세로 쌓기 — 가장 많이 쓴다 ──────────────────────────────────────────────────────
   🔑 **자식에 `margin-bottom` 을 주지 마라.** 마지막 자식만 여백이 남아 카드 아래가 뜬다.
      부모가 `gap` 으로 정하면 그 문제가 원리적으로 안 생긴다. */
.ds-stack        { display: flex; flex-direction: column; gap: var(--ds-sp-4); }
.ds-stack--tight { gap: var(--ds-sp-2); }   /* 같은 묶음 안 */
.ds-stack--loose { gap: var(--ds-sp-6); }   /* 큰 구역 사이 */

/* ── 28-2) 가로 줄 ────────────────────────────────────────────────────────────────────────
   ⚠️ 기본은 **줄바꿈 허용**이다 — 좁은 화면에서 넘치면 가로 스크롤이 생기고,
      이 저장소에서 가로 스크롤은 **절대 금지**다(2026-08-20 사용자 지시). */
.ds-row          { display: flex; align-items: center; gap: var(--ds-sp-3); flex-wrap: wrap; }
.ds-row--tight   { gap: var(--ds-sp-2); }
.ds-row--nowrap  { flex-wrap: nowrap; min-width: 0; }
.ds-row--between { justify-content: space-between; }
.ds-row--end     { justify-content: flex-end; }
.ds-row--top     { align-items: flex-start; }

/* ── 28-3) 격자 — 열 수를 정해 오와 열을 맞춘다 ───────────────────────────────────────────
   🔑 **`minmax(0,1fr)` 이다** — 그냥 `1fr` 이면 칸 안의 긴 글자가 칸을 밀어내 열이 어긋난다.
   🔑 좁아지면 자동으로 한 열이 된다(`--ds-grid-min` 보다 좁아질 때). 화면마다 미디어 쿼리를 쓰지 마라. */
.ds-grid   { display: grid; gap: var(--ds-sp-4);
             grid-template-columns: repeat(auto-fit, minmax(min(var(--ds-grid-min, 16rem), 100%), 1fr)); }
.ds-grid-2 { --ds-grid-min: 20rem; }
.ds-grid-3 { --ds-grid-min: 14rem; }
.ds-grid-4 { --ds-grid-min: 11rem; }
.ds-grid > * { min-width: 0; }   /* 🔥 빼지 말 것 — 긴 글자가 칸을 넘어 격자를 깬다 */

/* ── 28-3-1) **칸을 셋으로 잡아 두고, 모자라면 빈 칸으로 남긴다** ────────────────────────
   🔴 (2026-08-23 사용자 지시) *"테이블 2개가 나란히 나온 후 **한 칸은 공백**이 되겠지."*
   🔬 **왜 위 `.ds-grid` 만으로는 안 되나** — 기본 식은 `auto-fit` 이라 **내용이 없는 트랙을 0 으로 접고**
      남은 항목이 그 자리를 나눠 가진다. 항목이 둘이면 **둘이 폭을 반씩** 차지한다 →
      빈 칸이 안 생길 뿐 아니라 4K 에서 표 하나가 여전히 1,700px 가 넘는다.
      **이 지시가 없애려던 바로 그 증상이 그대로 남는다.**
   🔑 `auto-fill` 은 **빈 트랙을 남긴다.** 거기에 트랙 최소폭을 `(100% - gap×2)/3` 으로 두면
      **4열이 물리적으로 못 들어가** 상한이 3 으로 고정된다 — 9절 목록 카드 격자가 쓰는 것과 **같은 식**이다
      (새로 지어낸 값이 아니다).
   🔑 화면이 좁아져 바닥값(`--ds-grid-min`)이 이기기 시작하면 3열 → 2열 → 1열로 **저절로 풀린다.**
      🔥 화면마다 미디어 쿼리를 새로 쓰지 말 것.
   🔥 **`auto-fit` 으로 되돌리지 말 것** — 그 한 단어가 「빈 칸을 남긴다」를 없앤다.
   ⚠️ `gap` 을 바꾸면 `(100% - gap×2)/3` 의 `gap` 도 함께 바꿔야 한다(둘이 맞물려 상한을 만든다).
   📌 쓰는 곳: 여행사 상품 상세의 **통화별 가격 표**(통화가 셋을 넘으면 다음 줄로 내려간다). */
.ds-grid--slots3 {
    grid-template-columns: repeat(auto-fill,
        minmax(min(100%, max(var(--ds-grid-min, 14rem), (100% - var(--ds-sp-4) * 2) / 3)), 1fr));
}

/* ── 28-4) 행위 버튼 줄 ───────────────────────────────────────────────────────────────────
   🔴 **행위 버튼은 오른쪽 끝에 선다**(이 저장소 원칙). 삭제·닫기 같은 한 개짜리도 마찬가지다.
   🔬 2026-08-21 에 견적 항목의 삭제 버튼이 칸 중간에 서 있던 것이 이 규칙이 없어서였다.
   ⚠️ 좁은 화면에서는 **전폭으로 눕힌다** — 폰에서 오른쪽에 몰린 작은 버튼은 누르기 어렵다. */
.ds-actions { display: flex; align-items: center; justify-content: flex-end; gap: var(--ds-sp-3); flex-wrap: wrap; }
.ds-actions--start  { justify-content: flex-start; }
.ds-actions--spread { justify-content: space-between; }
/* 🔴 **카드 본문 끝에 붙는 행위 줄의 위 여백은 이 한 값이다** (2026-08-27 · R6).
   🔬 그전에는 같은 자리에 `mt-4`(1rem) · `mt-5`(1.25) · `mt-6`(1.5) · `mt-8`(2) 이 섞여 있었다 —
      화면마다 「값과 버튼 사이」가 달라 어떤 카드만 늘어져 보였다(사장님 지적 R6).
   🔥 화면에서 `mt-*` 로 다시 정하지 말 것. 값이 바뀌어야 하면 **여기 한 줄**을 고친다. */
.ds-actions--sep { margin-top: var(--ds-sp-5); }
/* 🔴 **C8 — 이 줄의 버튼은 글자를 가운데 둔다** (2026-08-24 사장님 확정).
   🐛 `.btn` 은 Metronic 에서 `display:flex` 인데 `justify-content` 가 **`normal`** 이라
      **`text-align:center` 가 아무 일도 하지 않았다** — flex 컨테이너에서 글자를 정렬하는 것은
      `text-align` 이 아니라 `justify-content` 다.
   🔬 실측(전폭이 된 「견적 발송」 버튼): 글자 왼쪽 여백 **14px** / 오른쪽 **367px** →
      고친 뒤 **191 / 191px**. 「임시저장」도 같았다.
   🔑 **전폭일 때만 드러나는 결함이다** — 폭이 내용에 맞을 때는 여백이 없어 눈에 안 띈다.
      그래서 아래 좁은 화면 블록이 아니라 **여기(모든 폭)** 에 적는다. 넓은 화면에서 버튼이
      전폭이 되는 자리(카드 푸터 안 단독 버튼 등)도 함께 낫는다.
   ⚠️ `.btn` 에 `display:flex` 가 없는 상태로 바뀌면 이 줄은 조용히 아무 일도 안 한다 —
      그때는 `text-align:center` 가 다시 이긴다(둘 다 두어도 서로 방해하지 않는다).
   📌 이 앱에는 아직 `.ds-actions` 사용처가 **0곳**이다(2026-08-24 전수). 🔥 그래도 지우지 말 것 —
      세 앱 한 벌이고, 이 앱이 그 골격을 쓰기 시작하면 그날로 같이 적용된다. */
.ds-actions > .btn { justify-content: center; }
/* 🔴 **카드 푸터 안의 `.ds-actions` 는 푸터 폭을 다 쓴다** (2026-08-31 사장님 지시 · 항목 36 ⑦)
   > *"버튼은 다른 화면 처럼 가로 100%로 버튼 하나당 1줄"*

   🔬 **왜 아래 좁은 화면 규칙만으로는 부족했나** — `.card-footer` 자체가 `display:flex` 라
      그 안의 `.ds-actions` 는 **내용 폭(실측 90.7px)** 만큼만 잡는다. 그러면 `> .btn{width:100%}` 가
      걸려도 **그 90.7px 의 100%** 라 아무 일도 안 일어난다.
   🔬 실측(여행사 계약 상세 · 전): 375·430 에서 버튼이 **90.7px** — 넓은 화면(121px)보다 **30px 좁았다**.
      고친 뒤 **375 = 302px · 430 = 357px**.
   🔑 **넓은 화면은 안 바뀐다** — 푸터가 이미 오른쪽 정렬이고 `.ds-actions` 도 `justify-content: flex-end`
      라, 폭만 늘어나고 버튼 자리는 그대로다.
   ⚠️ 버튼 줄이 `.ds-da-end` 인 화면에는 **이 규칙도 아래 규칙도 안 걸린다** — 마크업을 `.ds-actions` 로
      바꾸는 것과 **한 벌**이다. 🔥 한쪽만 하지 말 것. */
.ds .card > .card-footer > .ds-actions { flex: 1 1 auto; width: 100%; }
/* 🕘 **문턱은 `575.98px` 이었다** — *"🔥 옮기면 576~767px 구간의 버튼 줄이 세로로 쌓인다"* 가 그 근거였다.
   🔴 **2026-09-07 에 `767.98px` 로 옮겼다 — 그 대가를 알고 옮긴 것이다**(사장님 확정 「구간 수를 줄인다」).
      🔑 사장님 판단: *"레이아웃이 깨지는건 좁아서 깨지는거지 넓어서 깨지지는 않으니까"* —
         576~767px 에서 버튼이 세로로 쌓이는 것은 **깨짐이 아니라 좁은 폭의 정상 모습**이다.
      🔥 「576~767 이 세로로 쌓인다」를 이유로 575.98 로 되돌리지 말 것. */
@media (max-width: 767.98px) {
    .ds-actions:not(.ds-actions--keep) { flex-direction: column; align-items: stretch; }
    .ds-actions:not(.ds-actions--keep) > .btn { width: 100%; }
}

/* ── 28-5) 끝선 맞추기 — 「오와 열」의 실체 ────────────────────────────────────────────────
   🔑 **한 카드 안의 요소는 좌우 끝선이 하나여야 한다.** 어떤 요소만 따로 여백을 받으면
      그 요소만 안으로 밀려 「제각각」으로 보인다 — 항목명 24px 이 정확히 그 모양이었다.
   🔑 `.ds-bleed` 는 **일부러 끝선 밖으로** 빼는 자리(구분선·전폭 배너)에만 쓴다. */
.ds-edge   { padding-left: 0 !important; padding-right: 0 !important; }
.ds-bleed  { margin-left: calc(-1 * var(--ds-pad-x)); margin-right: calc(-1 * var(--ds-pad-x)); }

/* ── 28-5-1) 한 줄 말줄임 — `.ds-ellipsis` (2026-08-28 · D-6 E12 · A-E5 · D-E19) ───────────
   🔬 **왜 부품인가** — 좁은 칸에 든 설명·이름이 화면마다 다르게 접혔다. 실측(관리자 역할·권한
      역할 설명): 375px **2줄** · 1100px **3줄** · 1440px **2줄** · 768~900px 1줄 —
      **폭을 넓힐수록 줄 수가 오히려 늘어** 목록 줄 높이가 제각각이 됐다(1100px 은 상세 카드가
      옆에 서면서 목록 칸이 152px 로 줄어드는 구간이다).
   🔴 **`title` 을 함께 주지 않으면 쓰지 마라.** 말줄임은 글자를 **없애는** 것이라, 전문을 볼 길이
      남아 있어야 한다. `title` 은 마우스 말풍선이자 낭독기가 읽는 이름이다.
      🔥 CSS 만 붙이고 `title` 을 빠뜨리면 **정보가 조용히 사라진다** — 이 부품의 존재 이유가 무너진다.
   ⚠️ **부모가 줄어들 수 있어야 말줄임이 걸린다.** flex·grid 칸의 `min-width` 초기값은 `auto`
      (= min-content)라 그대로 두면 칸이 글자만큼 넓어져 **말줄임이 아예 동작하지 않는다.**
      → 감싼 칸에 **`.ds-ellipsis-box`** 를 준다(`.ds-grid > *` 가 이미 하는 일과 같다).
   📌 여러 줄을 허용하며 상한만 두는 자리는 이 부품이 아니라 **`.ds-longtext`**(23절)다. */
.ds-ellipsis     { min-width: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-ellipsis-box { min-width: 0; }

/* ── 28-6) 입력 항목 줄 — `.ds-form-grid` (2026-08-27 · E32) ──────────────────────────────
   🔬 **왜 필요한가**(실측) — 첨부·보존 화면을 `row g-5` + `col-md-4` 로 3열로 놓았더니
      **어떤 칸의 라벨만 두 줄로 접혀** 그 칸의 입력창이 25px 위에 섰다.
      영문 900px: 라벨 높이 50 / 50 / **25** → 입력창 top 342 / 342 / **317**.
      1100px 도 같다(316 / 316 / **291**). 🔑 **열 수를 줄이는 것으로는 못 막는다** —
      칸마다 라벨 길이가 다르면 어느 폭에서든 다시 난다(언어를 바꾸면 더 그렇다).
   🔑 **`subgrid` 로 라벨·입력칸·안내를 부모의 같은 줄에 박는다.** 라벨 줄의 높이는 그 줄에 선
      칸들 중 **가장 높은 것**이 정하므로 입력칸의 세로 줄이 언제나 맞는다.
   🔑 한 칸은 **최대 세 요소**다(라벨 · 입력칸 · 안내). 둘이면 셋째 줄이 비어 있을 뿐 정렬은 그대로다.
      🔥 **네 개를 넣지 말 것** — 넷째가 다음 칸의 줄로 흘러 그 줄만 어긋난다.
   ⚠️ **열 수를 박지 마라** — `auto-fit` 이 컨테이너 폭으로 센다. 바닥값은 `rem` 이라
      글자 크기 3단계를 따라 커진다(px 로 박으면 큰 글자에서 라벨이 다시 접힌다).
   ⚠️ 줄 간격은 **`gap` 하나**가 정한다(배치 규칙 ⑤ — 자식이 `margin` 으로 주면
      마지막 줄만 여백이 남는다) → 라벨·안내가 스스로 들고 있던 여백을 아래에서 걷는다. */
.ds .ds-form-grid {
    display: grid;
    gap: var(--ds-sp-3) var(--ds-sp-5);
    grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--ds-form-grid-min, 15rem)), 1fr));
}
.ds .ds-form-grid > * {
    display: grid;
    grid-template-rows: subgrid;
    grid-row: span 3;
    min-width: 0;
}
.ds .ds-form-grid > * > .form-label { margin-bottom: 0; }
.ds .ds-form-grid > * > .form-text,
.ds .ds-form-grid > * > .ds-hint    { margin-top: 0; }

/* 🔴 **설명이 긴 줄은 칸을 넓게 잡는다** — `.ds-form-grid--wide` (2026-08-27 · E18).
   🔬 약관 등록의 「가입 화면 표시」 안내가 **768px 에서 9줄, 900·1100 에서 7줄**로 접혔다.
      원인은 「체크박스 오른쪽」이 아니라(설명은 이미 아래에 있었다) **칸이 211~255px 로 좁은 것**이다
      — `col-md-4` 는 768px 이상이면 무조건 3열이라 폭이 어떻든 1/3 을 준다.
   🔑 바닥값을 올리면 **좁을 때 열이 줄고 넓을 때는 그대로**다(3열 상한이 아니라 폭이 정한다).
   🔥 화면에 `style="--ds-form-grid-min:…"` 을 적지 말 것 — 값은 여기 한 곳이다. */
.ds .ds-form-grid--wide { --ds-form-grid-min: 20rem; }

/* 🔴 **라벨이 없는 칸은 라벨 줄을 비우고 아래 두 줄만 쓴다** — `.ds-nolabel`.
   🔬 체크박스 묶음처럼 라벨이 없는 칸은 자식이 둘(묶음 · 설명)이라 그냥 두면 **첫째가 라벨 줄에**
      들어가 옆 칸의 입력창과 나란히 서지 못한다.
   🔑 첫 자식을 **둘째 줄**에서 시작시키면 나머지는 저절로 따라간다.
   🔥 이 칸에 `d-flex`·`justify-content-end` 를 다시 붙이지 말 것 — 그 유틸리티는 `!important` 라
      `display:grid` 를 이겨 **subgrid 가 통째로 꺼진다**(그러면 줄이 다시 어긋난다). */
.ds .ds-form-grid > .ds-nolabel > :first-child { grid-row: 2; }
/* 🔴 **둘째 자식(설명)은 셋째 줄에 못 박는다** (2026-09-19 관리자 1회차 E5-29).
   🔬 첫째만 2줄에 박아 두면 둘째는 **자동 배치**라 비어 있는 **1줄(라벨 줄)**로 올라가
      설명이 체크박스 줄 **위**에 그려졌다(1920: 설명 top 127~210 · 체크박스 232).
      「나머지는 저절로 따라간다」는 위 주석의 전제가 틀렸다 — 명시 배치된 칸이 먼저 서고 자동 배치는 앞에서부터 빈칸을 채운다. */
.ds .ds-form-grid > .ds-nolabel > :nth-child(2) { grid-row: 3; }

/* ═══════════════════════════════════════════════════════════════════════════════════════════
   29) 목록의 **발치** — 「총 N개」 줄 + 페이징 (2026-08-23 사용자 실주행 지적, 세 앱 공통)
   ═══════════════════════════════════════════════════════════════════════════════════════════
   🎯 대상은 세 앱 목록 **44화면이 똑같이 쓰는 한 덩어리**다:
      `.card-body` 안의 `<div class="d-flex flex-stack flex-wrap pt-10">` +
      그 안의 `[총 N개]` 와 `<ul class="pagination">`.
   🔬 전수 확인(2026-08-23): 페이징이 있는 목록 화면 **44곳 전부** 이 골격이고, 전부 `#kt_content` 에
      `ds` 가 붙어 있다 → **화면을 하나도 고치지 않고 여기 한 곳에서 끝난다.**
      📌 이 앱만 18화면으로 가장 많다.
   🔑 **선택자에 `:has(> .pagination)` 을 넣은 이유** — `.flex-stack` 은 마이페이지·가입·약관·모달에도
      쓰인다(세 앱 30곳 남짓). **페이징을 품은 줄만** 목록의 발치다.
      🔥 이 조건을 빼지 말 것 — 빼는 순간 목록이 아닌 화면에 구분선이 그어진다.
   ⚠️ `.card-body` 를 **후손 선택자**로 둔 이유: 화면마다 이 줄이 `.card-body` 직계일 때도 있고
      한 겹 더 안에 있을 때도 있다. `>` 로 못 박으면 **그 화면들만 조용히 빠진다.**

   ① **총 개수 줄과 목록이 구분되지 않는다** → 위에 선을 긋고 여백을 준다.
      🔑 색은 오늘 합계선·카드 머리선과 **같은 `--ds-line-soft`** 다 — 새 규격이 늘지 않는다.
      🔑 여백은 척도의 `--ds-sp-5`(24px, 「구역과 구역 사이」) — 목록 격자의 세로 gap 보다 작으면
         **항목 사이보다 절 사이가 좁아** 발치가 목록의 마지막 줄로 읽힌다.
      ⚠️ **`!important` 가 필요하다** — 마크업의 `pt-10` 이 Bootstrap 유틸(`padding-top:2.5rem !important`)
         이라 특이도만으로는 못 이긴다. 🔥 **44화면에서 `pt-10` 을 걷어내는 방식으로 고치지 말 것**
         (한 곳이라도 빠지면 그 화면만 40px 로 남고, 아무도 그것을 다시 세지 않는다).
   ═══════════════════════════════════════════════════════════════════════════════════════════ */
.ds .card .card-body .flex-stack:has(> .pagination) {
    border-top: 1px solid var(--ds-line-soft);
    margin-top: var(--ds-sp-5);
    padding-top: var(--ds-sp-5) !important;
    align-items: center;
    row-gap: var(--ds-sp-5);
}

/* ── 29-1) 좁은 화면 — **총 개수는 왼쪽 끝선, 페이징은 가운데** ─────────────────────────────
   🔬 실측(375px): `.flex-stack` 은 `justify-content: space-between` 이라 두 덩어리가 각각 한 줄을
      차지하면서도 **한 줄에 하나씩인데 좌우로 밀려** 총 개수가 목록 카드의 왼쪽 끝선과 어긋났다.
   🔑 **총 개수는 목록의 왼쪽 끝선과 같아야 한다**(카드 안 좌우 끝선은 하나다 — 배치 규칙 ④).
   🔑 **페이징만 가운데**다 — 누르는 칸 묶음이라 한쪽에 붙으면 반대쪽 엄지로는 멀다.
   ⚠️ 이 블록의 폭 기준(767.98px)은 아래 페이징 축약과 **같아야** 한다 — 둘이 갈리면
      「가운데 정렬은 됐는데 두 줄로 접힌 구간」이 생긴다(배치 규칙 ①: 판정 기준을 둘로 쓰지 마라).
      🕘 2026-09-07: 문턱을 **575.98 → 767.98** 로 합쳤다(사장님 확정 「구간 수를 줄인다」).
         🔑 페이징 축약(29-2)은 **이 블록 안**에 있으므로 문턱이 저절로 같이 간다 — 밖으로 빼지 말 것.
   ───────────────────────────────────────────────────────────────────────────────────────── */
@media (max-width: 767.98px) {
    .ds .card .card-body .flex-stack:has(> .pagination) { justify-content: flex-start; }
    .ds .card .card-body .flex-stack:has(> .pagination) > * { width: 100%; display: flex; }
    .ds .card .card-body .flex-stack:has(> .pagination) > *:first-child { justify-content: flex-start; }
    .ds .card .card-body .flex-stack:has(> .pagination) > *:last-child  { justify-content: center; }

    /* ── 29-2) **페이징이 두 줄로 접히는 것을 막는다 — 가운데 번호를 줄인다** ───────────────
       🔬 실측(375px): 한 묶음이 최대 10쪽(`PaginationDto` 의 `pageCount`)이라 이전·다음까지 **12칸**이고
          칸마다 `min-width: var(--ds-touch-min)`(44px, 손가락 크기)이라 **528px** 이 필요한데
          카드 안쪽은 **312~325px** 이다 → `flex-wrap` 이 받아 **두 줄**이 됐다.
          🔑 `flex-wrap` 은 **가로 스크롤을 막는 안전장치라 그대로 둔다** — 여기서 고치는 것은
             「두 줄이 되지 않게 칸 수를 줄이는 것」이다.
       ✅ 남기는 것: `‹`(이전) · **묶음의 첫 쪽** · **지금 쪽** · **묶음의 끝 쪽** · `›`(다음) = 최대 5칸
          + 사이의 「…」 2칸 → 실측 **264px**, 한 줄에 들어간다.
       🔑 **「…」 은 누를 수 없는 칸이라 44px 이 필요 없다** → 절반(`--ds-touch-min` / 2 = 22px)이다.
          🔥 그래야 7칸이 들어간다. `pointer-events: none` 은 짝을 이루는 선언이다.
       🔑 **「…」 은 실제 `<li>` 가 아니라 지금 쪽 칸의 `::before`/`::after`** 다 — 페이징 마크업이
          **세 앱 44화면에 손으로 복제**돼 있어(공용 조각이 없다) 칸을 늘리려면 44곳을 고쳐야 한다.
          🔬 그래서 `<li>` 를 만들지 않고 **CSS 만으로** 끝냈다. 서버(`PaginationDto`)는 손대지 않았다.
       🔑 **숨긴 칸이 실제로 있을 때만 「…」 을 그린다** — 지금 쪽이 첫 쪽 바로 옆(`:nth-child(3)`)이면
          첫 쪽과 지금 쪽 사이에 숨은 것이 없으므로 「…」 을 그리지 않는다(끝 쪽도 대칭).
          🔥 이 조건을 빼면 **아무것도 안 숨겼는데 「…」 이 나오는** 거짓말이 된다.
       🔑 **묶음이 5쪽 이하이면 축약하지 않는다** — `:has(> .page-item:nth-child(8))` 이 그 문지기다
          (자식 = 쪽 수 + 이전 + 다음. 8칸이면 쪽이 6개다). 🔬 5쪽이면 7칸 = 308px 라 그대로 들어간다.
       ⚠️ **넓은 화면은 손대지 않는다** — 768px 이상에서는 12칸(528px)이 본문 폭 안에 들어간다(실측).
       📌 `.previous`/`.next` 는 비활성일 때도 **언제나 그려진다**(44화면 전수 확인) → 자리 셈이 흔들리지 않는다.
       ─────────────────────────────────────────────────────────────────────────────────────── */
    .ds .pagination:has(> .page-item:nth-child(8)) > .page-item:not(.previous):not(.next):not(.active):not(:nth-child(2)):not(:nth-last-child(2)) {
        display: none;
    }
    /* 「…」 이 링크 옆에 서려면 그 칸이 가로 묶음이어야 한다(`.page-link` 가 블록급 flex 라 그냥 두면 위로 올라간다) */
    .ds .pagination:has(> .page-item:nth-child(8)) > .page-item.active { display: flex; align-items: center; }
    .ds .pagination:has(> .page-item:nth-child(8)) > .page-item.active:not(:nth-child(2)):not(:nth-child(3))::before,
    .ds .pagination:has(> .page-item:nth-child(8)) > .page-item.active:not(:nth-last-child(2)):not(:nth-last-child(3))::after {
        content: "…";
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-width: calc(var(--ds-touch-min) / 2);
        color: var(--ds-text-faint);
        pointer-events: none;
    }
}


/* ══════════════════════════════════════════════════════════════════════════════════════
   30) **사유 상자 — 상대가 적어 보낸 말을 그대로 옮겨 적는 자리** (2026-08-27 A7·F18)

   🔬 **왜 만들었나** — 화면이 이 상자를 Metronic 의 `bg-white` 유틸리티로 칠하고 있었다.
      그 유틸리티는 **테마와 무관하게 흰색을 박는데** 글자색은 부모에서 물려받으므로
      다크에서 **흰 바탕 + 흰 글자**가 된다.
      🔬 실측(2026-08-27 · 랜드사 계약 상세 · 다크): `background-color rgb(255,255,255)` ·
         `color rgb(255,255,255)` → **대비 1.000**. 값은 DOM 에 그대로 있고 **색만 틀려서**
         사람 눈에는 **빈 상자**로 보인다(사장님 실주행 지적).
      🕘 같은 모양의 사고가 이미 있었다 — 다크 `.list-group-item` 대비 1.0(9절).
      🔑 **없는 유틸리티가 아니라 「있는데 테마를 모르는」 유틸리티**가 원인이다.
         🔥 그러니 `bg-light`·`bg-body` 같은 다른 유틸리티로 갈아타지 말 것 — 같은 종류다.

   🔑 **표면은 `--ds-surface`** — 라이트에서 `#FFFFFF` 라 **옛 모습과 한 픽셀도 다르지 않고**
      다크에서만 판 색으로 따라간다. 🔥 `bg-white`·`#fff` 로 되돌리지 말 것.
      📌 0절의 「다크용으로 색을 따로 가르지 말 것」에 어긋나지 않는다 — `--ds-surface` 는
         그 원칙이 **예외로 인정한 두 토큰 중 하나**이고, 이 자리가 정확히 그 용도(판의 표면)다.
   🔑 **테두리는 `--ds-panel-edge`**(라이트 투명 · 다크에서만 선) — 다크에서는 상자와 부모가
      둘 다 어두워 경계가 약해지는데 그 자리가 곧 이 토큰이 만들어진 이유다.
      🔑 `border` 가 아니라 **inset 링**이라 상자 크기가 안 변한다(10절과 같은 수법).
   🔑 **줄바꿈은 부품이 쥔다**(`pre-wrap`) — 화면마다 인라인 `style` 로 적던 것을 걷었다.
      ⚠️ 이 상자에 **블록 자식을 넣지 말 것** — `pre-wrap` 은 태그 사이의 공백까지 살려
         **「보이는 빈 줄」**을 만든다(`silent-traps.md` 의 `.invoice-notes` 사고와 같은 함정).
         여기 들어가는 것은 언제나 **글자 하나**(`th:text`)다.

   📌 크기·여백·모서리는 마크업의 유틸리티(`mt-2 p-2 rounded fs-8`)가 그대로 쥔다 —
      이 부품은 **색과 줄바꿈만** 정한다. 옛 모습을 바꾸지 않는 것이 목적이다.
   📌 쓰는 곳: 랜드사 계약 상세(보완 요청·반려·수정 요청·거절·취소 사유 6곳) ·
      여행사 계약 상세(플랫폼 반려 사유 1곳). 관리자 앱에는 아직 쓰는 곳이 없다 —
      🔥 **안 쓴다고 지우지 말 것**(세 앱 공용 파일은 한 벌로 유지한다).
   ══════════════════════════════════════════════════════════════════════════════════════ */
/* 🏷 **누구 말인지 보이게 한다** (2026-09-01 · 항목 56)
   🔬 **문제는 「섞였다」가 아니라 「구분 상자가 안 보였다」**였다 — 마크업에는 이미 이 전용 상자가
      있는데 배경이 `--ds-surface`(카드 배경과 거의 같다) + 아주 옅은 테두리뿐이라, 안내 문구와
      사유 원문이 **한 덩어리로 읽혔다**(여행사 계약 상세 `PLATFORM_REJECTED` 폭별 화면판).
   ✅ 처방 둘 — **왼쪽 색 세로선**(인용문 규격) + **작은 머리말**(`.ds-reason-label`).
   🔴 **머리말 문구는 부품이 정하지 않는다 — 마크업이 넘긴다.** 쓰는 자리마다 다르기 때문이다
      (플랫폼 반려 / 보완 요청 / 수정 요청 / 거절 / 취소). 🔥 여기에 `content` 로 박지 말 것.
   🔑 **세로선은 `border-left` 가 아니라 inset 링**이다 — border 면 상자가 3px 커져
      아래 「크기·여백·모서리는 안 건드린다」가 깨진다(바로 위 `--ds-panel-edge` 링과 같은 수법).
      ⚠️ **두 링을 한 선언에 함께** 적는다(`box-shadow` 는 뒤 선언이 앞을 통째로 덮는다).
   🎨 **색은 「그 사유가 무슨 성격인가」가 정한다** — 기본은 반려(붉은 계열)이고, 고쳐서 다시 낼 수 있는
      보완·수정·취소 요청은 `.is-warn`, 이미 끝난 일의 기록은 `.is-mute` 다.
      🔥 **전부 붉게 두지 말 것** — 「보완 요청」과 「반려」를 색으로 가르는 것이 2026-08-27 F18 의 요지다.
   🚫 **크기·여백·모서리는 그대로 마크업의 유틸리티가 쥔다**(아래 주석 그대로).
   📌 이 앱에는 아직 쓰는 화면이 없다 — 🔥 **안 쓴다고 지우지 말 것**(세 앱 공용 파일은 한 벌로 유지). */
.ds .ds-reason-box {
    --ds-reason-accent: var(--ds-danger);
    background-color: var(--ds-surface);
    color: var(--ds-text-strong);
    box-shadow: inset 3px 0 0 0 var(--ds-reason-accent),
                inset 0 0 0 1px var(--ds-panel-edge);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}
.ds .ds-reason-box.is-warn { --ds-reason-accent: var(--ds-warning); }
.ds .ds-reason-box.is-mute { --ds-reason-accent: var(--ds-text-faint); }
/* 🏷 머리말 — 「관리자 사유」·「여행사 사유」처럼 **누가 쓴 말인지**를 밝힌다.
   🔑 층은 상세 항목 그리드의 **항목명**과 같다(`.8rem`/600/gray-700 + 자간) — 값보다 먼저 눈에
      들어와야 하는 이름표라서다. 🔥 크기를 키우지 말 것(사유 본문보다 커지면 위계가 뒤집힌다). */
.ds .ds-reason-label {
    display: block;
    margin-bottom: var(--ds-sp-1);
    font-size: .8rem;
    font-weight: 600;
    color: var(--ds-text-label);
    letter-spacing: var(--ds-track-field-k);
}

/* ══════════════════════════════════════════════════════════════════════════════════════
   30-1) **항목 표 아래 총액 줄** — `.ds-total-row` (2026-09-01 · 항목 23 규격 · 항목 36 ②)
   ══════════════════════════════════════════════════════════════════════════════════════
   > 사장님 지적(2026-08-31 · 여행사 계약 상세): *"계약항목 테이블 아래에는 총액이 있어야 할 것 같아."*

   🔑 **한 줄에 「왼쪽 = 기준 문구 / 오른쪽 = 이름 + 금액」**이다. 견적 상세의 총액 줄과 같은 모양이고,
      그 화면이 마크업 유틸리티로 그리던 것을 **부품 하나로 모은 것**이다.
   📐 값은 항목 23 에서 확정된 규격 그대로다 — 위 굵은 구분선 **2px** · 위 여백 **12px** · 안쪽 위 **16px**.
      🔑 **색은 토큰(`--ds-primary`)** 이다. 🔥 `rgb(0,158,247)` 같은 생 색을 적지 말 것.
      ⚠️ 여백만은 척도(`--ds-sp-*`)가 아니라 확정 수치를 쓴다 — **사장님이 화면을 보고 정한 값**이라
         척도에 맞춰 반올림하면 그 결정이 바뀐다. 🔥 임의로 척도 값으로 바꾸지 말 것.
   🧾 **왼쪽 칸은 「부가가치세 별도」 자리**다 — **조건 없이 항상** 찍는다(2026-08-31 사장님 결정).
      🔥 **`!zeroRated` 조건을 되살리지 말 것.**
   📌 이 앱에는 아직 쓰는 화면이 없다 — 🔥 **안 쓴다고 지우지 말 것**(세 앱 공용 파일은 한 벌로 유지). */
.ds-total-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--ds-sp-3);
    margin-top: 12px;
    padding-top: 16px;
    border-top: 2px solid var(--ds-primary);
}
.ds-total-row > .ds-total-end {
    display: flex;
    align-items: baseline;
    gap: var(--ds-sp-3);
    /* 💰 **이름과 금액은 각자 한 덩어리다 — 좁으면 둘 사이에서만 접힌다** (2026-09-18 여행사 2회차 C8-02)
       🔬 실측(여행사 입금 등록 · 992·1200px): 「실제 이체할 금액」이 「실제 이체할」/「금액」,
          값 「0 KRW」가 「0」/「KRW」 두 줄이었다(영문 「Amount to」/「Transfer」).
       🔑 자식을 `nowrap` 으로 묶고 묶음만 `wrap` 한다 — 넓은 화면에서는 한 줄 그대로이고,
          정말 좁으면(360px) 이름 아래로 금액이 **통째로** 내려간다(오른쪽 정렬 유지).
       🔥 `.ds-total-row` 자체에 `flex-wrap` 을 주지 말 것 — 왼쪽 기준 문구가 max-content 로 잡혀
          넓은 화면에서도 오른쪽 묶음이 제 줄로 떨어진다. */
    flex-wrap: wrap;
    justify-content: flex-end;
}
.ds-total-row > .ds-total-end > * { white-space: nowrap; }

/* 💬 **대화 서랍 방 목록의 날짜는 한 줄이다** (2026-09-18 여행사 2회차 I1-30)
   🔬 실측(360px · 서랍 300px): 오른쪽 날짜 칸이 45px 로 눌려 「2026-08-」/「25」 두 줄로 끊겼다(768 이상은 한 줄).
   🔑 날짜 칸은 줄지 않고(`flex-shrink:0` + `nowrap`), 대신 왼쪽(상대 이름·마지막 글)이 `min-width:0` 으로 물러난다.
   📌 방 한 줄의 마크업은 `common/Message.js` 가 그린다(`.msg-room-item` = 왼쪽 묶음 + 오른쪽 날짜·건수 묶음).
      서랍은 `.ds` 밖이라 스코프를 붙이지 않는다. */
.msg-room-item > .d-flex:first-child { min-width: 0; }
.msg-room-item > .flex-column:last-child { flex-shrink: 0; }
.msg-room-item > .flex-column:last-child > span { white-space: nowrap; }
/* 🧾 **「부가가치세 별도」는 화면 어디서나 한 크기다 — `.95rem`(root 16 에서 15.2px)**
      (2026-09-06 사장님 결정: *한 화면에 두 크기로 나오니 큰 쪽으로 통일한다*).
   🔬 **고치기 전 실측**(`getComputedStyle`, 1440px): 랜드사 견적 상세 총액 줄 왼쪽 끝 **15.2px**(`fs-7`) ·
      같은 화면 대금 지급 규정 표 아래 **13.6px**(`fs-8 text-gray-600`) ·
      여행사 계약 상세 총액 줄 **12.8px**(`.ds-total-basis`). 같은 성격의 자리에서 **세 크기**였다.
   🔑 **화면마다 유틸리티 조합(`fs-7` / `fs-8 text-gray-600`)을 적지 말고 이 클래스를 쓴다** —
      조합이 화면마다 갈린 것이 원인이다. 🔥 여기 말고 다른 곳에서 이 문구의 크기를 정하지 말 것.
   🔑 **대상은 「독립 안내 줄」**이다 — 총액 줄의 왼쪽 끝 · 금액 표 아래 한 줄 · 금액 블록의 안내 행.
      ⚠️ **대상이 아닌 자리 둘**(옆에 선 것이 크기를 정한다):
      ① 발행 문서 절 제목 줄의 주기 `.invoice-label-note`(12px) — 절 제목(`.invoice-label` 0.75rem)과
         PDF `.label-note` 와 **한 벌**이다. 키우면 주기가 제목보다 커진다.
      ② 필드 값 아래 보조값 `.ds-field-sub`(12.8px) — **같은 필드의 「영세율」 안내와 나란히** 선다.
         한쪽만 키우면 그 자리에서 새 어긋남이 생긴다.
   🔥 **세 앱 같은 값이다** — 한 앱만 고치면 갈린다. */
.ds-vat-note,
.ds-total-row > .ds-total-basis {
    display: block;
    font-size: .95rem;
    font-weight: var(--ds-weight-body);
    line-height: 1.4;
    color: var(--ds-text-faint);
}

/* ══════════════════════════════════════════════════════════════════════════════════════
   31) **두 칸 줄의 전환 문턱을 「뷰포트」에서 「본문 폭」으로** (2026-08-27 · R11 / D-3 / E24)
   ══════════════════════════════════════════════════════════════════════════════════════
   🔬 **무엇이 문제였나**(실측 2026-08-27, 관리자 메인):
      | 뷰포트 | 375 | 430 | 768 | 900 | **1100** | 1440 | 1920 |
      | 줄(`.row`) 폭 | 351 | 406 | 693 | 825 | **760** | 1100 | 1580 |
      | 열 수(전) | 1 | 1 | 1 | 1 | **1** | 2 | 2 |
      **900(줄 폭 825) 이 1열인데 그보다 좁은 1100(760)도 1열, 1440(1100)부터 2열**이었다 —
      즉 **줄이 넓어지는 순서와 열이 늘어나는 순서가 어긋나** 있었다.
      범인은 마크업의 Bootstrap `col-xl-*` — 그 문턱이 **뷰포트 1200px** 이라
      **사이드바가 먹은 폭을 볼 수 없다.** 1100px 에서는 사이드바가 아이콘만 남아도
      본문이 820px 인데 뷰포트는 1100 이라 「아직 xl 이 아니다」가 된다.
   🔑 **처방** — 문턱을 **그 줄 자신의 폭**으로 바꾼다. 줄(`.row`)을 컨테이너로 만들고
      **컨테이너 쿼리 한 벌**이 열 수를 정한다. `col-xl-*` 은 **이름표로만 남고**
      폭은 여기서만 나온다(아래 `max-width` 블록이 뷰포트 규칙을 특이도로 이긴다).
      🔥 **`col-xl-*` 을 그대로 두고 이 블록만 지우지 말 것** — 그러면 1100 구간이 그대로 돌아온다.
      🔥 **반대로 `col-xl-*` 을 `col-lg-*` 로 바꾸지도 말 것** — 그건 문턱을 뷰포트에서
         뷰포트로 옮기는 것이라 **사이드바를 펼치면 다시 어긋난다**(배치 규칙 ①).
   🔑 **문턱 `46rem`(기본 글자에서 736px)을 고른 근거는 위 실측표다** — 693(768px 뷰포트)은 1열로
      남기고 760(1100)·825(900)는 2열이 되어야 하므로 고를 수 있는 값은 **694~760** 뿐이고,
      그 한가운데가 736 이다. 🔥 **여기서 「본문 폭」(820)으로 재지 말 것** — 재는 것은
      `container-type` 을 건 **그 줄의 폭**이고, 줄은 `.row` 의 음수 여백·카드 여백만큼 더 좁다
      (1100px 에서 본문 820 ↔ 줄 760). 실제로 처음에 본문 폭으로 50rem 을 잡았다가 1100 이 안 고쳐졌다.
      ⚠️ **`rem` 이라 글자 크기 3단계를 따라 커진다** — `xlarge`(root 20px)에서는 문턱이 920px 이 되어
         760·825 가 다시 1열이 된다. **그것이 의도한 동작이다**(큰 글자에서 한 단계 먼저 풀린다).
      🔥 px 로 박지 말 것.
   🔴 **이미 중간 단계를 가진 칸은 대상이 아니다** — `col-sm-*`·`col-md-*`·`col-lg-*` 뿐 아니라
      **`col-1`~`col-11`**(모든 폭에 걸리는 고정 열)도 뺀다. 그런 줄은 사다리가 이미 있어
      1100 구간에 구멍이 없다. 🔬 **실제로 밟았다**: 통계 지표 줄이 `col-6 col-xl-3` 인데
      `col-6` 을 안 뺐더니 900px 에서 **2열이던 지표 카드가 4열(190px)로 쪼그라들어** 값이 넘쳤다.
      ⚠️ **`col-12` 는 뺄 것이 아니다** — 그건 「바닥은 전폭」이라는 뜻이라 우리가 덮는 것이 맞다.
   ⚠️ 컨테이너가 된 요소는 **절대 위치 자식의 기준 상자**가 된다 → 대상을 `#kt_content_container`
      같은 큰 상자가 아니라 **그 줄 하나**로 좁혔다(모달·드로워가 딸려 들어가지 않게).
   🔑 폭 값은 **`--ds-xl-w` 한 곳**에 담아 두고 문턱 블록에서 **한 줄로** 쓴다 —
      제외 조건을 일곱 번 되풀이하면 한 곳만 고쳐져 갈린다.
   ══════════════════════════════════════════════════════════════════════════════════════ */
.ds .row:has(> [class*="col-xl-"]) { container-type: inline-size; }

.ds .row > .col-xl-3 { --ds-xl-w: 25%; }
.ds .row > .col-xl-4 { --ds-xl-w: 33.333333%; }
.ds .row > .col-xl-5 { --ds-xl-w: 41.666667%; }
.ds .row > .col-xl-6 { --ds-xl-w: 50%; }
.ds .row > .col-xl-7 { --ds-xl-w: 58.333333%; }
.ds .row > .col-xl-8 { --ds-xl-w: 66.666667%; }
.ds .row > .col-xl-9 { --ds-xl-w: 75%; }

@container (max-width: 45.99rem) {
    .ds .row > [class*="col-xl-"]:not(:is([class*="col-sm-"], [class*="col-md-"], [class*="col-lg-"],
        .col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11)) {
        width: 100%;
    }
}
@container (min-width: 46rem) {
    .ds .row > [class*="col-xl-"]:not(:is([class*="col-sm-"], [class*="col-md-"], [class*="col-lg-"],
        .col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11)) {
        width: var(--ds-xl-w);
    }
}

/* ══════════════════════════════════════════════════════════════════════════════════════
   32) **대화상자 안의 검색 표** — 마지막 열이 벽에 붙지 않게 (2026-08-27 · M-1 / M-2)
   ══════════════════════════════════════════════════════════════════════════════════════
   🔬 **무엇이 문제였나** — 「상품 등록」 대화상자의 템플릿 검색 표에서 **마지막 열(기간)이
      대화상자 오른쪽 끝에 딱 붙었다**(375·430px). 범인은 Metronic 의 `gs-0` 이다 —
      그 유틸리티가 **첫 칸의 왼쪽 여백과 마지막 칸의 오른쪽 여백을 0 으로** 만든다.
      🔑 표를 카드 끝까지 붙이려고 일부러 쓰는 유틸리티인데, **대화상자 안에서는 벽이 바로 옆**이라
         값이 테두리에 닿는다. 같은 이유로 「관리명」이 2~3줄로 접혀 행 높이가 제각각이었다
         (기간 열이 자기 폭을 크게 잡아 이름 열을 밀었다).
   🔑 **처방 둘** — ① 마지막 열에 오른쪽 여백을 되살린다(문구 규칙이 정한 수법과 같다:
      *"Metronic 의 `.table td:last-child{padding-right:0}` 때문에 `padding-right` 를 되살린다"*)
      ② 마지막 열을 **내용 폭으로 줄인다**(`width:1%` + `nowrap`) → 남는 폭이 이름 열로 간다.
   🔑 **선택자는 성격으로** 쓴다 — 「대화상자 본문 안의 `gs-0` 표」다. 화면 이름을 적지 않는다.
      🔬 대상 실측(2026-08-27, 세 앱 전수): 대화상자 안의 표는 **5개**이고 **다섯 다 같은 클래스 조합**이다
         (여행사·랜드사 `kt_modal_dispute_target` · 랜드사 `kt_modal_create_product` ·
          `kt_modal_search_template` · `kt_modal_search_item`). 관리자 앱에는 0개다 — 🔥 그래도 지우지 말 것.
   ⚠️ **`.ds` 로 스코프하지 않는다** — 대화상자는 `#kt_content` 밖에 놓일 수 있다.
   ══════════════════════════════════════════════════════════════════════════════════════ */
/* 🔴 **행 높이는 고르게 — 긴 값은 한 줄 말줄임**(M-2). 이름이 길어 두세 줄로 접히면
      행마다 높이가 달라 목록이 들쭉날쭉해진다(실측 375px: **55 / 43 / 55 / 55 / 43px**).
   🔑 `table-layout: fixed` 라야 열 폭이 **내용이 아니라 규칙**으로 정해져 말줄임이 걸린다.
      고정 배치에서는 `width:1%` 가 말 그대로 1% 라 마지막 열에 **실제 폭**을 준다(`5.5rem`,
      「5N / 6D」가 들어가는 최소 폭). 🔬 적용 후 다섯 행 전부 **43px** 로 고름.
   ⚠️ 첫 칸(체크박스)은 말줄임 대상이 아니다 — 넘칠 글자가 없다. */
.modal-body table.gs-0 { table-layout: fixed; }
.modal-body table.gs-0 > tbody > tr > td:not(:first-child) {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.modal-body table.gs-0 > thead > tr > th:last-child,
.modal-body table.gs-0 > tbody > tr > td:last-child {
    padding-right: var(--ds-sp-3) !important;
    white-space: nowrap;
    width: 5.5rem;
}
/* 🔴 **마지막 열이 「긴 글」이면 머리 칸에 `ds-col-fill` 을 단다** (2026-09-19 개인 랜드사 실주행 결함 4).
      🔬 위 `5.5rem` 은 **마지막 열이 짧은 값**(기간 「5N / 6D」·선택 버튼)인 표를 전제로 했다. 랜드사 템플릿 편집의
         「아이템 검색」 표는 마지막 열이 **아이템명**이라 5.5rem 에 갇혀 「[개인 …」처럼 잘렸다.
      🔑 그 열이 **남는 폭을 다 가져간다**(`width:auto`) — 고정 배치라 첫 줄(머리)의 폭이 열을 정한다.
         짧은 앞 열에는 마크업이 폭을 준다(`w-125px` 류). 말줄임·한 줄은 **그대로**다(행 높이를 고르게 — M-2).
      🔑 성격으로 가른다 — 관리자 앱에는 대화상자 안 표가 0개지만 세 앱 한 벌이라 함께 둔다. */
.modal-body table.gs-0 > thead > tr > th.ds-col-fill:last-child {
    width: auto;
}

/* ══════════════════════════════════════════════════════════════════════════════════════
   31) 🖼 **이미지를 못 불러왔을 때의 자리표시 — `.ds-img-error`** (2026-09-08 사장님 지시)
   🔬 그전에는 파일이 없으면 **브라우저 기본 「깨진 이미지」 아이콘**만 떠서, 보는 사람이
      「사진이 원래 없다」와 「가리키는 파일을 잃어버렸다」를 **구별할 수 없었다.**
      실물 사례가 있다 — 아이템 「도쿄 파크하얏트 호텔」의 첨부 6장은 DB 행만 있고 파일이 없다.
   🔑 **마크업 규약**: `<img onerror="…">` **바로 다음 형제**에 이 자리표시를 둔다. `onerror` 는
      ① `this.onerror = null` 로 **자기를 먼저 풀고** ② 자기 자신을 인라인 `display:none` 으로 감춘 뒤
      ③ 다음 형제에 `is-shown` 을 붙인다.
   🔴 **`onerror` 안에서 다른 이미지 주소를 넣지 말 것** — 그 대체 이미지도 실패하면 끝없이 돈다.
      여기서는 **넣을 이미지가 아예 없으므로** 무한 루프가 생길 자리 자체가 없다.
   🔑 슬라이드쇼 안에서는 자리표시도 `preview-slideshow-img` 와 **같은 `data-index`** 를 함께 받아
      기존 `.active` 토글에 그대로 올라탄다 → **「지금 보고 있는 장」일 때만** 나온다.
      🔥 아래 세 줄을 지우지 말 것 — 없으면 여섯 장이 다 실패했을 때 자리표시가 **여섯 개 한꺼번에** 쌓인다.
   ⚠️ 특이도를 일부러 올려 두었다 — `.preview-slideshow-img.active`(0,2,0)를 이기려고 `:not()` 을 섞어
      (0,3,0)·(0,4,0)으로 만들었다. **파일 로드 순서에 기대지 않기 위해서다.**
   ══════════════════════════════════════════════════════════════════════════════════════ */
.ds-img-error {
    display: none;
    box-sizing: border-box;
    width: 100%;
    min-height: 7.5rem;
    padding: var(--ds-sp-4);
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--ds-sp-2);
    text-align: center;
    border: 1px dashed var(--ds-line-strong);
    border-radius: .375rem;
    background: var(--ds-fill);
    color: var(--ds-text-faint);
    font-size: .8125rem;
    line-height: 1.4;
}
.ds-img-error > i {
    font-size: 1.5rem;
    line-height: 1;
}
.ds-img-error.is-shown { display: flex; }
.preview-slideshow-img.ds-img-error:not(.is-shown) { display: none; }
.preview-slideshow-img.ds-img-error.is-shown:not(.active) { display: none; }
.preview-slideshow-img.ds-img-error.is-shown.active { display: flex; }


/* ══ 21) 위쪽 고정 안내 줄 — `.ds-topbars` > `.ds-topbar` (2026-09-19 사장님 결정 · 세 앱 한 벌) ══

   쓰는 곳 둘 — `common.js` 가 `Common.TopBars.add()` 로 붙인다:
     · 세션이 끊긴 뒤 「이 화면에 남기」(`Common.SessionLost`) — 사유 + 복사 안내 + 로그인 버튼. **닫기 없음.**
     · 랜드사·여행사 서비스 중지(`Common.ServicePause`) — 버튼 없음. 정상 응답이 오면 저절로 걷힌다.
   🔑 **줄들은 `body` 끝의 `#dsTopBars` 한 상자에 쌓인다**(`position: fixed`) — `Common.refresh()`/`DynamicNav` 가
      본문·헤더를 갈아 끼워도 남고, 둘이 함께 떠도 겹치지 않고 위아래로 선다.
   🔑 **가리지 않고 밀어낸다** (2026-09-19 크롬 실측 지적 — 관리자 `/main/main` 에서 헤더 위를 덮었다).
      상자 높이를 `ResizeObserver` 로 **실제로 재어** `--ds-topbar-h`(html 인라인)에 싣고, 떠 있는 동안 `body.ds-has-topbar`:
      본문은 `body` 윗여백으로, 고정 헤더(`#kt_header`)·고정 사이드바(`#kt_aside`)·상세 요약 띠(sticky)는 `top` 으로 그만큼 내려간다.
      🔥 **높이를 px 로 박지 말 것** — 글자 크기 3단계·폰 폭에서 문장이 두 줄이 되면 높이가 바뀐다(재는 이유다).
      📌 화면 CSS(`menuLevel/*.css`)의 sticky `top: calc(65px + …)` 몇 곳은 이 변수를 모른다 — 줄이 떠 있는 동안
         그 높이만큼 머리 밑으로 들어간다(안내 줄은 드물게 잠깐 뜬다 · 가려지는 것은 스크롤 중의 여백뿐).
   🔑 `z-index` 110 — 고정 헤더(100)·오버레이(105~108) 위, SweetAlert2(1060) 아래.
   🔑 색은 **경고 토큰**(19절 `--ds-warning-text` · `--ds-warning-soft`) — 글자에 `--bs-warning` 을 쓰지 않는 이유는 19절. */
:root { --ds-topbar-h: 0px; }
.ds-topbars {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 110;
    display: flex;
    flex-direction: column;
}
body.ds-has-topbar { padding-top: var(--ds-topbar-h); }
body.ds-has-topbar #kt_header,
body.ds-has-topbar #kt_aside { top: var(--ds-topbar-h); }
body.ds-has-topbar :is(.ds, .quote-content) .ds-detail-summary { top: calc(65px + var(--ds-topbar-h)); }
.ds-topbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: var(--ds-sp-2) var(--ds-sp-4);
    padding: var(--ds-sp-2) var(--ds-sp-4);
    background: var(--ds-warning-soft);
    border-bottom: 1px solid var(--ds-warning);
    color: var(--ds-warning-text);
    font-size: var(--ds-font-body);
    font-weight: 600;
    line-height: 1.5;
}
.ds-topbar > .btn { flex: 0 0 auto; }
