﻿/* properties_grid.css — 매물(dsv2-properties) 화면 전용 잔류 스타일.
   정본 분리(2026-07-28, 센서스 ② 후속): --db-* 토큰→db_tokens / 공용 db-* 컴포넌트→db_components /
   앱 크롬·ERP 페이지셸→app_chrome / 고객 화면 조각→clients_detail / 전역 Tabulator 스킨→grid_skin.
   위 정본 파일들은 로드 순서상 항상 이 파일보다 먼저 온다.

   ⚠️ !important 30 → 9 (2026-07-29). 21개를 하나씩 빼보며 세 로드 구성(앱 / register_window /
      consult_window)에서 계산값을 대조해, 빼도 아무것도 안 변하는 것만 지웠다.
      걷어낼 수 있었던 근거: 이 파일이 grid_skin 보다 **뒤에 로드**되고, grid_skin 의 경쟁 선언들이
      normal 로 내려갔기 때문(grid_skin PR #69). 즉 **로드 순서가 이 파일의 승리 근거**다 —
      layouts/application·embed_modal·register_window·consult_window 넷 다 grid_skin → properties_grid
      순서이고, 이걸 뒤집으면 이 파일 규칙들이 통째로 안 먹는다.

   ⚠️ 남은 9개는 전부 `::-webkit-scrollbar*` 의사요소에 붙어 있다 — **계산값으로 검증이 불가능해
      손대지 않았다**(추측으로 지울 자리가 아니다). 이 스크롤바 동작은 2026-07-21~22 사이 3차에
      걸친 시행착오가 있었고(아래 우패널 주석 참조), scrollbar-width 가 auto 일 때만
      ::-webkit-scrollbar 커스터마이징이 반영된다는 실측이 그 결론이다. */

/* Tabulator list 드롭다운 + 우클릭 메뉴 CSS는 공용 컴포넌트로 분리 →
   app/assets/stylesheets/shared_tabulator_popup.css (정본 1곳). 여기엔 두지 않는다. */

/* 메모·비고·특이사항(c-note-cell) 편집 시 브라우저 기본 파란 아웃라인 제거 */
.c-note-cell textarea,
.c-note-cell input,
.c-note-cell textarea:focus,
.c-note-cell input:focus {
  outline: none;
  box-shadow: none;
}

/* 공용 체크박스는 shared_checkbox.css(.db-checkbox = label+SVG 토글, shared/_db_checkbox) 로 이관. 기존 native accent 방식(미사용)은 삭제. */

/* ─────────────────────────────────────────────────────────────────────────
   그리드 셀 버튼 룩 — **행마다 똑같은 픽셀은 CSS 가 그린다**.

   왜 이 규칙이 생겼나 (2026-08-11 실측, grid_data.json 2,634행 / 5,134,853 B):
     sheet_btn      1,601 KB  31.9%   623 B/행
     ledger_btn     1,185 KB  23.6%   461 B/행
     consult_btn      485 KB   9.7%   189 B/행
     request_status   460 KB   9.2%   179 B/행
     ────────────────────────────────
     버튼 HTML 4종 = 3,731 KB = 페이로드의 **74.4%**. 진짜 데이터는 약 25%뿐이었다.
   이 바이트는 전 행이 문자 단위로 동일하고 document_id 하나만 달랐다. 서버가 2,634번
   다시 실어 보낼 이유가 없다 — CSS 는 한 번 받고 캐시된다(자산 max-age=31556952 실측).

   #238(부동산정보 항목 lazy)이 같은 판정의 1차였고, 이건 그 2차다.
   차이: #238 은 항목을 클릭 시점에 JS 가 만들지만, 여기 4종은 **JS 를 안 거친다** —
   서버가 계속 마크업 주인이고 픽셀만 CSS 로 옮긴다. 그래서 Tabulator·Stimulus 계약이
   하나도 안 바뀌고(formatter:"html" 유지), 회귀 표면이 링크/툴팁뿐이다.

   ⚠️ data: URI 안에서는 **작은따옴표**를 쓴다 — 바깥 큰따옴표와 충돌한다.
   ⚠️ 색은 `#` 를 `%23` 로 이스케이프해야 한다. 안 하면 fragment 로 잘려 아이콘이 사라진다.
   같은 방식 선례: shared_pagination.css:42-51(페이저 화살표 4종, stroke 색도 같은 %23475569).
   ───────────────────────────────────────────────────────────────────────── */

/* 매물장 셀 — 프린터 아이콘. data-popup 없는 순수 링크라 같은 탭에서 매물장으로 이동한다
   (/properties 그리드 printCol 과 동일 동작). 우클릭 "새 탭에서 열기"가 살아 있어야 하므로
   href 는 서버가 계속 굽는다 — 여기서 걷어내는 건 아이콘과 배치뿐이다. */
.db-sheet-btn {
  display: flex;
  width: 100%;
  height: 100%;
  min-height: 28px;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  text-decoration: none;
  background-repeat: no-repeat;
  background-position: center;
  background-size: 16px 16px;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9V2h12v7'/><path d='M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2'/><rect x='6' y='14' width='12' height='8' rx='1'/></svg>");
}

/* 부동산정보 드롭다운 껍데기 — 항목은 #238 이후 클릭 시점에 JS 가 만든다(lib/dsv2_ledger_menu.js).
   남아 있던 461 B/행 중 약 110 B 가 메뉴 div 의 Tailwind 유틸 문자열과 min-width 인라인이었다.
   ⚠️ `hidden` 은 클래스에서 빼지 마라 — dropdown_menu_controller 가 classList 로 여닫는다.
   ⚠️ 메뉴는 열릴 때 document.body 로 포탈된다(Tabulator transform 조상 회피). 그래서 이 규칙은
      조상 셀렉터에 기대지 않는 **단일 클래스**여야 한다. */
.db-ledger-menu {
  z-index: 50;
  min-width: 130px;
  background: #fff;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  box-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);
  overflow: hidden;
}

/* 래퍼 width:100% 를 인라인에서 회수. data-doc-id 는 이 화면에서 ledger 껍데기에만 붙고
   그 사실을 dsv2_ledger_menu_lazy_test 가 잠근다 — 셀렉터로 쓰면 페이로드가 한 바이트도 안 는다.
   (공용 .db-select-wrap 은 inline-block 이라 전역으로 바꾸면 다른 화면이 깨진다.) */
.db-select-wrap[data-doc-id] { width: 100%; }

/* 로딩 표시는 **늦게** 나타난다 — 250ms 안에 끝나는 로딩엔 아예 안 보인다.
   그리드 왕복이 46ms 라 대부분의 로딩은 눈에 띄기도 전에 끝난다. 그런데 종전엔 그 짧은
   순간에도 스피너가 떴다 사라져서 «깜빡임»으로 보였다(써니 신고 2026-08-31: 「로딩 표시가
   너무 거슬려」). 실측: 로더가 화면에 떠 있던 시간 29~34ms.
   진짜 오래 걸릴 때만 보이면 되고, 그때는 250ms 뒤에 나타난다.
   ⚠️ display 로 감추지 않는다 — Tabulator 가 이 노드의 존재로 로딩 상태를 판단하는 자리라
      숨김 방식이 바뀌면 그쪽 계산이 흔들린다. 여기서는 «보이지 않게»만 한다. */
