        html { overflow-x: hidden; }
        /* ⚠ body는 hidden이 아니라 clip이다. hidden은 body를 **스크롤 컨테이너로 만들어 하위의
           position:sticky를 전부 무력화**한다 — 실제로 이것 때문에 헤더의 `sticky top-0`가 동작하지
           않고 있었다(실측: 1200px 스크롤 시 헤더 top이 -1200px로 밀려남). clip은 같은 클리핑을
           하면서 스크롤 컨테이너를 만들지 않아 sticky가 살아난다(실측: overflow-y가 auto가 아닌
           visible로 유지됨). 앞줄 hidden은 clip 미지원 구버전(Safari 15 이하)용 폴백 — 그 경우
           지금까지와 똑같이 동작한다(가로 클리핑 유지, sticky 미동작). 그냥 빼면 안 된다:
           매물 탭 툴바 버튼 행이 360px에서 578px까지 삐져나가 실제 가로 스크롤이 생긴다. */
        body { overflow-x: hidden; overflow-x: clip; }
        /* 모달 오픈 시 배경 스크롤 잠금 (js/18-delegation.js가 열림 상태를 감시해 토글) */
        html { scrollbar-gutter: stable; }
        html.modal-open, html.modal-open body { overflow: hidden; }
        ::-webkit-scrollbar { width: 8px; height: 8px; }
        ::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; }
        .dark ::-webkit-scrollbar-track { background: #1e293b; }
        ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
        .dark ::-webkit-scrollbar-thumb { background: #475569; }
        .option-checkbox:checked + label { background-color: #3b82f6; color: white; border-color: #3b82f6; }
        .dark .option-checkbox:checked + label { background-color: #2563eb; border-color: #2563eb; }
        .transition-all { transition: all 0.2s ease-in-out; }
        #connectionStatus.online { color: #10b981; }
        #connectionStatus.offline { color: #ef4444; }
        .row-animation { animation: fadeIn 0.3s ease-out; }
        @keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
        .dash-filter-active { outline: 2px solid #3b82f6; outline-offset: -2px; background-color: rgba(59, 130, 246, 0.05); }
        .dark .dash-filter-active { background-color: rgba(59, 130, 246, 0.1); }
        .search-hl { background-color: #fef08a; color: #1e293b; padding: 0 2px; border-radius: 2px; font-weight: 900; }
        .dark .search-hl { background-color: #fde047; color: #0f172a; }
        /* 탭 전환 */
        .tab-page { display: none; }
        .tab-page.active { display: block; }
        /* 고객 카드 호버 */
        .customer-card { transition: box-shadow 0.15s, transform 0.15s; }
        .customer-card:hover { box-shadow: 0 4px 20px rgba(59,130,246,0.13); transform: translateY(-1px); }
        /* 공유 선택 체크박스 행 강조 */
        .share-selected { background-color: rgba(59,130,246,0.06); }
        .dark .share-selected { background-color: rgba(59,130,246,0.12); }
        /* 플로팅 공유 바 */
        #shareFloatBar { transition: all 0.25s cubic-bezier(.4,0,.2,1); }
        /* 토스트 */
        #toastMsg { transition: opacity 0.3s; }
        /* 모바일 입력 자동확대 방지 (iOS Safari는 16px 미만 입력란 포커스 시 확대) */
        @media (max-width: 639.98px) {
            input, select, textarea { font-size: 16px; }
        }
        /* [LIST-FILTER-OVERLAP-SUBLG] 머리말(<header id="appHeader">)의 실제 높이. js/15-app.js initHeaderHeightVar()가
           ResizeObserver로 border-box 높이(safe-area padding·border 포함)를 px로 덮어쓴다. 아래 값은 JS 실행 전·실패 시 폴백.
           lg 미만에서 머리말 높이가 97.5(세로 폰)·113.5(태블릿)·69/73(가로)·+59(모바일 검색 바)로 갈려 고정 px로는 못 맞춘다.
           ⚠ lg↑는 이 변수를 쓰지 않는다 — 고정 클래스 lg:top-[124px…]·min-[1800px]:top-[76px…](smoke [106]). */
        :root { --hdr-h: calc(98px + env(safe-area-inset-top)); }
        @media (max-width: 1023.98px) {
            /* -1px: 머리말 border-b 밑으로 1px 겹쳐 소수점 높이에서 틈이 안 생기게(lg↑ 124=125-1과 같은 규약).
               .sticky까지 묶어 특이도(0,2,0)로 Tailwind top-16(0,1,0)을 로드 순서와 무관하게 이긴다. */
            .sticky.hdr-sticky { top: calc(var(--hdr-h) - 1px); }
        }
        /* 계약·매물·고객·수입지출 폼 헤더: 모바일(lg 미만)에서만 페이지 헤더 바로 아래에 고정한다.
           폼 본문이 길어(계약 2,155px·매물 1,116px 등) 헤더가 안 붙어 있으면 접으려고 맨 위까지 올라와야 했다.
           머리말 높이는 --hdr-h(위 정의 참조, safe-area 포함 실측)를 쓴다 — 예전 98px 고정값은 태블릿·가로 폰에서 틀렸다.
           배경을 inherit으로 두면 패널의 bg-white/dark:bg-slate-800를 그대로 따라가 다크모드가 자동 처리된다.
           좌우 음수 마진은 패널 안쪽 여백(p-3 / sm:p-4)까지 덮어 본문이 그 틈으로 비쳐 보이는 것을 막는다. */
        @media (max-width: 1023.98px) {
            #customerFormHeader, #cashFormHeader,
            #contractFormHeader, #propFormHeader {
                position: sticky;
                top: calc(var(--hdr-h) - 1px);
                z-index: 10;
                background: inherit;
                margin-left: -0.75rem; margin-right: -0.75rem;
                padding: 0.375rem 0.75rem;
            }
            /* #cashFormView(목록⇄폼 전환 래퍼, 2026-09-18)가 패널과 #cashbookModal 사이에 끼었다. 이 줄이 없으면
               체인이 여기서 transparent로 끊겨 수입지출 sticky 헤더가 다시 투명해진다. */
            #cashFormView { background: inherit; }
            /* #cashFormHeader만 패널의 손자다(부모 #cashbookModal은 배경이 없다). background:inherit은
               "부모의 계산값"을 물려받으므로 이 줄이 없으면 수입지출 헤더만 투명해져 본문이 비친다. */
            #cashbookModal { background: inherit; }
        }
        @media (min-width: 640px) and (max-width: 1023.98px) {
            /* 640px 미만에서는 두 패널 다 p-3(0.75rem)이라 위 규칙으로 충분하지만, sm 이상에서는
               패널 여백이 서로 다르다 — 계약 폼은 sm:p-4(1rem), 매물 폼은 sm:p-5(1.25rem).
               값을 틀리면 그 차이만큼 본문이 헤더 옆 틈으로 비쳐 보인다. */
            #contractFormHeader { margin-left: -1rem; margin-right: -1rem; padding-left: 1rem; padding-right: 1rem; }
            #propFormHeader { margin-left: -1.25rem; margin-right: -1.25rem; padding-left: 1.25rem; padding-right: 1.25rem; }
            /* 고객·수입지출 패널도 sm:p-5(1.25rem) */
            #customerFormHeader, #cashFormHeader { margin-left: -1.25rem; margin-right: -1.25rem; padding-left: 1.25rem; padding-right: 1.25rem; }
        }
        /* 모달 상자 높이 상한: dvh는 iOS 사파리 툴바를 뺀 실제 가시 높이. 미지원 브라우저는 앞줄 vh로 폴백 */
        .modal-fit-h { max-height: calc(100vh - 2rem); max-height: calc(100dvh - 2rem); }
        /* ── 매물 목록 모바일 압축 (2026-09-13, 2라운드 b) ─────────────────────────
           360px 기준 1행 213px → 94px(60건 20.0화면 → 8.8화면). PC(lg↑)는 현행 유지.
           ⚠ 여기서 담당하는 것은 "치수·여백"뿐이다. 보임/숨김 분기는 js/04-property.js 행
              템플릿의 Tailwind 접두사(sm:hidden / hidden sm:block / lg:hidden / hidden lg:flex)가
              담당한다. 이 분업 덕에 신규 Tailwind 클래스가 0종이라 npm run build:css가 필요 없다
              (sm:py-5·sm:w-[72px]·w-[44px]·lg:w-[140px]는 css/app.css에 없음 — 실물 대조 확인).
           ⚠ 선택자를 #tableBody/#tableContainer로 한정한 이유는 특이도(1,0,1↑)로 Tailwind
              유틸리티(0,1,0)를 로드 순서와 무관하게 이기기 위해서다. 이 한정을 풀지 말 것. */
        @media (max-width: 639.98px) {
            /* 행 세로 여백 py-5(1.25rem) → py-3(0.75rem) */
            #tableBody > tr > td { padding-top: 0.75rem; padding-bottom: 0.75rem; }
            /* 상태 배지 w-[72px]/px-2 py-1.5/text-[11px] → 모바일 축소 */
            #tableBody button[data-action="toggleStatus"] {
                width: 54px; padding: 2px 6px; font-size: 10px;
            }
        }
        @media (max-width: 1023.98px) {
            /* 관리 열: 아이콘 8개가 빠지고 「⋯」 하나만 남으므로 88/140px → 44px.
               table-fixed라 열 폭은 첫 행(thead th)이 결정한다 — <th> 마크업은 건드리지 않는다. */
            #tableContainer thead th:last-child { width: 44px; }
        }
        /* PDF 인쇄 */
        @media print {
            body > *:not(#pdfPrintArea) { display: none !important; }
            #pdfPrintArea { display: block !important; }
            body { min-height: 0 !important; height: auto !important; background: white !important; }
            /* beforeprint 리스너가 modal-open을 떼지만, 혹시 못 떼는 인쇄 경로에 대비한 보조 규칙 */
            html.modal-open, html.modal-open body { overflow: visible !important; }
        }
        /* [FONT-SCALE-DESKTOP] 통계 차트 SVG 텍스트는 xl에서만 확대한다.
           ⚠ 크기 리터럴은 js/13-stats.js의 font-size="N" 속성값이다 —
              그쪽 숫자를 바꾸면 이 선택자가 조용히 안 맞게 되므로 반드시 함께 고칠 것.
              (렌더 후 viewBox = clientWidth라 스케일이 1.0이어서 여기 px가 곧 화면 px다.)
           ⚠ 이 블록도 「PDF 인쇄」 블록 뒤에 둔다 — 앞(매물 압축 블록과 PDF 인쇄 사이)에 두면 smoke
              [93-C4]가 그 구간의 규칙을 매물 규칙으로 세어 실패한다(바로 아래 계약 거래유형 칩 블록의
              경고와 같은 함정, 2026-09-20 실제로 걸렸다가 이 자리로 옮김). */
        @media (min-width: 1280px) {
            #chartMonthly text[font-size="10"],
            #chartStatus text[font-size="10"],
            #chartCashCompare text[font-size="10"] { font-size: 12px; }

            #chartMonthly text[font-size="11"],
            #chartStatus text[font-size="11"],
            #chartType text[font-size="11"] { font-size: 13px; }

            #chartStatus text[font-size="12"] { font-size: 14px; }
        }
        /* 계약 탭 거래유형 칩 줄(모바일, lg 미만) — 360px에 「총 N건」+칩 5개를 넣는다.
           실측(2026-09-13, 콘텐츠 폭 302px): 현행 px 12/gap 8은 N=0·24·999에서 13·20·27px 넘침.
           px 8/gap 6은 N=999에서 23px, N=1234에서 16px 여유. 그래도 넘치는 폰 폰트·5자리 건수는
           flex-wrap으로 두 줄 접힘(가로 스크롤 대신).
           ⚠ 치수를 Tailwind 클래스로 옮기지 말 것 — setContractFilter·resetContractFilters(08-contract.js)가
              칩 className을 문자열로 통째 재대입하므로 HTML 클래스는 첫 탭에 날아간다. 이 규칙은 재대입 뒤에도
              남는 첫 토큰 `contract-filter-btn`에 걸어 구조적으로 면역이다. 특이도(1,1,0)로 유틸리티(0,1,0)를
              로드 순서와 무관하게 이기려고 id로 한정했다 — 한정을 풀지 말 것. PC(lg↑)는 적용 밖.
           ⚠ 이 블록은 「PDF 인쇄」 블록 뒤에 둔다 — 앞(매물 압축 블록과 PDF 인쇄 사이)에 두면 smoke [93-C4]가
              그 구간의 규칙을 매물 규칙으로 세어 실패한다. id 한정 특이도라 위치를 옮겨도 적용 결과는 같다. */
        @media (max-width: 1023.98px) {
            #contractTransRow { gap: 0.375rem; flex-wrap: wrap; }
            #contractTransRow > .contract-filter-btn { padding-left: 0.5rem; padding-right: 0.5rem; }
        }