.ds-loader { display: flex; flex-direction: column; align-items: center; gap: 14px;
             animation: ds-loader-in 1ms 250ms both; }
@keyframes ds-loader-in { from { opacity: 0; } to { opacity: 1; } }
/* 매물등록 모달 로딩 — 공용 ds-spinner 재사용. 밝은 배경이라 흰 바 대신 slate + 상하 여백 */
.register-loading { padding: 48px 0; }
.register-loading .ds-spinner i { background: #94a3b8; }
.register-loading .ds-loading-text { font-size: 12px; color: #94a3b8; }
.ds-spinner { position: relative; width: 60px; height: 60px; animation: ds-spin 1s steps(12, end) infinite; }
.ds-spinner i { position: absolute; top: 4px; left: calc(50% - 2px); width: 4px; height: 13px; border-radius: 4px; background: #fff; transform-origin: 2px 26px; }
.ds-spinner i:nth-child(1)  { transform: rotate(0deg);   opacity: 1;   }
.ds-spinner i:nth-child(2)  { transform: rotate(30deg);  opacity: .92; }
.ds-spinner i:nth-child(3)  { transform: rotate(60deg);  opacity: .84; }
.ds-spinner i:nth-child(4)  { transform: rotate(90deg);  opacity: .75; }
.ds-spinner i:nth-child(5)  { transform: rotate(120deg); opacity: .66; }
.ds-spinner i:nth-child(6)  { transform: rotate(150deg); opacity: .57; }
.ds-spinner i:nth-child(7)  { transform: rotate(180deg); opacity: .48; }
.ds-spinner i:nth-child(8)  { transform: rotate(210deg); opacity: .40; }
.ds-spinner i:nth-child(9)  { transform: rotate(240deg); opacity: .33; }
.ds-spinner i:nth-child(10) { transform: rotate(270deg); opacity: .27; }
.ds-spinner i:nth-child(11) { transform: rotate(300deg); opacity: .21; }
.ds-spinner i:nth-child(12) { transform: rotate(330deg); opacity: .15; }
.ds-loading-text { color: #fff; font-size: 13px; font-weight: 600; letter-spacing: 1px; font-family: "Wanted Sans Variable", "Wanted Sans", sans-serif; }
@keyframes ds-spin { to { transform: rotate(360deg); } }

#grid {
  flex: 1;
  min-height: 0;
  height: 100%;
  border: 1px solid #e2e8f0; /* 데이터 그리드 영역 정의 (카드 패딩 안의 패널) */
  border-radius: 8px;
  overflow: hidden;
  background: #f1f5f9;
}

/* 연결매물(자식) 그리드 — 좁은 폼 폭에 12컬럼이 들어가 항상 가로로 넘친다.
   스크롤바를 되살리는 일은 **정본(grid_skin.css `.ds-grid-scroll`)** 이 한다
   (컨트롤러가 gridTarget 에 그 클래스를 같이 붙인다). 여기선 두께만 덮는다.

   ⚠️ 종전엔 이 자리에 자체 `::-webkit-scrollbar` 규칙 4개가 있었고 **한 번도 적용된 적이 없다**
      (2026-08-13 실측). 같은 블록의 `scrollbar-width: thin` 이 표준 스펙상 webkit 커스터마이징을
      통째로 무효화하기 때문 — grid_skin.css 의 정본 주석이 세 번의 시행착오 끝에 못박은 바로 그
      함정이다. 그래서 화면에 뜨던 건 스타일이 아니라 **네이티브 thin 스크롤바**(좌우 화살표 버튼
      포함)였다. 규칙을 고치는 대신 정본으로 합쳤다 — 같은 것을 네 번째로 다시 쓰지 않는다.

   두께 8px: 정본 기본값 12px 는 폼 안에 끼는 이 그리드엔 두껍다(양희 요청 2026-08-13). */
.dsv2-linked-grid {
  --ds-grid-scroll-size: 8px;
}

/* 높이 상한 — 자식이 많은 매물에서 그리드가 폼 밖으로 자라는 걸 막고 **자기 세로 스크롤**을 준다.
   (실측: 문서 2616 = 자식 116건 → 상한 없으면 4,330px. edit 화면은 <main> 이 overflow:hidden 이라
    그 아래 행에 닿을 방법이 아예 없었다.)

   ⚠️ 왜 Tabulator 의 `maxHeight` 옵션이 아니라 **holder 의 CSS max-height** 인가 —
      maxHeight 는 요소(.tabulator)에 max-height 를 걸 뿐이고, 본문(.tabulator-tableholder)은
      Tabulator 가 inline 으로 `height: calc(100% - 28px)` 를 준다. 부모 높이가 auto(내용만큼)면
      그 퍼센트가 해석되지 않아 **본문은 전체 높이 그대로 남고 요소만 잘린다** — 내용이 잘리기만
      하고 스크롤은 안 생긴다. `redraw(true)` 로도 안 풀린다(타이밍이 아니라 퍼센트 해석 문제).
      라이브 실측 2026-08-13: 요소 420px / holder client·scroll 4292·4292 → 스크롤 불가.
      본문에 직접 상한을 걸면 요소 높이와 무관하게 잘리고 스크롤이 생긴다(384 / 4292).
   ⚠️ `!important` 는 위 inline `max-height: calc(100% - 28px)` 를 이기기 위한 것이다.

   392px = 그리드 전체 420px − 헤더 28px. 상한 정의는 **이 한 줄이 정본**이다
   (JS 쪽에 같은 수를 두면 한쪽만 고쳐진다). */
.dsv2-linked-grid .tabulator-tableholder {
  max-height: 392px !important;
}

/* 6: 편집 중 input 을 엑셀처럼 넓게 — 좁은 컬럼도 값 전체가 보이도록.
   좁은 셀에선 input 이 셀 밖으로 넘쳐 인접 셀 위에 표시(overflow:visible + z-index),
   넘친 부분이 인접 셀을 가리도록 불투명 흰 배경. 넓은 셀(≥240px)은 그대로 100%. */
#grid .tabulator-cell.tabulator-editing {
  overflow: visible;
  z-index: 60;
}
#grid .tabulator-cell.tabulator-editing input {
  /* 폭은 JS(cellEditing)가 값 길이에 맞춰 인라인 설정(content-fit). 여기선 배경만 불투명(넘친 부분이 인접 셀 가리게). */
  box-sizing: border-box;
  background: #fff;
}

/* ══════════════════════════════════════════════════════════════════════════
   아래는 dsv2_properties/index.html.erb 의 인라인 <style> 122줄에서 회수(2026-07-29).
   뷰에 CSS 정의가 살면 정본 밖 스타일이라 추적·재사용이 안 되고, 같은 규칙이 화면마다
   갈라진다. 규칙 내용은 원본 그대로 — 이동만.
   ⚠️ 스코프 유지 필수: data-*-target 속성 스코프가 grid_skin.css 전역 규칙과의
      specificity 싸움에서 이기는 근거다(원문 주석 참조). 스코프를 줄이면 안 먹는다.
      2026-07-29 !important 를 걷어낸 뒤로는 **이 스코프가 유일한 승리 근거**다 — 더 중요해졌다.
   ══════════════════════════════════════════════════════════════════════════ */

/* 페이지 배경(카드 뒤 영역)만 #e9edf2 — 이 화면 전용. 카드(.bg-surface)는 그대로.
   패딩은 override 하지 않는다 — 레이아웃 공통 main.p-4(16px)를 그대로 따른다(세 화면 여백 통일, 2026-07-24).
   ⚠️ 뷰 <style> 에 있을 땐 그 문서에만 적용돼 자동 스코프였다. 정본 시트는 전 페이지 로드라
      .screen-dsv2-properties(layouts/application 의 body 훅)로 명시 스코프해야 다른 화면에 안 샌다. */
.screen-dsv2-properties main { background: #e9edf2; }
/* 상가2 진입 중엔 <main> 자체 세로 스크롤을 끈다(좌/우패널이 각자 세로 스크롤을 가지고 있어
   페이지 레벨 세로 스크롤까지 겹치면 이중 스크롤바로 보이던 문제, 2026-07-21). 가로는 반드시
   유지(우패널 컬럼이 많아 가로 스크롤이 필요) — overflow-x는 명시로 auto 고정.
   토글은 dsv2_properties_grid_controller.js#_toggleGroupedView. */
main.dsv2-sanga2-no-vscroll { overflow-y: hidden; overflow-x: auto; }
/* 글자 안티앨리어싱(요청) — ⚠️ macOS(WebKit) 전용, Windows 크롬에선 무효(no-op). */
.screen-dsv2-properties { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
/* 유형 탭 = 공용 필터탭 shared/_filter_tabs(.db-tabs/.db-tab/.tab--on, shared_filter_tabs.css)로 이관. */
/* 검색(Ctrl+K) 회상 점프 — 선택된 매물 행 색상 강조(다음 점프/클릭 전까지 유지).
   ⚠️ 파일명은 properties_grid 지만 이 규칙은 **Tabulator 선택행 공용**이다(레이아웃에서 전역 로드).
      세 번째 소비자로 문서관리(tabulator-grid)가 붙었다 — 갤러리를 닫으면 마지막으로 보던
      파일 행을 이 강조로 표시한다(2026-08-04). 색을 그쪽에 새로 쓰면 같은 의미의 강조가
      두 벌이 되고 한쪽만 조정되며 톤이 갈린다 — 값은 여기 두 줄이 정본이다. */
[data-dsv2-properties-grid-target="table"] .tabulator-row.dsv2-row-selected,
[data-dsv2-properties-grid-target="table"] .tabulator-row.dsv2-row-selected .tabulator-cell,
[data-dsv2-sanga-grouped-target="leftTable"] .tabulator-row.dsv2-row-selected,
[data-dsv2-sanga-grouped-target="leftTable"] .tabulator-row.dsv2-row-selected .tabulator-cell,
[data-dsv2-sanga-grouped-target="rightTable"] .tabulator-row.dsv2-row-selected,
[data-dsv2-sanga-grouped-target="rightTable"] .tabulator-row.dsv2-row-selected .tabulator-cell,
[data-tabulator-grid-target="grid"] .tabulator-row.dsv2-row-selected,
[data-tabulator-grid-target="grid"] .tabulator-row.dsv2-row-selected .tabulator-cell { background:#fff3bf; }
[data-dsv2-properties-grid-target="table"] .tabulator-row.dsv2-row-selected,
[data-dsv2-sanga-grouped-target="leftTable"] .tabulator-row.dsv2-row-selected,
[data-dsv2-sanga-grouped-target="rightTable"] .tabulator-row.dsv2-row-selected,
[data-tabulator-grid-target="grid"] .tabulator-row.dsv2-row-selected { box-shadow: inset 3px 0 0 #f59f00; }

/* 부모(건물) 행 표시 — 왼쪽 남색 세로선 (써니 2026-08-26).
 *
 * ── 왜 배경색·글씨색이 아닌가 ────────────────────────────────────────────
 * 그 둘은 **이미 임자가 있다**:
 *     배경  얼룩말 줄무늬(grid_skin --db-grid-row-stripe) · hover · 「보고 있는 줄」(#fff3bf)
 *           · 셀 강조(사용자가 칠한 급매·계약임박…)
 *     글씨  셀 강조 (급매 = #9f1239 + font-weight:700)
 *     굵기  셀 강조
 * 실측 2026-08-26: 급매 2건 · 계약임박 1건 · 광고중 1건이 실제로 칠해져 있다.
 * 부모/자식은 값의 **상태**가 아니라 **구조**라, 상태 채널을 나눠 쓰면 「급매인 부모 행」에서
 * 둘 중 하나가 반드시 안 보인다. 그래서 아직 비어 있는 채널(왼쪽 세로선)을 쓴다.
 *
 * ── 셀에 준다, 행이 아니라 ───────────────────────────────────────────────
 * 행에 inset box-shadow 를 주면 **셀 배경에 가려 안 보인다**(셀이 자기 배경을 칠한다).
 * 실측: 행에 걸었을 때 확대해도 선이 안 보였고, 첫 셀에 거니 보였다.
 *
 * ── 「보고 있는 줄」과 같은 자리다 ───────────────────────────────────────
 * 위 규칙이 이미 왼쪽 3px 주황선을 쓴다. 부모 행을 선택하면 한쪽이 다른 쪽을 덮으므로
 * 겹칠 때는 **나란히** 놓는다 — 주황(선택)이 바깥, 남색(부모)이 안쪽.
 * 순서를 뒤집으면 「지금 이 줄을 보고 있다」가 부모 행에서만 사라진다. */
[data-dsv2-properties-grid-target="table"] .tabulator-row.dsv2-parent-row .tabulator-cell:first-child {
  box-shadow: inset 4px 0 0 var(--db-color-primary, #1e40af);
}
/* 부모 + 선택 — 두 선을 나란히(주황 0~3px, 남색 3~7px). 행 규칙이 아니라 셀 규칙이라
   위 .dsv2-row-selected(행 box-shadow)와 자리를 다투지 않지만, 셀에 걸린 남색이 행의
   주황을 시각적으로 덮어쓰므로 여기서 둘을 함께 그린다. */
[data-dsv2-properties-grid-target="table"] .tabulator-row.dsv2-parent-row.dsv2-row-selected .tabulator-cell:first-child {
  box-shadow: inset 3px 0 0 #f59f00, inset 7px 0 0 var(--db-color-primary, #1e40af);
}
/* 셀 텍스트 세로 중앙정렬 — grid_skin.css(전역) .tabulator-row .tabulator-cell 가
   line-height:1.3(≈15.6px, font-size 12px 기준)을 거는데, dsv2 실제 셀 높이(~36px,
   패딩 4px씩 제외 컨텐츠박스 ~28px)보다 훨씬 작아 텍스트가 위쪽에 붙고 아래에 여백만
   남는 문제(2026-07-14). line-height 를 컨텐츠박스 높이만큼 키워 단일행 텍스트를 세로
   중앙에 오게 함(실측: gapAbove≈gapBelow, 오차 0.3px 이내).
   ⚠️ 이기는 근거(2026-07-29 갱신): 예전엔 양쪽 다 !important 라 스코프로 갈렸지만, 지금은
      grid_skin 쪽이 normal 이고 이 파일이 **뒤에 로드**돼 순서로 이긴다. 그래도 data-*-target
      스코프는 그대로 둔다 — grid_skin 이 다시 !important 를 달면 스코프가 유일한 방어선이다.
      이 규칙을 지우면 line-height 가 15.6px 로 떨어진다(실측 확인).

   ★ 이 목록은 "행 높이 36px 인 그리드" 목록과 **같아야 한다**(2026-08-13).
     28px = 행 36px − 세로패딩 8px(4px씩). 셋 중 하나만 바뀌면 글자가 위/아래로 쏠린다.
     연결매물(.dsv2-linked-grid)이 여기 빠져 있었다 — 종전엔 셀이 24px 라 티가 덜 났는데,
     rowHeight:36 을 주면서 셀이 36px 가 되자 **위여백 4 / 아래여백 18** 로 글자가 위에
     붙었다(라이브 실측 2026-08-13, 매물등록 모달). 세로선은 이어졌지만 글자가 쏠린 것 —
     행 높이를 지정하면 line-height 도 같이 줘야 한다는 게 이 규칙의 진짜 계약이다.
     `grid_row_height_canon_test` 가 이 산술(28 = 36 − 8)을 정적으로 잠근다. */
[data-dsv2-properties-grid-target="table"] .tabulator-row .tabulator-cell,
[data-dsv2-sanga-grouped-target="leftTable"] .tabulator-row .tabulator-cell,
[data-dsv2-sanga-grouped-target="rightTable"] .tabulator-row .tabulator-cell,
.dsv2-linked-grid .tabulator-row .tabulator-cell { line-height: 28px; }
/* ── 셀 2줄 클램프 — 폭에 안 담기면 2줄, 그래도 넘치면 … (2026-08-19) ──────────
   좁은 컬럼에서 긴 글이 한 줄로 싹둑 잘리던 것(`text-overflow: clip`)을 고친다.
   실측(크롬 151 + tabulator 5.6.1, 주소 컬럼 폭을 400→100px 로 줄이며):
     400px  전부 1줄            / 260px  긴 것만 2줄
     160px  중간 2줄·긴 것 2줄+… / 100px  동일        — **행 높이는 전 구간 37px 불변**
   폭 150px 에서 서로 다른 두 매물이 똑같이 "…역삼동 823-" 로 보이던 게 갈린다.

   ★ 위 `line-height: 28px` 규칙은 **그대로 둔다**. 그건 1줄을 36px 셀 한가운데 띄우는
     트릭이고(28 = 행 36 − 세로패딩 8), 래퍼가 없는 셀 — 체크박스·버튼·뱃지·dateYmd 등
     이미 formatter 가 있는 컬럼 — 은 계속 그 경로로 정렬된다. 여기서는 래퍼가 있는 셀만
     `:has` 로 골라 덮으므로 기존 화면 회귀가 0 이고, grid_row_height_canon_test 의
     산술(28 = 36 − 4*2)도 손대지 않는다.

   래퍼(.ds-cell-clamp)는 grid_table_preset.js 의 trimZeroFormatter 가 만든다 —
   `if (!c.formatter)` 로 **포맷터 없는 컬럼 전부**(= 긴 글이 들어가는 순수 텍스트 칸)에
   자동으로 붙는다. 마크업 생성처와 스타일 정본이 떨어져 있으니 같이 옮길 것.

   ⚠️ 세로중앙을 `inline-flex` 로 잡는 이유: 2줄이면 줄상자가 32px 이라 28px 트릭이
      깨진다(28×2=56 > 36, 아랫 행 침범). `flex`(블록 레벨)로 하면 셀이 세로로 쌓여
      표가 무너지므로 **inline** 이어야 한다(실측). `-webkit-box-pack:center` 안은
      크롬 151 에서 안 먹어 폐기(1줄 셀이 위로 붙음 — 여백 위 0 / 아래 12).
   ⚠️ 편집 중에는 Tabulator 가 셀 내용을 input 으로 갈아끼워 래퍼가 사라진다 →
      `:has` 가 자동으로 안 맞아 기존 편집 스타일 그대로다(따로 뺄 필요 없음). */
[data-dsv2-properties-grid-target="table"] .tabulator-row .tabulator-cell:has(> .ds-cell-clamp),
[data-dsv2-sanga-grouped-target="leftTable"] .tabulator-row .tabulator-cell:has(> .ds-cell-clamp),
[data-dsv2-sanga-grouped-target="rightTable"] .tabulator-row .tabulator-cell:has(> .ds-cell-clamp),
.dsv2-linked-grid .tabulator-row .tabulator-cell:has(> .ds-cell-clamp) {
  display: inline-flex;
  align-items: center;
  vertical-align: middle;
  overflow: hidden;
  line-height: 16px;
}
[data-dsv2-properties-grid-target="table"] .tabulator-row .tabulator-cell > .ds-cell-clamp,
[data-dsv2-sanga-grouped-target="leftTable"] .tabulator-row .tabulator-cell > .ds-cell-clamp,
[data-dsv2-sanga-grouped-target="rightTable"] .tabulator-row .tabulator-cell > .ds-cell-clamp,
.dsv2-linked-grid .tabulator-row .tabulator-cell > .ds-cell-clamp {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-height: 16px;
  overflow: hidden;
  white-space: normal;
  word-break: break-word;
  width: 100%;
}
/* 셀 편집 시 textarea(비고/특이사항) 세로 확장용 — 컨트롤러 _setupCellWiden와 짝.
   overflow:visible+z-index는 세로 확장분이 아래 행 위로 겹쳐 보이는 데만 필요(가로 확장은 없앰 —
   옆 컬럼 침범 버그라 제거, 2026-07-14). input, textarea 둘 다 대상 — textarea만 빠지면 스타일이
   안 먹어 브라우저 기본 포커스 outline이 셀 자체의 파란 outline과 따로 겹쳐 "이중 테두리"로 보임.
   padding:0 — 기본 .tabulator-cell 패딩(4px 6px, grid_skin.css)이 편집 중에도 안 지워져
   input 자체 패딩(4px, JS 인라인)과 이중으로 겹쳐 input이 셀 경계보다 사방 ~5~7px 안쪽에 떠서
   "셀 경계와 안 맞고 붕 뜬다" 버그(2026-07-14 재신고, 전 컬럼 공통). 편집 중엔 셀 패딩 0으로 비워
   input의 바깥 경계 = 셀 경계로 정확히 일치시킴(실측: diff 0,0,0,0). */
[data-dsv2-properties-grid-target="table"] .tabulator-cell.tabulator-editing,
[data-dsv2-sanga-grouped-target="leftTable"] .tabulator-cell.tabulator-editing,
[data-dsv2-sanga-grouped-target="rightTable"] .tabulator-cell.tabulator-editing {
  overflow: visible; z-index: 60; padding: 0;
}
/* 좌패널(320px 고정폭) 주소 넓히기 escape — 조상(.tabulator 루트/.tabulator-tableholder)의
   overflow를 풀어보는 시도는 가상스크롤 밖 다른 행들까지 통째로 드러나는 부작용이 있어(2026-07-21
   "input만 덮으면 되는데 다 나온다" 신고) 폐기. 대신 편집 중인 input+검색버튼 DOM 자체를
   document.body 밑 position:fixed 오버레이로 옮기는 방식(dsv2_sanga_grouped_controller.js
   #_portalizeAddressCell)으로 교체 — 조상 overflow/z-index는 전혀 안 건드리므로 이 스타일은
   불필요해져 제거. 오버레이 자체의 룩(그림자 등)은 JS 인라인 style로 직접 지정. */
[data-dsv2-properties-grid-target="table"] .tabulator-cell.tabulator-editing input,
[data-dsv2-properties-grid-target="table"] .tabulator-cell.tabulator-editing textarea,
[data-dsv2-sanga-grouped-target="leftTable"] .tabulator-cell.tabulator-editing input,
[data-dsv2-sanga-grouped-target="leftTable"] .tabulator-cell.tabulator-editing textarea,
[data-dsv2-sanga-grouped-target="rightTable"] .tabulator-cell.tabulator-editing input,
[data-dsv2-sanga-grouped-target="rightTable"] .tabulator-cell.tabulator-editing textarea {
  box-sizing: border-box; background: #fff; border: none; outline: none; resize: none;
}
/* 주소 검색 에디터(daum) — 검색버튼(공용 .db-action-btn 32px)이 dsv2 행높이(36px)에 안 맞아 셀 위로
   3px 삐지고 top-앵커되던 것 교정: wrap을 셀 높이 채워 input·버튼 세로중앙 + 버튼 컴팩트(22px).
   address 편집셀만 스코프(다른 컬럼 에디터 영향 0). 실측: 세로중앙 오프셋 0, 위아래 여백 대칭.
   우패널(rightTable)도 스코프 — "미배정 상가" 선택 시 주소 컬럼이 같은 daum 에디터로 편집 가능. */
[data-dsv2-properties-grid-target="table"] .tabulator-cell.tabulator-editing[tabulator-field="address"] > div,
[data-dsv2-sanga-grouped-target="leftTable"] .tabulator-cell.tabulator-editing[tabulator-field="address"] > div,
[data-dsv2-sanga-grouped-target="rightTable"] .tabulator-cell.tabulator-editing[tabulator-field="address"] > div {
  height: 100%; display: flex; align-items: center;
}
[data-dsv2-properties-grid-target="table"] .tabulator-cell.tabulator-editing[tabulator-field="address"] .db-action-btn,
[data-dsv2-sanga-grouped-target="leftTable"] .tabulator-cell.tabulator-editing[tabulator-field="address"] .db-action-btn,
[data-dsv2-sanga-grouped-target="rightTable"] .tabulator-cell.tabulator-editing[tabulator-field="address"] .db-action-btn {
  height: 22px;
}
/* 의뢰자(client) 셀 클릭 → 상담기록 팝업(컨트롤러 cellClick). 버튼이 아닌 일반 셀이라
   클릭 가능함을 커서로만 알림(문구·아이콘 추가 없음 — 최소 변경). 상가2 우패널(자식 호실)도
   동일 컬럼·동일 팝업(dsv2_sanga_grouped_controller#_buildRightColumns) — 정본 재사용. */
[data-dsv2-properties-grid-target="table"] .dsv2-client-clickable,
[data-dsv2-sanga-grouped-target="rightTable"] .dsv2-client-clickable { cursor: pointer; }
/* 상담기록 셀 버튼 — 셀렉트 아닌 일반 버튼(팝업 오픈 단일 액션). 회색 배경·하늘색 텍스트·보더 없음·중앙정렬·28px.
   ⚠️ 클래스명을 Rails 헬퍼/컨트롤러가 뱉는다 — 마크업 생성처와 스타일 정본이 떨어져 있으니 같이 옮길 것. */
.dsv2-cell-btn {
  display:inline-flex; align-items:center; justify-content:center;
  height:28px; padding:0 14px;
  border:0; border-radius:6px;
  /* 닫기 버튼(.db-btn-secondary)과 동일 토큰 — 회색 배경 + 진한 회색 텍스트 */
  background:var(--db-color-fill-light); color:var(--db-color-text-sub);
  font-size:12px; font-weight:500; text-decoration:none; cursor:pointer;
  transition:background .15s ease;
}
.dsv2-cell-btn:hover { background:var(--db-color-fill-medium); }
/* 상가2 우패널 — 가로 스크롤바만 노출, 세로는 완전 숨김(둘 다 스크롤 기능 자체는 유지).
   ⚠️ 근인(2026-07-21 3차 → 2026-07-22 재수정, 두 번째 시행착오):
   1차 수정(2026-07-21, 3차 시도)이 scrollbar-width:thin을 걸어 가로바를 살리려 했는데,
   Chrome(121+) 표준 scrollbar-width는 축 구분 없이 세로·가로 전체에 적용되고
   ::-webkit-scrollbar 커스터마이징을 완전히 무효화한다(스펙상 scrollbar-width가 auto가
   아니면 우선) — thin이 세로바까지 되살렸는데 vertical{width:0} 규칙은 발동한 적이 없었음.
   2차 수정(2026-07-22, 첫 재수정)은 scrollbar-width:none으로 바꿔 세로는 확실히 숨겼지만,
   none도 auto가 아니므로 ::-webkit-scrollbar 커스터마이징을 그대로 무효화해 가로바까지
   통째로 사라짐 — "가로는 살리라고 했잖아" 재신고(2026-07-22).
   3차(현재): scrollbar-width는 auto로 둔다(스펙상 auto일 때만 ::-webkit-scrollbar가
   실제로 반영됨 — 실측: DOM CSSOM에서 auto로 세팅한 규칙만 webkit 커스터마이징과 공존,
   none/thin 둘 다 무효화 확인) — 그 위에 ::-webkit-scrollbar:vertical만 width:0+display:none
   으로 숨기고 :horizontal은 12px 두께로 스타일링해 노출. 좌패널(매물명/주소 2컬럼, 가로
   스크롤 불필요)은 전역 규칙(scrollbar-width:none, grid_skin.css)을 그대로 둬서
   안 건드림 — 우패널만 스코프. */
[data-dsv2-sanga-grouped-target="rightTable"].tabulator .tabulator-tableholder {
  scrollbar-width: auto;
}
[data-dsv2-sanga-grouped-target="rightTable"].tabulator .tabulator-tableholder::-webkit-scrollbar:vertical {
  width: 0 !important;
  display: none !important;
}
[data-dsv2-sanga-grouped-target="rightTable"].tabulator .tabulator-tableholder::-webkit-scrollbar:horizontal {
  height: 12px !important;
  display: block !important;
}
[data-dsv2-sanga-grouped-target="rightTable"].tabulator .tabulator-tableholder::-webkit-scrollbar-track {
  background: rgba(0,0,0,.05);
}
[data-dsv2-sanga-grouped-target="rightTable"].tabulator .tabulator-tableholder::-webkit-scrollbar-thumb {
  background: rgba(0,0,0,.32);
  border-radius: 6px;
}
[data-dsv2-sanga-grouped-target="rightTable"].tabulator .tabulator-tableholder::-webkit-scrollbar-thumb:hover {
  background: rgba(0,0,0,.48);
}


/* 상가 일괄등록 모달 — 호실 접미사 셀렉(2026-07-31 뷰 인라인에서 회수).
   Tailwind `w-[76px]` 로는 못 덮는다: `.db-input`(db_components.css:78)이 `width:100%` 를 주는
   **unlayered** 규칙이라 Cascade Layers 상 layered 유틸을 항상 이긴다. 이 시트는 레이아웃에서
   db_components 뒤(head 55행)라 같은 특이도에서 순서로 이긴다. */
.sanga-unit-suffix { width: 76px; }

/* ── 필터 모달 (dsv2_properties/_filter_modal) ─────────────────────────
   뷰에 인라인 style 을 못 쓴다(view_style_canon_test 가 래칫). 배치만 여기 둔다 —
   칸·버튼·셀렉트의 룩은 전부 공용 정본(.db-input / .db-action-btn / db_select)이다. */
.db-filter-row    { display: flex; flex-direction: column; gap: var(--db-space-2); }
.db-filter-label  { font: var(--db-text-label-2); color: var(--db-color-text-sub); }
.db-filter-hr     { border: 0; border-top: 1px solid var(--db-color-outline); margin: 4px 0; }
.db-filter-hint   { font-size: 11px; line-height: 1.6; color: var(--db-color-text-sub); margin: 0; }
/* 초기화 = 중립 버튼. 적용(주 파랑)과 무게를 갈라 실수로 지우는 걸 줄인다. */
.db-filter-reset  { background: var(--db-color-fill-light); color: var(--db-color-text-sub); }
.db-filter-reset:hover { background: var(--db-color-fill-medium); }
/* 필터가 걸려 있으면 버튼에 점을 찍는다 — 모달을 닫으면 뭐가 걸렸는지 알 길이 없어진다. */
.db-filter-btn--on::after {
  content: ""; display: inline-block; width: 6px; height: 6px; margin-left: 6px;
  border-radius: 50%; background: currentColor; vertical-align: middle;
}

/* ── "최신" 활동 피드 (dsv2_properties/index — activityWrap) ────────────
   표가 아니라 목록이다: 행 모양이 매물이 아니라 "언제·무엇을·누가"라 Tabulator 컬럼 계약과
   안 맞고, 눌러서 그 매물로 가는 게 전부라 표가 필요 없다. */
.db-activity-list { list-style: none; margin: 0; padding: 0; }
.db-activity-item {
  display: flex; align-items: baseline; gap: 10px;
  padding: 9px 10px; border-bottom: 1px solid var(--db-color-outline);
  font-size: 13px; line-height: 1.5;
}
.db-activity-item:hover { background: var(--db-color-fill-subtle); }
/* 시각은 폭을 고정해 세로로 줄이 맞는다 — 안 맞으면 훑을 때 눈이 계속 되돌아간다. */
.db-activity-when { flex: 0 0 62px; color: var(--db-color-text-muted); font-size: 12px; }
.db-activity-name {
  flex: 0 1 auto; max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  background: none; border: 0; padding: 0; font: inherit; cursor: pointer;
  color: var(--db-color-primary); text-align: left;
}
.db-activity-name:hover { text-decoration: underline; }
/* 문구가 길어졌다 — `매물 수정 「보증금 1,000만 · 월차임」 3곳 외 2건`, 메모면 내용 60자까지.
   종전 `flex: 0 0 auto` 는 **줄지 않는다**: 이 칸이 제 폭을 우겨서 오른쪽 사람 이름을 밀어내고
   줄이 넘친다. 줄이려면 셋이 같이 있어야 한다 — 줄 수 있게(0 1 auto) + 플렉스 항목 기본값
   min-width:auto 풀기(0) + 넘친 글자 처리(hidden/ellipsis/nowrap).
   ⚠️ 줄을 늘리지 않는다(한 줄 유지) — 행 높이가 곧 스크롤 길이다(_partner_list 와 같은 규율). */
.db-activity-what {
  flex: 0 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--db-color-text);
}
/* 사람 이름은 오른쪽 끝을 잡는 칸이라 남은 폭을 먹는다(1 1 auto). 문구가 길면 이쪽도 줄어야
   해서 min-width 를 같이 푼다 — 안 그러면 이름 글자폭이 바닥이 돼 그만큼 줄이 넘친다. */
.db-activity-who  {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-align: right; color: var(--db-color-text-sub); font-size: 12px;
}
.db-activity-item--deleted .db-activity-name { color: var(--db-color-text-muted); text-decoration: line-through; }
.db-activity-badge {
  flex: 0 0 auto; font-size: 11px; padding: 1px 6px; border-radius: 999px;
  background: var(--db-color-fill-light); color: var(--db-color-text-sub);
}
.db-activity-empty { padding: 24px 10px; color: var(--db-color-text-sub); font-size: 13px; }
.db-activity-more  { display: flex; justify-content: center; padding: 14px 0; }

/* ══════════════════════════════════════════════════════════════════════════
   그룹뷰(2유형) 좌패널 건물 요약 팝업 — `.bsp-*` (써니 2026-08-25 요청)

   요청받은 디자인의 **인라인 style 을 그대로 옮긴 것**이다(값은 한 픽셀도 안 바꿨다).
   인라인으로 두지 않은 이유: 이 저장소는 인라인 style 래칫이 있고, 같은 값이 20군데
   흩어지면 한 곳만 고쳐지는 사고가 이미 여러 번 났다(shared 파셜 회수 주석 참조).

   ⚠️ 색은 요청 HTML 의 hex 를 그대로 둔다. 토큰으로 바꾸지 않았다 — 이 팝업은 «사진과
      같게» 가 요구사항이고, 토큰 팔레트가 바뀌면 사진과 갈라진다. 토큰화하려면 그때
      «사진과 달라져도 되는가» 를 먼저 물어야 한다.
   ══════════════════════════════════════════════════════════════════════════ */
.bsp-body {
  /* flex:1 은 **크기조절의 전제**다(써니 2026-08-26). 패널에 명시 높이가 없던 동안은
     이 값이 아무 일도 안 했지만(높이 auto = 남는 공간이 0), 크기조절로 패널이 높이를
     받으면 이게 없을 때 늘린 만큼이 팝업 **아래 빈칸**으로 남는다. */
  flex: 1 1 auto;
  display: flex; flex-direction: column; min-height: 0;
}

/* ── 헤더 ─────────────────────────────────────────────────────────────── */
.bsp-head {
  /* 건물명·매매가는 **셸 헤더 줄**로 올라갔다(써니 2026-08-25 "여백이 너무 많다").
     여기 남은 건 4칸뿐이라 위 여백을 크게 줄인다 — 종전엔 셸 헤더(빈 65px) + 제목 줄이
     따로 있어 ✕ 아래로 21px 이 더 떠 있었다(브라우저 실측). */
  padding: 14px 26px 16px;
  background: #fff;
  border-bottom: 1px solid #e8eaee;
}
/* 셸 헤더 줄에 올라간 건물명 — 그 줄의 h2(14px)보다 커서 baseline 을 맞춰 준다. */
.bsp-name {
  font-size: 21px; font-weight: 800; letter-spacing: -.02em; color: #16181d;
  /* 헤더 오른쪽엔 호실 검색칸이 산다(shared/_modal 의 header_right). 이름이 길면 그 칸을
     밀어내 버리므로 «이름이 줄어든다»로 정한다 — 검색칸이 좁아지면 못 쓰게 되지만
     이름은 줄어도 읽을 수 있고, 어차피 그 건물을 눌러서 연 팝업이다. */
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.bsp-price-chip {
  display: inline-flex; align-items: baseline; gap: 6px; flex: none;
  padding: 4px 10px; border-radius: 999px; background: #eef3fd;
}
.bsp-price-chip[hidden] { display: none; }
.bsp-price-label { font-size: 11px; font-weight: 600; color: #666e7b; }
.bsp-price-value {
  font-size: 15px; font-weight: 800; color: #1b64da; letter-spacing: -.02em;
}

/* 4칸 요약 + 색 범례를 한 줄에 세운다(범례는 닫기 ✕ 아래, 우측). */
.bsp-head--split { display: flex; align-items: flex-start; justify-content: space-between; }
.bsp-facts { display: flex; align-items: center; flex-wrap: wrap; }
.bsp-legend {
  display: flex; flex-direction: column; align-items: flex-end;
  gap: 4px; flex: none; padding-left: 16px;
}
.bsp-legend-item {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 11px; font-weight: 600; color: #7c828a; white-space: nowrap;
}
.bsp-dot { width: 6px; height: 6px; border-radius: 50%; flex: none; }
/* ⚠️ 위 «금액 두 종류의 색 정본» 과 같은 값이어야 한다 — 다르면 범례가 거짓말이 된다. */
.bsp-dot--sale { background: #dc2626; }
.bsp-dot--rent { background: #1b64da; }
.bsp-divider { width: 1px; height: 26px; background: #e8eaee; }
/* 칸 사이 여백 22 → 12 (써니 2026-08-27). 범례가 헤더 우측으로 들어오면서 4칸이 쓸 수
   있는 폭이 390px 로 줄었는데, 22px 이면 428px 이 필요해 **연면적이 둘째 줄로 밀렸다**.
   브라우저 실측(오션파크, 팝업 기본폭 530):
     22→428px(2줄) · 18→404(2줄) · 16→392(2줄) · 14→380(1줄) · 12→368(1줄)
   14 부터 한 줄이 되지만 값이 더 긴 건물을 위해 12 로 둔다(여유 22px).
   그 결과 헤더 높이 114 → 72px. */
.bsp-fact { display: flex; flex-direction: column; gap: 3px; padding: 0 12px; }
.bsp-fact:first-child { padding-left: 0; }
.bsp-fact:last-child  { padding-right: 0; }
/* 라벨 11.5px · 값 14px (써니 2026-08-25 → 26). 요청받은 원본 HTML 은 10.5 / 14 였다.
   25일에 16 까지 키웠다가 26일에 14 로 되돌렸다 — 한 줄에 담기는 정보가 늘면서
   헤더가 본문을 밀어냈다. 라벨은 11.5 그대로다. */
.bsp-fact-label {
  font-size: 11.5px; font-weight: 600; color: #7c828a; letter-spacing: .02em;
}
.bsp-fact-value {
  font-size: 14px; font-weight: 700; color: #2b2f36;
  font-variant-numeric: tabular-nums;
}
/* 공공데이터를 기다리는 중 — 값이 아니라 «지금 오고 있다» 를 말한다.
   빈칸으로 두면 "이 건물은 값이 없구나" 로 읽혀서 사람이 헛되이 기다리지 않는다. */
.bsp-fact-value[data-loading] { color: #7c828a; font-weight: 500; }

/* ── 호실 목록 ────────────────────────────────────────────────────────── */
.bsp-rows { padding: 0 14px 18px; overflow-y: auto; min-height: 0; }
.bsp-row {
  display: grid;
  /* 폭·간격 정본 (써니 2026-08-26 → 27).
     gap 은 처음에 **0** 이었다 — 그땐 칸이 넉넉해 값 사이에 자연스러운 여백이 있었다.
     이후 칸을 좁히고(76/85/110 → 61/73/108) 넘치면 채우도록 맞추면서, 긴 값이 칸을
     꽉 채워 **옆 칸과 맞닿게** 됐다("너무 가까워서 가독성이 저해된다", 써니 2026-08-27).
     그래서 6px 를 준다.
     ⚠️ gap 은 **`1fr`(특이사항) 칸에서만** 빠진다 — 앞 셋과 끝의 22px 은 고정이라 안 줄고,
        남는 공간을 받는 게 1fr 하나뿐이라서다. 즉 호실·면적·금액의 «안 접히는 폭» 계약은
        그대로다. 실측(브라우저, 특이사항 칸 폭):
          팝업 390px  gap0 70 → gap6 46      (390 은 사람이 줄여 저장한 크기)
          팝업 530px  gap0 210 → gap6 186    (기본)
          팝업 700px  gap0 380 → gap6 356
        비용은 폭과 무관하게 24px(6×4) 고정이다. */
  grid-template-columns: 61px 73px 108px 1fr 22px;
  align-items: center; column-gap: 6px; row-gap: 0;
  padding: 3px 12px; border-radius: 10px; cursor: pointer;
}
.bsp-row:hover { background: #f7f8fa; }
.bsp-row + .bsp-row { border-top: 1px solid #f0f1f4; }
/* ── 호실 이름 — 축소 먼저, 그래도 길면 2줄 (써니 2026-08-27) ─────────────────
   칸은 61px 인데 이름이 최대 29자까지 온다("49동 (현 임차 23.08계약 2200/220만원)").
   실측(daesin_v2_db 4,210건 · 61px · 실제 폰트):
     1~5자 3,779건(89.8%)  "101호"      → 13px 한 줄
     6~8자   259건 (6.2%)  "1층 84호"    → 대부분 한 줄, 일부는 축소로 들어간다
     9자~    172건 (4.1%)  "건물및 토지 (휴하임)" → 10px 로 줄여도 한 줄엔 못 들어간다
   한글 기준 13px 5자 · 11px 6자 · 10px 7자 — **축소만으로는 4.1% 를 못 구한다.**
   그래서 3단계다: 한 줄 → 축소 → 2줄 클램프+말줄임(전체 문구는 title 툴팁).
   ⚠️ 단계 판정은 CSS 가 못 한다(글자 폭을 모른다). JS 가 scrollWidth 로 재서 붙인다
      (_fitUnits). 그래서 이 셋은 **JS 와 짝**이고, 클래스 이름을 바꾸면 같이 바꿔야 한다. */
.bsp-unit {
  font-size: 13px; font-weight: 600; color: #7c828a;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.bsp-unit--tight { font-size: 11px; }
.bsp-unit--wrap {
  white-space: normal; overflow: hidden;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  word-break: break-all;
}
.bsp-area { display: flex; flex-direction: column; gap: 1px; }
/* 전유면적 — **호실과 같은 톤**(써니 2026-08-26 3차). 종전엔 호실이 바뀌기 전 값
   (800 / #16181d)을 그대로 쓰고 있어서, 호실만 물러나고 면적만 검게 남아 있었다.
   ⚠️ 아래 `.bsp-area-py`(평)는 그대로 둔다 — 그건 값이 아니라 «환산 보조줄」이다. */
/* ── 면적 두 줄도 «넘치면 줄인다» (써니 2026-08-27) ────────────────────────────
   증상은 "평이 두 줄이 된다" 였지만 실측하니 **㎡ 줄이 쪼개진 것**이었다:
     면적 칸 73px · "2,914.95㎡" 가 14px 에서 **74px** → 1px 초과 → ㎡ 만 다음 줄로 떨어져
     칸이 «숫자 / ㎡ / (평)» 세 줄이 됐다. 13px 이면 들어간다.
     "(881.77평)" 은 11.5px 에서도 들어간다 — 평은 원래 안 넘쳤다.
   ⚠️ `nowrap` 이 **판정의 전제**다. 이게 없으면 글자가 조용히 접혀 `scrollWidth`가
      `clientWidth` 와 같아지고, JS 가 «안 넘쳤다» 로 읽는다(그래서 지금까지 못 잡았다). */
.bsp-area-m2 {
  font-size: 14px; font-weight: 600; letter-spacing: -.02em; color: #7c828a;
  font-variant-numeric: tabular-nums;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.bsp-area-m2--tight { font-size: 12px; }
.bsp-area-py {
  font-size: 11.5px; font-weight: 500; color: #82888f;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.bsp-area-py--tight { font-size: 10px; }
/* ── 금액 두 종류의 색 **정본** (써니 2026-08-27) ─────────────────────────────
   이 칸엔 «임대(보증금/월차임)» 또는 «매매가» 중 하나가 온다. 라벨이 없어 색으로만
   갈리는데, 그러면 보는 사람이 어느 쪽인지 알 수 없다 — 그래서 헤더에 색 범례를 뒀다.
   ⚠️ 범례의 점(.bsp-dot--*)은 **여기 두 값을 그대로 쓴다.** 한쪽만 바꾸면 범례가
      거짓말을 하고, 그건 «틀린 금액을 읽는» 사고라 화면이 멀쩡해도 위험하다.
      building_summary_popup_test 가 둘이 갈라지는 걸 막는다.
   빨강은 뱃지 팔레트의 red.fg(#dc2626, lib/badge_colors) — 이미 이 저장소가 쓰는 값이다. */
.bsp-money { display: flex; align-items: baseline; gap: 4px; }
.bsp-money-value {
  font-size: 13px; font-weight: 800; letter-spacing: -.02em; color: #1b64da;
  font-variant-numeric: tabular-nums;
}
.bsp-money-value--sale { color: #dc2626; }
/* 두 값이 나란히 설 때만 한 단계 작게 — 실측: "65,000 / 6,300" 은 13px 에서 94.7px 이고
   칸의 숫자 자리는 108 − gap 4 − 「만」 10.4 = 93.6px 다. 12px 이면 들어간다.
   ⚠️ 칸 폭(108px)과 짝이다. 칸을 줄이면 이 값도 다시 재야 한다. */
.bsp-money-value--tight { font-size: 12px; }

/* ── 매매 + 임대가 둘 다일 때: 위 매매 / 아래 보증금·월차임 (써니 2026-08-27) ─────
   한 줄에 셋을 세우면 "너무 정신없다" 는 판단이었다. 쌓으면 폭이 반으로 줄어드는 대신
   줄이 하나 늘어나므로 글씨를 한 단계 더 줄인다(11px).
   실측: 최장 "45,000 / 2,650" 이 11px 에서 80.1px + gap 3 + 「만」 8.7 = 91.8px < 108px.
   ⚠️ 색은 위 «금액 두 종류의 색 정본» 그대로다 — 쌓는다고 색 규칙이 갈리면 범례가 깨진다. */
.bsp-money-stack { display: flex; flex-direction: column; gap: 1px; align-items: flex-start; }
.bsp-money-line { display: flex; align-items: baseline; gap: 3px; }
.bsp-money-stack .bsp-money-value { font-size: 11px; }
.bsp-money-stack .bsp-money-unit { font-size: 10px; }
.bsp-money-unit { font-size: 12px; font-weight: 600; color: #7c828a; }
.bsp-note {
  font-size: 13.5px; line-height: 1.5; color: #5c626b; text-wrap: pretty;
}
.bsp-chevron { font-size: 15px; color: #a8adb4; text-align: right; }
.bsp-empty {
  padding: 28px 12px; text-align: center;
  font-size: 13.5px; color: #7c828a;
}
.bsp-empty[hidden] { display: none; }
