/* ═══════════════════════════════════════════════════════════════
   IoT 모니터링 — 디자인 토큰 + Tabler 커스텀 레이어
   규칙: 색은 아래 토큰 또는 Tabler 변수(--tblr-*)만 쓴다.
   컴포넌트에 새 hex 값을 직접 쓰지 않는다. 상세 규칙: web/DESIGN.md
   ═══════════════════════════════════════════════════════════════ */

:root {
  /* light-dark()의 라이트 기준. 다크는 아래 [data-bs-theme="dark"]가 dark로 덮는다.
     (Tabler도 다크에 color-scheme:dark를 주지만, 우리 :root가 나중에 로드돼 이기므로 직접 명시한다) */
  color-scheme: light;

  /* 브랜드 — 라이트/다크 값이 다르면 light-dark(라이트, 다크) 한 줄로 쌍을 이룬다.
     다크 값을 따로 덮어쓰지 않는다 (한 곳만 고치면 되게) */
  --iot-brand: light-dark(#1a3a5c, #1f2021);      /* 상단 바 전용 — 다크에선 무채색 */
  --iot-nav-accent: light-dark(#1a3a5c, #7ea6d8); /* 활성 탭 — 다크에선 네이비가 안 보여 밝힘 */
  --iot-identity: #2f7d4f;     /* 아바타 초록 — 사용자 아이덴티티 전용 */
  --iot-test-brand: light-dark(#8a4b08, #4d3413); /* 테스트 사이트의 상단 바 — 운영의 남색과 갈리게 (web/site_mode.py) */

  /* 카테고리 색 — gicon 원형 아이콘 전용 팔레트 (여기 외 용도 금지) */
  --iot-cat-purple: #7c5cd6;
  --iot-cat-green:  #4c9a52;
  --iot-cat-blue:   #3f8cd8;
  --iot-cat-pink:   #d86ba2;
  --iot-cat-orange: #e0913f;
  --iot-cat-teal:   #38a3a5;
  --iot-cat-red:    #d65c5c;
  --iot-cat-indigo: #5c7cd6;
  --iot-cat-gray:   #6b7280;

  /* 형태 — 바깥 모서리 lg, 그룹 안쪽 모서리 sm, 버튼 pill 3단계만 사용 */
  --iot-radius-lg: 16px;
  --iot-radius-sm: 6px;
  --iot-radius-pill: 999px;

  /* 필터 행(구글형 검색·칩) 전용 색 — 검색 채움 배경, 필터 적용 칩의 파란 톤.
     포커스 그림자는 색이 아니라 light-dark()를 못 쓰므로 라이트 값만 여기 두고
     다크 블록에서 링 형태로 덮는다 */
  --iot-field-bg: light-dark(#f1f3f4, #2a2b2c);
  /* 입력 필드(폼) — 화면마다 다른 색·크기를 두지 않기 위한 한 벌 (2026-08-31) */
  --iot-input-bg: light-dark(#f7f9fb, #24282d);
  --iot-input-border: light-dark(#c8d0d9, #454c55);
  --iot-chip-active-bg: light-dark(#e8f0fe, rgba(116, 169, 226, 0.16));
  --iot-chip-active-fg: light-dark(#174ea6, #a8c7ec);
  --iot-search-focus-shadow: 0 1px 6px rgba(32, 33, 36, 0.22);
  /* 자체 분류 열(파생 열) 바탕 — 그린링크 원본과 다른 출처임을 열 자체가 말한다.
     이상 행에서는 붉은 기가 도는 쪽으로 (raw-flag 배경과 어울리게) */
  --iot-own-bg: light-dark(#eef3f9, #242a31);
  --iot-own-bg-flag: light-dark(#f7ecef, #2f272c);
  /* 팝업 바깥 어둡게 — 라이트는 남색 40%, 다크는 검정 60%(남색은 다크 바탕에서 안 보인다). 2026-09-03 */
  --iot-backdrop: light-dark(rgba(24, 36, 51, 0.40), rgba(0, 0, 0, 0.60));
  /* 물러나기 버튼(.btn-link.link-secondary) 글자의 hover 색. Tabler 는 .link-secondary:hover 에
     라이트용 짙은 회색(#566274)을 !important 로 박아 두어 다크 바탕에서 hover 순간 글자가
     흐려졌다(#1e1f20 위 2.5:1). 라이트는 Tabler 값 그대로, 다크는 보조색보다 한 단계 밝게. 2026-09-04 */
  --iot-link-secondary-hover: light-dark(#566274, #c2c7cd);

  /* 표면·텍스트 — Tabler 변수에 위임해 다크모드 자동 대응 */
  --iot-surface: var(--tblr-bg-surface);
  --iot-surface-hover: var(--tblr-bg-surface-tertiary);
  --iot-border: var(--tblr-border-color-translucent);
  --iot-text-secondary: var(--tblr-secondary);
  --iot-title-sub-gap: 16px;   /* 제목 옆 부제·코드·배지 간격 — 페이지·섹션·카드 제목 공통 (2026-09-24, DESIGN.md) */

  /* 30분 원본 그래프 전용 색 (raw_chart.js). 선 색은 시설 순서대로 돌려 쓰고,
     분류 색은 선과 겹치지 않게 붉은·주황 계열로 몰아 두었다 — 선에 붉은 계열을 쓰지 않는 이유 */
  --iot-chart-1: light-dark(#3f8cd8, #74a9e2);
  --iot-chart-2: light-dark(#4c9a52, #6fbf76);
  --iot-chart-3: light-dark(#38a3a5, #5cc3c5);
  --iot-chart-4: light-dark(#5c7cd6, #90a8e6);
  --iot-chart-5: light-dark(#7d8b3f, #b0bd6a);
  --iot-chart-grid: light-dark(#e9ebee, #34363a);
  --iot-chart-guide: light-dark(#9aa0a6, #8a8f95);
  --iot-flag-missing: light-dark(#c0392b, #e57366);   /* 미수신 — 자료가 아예 오지 않음 */
  --iot-flag-power:   light-dark(#e0913f, #edb063);   /* 전원단절 */
  --iot-flag-comm:    light-dark(#c9a227, #ddbd52);   /* 통신불량 */
  --iot-flag-range:   light-dark(#d86ba2, #e896c0);   /* 비정상범위 */
  --iot-flag-prevent: light-dark(#7c5cd6, #a68ce6);   /* 방지시설 비정상 */

  /* 대시보드 상태 색 (지도 핀·현황 네 칸·표 배지) — 보고서 판정과 같은 낱말 넷 (2026-09-23 통일).
     배지 팔레트를 그대로 쓸 수 없어 따로 둔다 — 지도에서는 세 판정이 한눈에 갈려야 한다. 심한 쪽이 붉은색이다. */
  --iot-map-ok:     light-dark(#4c9a52, #6fbf76);   /* 양호 */
  --iot-map-advise: light-dark(#d98324, #edb063);   /* 개선 권고 — 자료는 괜찮지만 조치가 필요하다 (사람이 고른다) */
  --iot-map-check:  light-dark(#c0392b, #e57366);   /* 개선 필요 — 장기 미수신이거나 이상이 잦다 */
  --iot-map-none:   light-dark(#8b9099, #949aa3);   /* 자료 없음 — 판단하지 않았다 */
  --iot-map-ok-soft:     light-dark(#e8f3e9, rgba(111, 191, 118, 0.16));
  --iot-map-advise-soft: light-dark(#fbeedd, rgba(237, 176, 99, 0.16));
  --iot-map-check-soft:  light-dark(#fae7e4, rgba(229, 115, 102, 0.16));
  --iot-map-none-soft:   light-dark(#eceef1, rgba(148, 154, 163, 0.16));

  /* 사업장 지정 수집 모달의 하루 막대(uptime bar) 색 — 하루의 '수집 상태'라 분류색(flag)·
     대시보드 판정색(map)과 의미가 달라 따로 둔다 */
  --iot-cov-done: light-dark(#57a865, #4f9c5e);   /* 수집됨 */
  --iot-cov-todo: light-dark(#e8a54b, #d99a3f);   /* 안 받음 — 이번 실행이 받는다 */
  --iot-cov-fail: light-dark(#d65c4e, #cf5a4c);   /* 조회 실패 — 다시 시도한다 */
  --iot-cov-none: light-dark(#c3cbd4, #565e68);   /* 자료 없음(조회 0행) — 빗금은 아직 다시 받는 날 */
  --iot-cov-off:  light-dark(#e9edf1, #2e3236);   /* 오늘·앞날, 기록 없는 달 */

  /* 폼의 '확인 요청' 글색 (사업장 추가의 비슷한 이름 안내) — 오류(빨강)가 아니라 사람이
     판단할 일이라 색을 가른다. 배지 노랑보다 어둡게 두어 흰 바탕에서도 읽힌다 */
  --iot-warn-text: light-dark(#8a6410, #ddbd52);

  /* IBM Plex Sans KR 셀프호스팅(static/fonts/ibm-plex-sans-kr) — 상용서버에서도 동일 렌더.
     받아 둔 굵기는 400·500·600·700 뿐이다(index.css 머리말 참고) */
  --tblr-font-sans-serif: "IBM Plex Sans KR", "Apple SD Gothic Neo",
    "Malgun Gothic", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

[data-bs-theme="dark"] {
  color-scheme: dark; /* light-dark()가 다크 값을 고르는 기준 */

  /* 다크에서 대비를 맞춘 색 — 여기서 한 번만 정의하고 아래 규칙들이 참조한다.
     Tabler의 -rgb 짝(rgba 합성용)은 hex에서 계산할 수 없어 손으로 맞춰야 한다 */
  --iot-dark-blue: #74a9e2;        /* 링크·파랑 배지·외곽선 버튼 글자 */
  --iot-dark-blue-hover: #93bdeb;
  --iot-dark-blue-border: #4d7db3;
  --iot-dark-red: #e88a8a;
  --iot-dark-red-border: #b06060;  /* 빨강 외곽선 버튼 테두리 */
  --iot-dark-muted: #a8b3c2;

  /* 검색 포커스: 다크에선 그림자가 안 보여 파란 링으로 대체 */
  --iot-search-focus-shadow: 0 0 0 1px var(--iot-dark-blue-border);

  /* 무채색 톤 (구글 다크 참고): 검정 배경 / 회색 표면 / 흰 텍스트.
     하이라이트(파랑)·아이콘·배지 색은 그대로 둔다 */
  --tblr-body-bg: #131314;
  --tblr-bg-surface: #1e1f20;           /* 카드·리스트 표면 */
  --tblr-bg-surface-secondary: #131314; /* body 배경 */
  --tblr-bg-surface-tertiary: #2a2b2c;  /* hover */
  --tblr-border-color: #3c4043;
  --tblr-border-color-translucent: rgba(255, 255, 255, 0.08);
  --tblr-body-color: #e8eaed;
  --tblr-body-color-rgb: 232, 234, 237;

  /* 가시성 교정: Tabler 다크 기본 보조색·링크색이 WCAG AA(4.5:1) 미달이라 밝힘 */
  --tblr-secondary: #9aa0a6;
  --tblr-secondary-rgb: 154, 160, 166;
  --tblr-secondary-color: rgba(154, 160, 166, 0.85);
  --tblr-link-color: var(--iot-dark-blue);
  --tblr-link-color-rgb: 116, 169, 226;
  --tblr-link-hover-color: var(--iot-dark-blue-hover);
  --tblr-link-hover-color-rgb: 147, 189, 235;
}

/* 다크모드 배지: -lt 글자색이 라이트용 본색 그대로 와서 어두움 (red 3.15:1 등) */
[data-bs-theme="dark"] .bg-red-lt       { color: var(--iot-dark-red) !important; }
[data-bs-theme="dark"] .bg-blue-lt      { color: var(--iot-dark-blue) !important; }
[data-bs-theme="dark"] .bg-secondary-lt { color: var(--iot-dark-muted) !important; }

/* 다크모드 외곽선 버튼: 본색 파랑(3.14:1)이 어두워 글자·테두리만 밝힘 (hover 채움색은 유지) */
[data-bs-theme="dark"] .btn-outline-primary {
  --tblr-btn-color: var(--iot-dark-blue);
  --tblr-btn-border-color: var(--iot-dark-blue-border);
}
/* 빨강도 같은 이유로 교정한다 (#d63939 는 어두운 표면 위 3.5:1) — 고스트는 테두리가 없으니 글자만 */
[data-bs-theme="dark"] .btn-outline-danger {
  --tblr-btn-color: var(--iot-dark-red);
  --tblr-btn-border-color: var(--iot-dark-red-border);
}
[data-bs-theme="dark"] .btn-ghost-danger { --tblr-btn-color: var(--iot-dark-red); }

/* 다크모드 원본 표: 토·일 표기·이상 행 수·상태 문구가 본색 그대로면 어두워 안 보임
   (text-danger 는 Tabler 유틸리티가 !important 라 같은 강도로 덮는다) */
[data-bs-theme="dark"] .raw-day-sep .sat { color: var(--iot-dark-blue); }
[data-bs-theme="dark"] .raw-day-sep .sun { color: var(--iot-dark-red); }
[data-bs-theme="dark"] .raw-week-tab .cnt { color: var(--iot-dark-red); }
[data-bs-theme="dark"] .raw-month-scroll .text-danger { color: var(--iot-dark-red) !important; }
[data-bs-theme="dark"] .raw-chart-tip-row .wy.bad { color: var(--iot-dark-red); }
[data-bs-theme="dark"] .raw-chart-xlab.sat { fill: var(--iot-dark-blue); }
[data-bs-theme="dark"] .raw-chart-xlab.sun { fill: var(--iot-dark-red); }

/* ── 레이아웃 골격 ─────────────────────────────────── */
body { background: var(--tblr-bg-surface-secondary); }

/* 링크 hover 밑줄 전면 제거 — Tabler 기본(a:hover{underline})을 덮는다.
   .btn/.nav-link/.dropdown-item만 Tabler가 자체로 꺼 두어 화면마다 들쭉날쭉했음.
   .btn-link(모달 취소)는 a:hover보다 구체적이라 따로 명시. 링크 구분은 밑줄이 아니라 색으로 한다 */
a:hover,
.btn-link:hover,
.btn-link:focus-visible { text-decoration: none; }

/* 컨테이너 좌우 여백: Tabler 기본(8px)이 좁아 데스크톱 24px, 모바일 16px */
.container-xl { --tblr-gutter-x: 3rem; }
/* 데이터형 화면은 폭을 가득 쓴다 (2026-09-15) — 상한 없이 좌우 여백만. 표는 자기 상자 안에서
   스크롤하므로 넓어질수록 한 번에 보이는 열이 는다. 여백은 container-xl 의 좌우(1.5rem)와 같게 맞춰
   화면을 오갈 때 왼쪽 선이 흔들리지 않는다. 어느 화면이 데이터형인지는 shell/AppShell.tsx 의 isWide. */
.iot-container-wide { width: 100%; padding-inline: 1.5rem; }
@media (max-width: 576px) { .iot-container-wide { padding-inline: 1rem; } }
@media (max-width: 576px) {
  .container-xl { --tblr-gutter-x: 2rem; }
}

.page-title { font-size: 1.375rem; font-weight: 500; }
/* 제목 옆 부제(fs-4 회색)·사업장 코드·배지는 h2 안의 다음 항목 — 띄어쓰기 한 칸(≈5px)이 아니라 --iot-title-sub-gap 으로 뗀다.
   글자 크기가 달라 baseline 으로 맞추고, 좁은 화면에서는 부제가 다음 줄로 내려간다 (2026-09-24 시안 비교) */
.page-title { display: flex; flex-wrap: wrap; align-items: baseline; column-gap: var(--iot-title-sub-gap); row-gap: 2px; }

/* 제목 위 여백: Tabler 기본(page-body 24px + page-header 24px = 48px)이 과해서 축소 */
.page-body { margin-top: 1rem; }
.page-wrapper .page-header { margin-top: 0; }

/* 설정형 페이지의 좁은 본문 폭 (구글 계정 스타일) */
.page-narrow { max-width: 560px; margin-inline: auto; }

/* Tabler .page-header 기본값(column + justify-center)이 flex-row로 쓸 때
   가로 중앙 정렬로 새어나오므로 초기화 */
.page-header.flex-row { justify-content: flex-start; }

/* 2뎁스 페이지 뒤로가기 화살표 — 페이지 제목 왼쪽, 원형 hover */
.iot-back {
  width: 36px; height: 36px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  color: inherit; flex: none;
}
.iot-back:hover { background: var(--iot-surface-hover); color: inherit; }
.iot-back svg {
  width: 20px; height: 20px;
  stroke: currentColor; fill: none; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}

/* 읽기 전용 필드 표식 — datagrid 라벨 옆 자물쇠 (출처는 title 툴팁) */
.iot-lock {
  width: 12px; height: 12px; margin-left: 3px; vertical-align: -1px;
  stroke: var(--iot-text-secondary); fill: none; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}

/* ── 팝업(모달) 한 벌 (2026-09-03) ──────────────────────
   위치·바깥 어둡게·모서리·라벨·등장 효과를 여기 한 곳에서 정한다. 화면의 모달 18개가 전부
   손으로 쓴 HTML 이라(부품 modal() 은 카탈로그에서만 쓰인다) 화면마다 옵션을 붙이지 않고
   여기가 전부에 걸린다 — 화면 쪽에 modal-blur·modal-dialog-centered 를 붙이지 마라(테스트가 막는다).
   규칙과 근거는 DESIGN.md '팝업은 한 벌이다'. */
/* 모서리: 바깥 16px 을 .modal-content 에 직접 덮으면 머리·푸터 배경의 안쪽 반지름(Tabler 가
   바깥-1px 로 계산)이 7px 로 남아 푸터 회색이 곡선 밖으로 삐져나온다 — 계산의 근원 변수를 바꾼다 */
.modal { --tblr-modal-border-radius: var(--iot-radius-lg); }
/* 위치: 화면 높이의 40% 지점이 팝업의 중심(정가운데보다 조금 위 — 눈이 중심으로 느끼는 자리).
   최소 높이 80vh 짜리 상자의 가운데 = 40vh. 내용이 더 길면 상자가 늘어나 위 여백부터 채운다 */
.modal-dialog {
  display: flex; align-items: center;
  min-height: calc(80vh - var(--tblr-modal-margin) * 2);
}
/* 화면보다 긴 팝업(modal-dialog-scrollable)은 본문만 안에서 스크롤한다 — 제목·버튼은 항상 보인다.
   Tabler 의 height:100% 를 풀어야 짧을 때 위 40% 자리를 지킨다 */
.modal-dialog-scrollable { height: auto; }
.modal-dialog-scrollable .modal-content { max-height: calc(100vh - var(--tblr-modal-margin) * 2); }
/* 바깥은 흐림 없이 어둡게만. Tabler 기본(남색 24%)은 떠 있는 느낌이 약하고 다크에선 바탕과 같아
   보이지 않는다 — 라이트/다크 한 쌍은 :root 의 --iot-backdrop */
.modal-backdrop { background-color: var(--iot-backdrop); --tblr-backdrop-opacity: 1; }
/* 닫기 X 는 32px 원 — 뒤로가기 화살표·아이콘 버튼과 같은 문법 (2026-09-04). Tabler 기본은 머리 오른쪽
   위에 56px 사각을 절대 배치하고(그 자리를 비우려고 머리의 오른쪽 여백을 3.5rem 으로 넓혀 둔다) 아이콘만
   진해져(40%→75%) 눌리는 범위가 보이지 않았고 다크에서는 더 안 보였다. 흐름 안으로 되돌려 제목 줄에
   세우고(자료 수집 팝업은 대상 표시 뒤에 선다) **머리 여백을 좌우 같게 되돌린다** — 넓은 여백을 남기면
   X 가 그 빈 자리 안쪽에 서서 모서리에서 64px 들어간다(실측, 사용자 지적). 음수 여백 12px 로 아이콘 중심이
   예전과 같은 모서리 28px 지점에 온다 */
.modal-header { padding-right: 1.5rem; }
.modal-header .btn-close {
  position: static; box-sizing: border-box; width: 32px; height: 32px; padding: 0;
  margin: 0 -0.75rem 0 auto; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background-image: none; color: inherit; filter: none; opacity: 0.6;
}
/* X 는 배경 그림이 아니라 글자색으로 그린다(mask) — Tabler 는 다크에서 아이콘을 색 반전 필터로 밝히는데,
   그 필터가 hover 바탕까지 뒤집어 흰 원만 남고 X 가 사라졌다(실측). 글자색을 따르면 두 모드 다 맞다 */
.modal-header .btn-close::before {
  content: ""; width: 1em; height: 1em; background: currentColor;
  -webkit-mask: var(--tblr-btn-close-bg) center / 1em auto no-repeat;
  mask: var(--tblr-btn-close-bg) center / 1em auto no-repeat;
}
.modal-header .btn-close:hover { background-color: var(--iot-surface-hover); opacity: 1; }
.modal-header .btn-close:focus { box-shadow: none; }
.modal-header .btn-close:focus-visible { outline: 2px solid var(--tblr-primary); outline-offset: 2px; opacity: 1; }
/* 라벨은 읽기 화면의 항목 이름(.gitem-label)과 같은 단계 — 제목만 굵고, 값이 라벨보다 진하다.
   모든 폼에 한 벌(로그인·설정 화면 포함) — 팝업 안에서만 다르게 두면 언젠가 또 어긋난다 */
.form-label { font-size: 13px; font-weight: 400; color: var(--iot-text-secondary); margin-bottom: 0.375rem; }
.form-hint { font-size: 13px; }

/* ── hover 는 스며들게 (2026-09-01) ────────────────────
   바탕이 채워지는 hover 는 전부 같은 속도로 바뀐다. 예전에는 전환이 표 행(0.12s)과 필터
   검색(0.15s)에만 있어 나머지는 딸깍 하고 바뀌었고, 카탈로그에서 '뒤로가기 화살표에 hover 가
   없다'는 지적으로 드러났다(그 자리는 바탕색까지 hover 색과 같아 아예 안 보였다).
   **hover 에서 --iot-surface-hover 를 까는 부품을 새로 만들면 이 목록에 함께 적는다** —
   선택자로 묶을 방법이 없어(속성으로 고를 수 없다) 여기 한 줄이 그 역할을 한다.
   빠뜨리면 test_design_system.HoverShapeTest 가 잡는다 (2026-09-04).
   hover 의 모양 규칙은 DESIGN.md 'hover 는 눌리는 범위를 그대로 보인다' — 아이콘 하나는 원,
   글자 한 줄은 알약, 여러 줄 덩어리는 그 상자. 글자색만 바뀌는 hover 는 두지 않는다. */
.btn, .iot-iconbtn, .iot-back, .modal-header .btn-close,
.iot-brand-link, .iot-account, .iot-side-link, .iot-side-rail-btn, .iot-side-burger, .iot-colpick-opt, .iot-chip-add, .detail-tab::before,
a.gitem, button.gitem, .gitem[data-modal], .stat-row > td, .insp-pick-list button,
.iot-chip .dropdown-toggle, .iot-chip-toggle, .iot-chip-clear, .biz-tab, .pp-chip, .insp-more,
.raw-week-tab, .pp-preset, .pp-navbtn, .pp-day, .raw-chart-key, button.iot-sort,
.iot-switch, .info-item, .insp-status .dropdown-toggle, .rules-chip button,
.chg-fold > summary, .ds-navlink, .ds-fold > summary, .iot-filter-back, .iot-chip-filter .lbl,
.mail-tab, .mail-token, .iot-badge-btn {
  transition: background-color 0.12s ease, color 0.12s ease, border-color 0.12s ease, filter 0.12s ease;
}
/* 움직임을 줄이도록 설정한 환경에서는 즉시 바뀐다 (다른 전환과 같은 규칙) */
@media (prefers-reduced-motion: reduce) {
  .btn, .iot-iconbtn, .iot-back, .modal-header .btn-close,
  .iot-brand-link, .iot-account, .iot-side-link, .iot-side-rail-btn, .iot-side-burger, .iot-colpick-opt, .iot-chip-add, .detail-tab::before,
  a.gitem, button.gitem, .gitem[data-modal], .stat-row > td, .insp-pick-list button,
  .iot-chip .dropdown-toggle, .iot-chip-toggle, .iot-chip-clear, .biz-tab, .pp-chip, .insp-more,
  .raw-week-tab, .pp-preset, .pp-navbtn, .pp-day, .raw-chart-key, button.iot-sort,
  .iot-switch, .info-item, .insp-status .dropdown-toggle, .rules-chip button,
  .chg-fold > summary, .ds-navlink, .ds-fold > summary, .iot-filter-back, .iot-chip-filter .lbl,
  .mail-tab, .mail-token, .iot-badge-btn { transition: none; }
}

/* ── 상단 브랜드 바 + 보조 내비 ────────────────────── */
.iot-navbar {
  background: var(--iot-brand);
  min-height: 0;
}
/* 상단 바의 두 링크(서비스명·계정)는 흰색 12% 알약으로 범위를 보인다 — 남색 바탕과 다크의 무채색 바탕
   둘 다에서 같은 식이 통한다. 계정은 이름과 아바타를 한 링크로 묶었다(구글 계정 칩, 2026-09-04) —
   이름만 따로 두면 누를 수 없는 글자가 되고, 아바타만 반응하면 어디까지가 버튼인지 보이지 않는다 */
/* 서비스명은 로고 마크(favicon 과 같은 그림) + 글자를 한 알약으로 묶는다 (2026-09-08) */
/* 알약의 왼쪽 끝은 왼쪽 메뉴 항목의 알약(메뉴 안쪽 여백 8px)과 같은 선에 맞춘다 — 상단 바 여백 16px 에서 8px 을 되돌린다.
   높이도 메뉴 항목과 같은 36px */
.iot-brand-link { display: inline-flex; align-items: center; gap: 10px; height: 36px; padding: 0 16px 0 8px; margin-left: -8px; border-radius: var(--iot-radius-pill); }
.iot-brand-mark { width: 24px; height: 24px; flex: none; }
.iot-account {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 3px 3px 3px 12px; margin-right: -3px; border-radius: var(--iot-radius-pill);
  color: inherit; text-decoration: none;
}
.iot-brand-link:hover, .iot-account:hover { background: rgba(255, 255, 255, 0.12); }
.iot-brand-link:focus-visible, .iot-account:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.iot-navbar-user { color: rgba(255, 255, 255, 0.78); }
.iot-account:hover .iot-navbar-user { color: #fff; }
/* 테스트 사이트 (2026-09-28) — 서버가 <html data-iot-site="test"> 를 달아 준다(web/spa.py). 운영과 한눈에 갈리게
   상단 바를 갈색으로 바꾸고 서비스명 옆에 '테스트' 알약을 붙인다. 로그인 화면은 상단 바가 없어 제목 옆에 붙인다.
   탭 제목 앞의 [테스트] 는 frontend/src/lib/title.ts */
html[data-iot-site="test"] .iot-navbar { background: var(--iot-test-brand); }
html[data-iot-site="test"] .iot-brand-link::after,
html[data-iot-site="test"] .page-center h1::after {
  content: "테스트"; display: inline-block; padding: 5px 10px; border-radius: var(--iot-radius-pill);
  font-size: 12px; font-weight: 600; line-height: 1; color: #fff; vertical-align: middle;
}
html[data-iot-site="test"] .iot-brand-link::after { background: rgba(255, 255, 255, 0.2); }
html[data-iot-site="test"] .page-center h1::after { margin-left: 10px; background: var(--iot-test-brand); }
/* ── 왼쪽 메뉴 (2026-09-07, 상단 보조 내비를 대체) ──────────────
   묶음 제목 + 항목(자식은 들여쓰기), 현재 위치는 옅은 파랑 알약(밑줄이 아니다 — 세로 목록에 밑줄은 자리가 없다),
   hover 는 알약. [접기]는 아이콘 줄(60px)만 남기고 접힘은 브라우저에 기억한다(AppShell). 992px 아래에서는
   서랍이 되어 상단 바의 햄버거로 연다. 근거·비교는 docs/결정기록.md '사업장 정보 조회'. */
.iot-shell { --iot-side-w: 232px; --iot-topbar-h: 68px; }   /* 실제 상단 바 높이(로고 알약 36px + 여백)와 같게 — 60 이던 때는 페이지가 늘 8px 넘쳤다 (2026-09-09 실측) */
/* 상단 바를 고정해 왼쪽 메뉴가 그 아래 정확히 남은 높이만 쓰게 한다 — 100vh 로 두면 아래 60px 이 화면 밖으로
   밀려 맨 아래 [접기]가 보이지 않았다 (2026-09-08 실측: 버튼 top 873 > 창 857) */
.iot-shell .iot-navbar { position: sticky; top: 0; z-index: 1030; min-height: var(--iot-topbar-h); }
.iot-shell-rail { --iot-side-w: 60px; }
.iot-shell-body { display: grid; grid-template-columns: var(--iot-side-w) minmax(0, 1fr); min-height: calc(100vh - var(--iot-topbar-h)); }
.iot-main { grid-column: 2; min-width: 0; }
.iot-side {
  grid-column: 1; position: sticky; top: var(--iot-topbar-h); align-self: start; height: calc(100vh - var(--iot-topbar-h));
  display: flex; flex-direction: column; gap: 2px; padding: 16px 8px 10px;   /* 위 여백은 항목과 묶음 제목 사이(14px)와 비슷하게 */
  background: var(--iot-surface); border-right: 1px solid var(--iot-border); overflow: hidden auto;
}
.iot-side-nav { display: flex; flex-direction: column; gap: 2px; }
.iot-side-group { display: flex; flex-direction: column; gap: 2px; }
.iot-side-grp { font-size: 11px; font-weight: 600; letter-spacing: 0.06em; color: var(--iot-text-secondary); padding: 12px 12px 4px; white-space: nowrap; }
.iot-side-link {
  display: flex; align-items: center; gap: 10px; height: 36px; padding: 0 10px 0 12px;
  border-radius: var(--iot-radius-pill); color: var(--tblr-body-color); text-decoration: none; font-size: 0.9375rem; white-space: nowrap;
}
.iot-side-link:hover { background: var(--iot-surface-hover); color: inherit; }
.iot-side-link[aria-current="page"] { background: var(--iot-chip-active-bg); color: var(--iot-chip-active-fg); font-weight: 600; }
.iot-side-link.child { height: 32px; padding-left: 40px; font-size: 0.875rem; }
.iot-side-link.child::before { content: ""; width: 4px; height: 4px; border-radius: 50%; background: currentColor; opacity: 0.5; margin-left: -16px; margin-right: 12px; }
.iot-side-ic { width: 18px; height: 18px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.iot-side-rail-btn {
  margin-top: auto; height: 34px; border: 0; background: transparent; border-radius: var(--iot-radius-pill);
  color: var(--iot-text-secondary); cursor: pointer; align-items: center; gap: 10px; padding: 0 12px; font: inherit; font-size: 0.8125rem; white-space: nowrap;
}
.iot-side-rail-btn:hover { background: var(--iot-surface-hover); }
.iot-shell-rail .iot-side .lbl, .iot-shell-rail .iot-side-grp { display: none; }
.iot-shell-rail .iot-side-link { justify-content: center; padding: 0; }
/* 접힌 줄에서 자식 항목은 점 하나뿐이라 뜻이 없다 — 숨기고, 부모 아이콘이 그 자리를 대신한다 */
.iot-shell-rail .iot-side-link.child { display: none; }
.iot-shell-rail .iot-side-rail-btn { justify-content: center; padding: 0; }
.iot-side-burger {
  width: 36px; height: 36px; border: 0; border-radius: 50%; background: transparent; color: #fff;
  display: inline-grid; place-items: center; margin-right: 6px; cursor: pointer;
}
.iot-side-burger:hover { background: rgba(255, 255, 255, 0.12); }
.iot-side-burger .iot-side-ic { width: 22px; height: 22px; }
.iot-side-backdrop { display: none; }
@media (max-width: 991.98px) {
  .iot-shell-body { grid-template-columns: minmax(0, 1fr); }
  .iot-main { grid-column: 1; }
  .iot-side { position: fixed; inset: var(--iot-topbar-h) auto 0 0; height: auto; width: 260px; z-index: 1045; transform: translateX(-100%); transition: transform 0.16s ease; box-shadow: none; }
  .iot-shell-drawer .iot-side { transform: none; box-shadow: 0 8px 28px rgba(0, 0, 0, 0.25); }
  .iot-shell-drawer .iot-side-backdrop { display: block; position: fixed; inset: var(--iot-topbar-h) 0 0 0; z-index: 1040; background: var(--iot-backdrop); }
  .iot-shell-rail .iot-side .lbl, .iot-shell-rail .iot-side-grp { display: initial; }
  .iot-shell-rail .iot-side-link { justify-content: flex-start; padding: 0 10px 0 12px; }
  .iot-shell-rail .iot-side-link.child { display: flex; padding-left: 40px; }
  .iot-shell-rail .iot-side-link.child::before { margin-left: -16px; margin-right: 12px; }
}
@media (prefers-reduced-motion: reduce) { .iot-side { transition: none; } }
.iot-crumb { margin-bottom: 2px; }
.iot-crumb a { color: inherit; text-decoration: none; }
.iot-crumb a:hover { text-decoration: underline; }

/* ── 목록 표 도구: 열 선택·필터 추가 (2026-09-07) ───────────────── */
.iot-chip-add {
  height: 34px; padding: 0 14px; display: inline-flex; align-items: center; gap: 6px;
  font-size: 0.8125rem; font-family: inherit; color: var(--iot-text-secondary);
  border-radius: var(--iot-radius-pill); border: 1px dashed var(--tblr-border-color); background: transparent; cursor: pointer;
}
.iot-chip-add:hover { background: var(--iot-surface-hover); }
.iot-chip-filter .x {
  width: 22px; height: 22px; border: 0; border-radius: 50%; background: transparent; color: inherit;
  display: inline-grid; place-items: center; font-size: 12px; cursor: pointer; margin-left: 2px;
}
.iot-chip-filter .x:hover { background: color-mix(in srgb, currentColor 12%, transparent); }
/* ── 보고서 받기 메뉴·결과 줄 (보고서 화면 필터 행 끝, 2026-09-09) — 항목마다 건수, 받은 뒤 결과 한 줄 ── */
.iot-dl-menu { width: 300px; padding: 6px; }
.iot-dl-item { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: var(--iot-radius-sm); font-size: 0.8125rem; }
.iot-dl-item:disabled { opacity: 0.45; }
.iot-dl-ic { width: 28px; height: 28px; border-radius: 8px; display: grid; place-items: center; font-size: 11px; font-weight: 600; flex: none; }
.iot-dl-t { display: flex; flex-direction: column; line-height: 1.3; }
.iot-dl-t small { color: var(--iot-text-secondary); font-size: 11.5px; }
.iot-dl-n { margin-left: auto; font-variant-numeric: tabular-nums; font-weight: 500; }
.iot-dl-n small { color: var(--iot-text-secondary); font-weight: 400; }
.iot-dl-note { padding: 6px 10px 4px; font-size: 12px; color: var(--iot-text-secondary); line-height: 1.45; }
.iot-dl-result { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 10px; padding: 8px 20px; border-bottom: 1px solid var(--iot-border); font-size: 0.8125rem; }
.iot-dl-result .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--iot-map-ok); flex: none; }
.iot-dl-result .fname, .iot-dl-result .miss { color: var(--iot-text-secondary); }
.iot-dl-result .close { margin-left: auto; }
.iot-colpick { position: relative; }
.iot-colpick-menu { width: 340px; max-height: min(460px, var(--iot-float-max, 460px)); flex-direction: column; padding: 0; overflow: hidden; }
.iot-colpick-menu.show { display: flex; } /* Tabler 의 .dropdown-menu.show(display: block) 를 이기려면 두 클래스가 필요하다 — block 이면 푸터가 밀려 잘린다 */
.iot-colpick-head { padding: 10px 12px 8px; border-bottom: 1px solid var(--iot-border); }
.iot-colpick-body { overflow-y: auto; padding: 4px 0 6px; }
.iot-colpick-grp { display: flex; justify-content: space-between; align-items: center; padding: 8px 14px 4px; font-size: 11px; font-weight: 600; letter-spacing: 0.05em; color: var(--iot-text-secondary); }
.iot-colpick-grp .cnt { font-weight: 400; font-variant-numeric: tabular-nums; }
.iot-colpick-opt {
  display: flex; align-items: center; gap: 10px; width: 100%; padding: 5px 14px; margin: 0;
  font-size: 0.8125rem; font-family: inherit; color: inherit; text-align: left; background: transparent; border: 0; cursor: pointer;
}
.iot-colpick-opt:hover { background: var(--iot-surface-hover); }
.iot-colpick-opt .form-check-input { margin: 0; flex: none; }
.iot-colpick-opt .src { margin-left: auto; color: var(--iot-text-secondary); font-size: 11px; white-space: nowrap; }
.iot-colpick-opt.gone { color: var(--iot-text-secondary); }
.iot-colpick-foot { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-top: 1px solid var(--iot-border); }
.iot-colpick-foot .btn { margin-left: auto; }
.iot-colpick-foot .btn + .btn { margin-left: 0; }
.iot-colpick-opt .src.lock { color: var(--iot-chip-active-fg); }
.iot-filter-menu { width: 320px; }
.iot-filter-cond { padding: 8px 14px 12px; display: flex; flex-direction: column; gap: 8px; }
.iot-filter-cond > .iot-colpick-grp { padding-left: 0; padding-right: 0; }
.iot-filter-values { max-height: 220px; overflow-y: auto; margin: 0 -14px; }
/* 조건 종류(값 선택·글자 포함·날짜 이전/이후)와 빈칸만·값 있음 — 칩 문법 그대로 34px (팝업 안이라고 작게 만들지 않는다:
   크기는 .btn/.btn-sm 둘뿐이고 칩은 34px 하나다. 2026-09-09 — 28px 칩을 두었다가 옆 버튼과 키가 어긋나 되돌렸다) */
.iot-filter-ops { display: flex; flex-wrap: wrap; gap: 6px; }
.iot-filter-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.iot-filter-foot .btn { height: 34px; padding-top: 0; padding-bottom: 0; display: inline-flex; align-items: center; }   /* 옆 칩과 같은 34px — .btn 기본은 36px 이라 2px 어긋난다 */
/* 조건 편집의 맨 위 한 줄 — [‹ 다른 열]. 되돌아가는 길은 제목 옆이 아니라 항목 위에, 글자 한 줄이라 알약 */
.iot-filter-nav { padding: 6px 8px 0; }
.iot-filter-back {
  height: 34px; padding: 0 12px 0 6px; display: inline-flex; align-items: center; gap: 2px;
  font-size: 0.8125rem; font-family: inherit; color: var(--iot-text-secondary);
  border: 0; border-radius: var(--iot-radius-pill); background: transparent; cursor: pointer;
}
.iot-filter-back:hover { background: var(--iot-surface-hover); color: inherit; }
.iot-filter-back svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
/* 필터 칩 (2026-09-08) — 열 하나에 칩 하나. 왼쪽 글자(.lbl)는 누르면 조건을 고치는 버튼, 오른쪽 × 는 지우기.
   눌리는 범위가 둘이라 hover 도 둘로 갈린다(글자는 알약, × 는 원). 정렬 칩(.iot-chip-sort)은 글자가 버튼이 아니고
   테두리만 있는 칩이다 — 정렬을 고치는 자리는 표 머리다. */
/* .iot-chip-toggle(아래, padding 0 14px·gap 6px)보다 뒤에 오지 않으므로 두 클래스로 이긴다 — 한 클래스로 두었을 때
   칩 바깥 14px + 글자 버튼 안쪽 14px 이 겹쳐 양쪽에 빈 여백이 보였다 (2026-09-09 사용자 지적) */
.iot-chip-toggle.iot-chip-filter, .iot-chip-toggle.iot-chip-sort { padding: 0 4px 0 0; gap: 0; cursor: default; }
.iot-chip-filter .lbl {
  height: 100%; padding: 0 8px 0 14px; display: inline-flex; align-items: center; gap: 4px;
  border: 0; border-radius: var(--iot-radius-pill); background: transparent; color: inherit;
  font: inherit; font-weight: inherit; cursor: pointer; white-space: nowrap;
}
.iot-chip-filter .lbl:hover:not(:disabled) { background: color-mix(in srgb, currentColor 9%, transparent); }
.iot-chip-filter .lbl:disabled { cursor: default; }
.iot-chip-filter .lbl.static { cursor: default; }
.iot-chip-filter .k { font-weight: 400; opacity: 0.85; }
.iot-chip-sort .arrow { font-size: 12px; }
.tbl-num { font-variant-numeric: tabular-nums; }
.tbl-multi .more { color: var(--tblr-link-color); font-size: 0.75rem; white-space: nowrap; }
.tbl-unseen { color: var(--iot-text-secondary); font-style: italic; }

/* ── 현재 위치 밑줄: 한 모양 (2026-09-04) ──────────────
   사업장 상세 탭(왼쪽 1탭 바 포함)·규칙 화면 탭이 같은 밑줄을 쓴다 (상단 메뉴는 2026-09-07 왼쪽 메뉴가 되어 빠졌다) — 2px, 탭 폭 전체, 각진 끝,
   아래 경계선에 1px 겹쳐 그 자리의 회색 선을 대신한다. 정리 전에는 셋이 세 모양이었다(메뉴 2px inset 그림자 ·
   상세 탭 3px 둥근 글자 폭 · 규칙 탭 2px 인데 경계선 위에 층으로). GitHub·Vercel 이 이 밑줄에 우리와 같은
   글자 뒤 알약 hover 를 쓴다. 3px 둥근 쪽(Material)은 hover 가 칸 전체 채움이라 알약과 짝이 안 맞아 뺐다.
   탭 본체는 position: relative 여야 한다. 새 탭 화면은 이 목록에 선택자를 더한다 — 네 번째 모양을 만들지 않는다.
   탭 바에 overflow 를 걸면 밑줄의 겹침 1px 이 잘린다 — 그런 바는 경계선을 안쪽 그림자로 그린다(.detail-tabs 참고).
   규칙은 DESIGN.md '현재 위치 밑줄은 한 모양이다', 회귀는 test_design_system.HoverShapeTest */
.detail-tab[aria-selected="true"]::after,
.side-tabs .tab-like::after,
.rules-tab.active::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -1px;
  height: 2px; background: var(--iot-nav-accent);
}

/* ── 버튼: 구글식 알약 형태 + 넉넉한 좌우 패딩 ───────
   어느 자리에 어느 것을 쓰는지는 DESIGN.md '버튼'(역할 × 자리). 여기서는 모양과
   동작(hover·클릭·포커스·비활성)만 정한다. 크기는 둘뿐이다(.btn / .btn-sm) —
   세 번째 크기를 화면에서 만들지 않는다 (2026-09-03 정리 전에는 넷이었다). */
.btn {
  --tblr-btn-border-radius: var(--iot-radius-pill);
  --tblr-btn-padding-x: 1.25rem;
  --tblr-btn-box-shadow: none;
  --tblr-btn-active-shadow: none;   /* 눌림의 inset 그림자 — 그림자는 카드 하나뿐이다 */
}
.btn-sm {
  --tblr-btn-border-radius: var(--iot-radius-pill);
  --tblr-btn-padding-x: 0.875rem;
  --tblr-btn-padding-y: 0.25rem;
}
/* 외곽선·고스트·링크의 hover 는 속을 채우지 않고 자기 색으로 옅게 물든다 (2026-09-03).
   Tabler 기본은 hover 에 채움색 + 흰 글자로 뒤집혀, 옅은 회색으로 스며드는 나머지 부품
   (칩·표 행·뒤로가기)과 문법이 달랐다. 글자·테두리는 그대로, 바탕만 9%(클릭 16%).
   채움 버튼(btn-primary·btn-danger)은 Tabler 대로 한 단계 어두워진다. */
.btn-outline-primary, .btn-outline-secondary, .btn-outline-danger,
.btn-ghost-primary, .btn-ghost-secondary, .btn-ghost-danger {
  --tblr-btn-hover-color: var(--tblr-btn-color);
  --tblr-btn-hover-border-color: var(--tblr-btn-border-color);
  --tblr-btn-hover-bg: color-mix(in srgb, var(--tblr-btn-color) 9%, transparent);
  --tblr-btn-active-color: var(--tblr-btn-color);
  --tblr-btn-active-border-color: var(--tblr-btn-border-color);
  --tblr-btn-active-bg: color-mix(in srgb, var(--tblr-btn-color) 16%, transparent);
}
.btn-link {
  --tblr-btn-hover-bg: color-mix(in srgb, currentColor 9%, transparent);
  --tblr-btn-active-bg: color-mix(in srgb, currentColor 16%, transparent);
}
/* .link-secondary 의 hover·focus 글자색은 Tabler 가 !important 로 고정하므로 같은 강도로 덮는다 —
   토큰이 라이트/다크를 가른다(위 --iot-link-secondary-hover). 다른 .link-* 변형을 쓰게 되면
   같은 함정이 있으니(전부 라이트용 hex 고정) 같은 방식으로 교정한다 */
.link-secondary:hover,
.link-secondary:focus { color: var(--iot-link-secondary-hover) !important; }
/* 포커스 링은 한 가지 — 2px 파랑, 바깥 2px (칩·화살표 버튼과 같다). Tabler 의 반투명 그림자 링을 대체 */
.btn:focus-visible {
  box-shadow: none;
  outline: 2px solid var(--tblr-primary);
  outline-offset: 2px;
}

/* ── 아이콘 버튼: ‹ › ▲ ▼ 처럼 글자 하나짜리 정사각 버튼 (2026-09-03 한 벌로) ──
   기간 팝업·수집 모달의 달 넘김, 규칙 순서 바꾸기가 각각 다른 크기·테두리였다.
   28px 원, 테두리 없음, hover 는 옅은 회색 원(뒤로가기 화살표·팝업 X 와 같다 — 2026-09-04 에 작은
   모서리 사각에서 원으로. 아이콘 하나짜리는 원이다). 비활성은 0.4 (버튼과 같다) */
.iot-iconbtn {
  width: 28px; height: 28px; padding: 0; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-radius: 50%; background: none;
  color: var(--iot-text-secondary); font-family: inherit; font-size: 14px; line-height: 1;
  cursor: pointer;
}
.iot-iconbtn:hover { background: var(--iot-surface-hover); color: inherit; }
.iot-iconbtn[disabled], .iot-iconbtn:disabled { opacity: 0.4; cursor: default; }
.iot-iconbtn[disabled]:hover, .iot-iconbtn:disabled:hover { background: none; color: var(--iot-text-secondary); }
.iot-iconbtn:focus-visible { outline: 2px solid var(--tblr-primary); outline-offset: 2px; }

/* ── 입력 필드: 한 모양 (2026-08-31) ─────────────────
   글 상자·셀렉트·여러 줄 상자가 모두 같은 높이·테두리·채움색이다. 화면마다 다르면
   같은 폼 안에서도 칸마다 크기가 달라 보인다 (점검 기록 모달에서 드러났다).
   구 '셀렉트는 알약형' 규칙은 여기서 끝난다 — 알약은 고르는 칩(.iot-select·.btn)의 모양이고,
   폼 안의 셀렉트는 옆 칸(글 상자)과 같은 모양이어야 한 줄로 읽힌다. */
.form-control,
.form-select {
  min-height: 2.75rem;
  padding: 0.625rem 0.875rem;
  border-radius: var(--iot-radius-sm);
  border-color: var(--iot-input-border);
  background-color: var(--iot-input-bg);
}
/* 작은 칸(.form-control-sm)은 표 안에 들어가므로 높이만 되돌린다 — 색·모서리는 같다 */
.form-control-sm, .form-select-sm { min-height: 2rem; padding: 0.25rem 0.6rem; }
/* 여러 줄 상자 — 같은 테두리·채움색이되 높이는 줄 수가 정하고 줄 간격을 넓힌다 */
textarea.form-control {
  min-height: 5.5rem;
  line-height: 1.65;
}
/* 포커스: 채움색을 걷어 지금 쓰는 칸이 도드라지게 (테두리 강조는 Tabler 가 한다) */
.form-control:focus,
.form-select:focus { background-color: var(--iot-surface); }
.form-control::placeholder { color: var(--iot-text-secondary); }

/* 커스텀 셀렉트(.iot-select, _macros.html month_select) —
   메뉴가 버튼 바로 아래에 뜨고 글씨 크기·서체가 페이지와 동일하게 유지된다 */
.iot-select .dropdown-toggle {
  border: 1px solid var(--tblr-border-color);
  background: var(--iot-surface);
}
.iot-select .dropdown-menu, .iot-select-menu {
  border-radius: var(--iot-radius-sm);
  border-color: var(--iot-border);
  font-size: 0.875rem;
  min-width: 100%;   /* body 에 띄운 메뉴(.iot-select-menu)는 useFloatingMenu 가 버튼 폭을 인라인 minWidth 로 준다 */
}
.iot-select .dropdown-item, .iot-select-menu .dropdown-item { padding: 0.5rem 1rem; }

/* body 에 fixed 로 띄우는 메뉴 한 벌(useFloatingMenu, 2026-09-08) — 카드(.card 는 overflow: hidden)·가로 스크롤 상자 안에
   그리면 상자가 메뉴보다 작을 때 메뉴가 갇혀 잘린다. 자리(left/right/top/bottom)·minWidth 와 화면에 남는 높이
   --iot-float-max 는 인라인이고, 높이 상한은 메뉴마다의 규칙이 min(자기 상한, var(--iot-float-max)) 로 건다
   (여기에 max-height 를 두면 선언 순서에 따라 메뉴 자신의 상한을 이긴다 — 실측 열 선택이 460px 를 넘었다) */
/* body 에 붙는 드롭다운 메뉴 — 문서 좌표라 페이지와 함께 움직이고, 상단 바(1030) 밑으로 표와 똑같이 들어간다.
   모달 안에서 열린 것만 useFloatingMenu 가 인라인으로 fixed·1080 을 준다(모달 1055 위) */
.iot-float-menu { position: absolute; z-index: 1020; margin: 0; }

/* ── 필터 행: 구글형 채움 검색 + 칩 셀렉트 (DESIGN.md '필터 행') ──
   행은 좌우를 꽉 채우지 않는다 — 검색은 200~300px 가변, 칩은 내용 맞춤 폭.
   칩은 .iot-select 위에 얹는 변형이고, 필터가 적용되면 컨테이너에 .active가 붙어
   파란 톤 채움 + '지역: 김포' 라벨이 된다 (라벨·상태는 화면 JS의 syncChip이 관리) */
.iot-filter-row {
  display: flex; align-items: center; flex-wrap: wrap; gap: 10px;
  padding: 12px 20px;
  border-bottom: 1px solid var(--iot-border);
}
.iot-search { position: relative; flex: 0 1 300px; min-width: 200px; }
.iot-search > svg {
  position: absolute; left: 14px; top: 50%; transform: translateY(-50%);
  width: 18px; height: 18px;
  stroke: var(--iot-text-secondary); fill: none;
  stroke-width: 2; stroke-linecap: round;
  pointer-events: none;
}
.iot-search input {
  width: 100%; height: 38px;
  border: none; border-radius: var(--iot-radius-pill);
  background: var(--iot-field-bg); color: inherit;
  padding: 0 16px 0 42px; font-size: 0.875rem; font-family: inherit;
  transition: background-color 0.15s, box-shadow 0.15s;
}
.iot-search input::placeholder { color: var(--iot-text-secondary); }
.iot-search input:focus {
  outline: none;
  background: var(--iot-surface);
  box-shadow: var(--iot-search-focus-shadow);
}
.iot-chip .dropdown-toggle {
  height: 34px; padding: 0 12px 0 14px;
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 0.875rem; font-weight: 400;
  background: transparent;
}
.iot-chip .dropdown-toggle::after { margin-left: 0; } /* 라벨-화살표 간격은 gap이 담당 */
.iot-chip .dropdown-toggle:hover { background: var(--iot-surface-hover); }
.iot-chip.active .dropdown-toggle {
  background: var(--iot-chip-active-bg);
  border-color: transparent;
  color: var(--iot-chip-active-fg);
  font-weight: 500;
}
.iot-chip-menu { max-height: min(320px, var(--iot-float-max, 320px)); overflow-y: auto; }

/* 켜고 끄는 조건 칩(.iot-chip-toggle) — 값을 고르는 .iot-chip 과 같은 줄에 서므로
   높이·글꼴·적용 색이 같고, 화살표가 없는 것만 다르다 (누르면 바로 켜진다).
   상태는 aria-pressed 가 진실이고 .active 는 그 표시다 (사업장 목록 · 월 보고서 30분 원본) */
.iot-chip-toggle, .biz-tab {
  height: 34px; padding: 0 14px;
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 0.8125rem; font-family: inherit; color: inherit;
  border-radius: var(--iot-radius-pill);
  border: 1px solid var(--tblr-border-color);
  background: transparent; cursor: pointer;
}
.iot-chip-toggle:hover, .biz-tab:hover { background: var(--iot-surface-hover); color: inherit; }
.iot-chip-toggle.active {
  background: var(--iot-chip-active-bg);
  color: var(--iot-chip-active-fg);
  border-color: transparent;
  font-weight: 500;
}
/* 칩 안의 건수 — 켜지 않았을 때는 보조색, 켜면 칩 글자색을 따라간다 */
.iot-chip-toggle .cnt, .biz-tab .cnt { color: var(--iot-text-secondary); font-variant-numeric: tabular-nums; }
.iot-chip-toggle.active .cnt { color: inherit; }
/* 분류 탭(사업장 목록 전체/대상/미분류/제외) — '여럿 중 하나'라 aria-current 가 진실이다.
   2026-09-03 전에는 btn-primary 알약이라 같은 줄의 [사업장 추가]와 구분되지 않았고, 파란 칩
   하나로 바꾸자 카드 밖 회색 바탕에서 고른 것이 묻히고 넷이 똑같아 보였다(사용자 지적).
   그래서 **분류 색을 단다** — 대시보드·점검 현황 칸(.dash-sum)과 같은 문법을 칩 크기로:
   점 = 분류 색(표 배지와 같다 — 미분류 노랑·제외 회색·대상 파랑·전체는 빈 고리), 고르면
   그 분류의 옅은 바탕 + 같은 색 테두리 + 굵게. 색은 전부 기존 토큰이고 옅은 바탕은 그 색을
   섞어 낸다(새 hex 없음). 다크는 토큰이 이미 교정돼 있어 따로 없다 */
.biz-tab { text-decoration: none; padding-left: 12px; }
.biz-tab .dot {
  width: 9px; height: 9px; border-radius: var(--iot-radius-pill); flex: none;
  background: var(--biz-dot, transparent);
}
.biz-tab-all { --biz-fg: var(--tblr-body-color); --biz-soft: color-mix(in srgb, var(--tblr-secondary) 14%, transparent); }
.biz-tab-all .dot { border: 2px solid var(--iot-text-secondary); }
.biz-tab-target { --biz-dot: var(--tblr-link-color); --biz-fg: var(--iot-chip-active-fg); --biz-soft: var(--iot-chip-active-bg); }
.biz-tab-unclassified { --biz-dot: var(--tblr-yellow); --biz-fg: var(--iot-warn-text); --biz-soft: color-mix(in srgb, var(--tblr-yellow) 16%, transparent); }
.biz-tab-excluded {
  --biz-dot: var(--tblr-secondary);
  --biz-fg: color-mix(in srgb, var(--tblr-secondary) 75%, var(--tblr-body-color));
  --biz-soft: color-mix(in srgb, var(--tblr-secondary) 14%, transparent);
}
.biz-tab[aria-current="true"] { background: var(--biz-soft); color: var(--biz-fg); border-color: currentColor; font-weight: 600; }
.biz-tab[aria-current="true"] .cnt { color: inherit; }
.biz-tab-all[aria-current="true"] .dot { border-color: currentColor; }
/* 지금 걸린 것을 걷어내는 버튼(.iot-chip-clear) — 필터 해제, 오늘 기준 달로 되돌리기.
   칩과 같은 줄에 서지만 '고르는 것'이 아니라 '되돌리는 것'이라 테두리 없이 글자만 둔다.
   걸린 게 없으면 화면에서 아예 빠진다 (JS 가 .d-none 을 붙인다) */
.iot-chip-clear {
  height: 34px; padding: 0 10px;
  display: inline-flex; align-items: center;
  font-size: 0.8125rem; font-family: inherit;
  color: var(--tblr-primary);
  border: 0; border-radius: var(--iot-radius-pill);
  background: transparent; cursor: pointer;
}
.iot-chip-clear:hover { background: var(--iot-surface-hover); }

@media (max-width: 576px) {
  .iot-search { flex-basis: 100%; } /* 모바일: 검색만 한 줄 전체, 칩은 다음 줄에 나란히 */
}

/* ── 카드 (구글 계정 스타일) ───────────────────────── */
.card {
  border: none;
  border-radius: var(--iot-radius-lg);
  overflow: hidden;
  box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
}
.card .card-header { border-bottom-color: var(--iot-border); }
.card .card-footer { border-top-color: var(--iot-border); }

/* 의견 등 여러 줄 텍스트 박스 */
.opinion-box {
  white-space: pre-line;
  background: var(--tblr-bg-surface-secondary);
  border-radius: var(--iot-radius-sm);
  padding: 12px 14px;
}

/* ── 자동 의견 초안 (배출구 × 확인 항목 표)와 동일 이력 팝업 ──────────
   저장된 초안은 '[배출구 N · 분류] …' 한 덩어리 글이지만 원래 배출구별·항목별로 만들어진
   것이라 표로 되돌려 보여준다 (web/opinion_view.py, 템플릿은 report_detail.html). */
/* 배출구가 바뀌는 첫 행에만 굵은 구분선 — 한 배출구의 항목들이 한 덩어리로 읽힌다 */
.table tbody tr.op-head > td { border-top: 2px solid var(--iot-border); }
.table tbody tr.op-head:first-child > td { border-top: 0; }
.op-outlet { font-weight: 600; }
.op-cat { font-weight: 500; }
.op-count { font-variant-numeric: tabular-nums; }
.op-count .op-sub { display: block; font-size: 12px; color: var(--iot-text-secondary); }
/* 옛 양식(배출구 구분 없는 달)의 확정 의견은 줄바꿈이 뜻을 가진다 — .opinion-box 와 같은 규칙 */
.op-text { line-height: 1.7; white-space: pre-line; }
.op-empty { color: var(--iot-text-secondary); }
.op-note { color: var(--iot-text-secondary); font-weight: 400; }
/* 기본 문구 표시 — 규칙이 침묵해 채운 문장이라 사람이 봐야 한다 */
.op-src { display: block; margin-top: 6px; font-size: 12px; color: var(--iot-text-secondary); }
/* 이력 없음. 버튼과 같은 크기·모양이되 누를 것이 없으므로 button 이 아니라 표시다 —
   칸을 비우면 '아직 안 만든 자리'로 읽히고, 실선 버튼이면 눌러 보게 된다 */
.op-hist-none {
  pointer-events: none;
  background: transparent;
  border-style: dashed;
  border-color: var(--iot-border);
  color: var(--iot-text-secondary);
}
.op-card {
  border: 1px solid var(--iot-border);
  border-radius: var(--iot-radius-sm);
  padding: 14px 16px;
}
.op-card + .op-card { margin-top: 10px; }
.op-hist-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.op-hist-month { font-size: 15px; font-weight: 600; }
.op-hist-facts { margin-top: 4px; font-size: 13px; color: var(--iot-text-secondary); }
.op-facts { margin: 6px 0 0; padding-left: 18px; font-size: 13px; color: var(--iot-text-secondary); }
.op-hist-label {
  margin: 10px 0 2px;
  font-size: 12px; font-weight: 600; letter-spacing: 0.04em;
  color: var(--iot-text-secondary);
}
.op-hist-open { display: inline-block; margin-top: 10px; font-size: 13px; }
/* 의견 확정 표의 입력칸 — 행마다 문장 하나라 3줄에서 시작하고 사람이 늘릴 수 있다 */
.op-edit { min-height: 88px; resize: vertical; }
.op-editrow {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-top: 6px; font-size: 12px; color: var(--iot-text-secondary);
}
/* 이번 달에 사람이 손댄 행 — 다음 달 규칙(opinion_rules.toml)을 고칠 후보가 그대로 보인다 */
.op-editrow.edited .op-state { color: var(--tblr-yellow); font-weight: 500; }

/* ── 카드 제목 좌측 원형 아이콘 ────────────────────── */
.gicon {
  width: 34px; height: 34px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  flex: none;
  background: var(--iot-cat-gray);
}
.gicon svg { width: 18px; height: 18px; stroke: #fff; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.card-title { display: flex; align-items: center; gap: 10px; }
.card-title .card-sub { margin-left: calc(var(--iot-title-sub-gap) - 10px); }   /* 제목 옆 작은 부제 — gap 10 을 더해 16px */

/* 아이콘 카테고리 색 — _macros.html 의 icon() 이름과 1:1 */
.gicon-calendar { background: var(--iot-cat-purple); }
.gicon-building { background: var(--iot-cat-green); }
.gicon-clock    { background: var(--iot-cat-blue); }
.gicon-send     { background: var(--iot-cat-pink); }
.gicon-mail     { background: var(--iot-cat-orange); }
.gicon-file     { background: var(--iot-cat-teal); }
.gicon-person   { background: var(--iot-cat-red); }
.gicon-users    { background: var(--iot-cat-green); }
.gicon-edit     { background: var(--iot-cat-indigo); }
.gicon-logout   { background: var(--iot-cat-gray); }
.gicon-moon     { background: var(--iot-cat-indigo); }
.gicon-collect  { background: var(--iot-cat-teal); }
.gicon-chart    { background: var(--iot-cat-indigo); }
.gicon-alert    { background: var(--iot-cat-red); }
.gicon-inspect  { background: var(--iot-cat-orange); }
.gicon-lock     { background: var(--iot-cat-blue); }

/* ── 자료 수집 화면 입력 폭 ───────────────────────── */
.iot-year-input { max-width: 7.5rem; }
.iot-code-input { max-width: 10rem; }

/* ── 그룹 리스트: 항목 사이 좁은 간격, 안쪽 sm / 바깥 lg 모서리 ── */
.ggroup { display: flex; flex-direction: column; gap: 3px; }
.gitem {
  background: var(--iot-surface); display: flex; align-items: center; gap: 14px;
  padding: 14px 18px; border-radius: var(--iot-radius-sm); color: inherit; text-decoration: none;
  border: none; width: 100%; text-align: left; font: inherit;
}
.ggroup > .gitem:first-child { border-radius: var(--iot-radius-lg) var(--iot-radius-lg) var(--iot-radius-sm) var(--iot-radius-sm); }
.ggroup > .gitem:last-child { border-radius: var(--iot-radius-sm) var(--iot-radius-sm) var(--iot-radius-lg) var(--iot-radius-lg); }
.ggroup > .gitem:only-child { border-radius: var(--iot-radius-lg); }
button.gitem { cursor: pointer; }
a.gitem:hover, button.gitem:hover { background: var(--iot-surface-hover); }
a.gitem:focus-visible, button.gitem:focus-visible {
  outline: 2px solid var(--tblr-primary);
  outline-offset: -2px;
}
.gitem .gitem-label { font-size: 13px; color: var(--iot-text-secondary); margin-bottom: 3px; }
.gitem .gitem-value { font-weight: 500; }
/* 그린링크 계정 행의 마스크 — 값이 두 개(아이디/비밀번호)라는 것만 알리고 값은 모달에서 본다.
   글자 수는 고정 4개다 (길이가 새 나가지 않게) */
.cred-mask { letter-spacing: .1em; }
.gitem-chevron {
  margin-left: auto; width: 16px; height: 16px; flex: none;
  stroke: var(--iot-text-secondary); fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
}

/* ── 상세형 섹션 — 제목·액션·안내문은 표면 밖 (기준: 사업장 상세) ──
   카드 헤더 대신 3층: 표면 밖 헤딩 행(.sect-head) → 내용 표면(ggroup 필드 그룹
   또는 헤더 없는 card 표) → 표면 밖 캡션(.sect-caption). 상세형 섹션 제목에는
   gicon을 붙이지 않는다. 규칙: DESIGN.md '페이지 유형' 설정·상세형 */
.sect-head {
  display: flex; align-items: center; flex-wrap: wrap; gap: 10px;
  margin: 0 2px 10px;
  min-height: 34px; /* 버튼 유무와 무관하게 헤딩 행 높이 통일 */
}
.sect-head .sect-title { margin: 0; font-size: 17px; font-weight: 600; }
.sect-title .sect-count { margin-left: 8px; font-size: 14px; }                          /* 건수는 제목에 붙는다 */
.sect-head > .sect-title + .sect-sub { margin-left: calc(var(--iot-title-sub-gap) - 10px); }   /* 부제는 gap 10 을 더해 16px (ms-auto 는 그대로 오른쪽 끝) */
.sect-count { color: var(--iot-text-secondary); font-weight: 400; }
.sect-sub { color: var(--iot-text-secondary); font-size: 13px; }
.sect-caption { margin: 8px 4px 0; font-size: 12.5px; color: var(--iot-text-secondary); }
/* 섹션 사이 간격 — 카드 테두리가 없어진 자리를 여백이 경계로 대신한다 */
.row-sections { --tblr-gutter-x: 2.5rem; --tblr-gutter-y: 2rem; }

/* ── 레코드형 레이아웃 (기준: 사업장 상세) — DESIGN.md '페이지 유형' 레코드형 ──
   좌 312px 사이드바(sticky) = '사업장 정보' 대분류(정적 속성), 우 = 시간축 기록 탭.
   좌측 상단은 우측 탭 바와 같은 문법의 1탭짜리 바(.side-tabs)라 두 컬럼의 상단 라인이 나란하고,
   모바일(<lg)에서는 사이드바가 상단 탭 줄의 '사업장 정보' 탭으로 합류한다
   (같은 DOM — 표시 전환은 .detail-grid[data-tab] + business_detail.js) */
.detail-grid {
  display: grid;
  /* 312px = 카드 폭 284 + 스크롤바 16 + 카드와 스크롤바 사이 여백 12 */
  grid-template-columns: 312px minmax(0, 1fr);
  gap: 2rem;
  align-items: start;
}
/* max-height: sticky 요소가 뷰포트보다 길면 하단이 화면 밖에 고정돼 접근 불가 —
   담당자·배출구가 많은 사업장은 사이드바 안에서 스크롤한다.
   padding-right: 스크롤바가 카드 모서리에 붙지 않게 12px 띄운다 (더 벌리면 두 컬럼 사이에 떠 보인다).
   scrollbar-gutter: 넘치지 않는 사업장에서도 같은 자리를 비워 둔다 — 배출구 수에 따라 카드 폭이
   16px씩 달라지지 않는다 (오버레이 스크롤바인 환경에는 영향 없음) */
.detail-side {
  position: sticky; top: 1rem; max-height: calc(100vh - 2rem); overflow-y: auto;
  padding-right: 12px; scrollbar-gutter: stable;
}

/* 탭 바 — 서브내비 활성 탭과 같은 의미의 밑줄 탭 (페이지 안 버전) */
.detail-tabs {
  display: flex; gap: 2px;
  /* 경계선은 border 가 아니라 안쪽 그림자 + 아래 여백 1px 로 그린다 — 이 바는 좁은 화면에서 가로로 넘기려고
     overflow 가 걸려 있어, 상자 밖으로 1px 나가는 밑줄의 아랫부분이 잘려 파랑 1px 위에 회색 선이 남았다
     (2026-09-04 실측, 왼쪽 1탭 바와 달라 보였다). 안쪽에 그리면 밑줄 2px 이 그 선을 그대로 덮는다 */
  padding-bottom: 1px; box-shadow: inset 0 -1px 0 var(--iot-border);
  margin-bottom: 1rem;
  overflow-x: auto; scrollbar-width: none;
}
.detail-tabs::-webkit-scrollbar { display: none; }
.detail-tab,
.side-tabs .tab-like {
  border: 0; background: none; margin: 0;
  padding: 0.55rem 0.9rem 0.7rem;
  font: inherit; font-size: 0.9375rem; color: var(--iot-text-secondary);
  white-space: nowrap; position: relative;
}
.detail-tab { cursor: pointer; isolation: isolate; }
/* hover 알약은 상단 메뉴와 같은 방식 — 밑줄(::after)은 탭에, 알약은 ::before 에 (2026-09-04) */
.detail-tab::before {
  content: ""; position: absolute; inset: 4px 2px 7px; z-index: -1;
  border-radius: var(--iot-radius-pill); background: transparent;
}
.detail-tab:hover { color: var(--iot-nav-accent); }
.detail-tab:hover::before { background: var(--iot-surface-hover); }
.detail-tab[aria-selected="true"],
.side-tabs .tab-like { color: var(--iot-nav-accent); font-weight: 600; }
/* 밑줄은 위 '현재 위치 밑줄' 공용 규칙 */
.detail-tab:focus-visible {
  outline: 2px solid var(--tblr-primary);
  outline-offset: -2px;
  border-radius: var(--iot-radius-sm);
}
.detail-tab .sect-count { font-size: 0.875em; }
.side-tabs { display: flex; border-bottom: 1px solid var(--iot-border); margin-bottom: 1rem; }

/* 사이드바 부제목 — '사업장 정보' 대분류 아래 하위 그룹 (기본 정보·그린링크·발송 담당자·배출구) */
.subsect {
  display: flex; align-items: center; flex-wrap: wrap; gap: 10px;
  margin: 1.5rem 2px 8px;
  min-height: 30px; /* 버튼 유무와 무관하게 부제목 행 높이 통일 */
}
.side-tabs + .subsect { margin-top: 0; }
.subsect-title { margin: 0; font-size: 14px; font-weight: 600; }
.subsect-title .sect-count { font-size: 13px; }

/* 기본 정보 접기 — 핵심 필드(지역·주소)만 기본 노출, 나머지는 '더 보기'로.
   탭 폴백과 같은 원칙으로 .tabbed(JS 초기화) 아래에서만 접는다 — JS 없이는 전부 펼쳐 보이고
   동작하지 않는 펼침 버튼은 숨긴다 */
.tabbed .ggroup:not(.expanded) > .gitem-more { display: none; }
/* 접힌 행만 가리키는 캡션(그린링크의 자물쇠 안내)은 그 행들과 함께 접는다 */
.tabbed .ggroup:not(.expanded) + .sect-caption-more { display: none; }
.detail-grid:not(.tabbed) .gitem-expand { display: none; }
button.gitem-expand {
  padding-top: 9px; padding-bottom: 9px;
  font-size: 13px; color: var(--tblr-link-color);
}

/* 사이드바 행 리스트(담당자·배출구) — gitem 을 가로 나열로 쓴다.
   담당자 행은 역할 태그(To/CC/—) + 이름 + 연락처·이메일 2단이고, 행 전체가 상세 모달을 연다
   (2026-08-17 통합 — 이름·연락처·이메일이 한 행에 들어와 한 줄로는 안 담긴다) */
.contact-tag { width: 26px; flex: none; font-size: 13px; align-self: flex-start; padding-top: 1px; }
.contact-item { align-items: flex-start; gap: 10px; }
.contact-main { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1 1 auto; }
.contact-name { font-weight: 500; }
.contact-hint { font-weight: 400; font-size: 12.5px; color: var(--iot-text-secondary); }
/* 연락처·이메일 — 자리가 모자라면 이메일이 제 줄로 내려간다 (단어 중간에서 꺾이지 않게) */
.contact-meta {
  display: flex; flex-wrap: wrap; gap: 1px 10px;
  font-size: 13px; color: var(--iot-text-secondary);
}
.contact-meta .contact-tel { font-variant-numeric: tabular-nums; white-space: nowrap; }
.contact-meta .contact-email { word-break: break-all; min-width: 0; }
.contact-meta .contact-none { font-style: italic; }
/* 번호 앞의 종류(휴대폰·전화·팩스)와 '외 N' — 번호가 여럿이라 무슨 번호인지 한 낱말로 밝힌다.
   전부는 행을 눌러 여는 담당자 창에서 본다 (2026-09-22) */
.contact-kind, .contact-more { font-variant-numeric: normal; opacity: 0.75; }
/* 담당자 창의 번호 줄 — [종류][번호][지우기] 한 줄이 늘어난다. 입력칸은 알약이 아니라
   폼 칸 모양 그대로고(DESIGN.md '알약은 고르는 것의 모양이다'), 지우기는 28px 아이콘 버튼이다. */
.contact-phone-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.contact-phone-row .form-select { width: 104px; flex: none; }
.contact-phone-row .form-control { flex: 1 1 auto; min-width: 0; }
/* 상세 모달이 있는 행은 손가락 커서 + hover (rec-table 의 .row-link 와 같은 의미) */
.gitem[data-modal] { cursor: pointer; }
.gitem[data-modal]:hover { background: var(--iot-surface-hover); }

/* ── 레코드형 탭 표 (.rec-table) — 세 탭(월별 보고서 이력·점검 내역·변경 이력)의 공통 골격 ──
   표마다 다른 행 카드를 쓰지 않고 앱 공용 표 문법(card > table-responsive > table card-table)에
   맞춘다. 여기서 더하는 것은 세 가지뿐이고, 열 구성만 표마다 다르다 (DESIGN.md 레코드형):
     ① 라벨 행(thead)을 제외한 모든 행에 hover — 상세가 없어도 지금 보는 줄이 보인다
     ② 상세가 있는 행만 손가락 커서(.row-link) — 목적지는 상세 페이지 또는 상세 모달(data-modal)
     ③ 행 안의 링크는 검은색·밑줄 없음 — 행 전체가 클릭 대상이라 한 칸만 파랄 이유가 없다
        (버튼은 제외 — xlsx/pdf 처럼 행 안에서 자기 동작을 하는 것은 버튼 모양을 유지한다)
   행 높이는 좁은 화면에서도 한 줄이다: 보조 열은 템플릿이 d-none 으로 접고, 그래도 넘치는 폭은
   .table-responsive 의 가로 스크롤이 받는다 (다른 표들과 같은 처리). 진행 단계를 여러 줄로
   접어 보았지만 한 행이 5줄까지 자라 표의 리듬이 깨졌다 */
.rec-table tbody tr:not(.rec-empty):not(.insp-group) > * { transition: box-shadow 0.12s ease; }
.rec-table tbody tr:not(.rec-empty):not(.insp-group):hover > *,
.rec-table tbody tr:not(.rec-empty):not(.insp-group):focus-within > * {
  --tblr-table-bg-state: var(--iot-surface-hover);
}
.rec-table tbody tr:focus-visible {
  outline: 2px solid var(--tblr-primary);
  outline-offset: -2px;
}
.rec-table tbody a:not(.btn) { color: inherit; text-decoration: none; }
.rec-table tbody a:not(.btn):hover { color: inherit; text-decoration: none; cursor: pointer; }
/* 판정 열의 자동 초안 표시처럼 값에 딸린 보조 문구 */
.rec-table .rec-sub { font-size: 12.5px; color: var(--iot-text-secondary); }
/* 행 안 액션 열(다운로드 버튼) — 버튼 사이 간격만 준다 */
.rec-table .rec-actions { white-space: nowrap; }
.rec-table .rec-actions > .btn + .btn { margin-left: 6px; }
@media (prefers-reduced-motion: reduce) {
  .rec-table tbody tr:not(.rec-empty):not(.insp-group) > * { transition: none; }
}

/* 모바일(<lg): 사이드바 → '사업장 정보' 탭 합류. JS 초기화가 .tabbed 를 붙인 뒤에만
   사이드바를 탭 상태에 종속시킨다 — JS 없이는 사이드바가 본문 아래에 그대로 보인다 */
@media (max-width: 991.98px) {
  .detail-grid { grid-template-columns: minmax(0, 1fr); gap: 0; }
  /* 내부 스크롤이 없으니 스크롤바 자리(padding-right)도 되돌린다 — 안 그러면 우측만 12px 들어간다 */
  .detail-side { position: static; order: 2; max-height: none; overflow-y: visible; padding-right: 0; }
  .detail-grid.tabbed .detail-side { display: none; }
  .detail-grid.tabbed[data-tab="info"] .detail-side { display: block; }
  .detail-main { order: 1; }
  .side-tabs { display: none; }
  .detail-tabs { position: sticky; top: 0; z-index: 5; background: var(--tblr-bg-surface-secondary); }
  .detail-tab { padding-top: 0.75rem; padding-bottom: 0.85rem; } /* 터치 타깃 */
}

/* ── 모니터링 대상 스위치 (사업장 상세 머리) ──────────
   라벨 글자와 스위치를 통째로 감싸는 알약이 눌리는 범위다 (2026-09-04). Tabler 는 라벨을 기본 커서로 두어
   글자 위에서는 누를 수 있는지도 안 보였다. 손잡이 둘레의 원(Material)은 이 화면 어디에도 없는 표현이라
   쓰지 않는다. 음수 여백은 알약이 없을 때와 같은 자리에 글자를 둔다(ms-auto 의 왼쪽은 그대로) */
.iot-switch {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 5px 14px 5px 12px; margin: -5px -14px -5px 0; min-height: 0;
  border-radius: var(--iot-radius-pill); cursor: pointer;
}
.iot-switch .form-check-input { float: none; margin: 0; cursor: pointer; }
.iot-switch .form-check-label { cursor: pointer; }
.iot-switch:hover { background: var(--iot-surface-hover); }

/* ── 프로필 아바타 ─────────────────────────────────── */
.avatar-btn {
  width: 36px; height: 36px; border-radius: 50%;
  background: var(--iot-identity); color: #fff; font-weight: 700; font-size: 16px;
  display: inline-flex; align-items: center; justify-content: center;
  text-decoration: none; border: 2px solid rgba(255, 255, 255, 0.35);
}
.avatar-big {
  width: 72px; height: 72px; border-radius: 50%;
  background: var(--iot-identity); color: #fff; font-weight: 700; font-size: 32px;
  display: inline-flex; align-items: center; justify-content: center;
}

/* ── 표 ────────────────────────────────────────────── */
/* 행 상하 간격: 모든 표·모든 화면 동일 — Tabler 기본 0.75rem의 1.2배.
   좌우는 Tabler 기본 0.75rem 그대로 — table-sm(좌우 0.25rem)은 쓰지 않는다 */
.table > :not(caption) > * > * {
  padding-top: 0.9rem;
  padding-bottom: 0.9rem;
}

/* 유연 열(폭 미지정 주 텍스트 열)의 td에 사용 — 남는 폭만 차지하고 넘치는 글자는 말줄임.
   나머지 열은 전부 w-1(내용 맞춤) 또는 w-25(보조 유연 열)로 폭을 지정한다 (DESIGN.md 표 규칙) */
.table .cell-truncate {
  max-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 목록 표의 보조 열 폭 — 폭 미지정 주 열(사업장명)이 남는 폭을 독식하지 않게
   보조 열에 고정 폭을 줘 여백을 나눈다. w-1(내용 맞춤) 대신 쓰고,
   '유연 열 1개 원칙'은 그대로다 (주 열만 폭 미지정, td 에 cell-truncate) */
.table th.col-w-sm { width: 92px; }   /* 지역·담당 */
.table th.col-w-md { width: 112px; }  /* 코드·그린링크 상태 */
.table th.col-w-lg { width: 158px; }  /* 날짜 + 배지 */
.table th.col-w-xl { width: 200px; }  /* 사업장명이 보조 열일 때 — 한글 14자(0.892em×14px=12.5px/자 + 좌우 여백 24px).
                                          점검 내역처럼 유연 열이 내용(문단)인 표에서 사업장 열을 폭 없이 두면
                                          cell-truncate 의 max-width:0 이 그 열의 폭 요구를 0 으로 만들어 이름이 한두 글자만 남는다 */

/* ── 열 너비를 정해 그리는 표 (.tbl-fixed — 사업장·배출구 목록, 2026-09-09) ────────
   모든 열이 colgroup 의 px 너비를 갖고(값의 상위 90%와 열 이름 중 긴 쪽, 종류별 하한·상한 — frontend/src/lib/columnWidths.ts),
   표가 카드보다 넓어지면 이 상자 안에서 좌우로, 길면 위아래로 스크롤된다. 첫 열(사업장명)과 헤더가 붙고,
   옆으로 밀면 첫 열 오른쪽에 그림자가 서서 뒤에 열이 더 있음을 알린다(Notion·Airtable). 넘치는 글자는 말줄임 + title.
   옛 '유연 열 1개(max-width:0)' 규칙은 이 두 표에서 물러났다 — 열을 많이 켜면 사업장명이 한두 글자만 남았다. */
.tbl-fixed { overflow: auto; position: relative; }
.tbl-fixed .table { table-layout: fixed; border-collapse: separate; border-spacing: 0; margin: 0; }
.tbl-fixed .table th, .tbl-fixed .table td { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tbl-fixed .table td.cell-truncate { max-width: none; }
.tbl-fixed .table thead th { position: sticky; top: 0; z-index: 2; background: var(--iot-surface); }
.tbl-fixed .table th:first-child, .tbl-fixed .table td:first-child { position: sticky; left: 0; z-index: 3; background: var(--iot-surface); }
.tbl-fixed .table thead th:first-child { z-index: 4; }
/* 고정 첫 열은 자기 바탕(surface)을 가져서 행 hover 가 그 칸만 비껴간다 — 함께 물들인다(전환은 표 행과 같은 속도) */
.tbl-fixed .table tbody td { transition: background-color 0.12s ease; }
.tbl-fixed .table tbody tr:hover td { background: var(--iot-surface-hover); transition: background-color 0.12s ease; }
.tbl-fixed.scrolled .table th:first-child, .tbl-fixed.scrolled .table td:first-child {
  box-shadow: 6px 0 10px -6px rgba(4, 32, 69, 0.22); clip-path: inset(0 -14px 0 0);
}
[data-bs-theme="dark"] .tbl-fixed.scrolled .table th:first-child, [data-bs-theme="dark"] .tbl-fixed.scrolled .table td:first-child {
  box-shadow: 6px 0 10px -6px rgba(0, 0, 0, 0.6);
}

/* ── 보고서 목록의 긴 열 (점검 내역·의견 확정·담당자, 2026-09-14) ────────
   접힌 두 줄(메타 한 줄 + 글 첫 문장 한 줄) → [펼치기] → 행 아래 패널에 둘 다 전문.
   펼침은 **행 단위**다: 어느 칸을 눌러도 같은 패널이 열린다 (판단에 의견과 조치를 같이 봐야 한다).
   요약은 저장하지 않고 화면이 같은 자료에서 그때 만든다 (web/report_columns.py). */
.rpt-cell { display: flex; flex-direction: column; gap: 0.125rem; white-space: normal; min-width: 0; }
.rpt-meta {
  display: flex; align-items: center; gap: 0.375rem; flex-wrap: wrap;
  font-size: 0.8125rem; color: var(--iot-text-secondary); line-height: 1.5;
}
.rpt-meta b { color: var(--tblr-body-color); font-weight: 600; }
/* 배출구·항목 칩 — 의견 글의 태그에서 온다. 태그가 없는 달(2026-06 이전)은 칩이 서지 않는다 */
.rpt-chip {
  display: inline-block; border-radius: var(--iot-radius-sm); padding: 0 0.3125rem;
  background: var(--iot-field-bg); color: var(--iot-text-secondary); font-size: 0.75rem; line-height: 1.5;
  /* 분류 이름은 가운뎃점으로 이어져 끊을 자리가 없다 — 열보다 길면 칸 밖으로 잘리므로 어디서든 접게 둔다 */
  min-width: 0; overflow-wrap: anywhere;
}
.rpt-chip b { color: var(--tblr-body-color); font-weight: 600; }
.rpt-draft { border: 1px dashed var(--iot-text-secondary); background: transparent; color: var(--iot-text-secondary); font-weight: 400; }
/* 글 첫 문장 — 접힌 상태에서는 한 줄, 넘치면 말줄임 */
.rpt-lead { display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; white-space: normal; }
.rpt-none { color: var(--iot-flag-missing); font-size: 0.8125rem; }
/* 담당자 — 사람마다 이름 한 줄, 연락처 한 줄. 전화·이메일에 링크를 걸지 않는다(읽고 복사하는 칸) */
.rpt-people { display: flex; flex-direction: column; gap: 0.25rem; white-space: normal; }
.rpt-person { display: flex; flex-direction: column; line-height: 1.45; }
.rpt-person .n { font-weight: 600; }
.rpt-person .c { font-size: 0.8125rem; color: var(--iot-text-secondary); }
/* 발송 표의 받는 사람 — 사람마다 한 줄(표시 · 이름 · 이메일). 이메일 없는 담당자는 흐리게 남는다 (2026-09-26) */
.send-people { display: flex; flex-direction: column; gap: 0.25rem; }
.send-person { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.125rem 0.5rem; line-height: 1.45; }
.send-person .n { font-weight: 600; }
.send-person .c { font-size: 0.8125rem; color: var(--iot-text-secondary); }
.send-person.off .n, .send-person.off .c { color: var(--iot-text-secondary); font-weight: 400; opacity: 0.8; }
.send-note { align-self: flex-start; font-size: 0.75rem; color: var(--iot-warn-text); background: var(--iot-map-advise-soft); border-radius: var(--iot-radius-sm); padding: 0.0625rem 0.5rem; }
/* 발송 표 골라 보내기 (2026-09-27) — 줄 누르기 = 선택. hover 는 눌리는 범위(줄 전체)를 보이고, 고른 줄은 옅은 파랑이다.
   고를 수 없는 줄(못 보냄·보냄)은 hover 가 없고 글자가 흐리다. 고르는 동안 필터 줄도 같은 파랑으로 물들어 '선택 모드'를 알린다. */
.table th.send-ck, .table td.send-ck { width: 2.75rem; padding-right: 0; }
.send-ck .form-check-input { margin: 0.125rem 0 0; cursor: pointer; }
.send-ck .form-check-input:disabled { cursor: not-allowed; }
.table tbody tr.send-row-pick { cursor: pointer; }
.table tbody tr.send-row-pick > td { transition: background-color 0.12s ease; }
.table tbody tr.send-row-pick:hover > td { background: var(--iot-surface-hover); }
.table tbody tr.send-row-on > td, .table tbody tr.send-row-on:hover > td { background: var(--iot-chip-active-bg); }
.table tbody tr.send-row-off > td { color: var(--iot-text-secondary); }
.send-row-off .send-name { font-weight: 400; }
.send-name { font-weight: 500; }
.send-bar-on .iot-filter-row { background: var(--iot-chip-active-bg); }
.send-picked { display: inline-flex; align-items: center; gap: 0.5rem; font-weight: 600; color: var(--iot-chip-active-fg); }
.send-picked-hidden { font-size: 0.75rem; font-weight: 400; color: var(--iot-warn-text); background: var(--iot-map-advise-soft); border-radius: var(--iot-radius-sm); padding: 0.0625rem 0.5rem; }
.send-fix { padding-block: 0; line-height: 1.6; }
.send-state { font-size: 0.8125rem; }
.send-state.ok { color: var(--iot-map-ok); }
.send-state.bad { color: var(--iot-map-check); font-weight: 500; }
.send-state.bad a { color: inherit; text-decoration: underline; }
.send-state.sent { color: var(--iot-text-secondary); }
/* 전달 완료 표시 (2026-09-27) — 메일로 보낸 '보냄'(회색)과 가른다. 사람이 적은 것이라 보라 */
.send-state.hand { color: var(--tblr-purple); font-weight: 500; }
.send-state-stack { display: flex; flex-direction: column; align-items: flex-start; gap: 0.125rem; }
.send-state-sub { font-size: 0.75rem; color: var(--iot-text-secondary); white-space: normal; }
.send-confirm-list { max-height: 8rem; overflow: auto; font-size: 0.8125rem; color: var(--iot-text-secondary); border: 1px solid var(--iot-border); border-radius: var(--iot-radius-sm); padding: 0.375rem 0.625rem; }
/* 긴 열은 말줄임이 아니라 두 줄로 접는다 — .tbl-fixed 의 nowrap 을 여기서만 푼다 */
.tbl-fixed .table td:has(> .rpt-cell), .tbl-fixed .table td:has(> .rpt-people) { white-space: normal; overflow: hidden; }
/* 펼친 행 — 행과 패널이 한 덩어리로 읽히게 같은 바탕을 깔고 사이 선을 없앤다 */
.table tbody tr.rpt-open > td { background: var(--tblr-bg-surface-secondary); border-bottom: 0; }
.tbl-fixed .table tbody tr.rpt-open > td:first-child { background: var(--tblr-bg-surface-secondary); }
.table tbody tr.rpt-panel > td { background: var(--tblr-bg-surface-secondary); padding: 0.25rem 0.75rem 0.875rem; }
.rpt-panel-grid { display: grid; grid-template-columns: minmax(16rem, 2fr) minmax(20rem, 3fr); gap: 0.75rem 1.5rem; align-items: start; white-space: normal; }
@media (max-width: 1200px) { .rpt-panel-grid { grid-template-columns: 1fr; } }
.rpt-box { display: flex; flex-direction: column; gap: 0.5rem; min-width: 0; }
.rpt-ph { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; font-size: 0.8125rem; font-weight: 600; }
.rpt-visit { border-left: 3px solid var(--iot-border); padding: 0.125rem 0 0.125rem 0.625rem; display: flex; flex-direction: column; gap: 0.125rem; }
.rpt-vh { display: flex; align-items: center; gap: 0.5rem; font-size: 0.8125rem; color: var(--iot-text-secondary); }
.rpt-vh b { color: var(--tblr-body-color); font-weight: 600; }
.rpt-part { display: grid; grid-template-columns: 5rem 1fr; gap: 0.5rem; font-size: 0.8125rem; }
.rpt-part .k { color: var(--iot-text-secondary); }
.rpt-divide {
  font-size: 0.75rem; font-weight: 600; letter-spacing: 0.04em; color: var(--iot-text-secondary);
  border-top: 1px dashed var(--iot-border); padding-top: 0.25rem; margin: 0.375rem 0 0.375rem;
}
.rpt-oprow { display: grid; grid-template-columns: auto 1fr; gap: 0.375rem 0.625rem; align-items: start; }
.rpt-oprow.head { border-top: 1px solid var(--iot-border); padding-top: 0.375rem; }
.rpt-oprow.head:first-of-type { border-top: 0; padding-top: 0; }
.rpt-oprow > .rpt-chip { white-space: nowrap; }
.rpt-opwhole { white-space: pre-wrap; }

/* ── 행 전체 클릭(.row-link) ────────────────────────
   목록 표는 행 어디를 눌러도 그 줄의 주 링크(.row-link-main)로 이동한다.
   이동은 AppShell 의 위임 핸들러(frontend/src/lib/rowLink.ts)가 맡고, 행 안의 링크·버튼은 자기 동작이 우선한다.
   주 링크에 링크색을 쓰지 않는 이유: 행 전체가 링크이므로 한 칸만 파랄 이유가 없다.
   hover 채움은 Tabler .table-hover 와 같은 방식(--tblr-table-bg-state)으로 넣는다 */
.table tbody tr.row-link { cursor: pointer; }
.table tbody tr.row-link > * { transition: box-shadow 0.12s ease; }
.table tbody tr.row-link:hover > *,
.table tbody tr.row-link:focus-within > * {
  --tblr-table-bg-state: var(--iot-surface-hover);
}
.row-link-main {
  color: inherit;
  text-decoration: none;
  font-weight: 500; /* 링크색을 뺀 자리를 굵기로 대신한다 — gitem-value 와 같은 단계 */
}
.row-link-main:hover { color: inherit; }
@media (prefers-reduced-motion: reduce) {
  .table tbody tr.row-link > * { transition: none; }
}

/* ── 진행 단계 표시 (사업장 상세 '월별 보고서 이력') ──
   점+라벨 파이프라인. 배지 색 의미와 동일: 초록=완료 / 노랑=다음 대기 / 회색=미도달 */
.iot-steps { display: inline-flex; align-items: center; }
.iot-step { display: inline-flex; align-items: center; gap: 5px; font-size: 12.5px; white-space: nowrap; }
.iot-step::before { content: ""; width: 8px; height: 8px; border-radius: 50%; }
.iot-step.done { color: var(--tblr-green); }
.iot-step.done::before { background: var(--tblr-green); }
.iot-step.wait { color: var(--tblr-yellow); }
.iot-step.wait::before { background: var(--tblr-yellow); }
.iot-step.off { color: var(--iot-text-secondary); }
.iot-step.off::before { box-shadow: inset 0 0 0 1.5px currentColor; opacity: 0.55; }
.iot-step-bar { width: 16px; height: 2px; margin: 0 6px; border-radius: 1px; background: var(--iot-border); flex: none; }
.iot-step-bar.done { background: var(--tblr-green); opacity: 0.45; }

/* ── 정렬 헤더 (목록 화면 공용) ─────────────────────
   화면마다 다른 클래스를 쓰면 새 목록이 스타일 없는 버튼으로 나온다 — 이름을 하나로 둔다 */
.iot-sort { cursor: pointer; user-select: none; white-space: nowrap; }
.iot-sort:hover { color: var(--tblr-body-color); }
.iot-sort::after { content: "↕"; margin-left: 4px; font-size: 0.8em; opacity: 0.3; }
.iot-sort.sort-asc::after { content: "↑"; opacity: 1; }
.iot-sort.sort-desc::after { content: "↓"; opacity: 1; }
/* 접근성 형태: th 안 <button class="iot-sort"> + th aria-sort (DESIGN.md 접근성).
   월 보고서 원본 표가 기준이었고, 대시보드·사업장 목록도 2026-08-24 에 같은 형태로 옮겼다 */
button.iot-sort {
  background: none; border: 0;
  padding: 2px 8px; margin: -2px -8px;   /* 알약은 글자 둘레에만, 열의 글자 위치는 그대로 (2026-09-04) */
  border-radius: var(--iot-radius-pill);
  font: inherit; color: inherit;
}
button.iot-sort:hover { background: var(--iot-surface-hover); }

/* 커스텀 컨트롤 공통 포커스 링 — gitem focus-visible과 같은 표현 (DESIGN.md 접근성) */
.raw-week-tab:focus-visible,
.iot-chip-toggle:focus-visible,
.biz-tab:focus-visible,
.iot-chip-clear:focus-visible,
button.iot-sort:focus-visible,
.gitem[data-modal]:focus-visible,
tr[tabindex]:focus-visible {
  outline: 2px solid var(--tblr-primary);
  outline-offset: -2px;
}

/* ── 모바일: 터치 타깃 확보 ────────────────────────── */
@media (max-width: 576px) {
  .btn { min-height: 38px; }
  .page-body { margin-top: 0.75rem; }
}

/* ── 월 보고서 (report_detail) — 배출구 현황·30분 원본 전체 보기·PDF 미리보기 ── */
.iot-meter { display: inline-flex; align-items: center; gap: 0.5rem; font-variant-numeric: tabular-nums; }
.iot-meter-bar { width: 72px; height: 5px; border-radius: 3px; background: var(--iot-field-bg); overflow: hidden; flex: none; }
/* 너비는 데이터값이라 템플릿이 --iot-meter-w 커스텀 속성으로 넘긴다 (인라인 스타일 금지의 예외) */
.iot-meter-bar > i { display: block; height: 100%; width: var(--iot-meter-w, 0%); border-radius: 3px; background: var(--tblr-green); }
.iot-meter-bar > i.warn { background: var(--tblr-yellow); }
/* 배출구별 현황 행: 클릭하면 아래 원본 패널로 점프한다 */
.stat-row { cursor: pointer; }
.stat-row:hover > td { background: var(--iot-surface-hover); }
.raw-flag > td { background: color-mix(in srgb, var(--tblr-red) 9%, transparent); }
/* 아직 오지 않은 칸 — 오늘 자료를 받으면 앞으로의 시각도 '미수신' 행으로 함께 온다(2026-09-22 실측).
   지운 자료가 아니므로 감추지 않고 흐리게만 둔다 — 진짜 미수신과 눈으로 갈린다. */
.raw-not-yet > td { color: var(--iot-text-secondary); background: var(--iot-surface-hover); }
/* 이상 사유 — title 툴팁이 아니라 측정상태 옆 보조 텍스트로 상시 표시 (터치·키보드 접근) */
.raw-flag-why { font-size: 0.8125rem; color: var(--iot-text-secondary); }
/* 현황·이상 행 점프의 프로그램 포커스 목적지(tabindex="-1") — 시각 링은 그리지 않는다 */
#raw-month-card:focus { outline: none; }
.report-pdf-frame { width: 100%; height: 75vh; border: 1px solid var(--iot-border); border-radius: var(--iot-radius-sm); background: var(--iot-field-bg); }

/* ── 30분 원본 전체 보기 — 주(월~일) 탭·필터·정렬 표 ──
   월 전체를 1회 로드한 뒤 클라이언트가 나눠 그린다 (report_detail.js).
   주 탭·'이상만' 칩의 파란 활성 톤은 필터 행 칩과 같은 의미라 같은 토큰을 쓴다 */
.raw-load-box { padding: 2.5rem 1rem; }
.raw-toolbar {
  display: flex; flex-direction: column; gap: 10px;
  padding: 12px 20px;
  border-bottom: 1px solid var(--iot-border);
}
.raw-week-tabs { display: flex; flex-wrap: wrap; gap: 6px; }
.raw-week-tab {
  font-size: 0.8125rem; font-family: inherit; color: inherit;
  padding: 4px 14px; border-radius: var(--iot-radius-pill);
  border: 1px solid var(--tblr-border-color); background: transparent;
  display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
  font-variant-numeric: tabular-nums;
}
.raw-week-tab:hover { background: var(--iot-surface-hover); }
.raw-week-tab.active {
  background: var(--iot-chip-active-bg);
  color: var(--iot-chip-active-fg);
  border-color: transparent;
  font-weight: 500;
}
/* 주 탭의 이상 행 수 — 어느 주를 봐야 할지 먼저 보이게 */
.raw-week-tab .cnt {
  font-size: 0.6875rem; font-weight: 600; padding: 0 7px;
  border-radius: var(--iot-radius-pill);
  background: color-mix(in srgb, var(--tblr-red) 12%, transparent);
  color: var(--tblr-red);
}
.raw-filters { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.raw-row-info { margin-left: auto; font-size: 0.8125rem; color: var(--iot-text-secondary); }

/* ── 자체 분류 열 — 우리 규칙이 붙인 해석이라 옅은 바탕·좌우 구분선·헤더 점으로 출처를 표시 ── */
.raw-own {
  background: var(--iot-own-bg);
  border-left: 1px solid var(--iot-border);
  border-right: 1px solid var(--iot-border);
}
.raw-flag > td.raw-own { background: var(--iot-own-bg-flag); }
.raw-own-head { color: var(--iot-nav-accent); }
.raw-own-head::before {
  content: ""; display: inline-block;
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--iot-nav-accent);
  margin-right: 6px; vertical-align: 1px;
}
/* sticky thead 는 --iot-surface 로 덮이므로 자체 분류 헤더는 own 바탕을 유지시킨다 */
.raw-month-scroll thead th.raw-own { background: var(--iot-own-bg); }
/* 배지 버튼(근거 팝오버 여는) — 배지 모양은 .iot-badge 가, 여기선 버튼 리셋만 */
.raw-lb { border: 0; cursor: pointer; font: inherit; }
.raw-lb:focus-visible { outline: 2px solid var(--tblr-primary); outline-offset: 2px; }
.raw-lb-none { font-size: 0.8125rem; color: var(--iot-text-secondary); }

/* 자체 분류 근거 팝오버 — document.body 에 붙어 fixed 로 뜬다 (표 overflow 에 안 잘리게) */
.raw-pop {
  position: fixed; z-index: 1080; width: 320px; max-width: calc(100vw - 16px);
  background: var(--iot-surface); border: 1px solid var(--iot-border);
  border-radius: var(--iot-radius-sm);
  box-shadow: var(--tblr-box-shadow-dropdown, 0 8px 28px rgba(0, 0, 0, 0.18));
  padding: 14px 16px; font-size: 0.8125rem; line-height: 1.6;
}
.raw-pop[hidden] { display: none; }
.raw-pop-head { margin-bottom: 8px; }
.raw-pop-axes { display: grid; grid-template-columns: auto 1fr; gap: 2px 12px; margin: 0 0 10px; }
.raw-pop-axes dt { color: var(--iot-text-secondary); margin: 0; }
.raw-pop-axes dd { margin: 0; }
.raw-pop-empty { color: var(--iot-text-secondary); opacity: 0.7; }
.raw-pop-desc { margin: 0 0 10px; border-top: 1px solid var(--iot-border); padding-top: 8px; }
.raw-pop-foot a { color: var(--iot-nav-accent); text-decoration: none; }
.raw-pop-foot a:hover { text-decoration: underline; }
.raw-month-scroll { max-height: 36rem; overflow: auto; }
.raw-month-scroll thead th { position: sticky; top: 0; background: var(--iot-surface); z-index: 1; }
.raw-day-sep td {
  background: var(--iot-field-bg);
  font-weight: 600; font-size: 0.8125rem;
  padding-top: 5px; padding-bottom: 5px;
}
.raw-day-sep .sat { color: var(--tblr-blue); }
.raw-day-sep .sun { color: var(--tblr-red); }

/* 그래프에서 고른 행 — 표에서 어디로 갔는지 보이게 (클릭 직후 잠깐이 아니라 다음 선택까지 유지) */
.raw-month-scroll tr.raw-row-focus > td {
  background: var(--iot-chip-active-bg);
  box-shadow: inset 0 0 0 1px var(--tblr-primary);
}
.raw-month-scroll tr.raw-row-focus:focus { outline: none; }

/* ── 30분 원본 그래프 (static/raw_chart.js) ──
   그래프 하나 = 배출구 × 측정항목, 선 = 시설명. svg 자체는 aria-hidden 이고
   조작 가능한 것은 범례 버튼뿐이다 (자료의 접근 경로는 아래 표) */
/* 위는 필터 행과 붙지 않게 띄우고, 아래는 표와 맞닿으므로 경계선을 둔다
   (선이 없으면 마지막 그래프와 표 머리글이 한 덩어리로 보인다) */
.raw-chart-area { padding: 16px 20px 18px; border-bottom: 1px solid var(--iot-border); }
.raw-chart-note { margin: 12px 0 -4px; font-size: 0.8125rem; color: var(--iot-text-secondary); padding: 0 20px; }
.raw-chart-empty { padding: 20px 0; font-size: 0.875rem; color: var(--iot-text-secondary); }
.raw-chart-grid { display: grid; gap: 10px; grid-template-columns: repeat(auto-fill, minmax(460px, 1fr)); }
/* 배출구 전체를 고르면 그래프가 20개를 넘을 수 있다 — 아래 표가 화면 밖으로 밀리지 않게 가둔다.
   가두면 마지막 그래프가 중간에서 잘리므로, 틀을 그려 '더 있고 스크롤된다'로 읽히게 한다
   (틀이 없으면 잘린 그래프가 표와 맞붙어 화면이 깨진 것처럼 보인다) */
.raw-chart-grid.scroll {
  max-height: 34rem; overflow-y: auto;
  padding: 8px; gap: 8px;
  border: 1px solid var(--iot-border); border-radius: var(--iot-radius-sm);
  background: var(--iot-field-bg);
}
.raw-chart {
  margin: 0; padding: 8px 10px 4px;
  border: 1px solid var(--iot-border); border-radius: var(--iot-radius-sm);
  background: var(--iot-surface);
}
.raw-chart-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 10px; margin-bottom: 2px; }
.raw-chart-head .ttl { font-size: 0.8125rem; font-weight: 600; }
.raw-chart-head .unit { font-weight: 400; color: var(--iot-text-secondary); }
.raw-chart-keys { display: flex; flex-wrap: wrap; gap: 2px 2px; margin-left: auto; }
.raw-chart-key {
  display: inline-flex; align-items: center; gap: 5px;
  border: 0; background: transparent; padding: 2px 8px; cursor: pointer;
  border-radius: var(--iot-radius-pill);
  font-family: inherit; font-size: 0.75rem; color: var(--iot-text-secondary);
}
.raw-chart-key:hover { background: var(--iot-surface-hover); color: inherit; }
.raw-chart-key > i { width: 10px; height: 3px; border-radius: 2px; background: currentColor; }
.raw-chart-key.c1 > i { background: var(--iot-chart-1); }
.raw-chart-key.c2 > i { background: var(--iot-chart-2); }
.raw-chart-key.c3 > i { background: var(--iot-chart-3); }
.raw-chart-key.c4 > i { background: var(--iot-chart-4); }
.raw-chart-key.c5 > i { background: var(--iot-chart-5); }
.raw-chart-key.off { text-decoration: line-through; opacity: 0.5; }
.raw-chart-plot { position: relative; }
.raw-chart-svg { display: block; width: 100%; }
.raw-chart-gline { stroke: var(--iot-chart-grid); stroke-width: 1; }
.raw-chart-daysep { stroke: var(--iot-chart-grid); stroke-width: 1; }
.raw-chart-guide { stroke: var(--iot-chart-guide); stroke-width: 1; stroke-dasharray: 3 3; }
.raw-chart-ylab, .raw-chart-xlab, .raw-chart-none {
  font-size: 10px; fill: var(--iot-text-secondary);
}
.raw-chart-xlab.sat { fill: var(--tblr-blue); }
.raw-chart-xlab.sun { fill: var(--tblr-red); }
.raw-chart-line { fill: none; stroke-width: 1.4; }
.raw-chart-line.c1, .raw-chart-mark.c1 { stroke: var(--iot-chart-1); }
.raw-chart-line.c2, .raw-chart-mark.c2 { stroke: var(--iot-chart-2); }
.raw-chart-line.c3, .raw-chart-mark.c3 { stroke: var(--iot-chart-3); }
.raw-chart-line.c4, .raw-chart-mark.c4 { stroke: var(--iot-chart-4); }
.raw-chart-line.c5, .raw-chart-mark.c5 { stroke: var(--iot-chart-5); }
.raw-chart-mark { fill: var(--iot-surface); stroke-width: 2; }
.raw-chart-dot { stroke: none; }
.raw-chart-svg { cursor: crosshair; }
.raw-chart-flag.f-missing, .raw-chart-dot.f-missing { fill: var(--iot-flag-missing); }
.raw-chart-flag.f-power,   .raw-chart-dot.f-power   { fill: var(--iot-flag-power); }
.raw-chart-flag.f-comm,    .raw-chart-dot.f-comm    { fill: var(--iot-flag-comm); }
.raw-chart-flag.f-range,   .raw-chart-dot.f-range   { fill: var(--iot-flag-range); }
.raw-chart-flag.f-prevent, .raw-chart-dot.f-prevent { fill: var(--iot-flag-prevent); }
/* 분류 색 안내 — 이 주에 실제로 나타난 분류만 그린다 */
.raw-chart-flagkeys { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; margin: 0 0 8px; }
.raw-chart-flagkey { display: inline-flex; align-items: center; gap: 5px; font-size: 0.75rem; color: var(--iot-text-secondary); }
.raw-chart-flagkey > i { width: 12px; height: 8px; border-radius: 2px; }
.raw-chart-flagkey.f-missing > i { background: var(--iot-flag-missing); }
.raw-chart-flagkey.f-power > i   { background: var(--iot-flag-power); }
.raw-chart-flagkey.f-comm > i    { background: var(--iot-flag-comm); }
.raw-chart-flagkey.f-range > i   { background: var(--iot-flag-range); }
.raw-chart-flagkey.f-prevent > i { background: var(--iot-flag-prevent); }
.raw-chart-flagnote { font-size: 0.75rem; color: var(--iot-text-secondary); margin-left: auto; }
/* 호버 노트 — 그 시각의 모든 선 값을 한 번에 (같은 내용이 아래 표에도 있다) */
.raw-chart-tip {
  position: absolute; top: 6px; z-index: 2; pointer-events: none;
  background: var(--iot-surface); border: 1px solid var(--iot-border);
  border-radius: var(--iot-radius-sm); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12);
  padding: 6px 9px; font-size: 0.75rem; white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.raw-chart-tip-head { font-weight: 600; margin-bottom: 3px; }
.raw-chart-tip-row { display: flex; align-items: center; gap: 6px; }
.raw-chart-tip-row > i { width: 8px; height: 3px; border-radius: 2px; flex: none; }
.raw-chart-tip-row > i.c1 { background: var(--iot-chart-1); }
.raw-chart-tip-row > i.c2 { background: var(--iot-chart-2); }
.raw-chart-tip-row > i.c3 { background: var(--iot-chart-3); }
.raw-chart-tip-row > i.c4 { background: var(--iot-chart-4); }
.raw-chart-tip-row > i.c5 { background: var(--iot-chart-5); }
.raw-chart-tip-row .nm { min-width: 5.5em; }
.raw-chart-tip-row .vl { min-width: 4.5em; text-align: right; font-weight: 500; }
.raw-chart-tip-row .wy { color: var(--iot-text-secondary); }
.raw-chart-tip-row .wy.bad { color: var(--tblr-red); }

/* ── 수집 진행 화면 '이번 실행으로 달라진 것' 카드 (templates/_run_changes.html) ──
   요약 숫자 줄(.chg-tally) · 그룹 제목(.chg-grp) · 접어 둔 항목 목록(.chg-lines).
   칩은 값을 고르는 것이 아니라 읽는 것이라 .iot-chip 계열(버튼)을 쓰지 않는다 */
.chg-tally { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 4px; }
.chg-tally > span {
  font-size: 0.8125rem; padding: 3px 13px; border-radius: var(--iot-radius-pill);
  border: 1px solid var(--tblr-border-color); color: var(--iot-text-secondary);
  font-variant-numeric: tabular-nums;
}
.chg-tally > span > b { color: var(--tblr-body-color); font-weight: 600; }
.chg-grp {
  font-size: 0.8125rem; font-weight: 600; margin: 18px 0 2px;
}
.chg-grp > em { font-style: normal; font-weight: 400; color: var(--iot-text-secondary); }
.chg-old { color: var(--iot-text-secondary); text-decoration: line-through; }
.chg-arrow { color: var(--iot-text-secondary); margin: 0 5px; }
.chg-lines { list-style: none; margin: 0; padding: 0; }
.chg-lines > li {
  display: flex; gap: 12px; padding: 6px 0; font-size: 0.875rem;
  border-bottom: 1px solid var(--iot-border);
}
.chg-lines > li:last-child { border-bottom: 0; }
.chg-lines > li > span:first-child { flex: 1; color: var(--iot-text-secondary); }
.chg-num { font-variant-numeric: tabular-nums; }
.chg-fold > summary {
  display: inline-block; cursor: pointer; font-size: 0.875rem; color: var(--iot-text-secondary);
  padding: 6px 12px; margin-left: -12px; border-radius: var(--iot-radius-pill);
}
.chg-fold > summary:hover { background: var(--iot-surface-hover); color: inherit; }
.chg-fold > summary:focus-visible { outline: 2px solid var(--tblr-primary); outline-offset: 2px; }

/* ═══ 대시보드 (지도 + 사업장 현황) ═══════════════════════════════════
   상태 넷(정상·점검 필요·전원 꺼짐·자료 없음)을 지도 범례와 현황 카드가 같이 쓴다.
   **개수는 현황에만 적는다** — 범례는 색을 읽는 사전이라, 같은 숫자를 두 곳에 두면
   언젠가 한쪽만 고쳐져 어긋난다 (web/DESIGN.md '대시보드'). */
.dash-map {
  position: relative; height: 660px; border-radius: 0;
  background: var(--iot-surface-hover);
}
@media (max-width: 720px) { .dash-map { height: 510px; } }
/* 크기를 inset(늘리기)이 아니라 width/height 로 준다 — 네이버 지도가 컨테이너에
   `position: relative` 를 인라인으로 덮어써서, 늘려 둔 높이가 그 순간 0 이 된다
   (2026-08-25 실측: 핀은 51개 다 만들어졌는데 지도 칸 높이만 0이었다). */
.dash-map-canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
/* 키가 없거나 지도를 못 띄웠을 때 — 회색 사각형만 남지 않게 이유를 적는다 */
.dash-map-fallback {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 6px; padding: 24px; text-align: center;
  color: var(--iot-text-secondary); font-size: 0.875rem;
}
.dash-legend {
  position: absolute; left: 12px; bottom: 12px; z-index: 5;
  display: flex; flex-wrap: wrap; gap: 4px 14px; align-items: center;
  background: var(--iot-surface); border: 1px solid var(--iot-border);
  border-radius: var(--iot-radius-pill); padding: 6px 14px;
  font-size: 0.8125rem; color: var(--iot-text-secondary);
}
.dash-legend > span { display: inline-flex; align-items: center; gap: 5px; }
.dash-legend > span.on { color: var(--tblr-body-color); font-weight: 500; }
.dash-legend > span.dim { opacity: 0.4; }
.dash-legend i { width: 9px; height: 9px; border-radius: var(--radius-pill, 999px); }

/* 핀 — 지도 위 마커. 색은 상태 토큰, 테두리는 흰색이라 겹쳐도 개수가 읽힌다 */
.dash-pin { width: 22px; height: 30px; cursor: pointer; }
.dash-pin path { stroke: #fff; stroke-width: 1.4; }
.dash-pin.dim { opacity: 0.3; }
.dash-pin-ok path { fill: var(--iot-map-ok); }
.dash-pin-advise path { fill: var(--iot-map-advise); }
.dash-pin-check path { fill: var(--iot-map-check); }
.dash-pin-none path { fill: var(--iot-map-none); }

/* 핀 설명 — 지도 위에 뜨는 말풍선. 여기서만 볼 수 있는 정보는 두지 않는다(아래 표에 다 있다).
   지도 라이브러리의 정보창이 아니라 우리가 그리는 요소다 (핀 위 8px, 가운데 정렬) */
.dash-tip {
  position: absolute; z-index: 20; transform: translate(-50%, -100%); pointer-events: none;
  min-width: 210px; max-width: 280px; padding: 10px 12px;
  background: var(--iot-surface); border: 1px solid var(--iot-border);
  border-radius: var(--iot-radius-sm); box-shadow: var(--tblr-box-shadow-card, 0 1px 3px rgba(0,0,0,.1));
  font-size: 0.8125rem; line-height: 1.5;
}
.dash-tip b { display: block; font-size: 0.875rem; margin-bottom: 2px; }
.dash-tip .tip-meta, .dash-tip .tip-last { color: var(--iot-text-secondary); }
.dash-tip .tip-why { margin-top: 5px; font-weight: 500; }
.dash-tip .tip-why.check { color: var(--iot-map-check); }
.dash-tip .tip-why.advise { color: var(--iot-map-advise); }
.dash-tip .tip-why.ok { color: var(--iot-map-ok); }
.dash-tip .tip-why.none { color: var(--iot-map-none); }

/* 현황 다섯 칸(전체 + 상태 넷) — 라디오처럼 하나만 선택되고, 누르면 지도와 표가 함께
   걸러진다 (같은 선택을 두 곳에 두지 않는다). 전체 칸이 기본 선택 = 필터 해제. */
.dash-summary { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; }
@media (max-width: 940px) {
  .dash-summary { grid-template-columns: repeat(2, 1fr); }
  /* 전체는 상태가 아니다 — 좁은 화면에선 한 줄을 통으로 차지해 배치로도 구분된다 */
  .dash-sum-all { grid-column: 1 / -1; }
}
@media (max-width: 560px) { .dash-summary { grid-template-columns: 1fr; } }
.dash-sum {
  display: flex; align-items: center; gap: 14px; text-align: left;
  padding: 16px 18px; background: var(--iot-surface);
  border: 1px solid var(--iot-border); border-radius: var(--iot-radius-lg);
  cursor: pointer; transition: background 0.12s, border-color 0.12s;
}
.dash-sum:hover { background: var(--iot-surface-hover); }
.dash-sum:focus-visible { outline: 2px solid var(--tblr-primary); outline-offset: 2px; }
.dash-sum > .sum-dot { width: 12px; height: 12px; border-radius: var(--iot-radius-pill); flex: none; }
.dash-sum > .sum-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.dash-sum .sum-num {
  font-size: 1.875rem; font-weight: 700; line-height: 1.1;
  font-variant-numeric: tabular-nums; letter-spacing: -0.02em;
}
.dash-sum .sum-label { font-size: 0.875rem; font-weight: 500; }
.dash-sum .sum-note { font-size: 0.78125rem; color: var(--iot-text-secondary); }
/* 전체 칸의 점은 속이 빈 고리 — '자료 없음'의 회색 점과 헷갈리지 않게.
   테두리 포함 12px 로 다른 점과 같은 크기다 */
.dash-sum-all .sum-dot {
  width: 10px; height: 10px;
  background: transparent; border: 2px solid var(--iot-text-secondary);
}
.dash-sum-ok .sum-dot { background: var(--iot-map-ok); }
.dash-sum-advise .sum-dot { background: var(--iot-map-advise); }
.dash-sum-check .sum-dot { background: var(--iot-map-check); }
.dash-sum-none .sum-dot { background: var(--iot-map-none); }
.dash-sum[aria-pressed="true"] { border-color: currentColor; }
/* 전체 칸의 선택색은 상태 넷과 섞이지 않게 필터 칩과 같은 파란 톤 */
.dash-sum-all[aria-pressed="true"] {
  background: var(--iot-chip-active-bg); color: var(--iot-chip-active-fg);
}
.dash-sum-all[aria-pressed="true"] .sum-dot { border-color: currentColor; }
.dash-sum-ok[aria-pressed="true"] { background: var(--iot-map-ok-soft); color: var(--iot-map-ok); }
.dash-sum-advise[aria-pressed="true"] { background: var(--iot-map-advise-soft); color: var(--iot-map-advise); }
.dash-sum-check[aria-pressed="true"] { background: var(--iot-map-check-soft); color: var(--iot-map-check); }
.dash-sum-none[aria-pressed="true"] { background: var(--iot-map-none-soft); color: var(--iot-map-none); }
/* 칸 안의 숫자·라벨은 선택해도 본문 색을 지킨다 (배경만 옅게 물든다) */
.dash-sum[aria-pressed="true"] .sum-num,
.dash-sum[aria-pressed="true"] .sum-label { color: var(--tblr-body-color); }

/* 표의 상태 배지 — 배지 매크로(bg-*-lt)와 같은 모양이되 지도 색을 쓴다 */
.dash-tag {
  display: inline-block; border-radius: var(--iot-radius-pill);
  padding: 2px 10px; font-size: 0.8125rem; font-weight: 500; white-space: nowrap;
}
.dash-tag-ok { background: var(--iot-map-ok-soft); color: var(--iot-map-ok); }
.dash-tag-advise { background: var(--iot-map-advise-soft); color: var(--iot-map-advise); }
.dash-tag-check { background: var(--iot-map-check-soft); color: var(--iot-map-check); }
.dash-tag-none { background: var(--iot-map-none-soft); color: var(--iot-map-none); }
.dash-reason { color: var(--iot-text-secondary); }

/* ── 기간 선택 팝업 (사업장 상세 수신 자료, 2026-08-27) ─────────────────────
   기간 칩 + 달력 두 달 팝업. 시안: https://claude.ai/code/artifact/c4981939-f3f5-42dc-9126-152bb23bd4e6
   달력 그리기와 상태는 static/period_picker.js. 잘못된 날짜(31일 초과·미래)는
   비활성으로 그려져 아예 눌리지 않는다 — 오류 문구가 필요 없다. */
:root {
  --iot-range-bg: var(--iot-chip-active-bg);                 /* 구간 채움 */
  --iot-range-edge: light-dark(#1a3a5c, #7ea6d8);            /* 시작·끝 날짜 */
  --iot-range-edge-fg: light-dark(#ffffff, #14161a);
}
.pp-anchor { position: relative; display: inline-block; }
.pp-chip {
  height: 34px; padding: 0 12px 0 14px;
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 0.875rem; font-weight: 500; font-family: inherit;
  border: 1px solid transparent; border-radius: var(--iot-radius-pill);
  background: var(--iot-chip-active-bg); color: var(--iot-chip-active-fg);
  cursor: pointer;
}
.pp-chip:hover { background: color-mix(in srgb, var(--iot-chip-active-fg) 16%, transparent); }
.pp-caret { font-size: 10px; opacity: 0.7; }
.pp-pop {
  position: absolute; z-index: 1050; top: calc(100% + 8px); left: 0;
  display: flex; overflow: hidden;
  background: var(--iot-surface); border: 1px solid var(--iot-border);
  border-radius: var(--iot-radius-lg);
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);
  max-width: min(680px, calc(100vw - 32px));
}
.pp-pop[hidden] { display: none; }
.pp-presets {
  min-width: 116px; padding: 10px 0;
  border-right: 1px solid var(--iot-border);
  background: var(--tblr-bg-surface-secondary, var(--iot-surface));
}
.pp-preset {
  display: block; width: 100%; padding: 7px 16px;
  font-family: inherit; font-size: 0.8125rem; text-align: left;
  background: none; border: 0; color: inherit; cursor: pointer;
}
.pp-preset:hover { background: var(--iot-surface-hover); }
.pp-cal { padding: 12px 14px 14px; }
.pp-months { position: relative; display: flex; gap: 20px; }
.pp-mb { width: 238px; }
.pp-mtitle { text-align: center; font-size: 0.84rem; font-weight: 700; margin-bottom: 6px; }
/* 달 넘김 화살표 — 모양은 .iot-iconbtn, 여기서는 자리만 */
.pp-navbtn { position: absolute; top: -4px; z-index: 1; }
.pp-navbtn.prev { left: 0; }
.pp-navbtn.next { right: 0; }
.pp-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px 0; }
.pp-wd {
  font-size: 0.7rem; color: var(--iot-text-secondary);
  text-align: center; padding: 4px 0;
}
.pp-day {
  height: 30px; position: relative;
  font-family: inherit; font-size: 0.78rem; font-variant-numeric: tabular-nums;
  border: 0; border-radius: 0; background: none; color: inherit; cursor: pointer;
}
.pp-day:hover:not([disabled]):not(.edge) {
  background: var(--iot-surface-hover); border-radius: var(--iot-radius-pill);
}
.pp-day[disabled] { color: var(--iot-text-secondary); opacity: 0.45; cursor: default; }
.pp-day.blank { visibility: hidden; }
.pp-day.preview { background: var(--iot-surface-hover); }  /* 마우스가 지나가는 범위 미리보기 */
.pp-day.inrange { background: var(--iot-range-bg); }
.pp-day.edge {
  background: var(--iot-range-edge); color: var(--iot-range-edge-fg);
  border-radius: var(--iot-radius-pill); font-weight: 700;
}
/* 기간의 양 끝만 둥글고 가운데는 띠로 이어진다 (구글 캘린더와 같다, 2026-09-04) */
.pp-day.start.hasend { border-radius: var(--iot-radius-pill) 0 0 var(--iot-radius-pill); }
.pp-day.end { border-radius: 0 var(--iot-radius-pill) var(--iot-radius-pill) 0; }
/* 폭이 모자라면 한 달만 (‹ › 로 넘겨 본다) */
@media (max-width: 760px) {
  .pp-mb.m2 { display: none; }
}
/* 좁은 화면: 칩이 아니라 화면 기준 가운데, 프리셋은 달력 위에 가로로 */
@media (max-width: 560px) {
  .pp-pop {
    position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
    flex-direction: column; width: calc(100vw - 32px); max-width: 340px;
  }
  .pp-presets {
    display: flex; flex-wrap: wrap; gap: 2px; padding: 8px; min-width: 0;
    border-right: 0; border-bottom: 1px solid var(--iot-border);
  }
  .pp-preset { width: auto; padding: 5px 10px; border-radius: var(--iot-radius-pill); }
}

/* ── 사업장 지정 [자료 수집] 모달 — 달 넘김 + 하루 막대(uptime bar) 현황 ──
   시안: https://claude.ai/code/artifact/10ec07e4-cfe5-4727-aebb-f1d14c93f79a */
.cov-nav { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; }
.cov-nav .cov-month { font-weight: 700; min-width: 108px; text-align: center; }
.cov-nav .cov-fetch { margin-left: auto; }
/* 달 넘김 화살표는 .iot-iconbtn (기간 팝업과 같은 모양) */
.cov-strip { display: flex; gap: 3px; align-items: stretch; height: 38px; }
.cov-strip .cov-bar {
  flex: 1; min-width: 4px; border-radius: 3px; background: var(--iot-cov-off);
}
.cov-strip .cov-bar:hover { transform: scaleY(1.12); }
@media (prefers-reduced-motion: reduce) { .cov-strip .cov-bar:hover { transform: none; } }
.cov-bar.done, .cov-sw.done { background: var(--iot-cov-done); }
.cov-bar.recheck, .cov-sw.recheck {
  /* 받았지만 최근 이틀이라 다시 확인 — 초록에 빗금 */
  background: repeating-linear-gradient(-45deg, var(--iot-cov-done) 0 4px,
    color-mix(in srgb, var(--iot-cov-done) 55%, var(--iot-surface)) 4px 7px);
}
.cov-bar.todo, .cov-sw.todo { background: var(--iot-cov-todo); }
.cov-bar.fail, .cov-sw.fail { background: var(--iot-cov-fail); }
.cov-bar.nodata, .cov-sw.nodata { background: var(--iot-cov-none); }
.cov-bar.none, .cov-sw.none {
  /* 자료 없음이지만 아직 4일이 지나지 않아 다시 받는 날 — 회색에 빗금 (recheck 와 같은 규칙) */
  background: repeating-linear-gradient(-45deg, var(--iot-cov-none) 0 4px,
    color-mix(in srgb, var(--iot-cov-none) 55%, var(--iot-surface)) 4px 7px);
}
.cov-bar.off, .cov-bar.norec, .cov-sw.off, .cov-sw.norec { background: var(--iot-cov-off); }
.cov-ticks {
  display: flex; justify-content: space-between; margin-top: 3px;
  font-size: 11px; color: var(--iot-text-secondary); font-variant-numeric: tabular-nums;
}
.cov-readout {
  min-height: 21px; margin-top: 5px; font-size: 12.5px;
  color: var(--iot-text-secondary); font-variant-numeric: tabular-nums;
}
.cov-counts {
  display: flex; gap: 4px 14px; flex-wrap: wrap; margin-top: 6px;
  font-size: 12.5px; color: var(--iot-text-secondary); font-variant-numeric: tabular-nums;
}
.cov-sw {
  width: 10px; height: 10px; border-radius: 3px; display: inline-block;
  margin-right: 4px; transform: translateY(1px);
}
.cov-sw.off { border: 1px solid var(--iot-border); }

/* ── 자료 수집 팝업 배치 — 사업장 상세 (2026-09-03) ───────────────────────────
   팝업 본문 여백 안에 카드 두 장을 넣으니 테두리가 세 겹이 되어 상자 속 상자처럼 보였다.
   팝업 안에는 카드를 두지 않는다: 세션은 옅은 바탕 한 덩어리, 단은 작은 제목과 여백으로만
   나누고, 머리·푸터는 스크롤 밖에 고정이며 본문만 스크롤된다.
   규칙은 DESIGN.md '팝업 안에는 카드를 두지 않는다'. */
/* 대상(사업장명 · 코드)은 제목 오른쪽 — 제목 위에 두면 작은 글씨가 머리 위에 떠 어색하다 */
.cm-kicker { margin-left: auto; font-size: 12.5px; color: var(--iot-text-secondary); }
.collect-modal .modal-header .btn-close { margin-left: 8px; }
/* 스크롤바가 생겨도 좌우 여백이 같도록 그 자리를 양쪽에 미리 잡는다 (스크롤바가 오른쪽 여백을 먹던 문제) */
.collect-modal .modal-body { scrollbar-gutter: stable both-edges; scrollbar-width: thin; }
.collect-modal .modal-footer { display: flex; align-items: center; gap: 8px; justify-content: flex-start; }
.collect-modal .modal-footer > * { margin: 0; }
/* 세션 카드는 공용 부분 템플릿 그대로 두고 여기서만 옅은 바탕 한 덩어리로 칠한다 */
.collect-modal #gl-card { border: 0; box-shadow: none; background: var(--tblr-bg-surface-secondary); }
.collect-modal #gl-card .card-header { background: transparent; border: 0; padding: 10px 14px 0; min-height: 0; }
.collect-modal #gl-card .card-title { font-size: 13.5px; }
.collect-modal #gl-card .card-body { padding: 8px 14px 12px; font-size: 13.5px; }
.cm-sec { margin-top: 22px; }
.cm-h { font-size: 13.5px; font-weight: 600; margin: 0 0 10px; }
.cm-sec .sect-caption { margin-left: 0; }
/* 갱신 항목 — 한 줄에 하나, 켜진 항목은 옅은 파랑(고르기 문법: 칩·현황 칸과 같은 색) */
.info-pick { display: grid; gap: 8px; }
.info-item {
  display: flex; gap: 9px; align-items: flex-start; margin: 0; padding: 9px 12px;
  border: 1px solid var(--iot-border); border-radius: 8px; cursor: pointer; font-size: 13.5px;
}
.info-item:hover:not(:has(input:checked)) { background: var(--iot-surface-hover); }
.info-item:has(input:checked) { border-color: var(--tblr-primary); background: var(--iot-chip-active-bg); }
.info-item .form-check-input { margin-top: 3px; flex: none; }
.info-hint { display: block; font-size: 12px; color: var(--iot-text-secondary); }
.cm-running { margin-top: 22px; padding: 12px; border-radius: 8px; background: var(--tblr-bg-surface-secondary); font-size: 13.5px; }
/* 로그인 전에는 고를 것이 없다 — 두 단을 흐린다 (버튼은 잠긴다) */
#gl-collect-card[data-collect-state="locked"] .cm-sec { opacity: 0.45; pointer-events: none; }

/* ── 사업장 추가 모달: 두 칸에 걸친 중복·유사 안내 (2026-08-28) ──
   칸 밑(invalid-feedback)은 그 칸만의 짧은 형식 오류, 이 영역은 중복·비슷한 이름처럼
   두 칸에 걸친 안내를 줄 아래 전체 너비 한 줄씩 편다 — 좁은 칸 안에서 긴 문장이
   여러 줄로 접히지 않게 (Stripe 결제 폼의 카드 묶음 오류와 같은 배치). */
.pair-msgs { margin: -8px 0 16px; }
.pair-msgs .line { margin: 4px 0 0; font-size: 0.8125rem; color: var(--tblr-danger); }
.pair-msgs .line.warn { color: var(--iot-warn-text); }
.pair-msgs .cand { display: inline-block; margin-right: 10px; }


/* ── 상태 배지: 한 모양 (2026-08-31) ───────────────────────────────
   앞의 점은 색을 이름과 붙여 읽게 한다 — 색만으로 구분하면 색을 못 보는 사람에게는
   같은 회색 알약 넷이다. 색은 지금까지처럼 Tabler 라이트 톤(bg-*-lt)이 주고,
   점은 글자색(currentColor)을 따라간다. 상태를 말하는 배지에만 붙인다
   (담당 배지처럼 분류를 말하는 것에는 붙이지 않는다 — 점이 상태로 읽힌다). */
.iot-badge {
  border-radius: var(--iot-radius-pill);
  padding: 0.3rem 0.75rem;
  min-height: 1.625rem;
  font-size: 0.8125rem;
  font-weight: 600;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}
.iot-badge::before {
  content: "";
  width: 7px; height: 7px; border-radius: 50%;
  background: currentColor;
  flex: none;
}

/* 있음/없음을 나란히 보이는 배지 — 없는 것도 자리를 지킨다 (2026-09-17, 보고서 목록 '파일' 열).
   있는 것만 그리면 '엑셀만 있다'와 '무엇이 있는지 모르겠다'가 같은 모양이 된다. 점은 속 빈 원. */
.iot-badge-off {
  background: transparent;
  color: var(--iot-text-secondary);
  border: 1px dashed var(--iot-border);
  font-weight: 500;
}
.iot-badge-off::before { background: transparent; box-shadow: inset 0 0 0 1px currentColor; }

/* 누를 수 있는 배지 — 모양은 .iot-badge 가, 여기선 버튼 리셋과 hover (2026-09-17, 보고서 목록 '파일' 열).
   이미 색이 있는 알약이라 자기 색으로 한 단계 진해진다(DESIGN.md 'hover 는 눌리는 범위를 그대로 보인다').
   배지에 hover 를 주는 자리는 여기뿐이다 — 누를 수 있게 만들었기 때문이다. */
.iot-badge-btn {
  border: 0; cursor: pointer; font: inherit;
  border-radius: var(--iot-radius-pill);
}
/* Tabler 의 bg-*-lt 가 배경색을 !important 로 박아 두어 hover 도 같은 무게로 덮어야 한다 */
.iot-badge-btn:hover { background-color: color-mix(in srgb, currentColor 22%, var(--iot-surface)) !important; }
.iot-badge-btn:active { background-color: color-mix(in srgb, currentColor 32%, var(--iot-surface)) !important; }
.iot-badge-btn:focus-visible { outline: 2px solid var(--tblr-primary); outline-offset: 2px; }

/* ── 점검 내역 (2026-09-01) ─────────────────────────────────────────────
   한 기록 = 한 번의 방문. 내용은 공통 + 배출구별이고, 표에는 공통 두 줄 + 적은 배출구 이름만
   보여주고 [펼치기]로 **같은 칸에서** 나머지를 편다 (Material 은 목록 한 줄을 최대 세 줄로 보고
   그보다 길면 목록이 아닌 다른 표현을 쓰라고 한다 — 우리 내용은 한 칸에 문단이 여럿이라
   표에는 실마리만 둔다). 다른 행을 열지 않으므로 읽던 자리가 그대로 있다. */
.insp-lead {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; white-space: pre-wrap;
}
.insp-cell.open .insp-lead { display: block; overflow: visible; }
.insp-foot { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.insp-outlets { color: var(--iot-text-secondary); font-size: 0.8125rem; }
.insp-cell.open .insp-outlets { display: none; }   /* 펼치면 이름이 아니라 내용이 보인다 */
.insp-foot-open { display: none; }
.insp-cell.open .insp-foot-open { display: flex; }
.insp-cell.open > .insp-foot:not(.insp-foot-open) { display: none; }
/* 배출구별 내용 — 접혀 있다가 [펼치기]로 열린다 */
.insp-parts { display: none; }
.insp-cell.open .insp-parts { display: grid; gap: 0.375rem; margin-top: 0.375rem; }
.insp-parts .ipart { display: grid; grid-template-columns: 6rem 1fr; gap: 0.75rem; align-items: baseline; }
.insp-parts .ipart .k { color: var(--iot-text-secondary); font-size: 0.8125rem; }
.insp-parts .ipart .t { white-space: pre-wrap; }
@media (max-width: 576px) { .insp-parts .ipart { grid-template-columns: 1fr; gap: 0.125rem; } }
/* [펼치기]·[접기] — 글자만 있으면 누를 수 있는지 알기 어려워 hover 에 알약 배경을 준다.
   앞의 삼각형은 상태 배지(드롭다운)의 것과 같은 모양·크기다 (Bootstrap caret) */
.insp-more {
  display: inline-flex; align-items: center; gap: 0.4rem;
  border: 0; background: transparent; color: var(--iot-chip-active-fg);
  font-family: inherit; font-size: 0.8125rem; line-height: 1.6;
  padding: 0.125rem 0.625rem; border-radius: var(--iot-radius-pill); cursor: pointer;
}
.insp-more::before {
  content: ""; display: inline-block;
  border-top: 0.3em solid; border-right: 0.3em solid transparent;
  border-bottom: 0; border-left: 0.3em solid transparent;
}
.insp-more.insp-less::before { transform: rotate(180deg); }
.insp-more:hover { background: var(--iot-chip-active-bg); }
.insp-more:focus-visible { outline: 2px solid var(--tblr-primary); outline-offset: 2px; }

/* 월 보고서 상세의 점검 내역 — 다음 달 방문을 가르는 구분 줄 (2026-09-04).
   행이 아니라 표 안의 소제목이라 hover·커서가 없고, 글자는 표 머리와 같은 단계다 */
.rec-table tbody tr.insp-group > td {
  padding-top: 5px; padding-bottom: 5px;
  font-size: 12px; font-weight: 600; letter-spacing: 0.04em;
  color: var(--iot-text-secondary);
  background: var(--tblr-bg-surface-secondary);
}

/* 표의 상태 — 배지 모양 그대로 누르면 셋이 뜬다 */
.insp-status .dropdown-toggle { border: 0; cursor: pointer; }
/* 배지는 이미 알약이라 hover 는 한 단계 진하게 — 다크는 바탕이 어두워 밝히는 쪽이 진해 보인다 */
.insp-status .dropdown-toggle:hover { filter: brightness(0.94); }
[data-bs-theme="dark"] .insp-status .dropdown-toggle:hover { filter: brightness(1.2); }
.insp-status .dropdown-toggle::after { margin-left: 0.35rem; }
/* 메뉴는 document.body 에 붙어 fixed 로 뜬다(.iot-float-menu) — 표의 가로 스크롤 상자 안에 그리면 잘리고 스크롤 영역만 넓어진다 (.raw-pop 과 같은 처리) */
.insp-status-menu { min-width: 8rem; max-height: var(--iot-float-max, none); overflow-y: auto; padding: 0.25rem; border: 0; }
.insp-status-menu .dropdown-item { border-radius: var(--iot-radius-sm); }
/* 쓰기 폼 — 공통 + 배출구별 칸. 라벨은 칸 위에 둔다 (다른 폼 칸과 같은 문법이고,
   배출구 이름이 길어져도 칸 너비가 흔들리지 않는다) */
.insp-write { display: grid; gap: 0.625rem; }
.insp-write .iwrite { display: grid; gap: 0.375rem; }
.insp-write .ilabel { font-size: 0.8125rem; color: var(--iot-text-secondary); margin: 0; }
.insp-write .iwrite:first-child .ilabel { color: var(--tblr-body-color); font-weight: 500; }
.insp-write textarea.form-control { min-height: 2.75rem; resize: vertical; overflow: hidden; }
/* 점검 기록 모달 — 내용 칸이 공통 + 배출구 수만큼이라 기본 폭(500px)으로는 한 줄이 자주 접힌다 */
.modal-dialog.insp-dialog { max-width: 40rem; }

/* 폼 안의 상태 배지 — 옆 칸(글 상자)과 같은 높이 슬롯 안에 세로 가운데로 둔다.
   배지를 칸처럼 늘리지 않는 이유는 표에서 쓰는 것과 같은 부품으로 보여야 하기 때문이다 */
.insp-status-field { min-height: 2.75rem; display: flex; align-items: center; }

/* 상태 메뉴의 색점 — 배지와 같은 색을 항목 앞에 둔다 (고르기 전에도 무엇이 무엇인지 보이게) */
.insp-status-menu .dropdown-item { display: flex; align-items: center; gap: 0.5rem; }
.insp-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.insp-dot-log { background: var(--iot-map-none); }
.insp-dot-doing { background: var(--tblr-yellow); }
.insp-dot-done { background: var(--tblr-green); }

/* 전사 목록의 사업장 고르기 — 검색 칸(돋보기)과 그 아래 결과 목록 */
.insp-pick-field { position: relative; }
.insp-pick-field svg {
  position: absolute; left: 0.875rem; top: 50%; transform: translateY(-50%);
  width: 18px; height: 18px; fill: none; stroke: var(--iot-text-secondary); stroke-width: 2;
  stroke-linecap: round; pointer-events: none;
}
.insp-pick-field .form-control { padding-left: 2.5rem; }
.insp-pick-list {
  border: 1px solid var(--iot-border); border-radius: var(--iot-radius-sm);
  margin-top: 0.375rem; overflow: hidden; background: var(--iot-surface);
}
.insp-pick-list button {
  display: flex; align-items: center; gap: 0.625rem; width: 100%; text-align: left;
  padding: 0.5rem 0.875rem; font: inherit; font-size: 0.875rem; color: inherit;
  background: transparent; border: 0; border-bottom: 1px solid var(--iot-border); cursor: pointer;
}
.insp-pick-list button:last-child { border-bottom: 0; }
.insp-pick-list button:hover { background: var(--iot-surface-hover); }
.insp-pick-list .code { margin-left: auto; color: var(--iot-text-secondary); font-size: 0.8125rem; font-variant-numeric: tabular-nums; }
.insp-picked {
  display: flex; align-items: center; gap: 0.625rem;
  border: 1px solid var(--iot-input-border); border-radius: var(--iot-radius-sm);
  background: var(--iot-input-bg); padding: 0.5rem 0.875rem; min-height: 2.75rem;
}

/* 전사 점검 목록의 현황 칸 — 대시보드 현황(.dash-summary/.dash-sum)과 같은 부품이고
   칸 수와 색만 다르다. **색은 표의 상태 배지와 같은 것을 쓴다** — 같은 상태가 위아래에서
   다른 색이면 보는 사람이 내용보다 '왜 다르지'를 먼저 생각한다 (대시보드는 지도 색을 쓰는데
   그건 지도 핀과 짝지어야 해서다). 눌린 칸의 배경은 배지 배경(bg-*-lt)과 같은 변수다 */
.insp-summary { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 940px) { .insp-summary { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 560px) { .insp-summary { grid-template-columns: 1fr; } }
.insp-sum-doing .sum-dot { background: var(--tblr-yellow); }
.insp-sum-done .sum-dot { background: var(--tblr-green); }
.insp-sum-log .sum-dot { background: var(--tblr-secondary); }
.insp-sum-doing[aria-pressed="true"] { background: var(--tblr-yellow-lt); }
.insp-sum-done[aria-pressed="true"] { background: var(--tblr-green-lt); }
.insp-sum-log[aria-pressed="true"] { background: var(--tblr-secondary-lt); }

/* ── 부품 카탈로그 (/design, templates/design.html) ────────────────
   화면을 만들 때 쓰는 부품을 한 자리에 늘어놓는 페이지. 여기 스타일은 카탈로그의
   설명 틀(이름·설명·예시·코드)만 담당한다 — 예시 자체는 부품이 자기 스타일로 그린다.
   새 색을 만들지 않고 기존 토큰만 쓴다 (카탈로그가 규칙을 어기면 안 된다) */
/* 목차 + 본문 두 칸. 레코드형(.detail-grid)과 같은 문법이되 폭이 다르다 —
   여기 사이드바는 카드가 아니라 이름 목록이라 312px 을 다 쓰지 않는다.
   좁은 화면에서는 목차가 본문 위로 간다 (사업장 상세와 달리 탭으로 합칠 곳이 없다) */
.ds-layout {
  display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 2rem; align-items: start;
}
.ds-nav {
  position: sticky; top: 1rem; max-height: calc(100vh - 2rem); overflow-y: auto;
  padding-right: 12px; scrollbar-gutter: stable;
}
.ds-find { margin-bottom: 1rem; }
.ds-navgrp + .ds-navgrp { margin-top: 1rem; }
.ds-navgrp-title {
  margin: 0 0 4px 10px; font-size: 12px; font-weight: 600; color: var(--iot-text-secondary);
}
.ds-navlink {
  display: block; padding: 5px 10px; border-radius: var(--iot-radius-sm);
  font-size: 13px; color: var(--tblr-body-color);
}
.ds-navlink:hover { background: var(--iot-surface-hover); }
/* 지금 보고 있는 자리 — 스크롤을 따라 옮겨진다 (aria-current 로도 알린다) */
.ds-navlink[aria-current] { background: var(--iot-chip-active-bg); color: var(--iot-chip-active-fg); font-weight: 600; }
.ds-navlink:focus-visible { outline: 2px solid var(--tblr-primary); outline-offset: -2px; }
@media (max-width: 991.98px) {
  .ds-layout { grid-template-columns: minmax(0, 1fr); gap: 1rem; }
  .ds-nav { position: static; max-height: none; overflow-y: visible; padding-right: 0; }
}

/* 코드는 접어 둔다 — 훑어볼 때 눈에 걸리지 않게 (2026-09-01) */
.ds-fold > summary {
  display: inline-block; margin-top: .5rem; cursor: pointer;
  font-size: .8125rem; color: var(--iot-text-secondary);
}
.ds-fold > summary:hover { color: var(--iot-nav-accent); background: var(--iot-surface-hover); }
.ds-fold > summary:focus-visible { outline: 2px solid var(--tblr-primary); outline-offset: 2px; border-radius: var(--iot-radius-sm); }

.ds-item { padding: 1rem 0; border-top: 1px solid var(--iot-border); }
.ds-item:first-child { padding-top: 0; border-top: 0; }
.ds-item-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: .5rem; margin-bottom: .75rem; }
.ds-name { font-weight: 600; font-size: .9375rem; color: var(--tblr-body-color); }
.ds-desc { color: var(--iot-text-secondary); font-size: .8125rem; }
/* 예시 자리 — 부품이 실제로 놓이는 바탕(카드 뒤 페이지 배경)을 깐다.
   **hover 색(--iot-surface-hover)을 여기 쓰면 안 된다** — 부품 열세 곳이 그 색으로 hover 하므로
   바탕이 같으면 hover 가 일어나도 아무 일도 안 일어난 것처럼 보인다 (2026-09-01 실측: back·gitem·
   칩·주 탭이 전부 그랬다). 카드(흰 표면)를 그대로 쓰면 반대로 ggroup·card 예시가 바탕에 묻힌다 */
.ds-demo {
  display: flex; flex-wrap: wrap; align-items: center; gap: .5rem;
  padding: 1rem; border-radius: var(--iot-radius-sm);
  background: var(--tblr-bg-surface-secondary);
}
.ds-demo > * { max-width: 100%; }
/* '버튼' 칸 — 역할마다 한 줄, 왼쪽에 역할 이름 */
.ds-btn-roles { display: grid; gap: 10px; width: 100%; }
.ds-btn-role { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.ds-btn-label { flex: 0 0 15em; font-size: 0.8125rem; color: var(--iot-text-secondary); }
@media (max-width: 576px) { .ds-btn-label { flex-basis: 100%; } }
/* 예시 안에서 세로로 쌓여야 하는 부품(필드 그룹·표·섹션)은 한 줄을 통째로 쓴다 */
.ds-demo > .ggroup, .ds-demo > .card, .ds-demo > .sect-head, .ds-demo > .subsect,
.ds-demo > .iot-filter-row, .ds-demo > .sect-caption,
.ds-demo > .row, .ds-demo > .ds-show-hidden { flex: 1 0 100%; }
.ds-code {
  margin: .5rem 0 0; padding: .75rem; overflow-x: auto;
  border-radius: var(--iot-radius-sm); background: var(--iot-field-bg);
  color: var(--iot-text-secondary); font-size: .8125rem; line-height: 1.6; white-space: pre;
}
.ds-swatches { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: .5rem; }
.ds-swatch { display: flex; align-items: center; gap: .5rem; font-size: .8125rem; }
.ds-chipcolor {
  width: 1.5rem; height: 1.5rem; flex: none;
  border-radius: var(--iot-radius-sm); border: 1px solid var(--iot-border);
}
.ds-icon { display: inline-flex; align-items: center; gap: .375rem; margin-right: .75rem; }

/* 카탈로그에서만: 평소 숨어 있는 부품(list_empty)을 보여 주는 자리 */
.ds-show-hidden > .d-none { display: block !important; }

/* ── 자체 분류 규칙 페이지 (rules.html, 읽기 전용 참조 · 1차) ── */
.rules-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--iot-border); }
.rules-tab {
  font-size: 0.9375rem; padding: 8px 14px; position: relative;
  color: var(--iot-text-secondary);
  display: inline-flex; align-items: center; gap: 6px;
}
.rules-tab.active { color: var(--iot-nav-accent); font-weight: 600; }   /* 밑줄은 '현재 위치 밑줄' 공용 규칙 */
.rules-tab.disabled { opacity: 0.6; }
.rules-class-name { flex: none; min-width: 12rem; }
.rules-class-plain { color: var(--iot-text-secondary); font-size: 0.8125rem; }
.rules-combo-table td, .rules-combo-table th { white-space: nowrap; }
.rules-num { font-variant-numeric: tabular-nums; }
.rules-empty { color: var(--iot-text-secondary); opacity: 0.7; font-size: 0.75rem; }

/* ── 자체 분류 규칙 페이지 편집 UI (rules.html, 2차) ── */
.rules-tab { text-decoration: none; }
.cond-any { color: var(--iot-text-secondary); opacity: .55; }
.cond-blank { color: var(--iot-text-secondary); font-style: italic; }
.cond-in { }
.rules-decision td, .rules-decision th { white-space: nowrap; }
.rules-ord { width: 1%; text-align: center; font-variant-numeric: tabular-nums; }
.rules-fallback td { background: var(--iot-field-bg); }
.rules-rowacts { text-align: right; }
.rules-icon { font-size: 0.7rem; }  /* ▲▼ 글자만 작게 — 크기·모양은 .iot-iconbtn */
.rules-class-desc { min-width: 0; }
/* 미분류 인박스 — 나타나는 것 자체가 알림 (건수 0이면 렌더되지 않는다) */
.rules-inbox { border: 1px solid var(--tblr-yellow); border-radius: var(--iot-radius-lg);
  background: color-mix(in srgb, var(--tblr-yellow) 8%, transparent); padding: 14px 18px; margin-bottom: 20px; }
.rules-inbox-t { font-weight: 700; font-size: 0.95rem; margin-bottom: 8px; }
.rules-inbox-row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin: 6px 0; }
.rules-combo { display: flex; flex-wrap: wrap; gap: 6px; flex: 1; min-width: 0; }
.rules-combo .kv { font-size: 0.78rem; background: var(--iot-surface); border: 1px solid var(--iot-border);
  border-radius: 6px; padding: 2px 8px; }
/* 규칙 조건 편집 모달 */
.rules-cond-title { font-size: 0.8rem; color: var(--iot-text-secondary); margin-bottom: 10px; }
.rules-axis { display: grid; grid-template-columns: 138px 120px 1fr; gap: 10px; align-items: center;
  padding: 8px 0; border-bottom: 1px solid var(--iot-border); }
.rules-axis:last-of-type { border-bottom: none; }
.rules-axis-name { font-size: 0.85rem; font-weight: 500; }
.rules-val { min-height: 34px; display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
.rules-muted { color: var(--iot-text-secondary); font-size: 0.85rem; }
.rules-eq, .rules-addval { max-width: 260px; }
.rules-chip { display: inline-flex; align-items: center; gap: 5px; font-size: 0.78rem;
  background: var(--iot-chip-active-bg); color: var(--iot-chip-active-fg); border-radius: 999px;
  padding: 2px 6px 2px 10px; font-weight: 600; }
.rules-chip button {
  width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center;
  border: none; border-radius: 50%; background: none; color: inherit; cursor: pointer;
  font-size: 0.9rem; line-height: 1; padding: 0;
}
.rules-chip button:hover { background: color-mix(in srgb, currentColor 14%, transparent); }
.rules-outsel { display: grid; grid-template-columns: 138px 1fr; gap: 10px; align-items: center;
  margin-top: 14px; padding-top: 14px; border-top: 2px solid var(--iot-border); }
.rules-outlabel { font-size: 0.85rem; font-weight: 500; }

/* ── 변경 이력 (2026-09-04, change_row / change_modal) ─────────────────────────
   저장 이름 대신 화면 이름, 같은 순간의 여러 줄은 한 줄(web/change_view.py). 동작은 작은 색 표(.cl-act):
   초록=생김 · 파랑=바뀜 · 빨강=없앰 · 노랑=사람의 판단(분류) · 회색=일어난 일(실행·열람).
   색은 상태 배지(bg-*-lt)와 같은 다섯이고, 빨강은 어디서든 '없앴다'다. 점(iot-badge::before)은
   붙이지 않는다 — 상태가 아니라 동작이다. 작업자가 사람이 아니면 회색 글자 '시스템'(.cl-sys),
   아이콘은 없다(앱의 다른 작업자 표기 15곳이 전부 글자다). 접두어 .chg- 는 수집 실행 화면 것이라 피했다. */
.cl-act {
  display: inline-flex; align-items: center; height: 20px; padding: 0 7px;
  border-radius: 5px; font-size: 11.5px; font-weight: 500; vertical-align: middle;
  margin-right: 4px; white-space: nowrap;
}
.cl-add { background: var(--tblr-green-lt); color: var(--tblr-green); }
.cl-upd { background: var(--tblr-blue-lt); color: var(--tblr-blue); }
.cl-del { background: var(--tblr-red-lt); color: var(--tblr-red); }
.cl-cls { background: var(--tblr-yellow-lt); color: var(--iot-warn-text); }
.cl-run { background: var(--tblr-secondary-lt); color: var(--tblr-secondary); }
.cl-fld { font-weight: 500; }
.cl-old { color: var(--iot-text-secondary); }
.cl-arrow { color: var(--iot-text-secondary); margin: 0 5px; }
.cl-note { color: var(--iot-text-secondary); }
.cl-sys { color: var(--iot-text-secondary); }
.cl-biz { font-weight: 500; }
.cl-code { color: var(--iot-text-secondary); font-size: 12px; font-variant-numeric: tabular-nums; }

/* 세부 창 — 메타 4줄(라벨 13px 회색은 다른 폼과 같다) + 항목별 이전/새 값 표 + 기록 번호 한 줄 */
.cl-meta { display: grid; grid-template-columns: 6rem 1fr; gap: 6px 14px; margin: 0 0 16px; }
.cl-meta dt { font-size: 13px; font-weight: 400; color: var(--iot-text-secondary); margin: 0; }
.cl-meta dd { margin: 0; overflow-wrap: anywhere; }
.cl-meta .cl-raw { color: var(--iot-text-secondary); }
.cl-diff-wrap { border: 1px solid var(--iot-border); border-radius: 10px; overflow: auto; }
.cl-diff { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.cl-diff th {
  text-align: left; font-weight: 500; font-size: 12px; color: var(--iot-text-secondary);
  padding: 8px 10px; border-bottom: 1px solid var(--iot-border); background: var(--tblr-bg-surface-secondary);
  white-space: nowrap;
}
.cl-diff td { padding: 8px 10px; border-bottom: 1px solid var(--iot-border); vertical-align: top; overflow-wrap: anywhere; }
.cl-diff tbody tr:last-child td { border-bottom: 0; }
.cl-diff td.cl-old { color: var(--iot-text-secondary); }
.cl-diff td.cl-empty { color: var(--iot-text-secondary); }
.cl-ids { margin: 12px 0 0; font-size: 12.5px; color: var(--iot-text-secondary); }
.cl-ids code { font-size: 12px; background: var(--iot-field-bg); padding: 1px 6px; border-radius: 4px; }

/* 전체 이력의 기간 칩 — 셋 중 하나를 고르는 것이라 aria-current 가 진실이고 .active 는 그 표시.
   서버 링크다(기간은 서버가 거른다) — 값을 고르는 칩(.iot-chip)이 아니라 켜진 칩 모양을 빌린다 */
.cl-period { display: inline-flex; gap: 6px; }
.cl-period .iot-chip-toggle { text-decoration: none; }

/* 사업장 상세 탭의 [N건 더 보기] — 표 아래 오른쪽. 다섯 줄을 다 읽은 눈이 멈추는 자리다 */
.cl-more-row { display: flex; justify-content: flex-end; margin-top: 8px; }

/* ── 메일 문구 설정 (2026-09-17) ─────────────────────────────────────────────
   왼쪽에서 고치고 오른쪽에서 실제로 나갈 메일을 본다. 색은 전부 토큰이다.
   시안: https://claude.ai/artifact/NbMexvKN23q7JUDYog2YjL */

/* 보낸사람 줄 — 서버 설정에서 오는 값이라 자물쇠를 달고 못 고친다 */
.mail-sender { display: flex; flex-wrap: wrap; }
.mail-sender-cell { flex: 1 1 220px; padding: 12px 18px; border-right: 1px solid var(--iot-border); }
.mail-sender-cell:last-child { border-right: 0; }
.mail-sender-k { font-size: 12px; color: var(--iot-text-secondary); display: flex; align-items: center; gap: 5px; }
.mail-sender-v { font-weight: 600; font-size: 14px; margin-top: 2px; overflow-wrap: anywhere; }
.mail-sender-src { font-size: 11px; color: var(--iot-text-secondary); margin-top: 2px; }

/* 탭 — 알약 칩 한 벌(34px). 고르는 것이라 aria-selected 가 진실이다 */
.mail-tabs { display: flex; gap: 8px; flex-wrap: wrap; padding: 14px 16px 0; }
.mail-tab {
  height: 34px; padding: 0 16px; border-radius: var(--iot-radius-pill); font-size: 13px; font-weight: 500;
  border: 1px solid var(--iot-input-border); background: var(--iot-surface); color: var(--iot-text-secondary);
}
.mail-tab:hover { background: var(--iot-surface-hover); }
.mail-tab[aria-selected="true"] {
  background: var(--iot-chip-active-bg); border-color: transparent; color: var(--iot-chip-active-fg); font-weight: 600;
}

/* 편집칸 — 자리표가 배지로 선다. 저장되는 값은 그대로 {사업장명} 이다 */
.mail-editor {
  min-height: 40px; padding: 9px 12px; font-size: 14px; line-height: 1.7;
  background: var(--iot-input-bg); border: 1px solid var(--iot-input-border); border-radius: var(--iot-radius-sm);
  overflow-wrap: anywhere;
}
.mail-editor-big { min-height: 190px; line-height: 1.8; }
.mail-editor:focus { outline: 2px solid var(--iot-chip-active-fg); outline-offset: 1px; }
.mail-chip {
  display: inline-block; padding: 0 9px; margin: 0 1px; border-radius: var(--iot-radius-pill);
  background: var(--iot-chip-active-bg); color: var(--iot-chip-active-fg);
  border: 1px solid var(--iot-border); font-size: 12.5px; font-weight: 600; line-height: 1.7;
  white-space: nowrap; user-select: all;
}
.mail-tokens { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin-top: 8px; }
.mail-token {
  padding: 3px 11px; border-radius: var(--iot-radius-pill); font-size: 12.5px; font-weight: 500;
  border: 1px dashed var(--iot-input-border); background: var(--iot-surface); color: var(--iot-chip-active-fg);
}
.mail-token:hover { background: var(--iot-chip-active-bg); }

/* 미리보기 — 받는 사람 눈에 보이는 차례 그대로 */
.mail-preview { margin: 16px; border: 1px solid var(--iot-border); border-radius: var(--iot-radius-sm); overflow: hidden; }
.mail-pv-head {
  padding: 12px 16px; border-bottom: 1px solid var(--iot-border);
  background: var(--tblr-bg-surface-secondary); font-size: 13px; display: grid; gap: 4px;
}
.mail-pv-head > div { display: flex; gap: 8px; }
.mail-pv-k { color: var(--iot-text-secondary); flex: none; width: 62px; }
.mail-pv-subject { font-weight: 600; color: var(--tblr-body-color); }
.mail-pv-body { padding: 14px 16px; font-size: 13.5px; color: var(--iot-text-secondary); line-height: 1.8; white-space: pre-wrap; }
.mail-pv-attach {
  display: inline-flex; align-items: center; gap: 8px; margin: 0 16px 14px; padding: 7px 12px;
  border: 1px solid var(--iot-border); border-radius: var(--iot-radius-sm);
  background: var(--tblr-bg-surface-secondary); font-size: 12.5px; color: var(--iot-text-secondary);
}
/* 서명 — 메일에 실제로 실리는 것과 같은 배치(로고 62px, 위에 남색 선) */
.mail-sig { display: flex; gap: 16px; align-items: flex-start; margin: 16px; padding-top: 16px; border-top: 2px solid var(--iot-nav-accent); }
.mail-sig img { flex: none; width: 62px; height: auto; }
.mail-sig-bar { width: 1px; align-self: stretch; background: var(--iot-border); }
.mail-sig-name { font-size: 15px; font-weight: 700; color: var(--iot-nav-accent); }
.mail-sig-role { font-size: 12.5px; color: var(--iot-text-secondary); margin-top: 1px; }
.mail-sig-lines { margin-top: 9px; display: grid; gap: 2px; font-size: 12px; color: var(--iot-text-secondary); }
.mail-sig-lb { color: var(--iot-text-secondary); opacity: .75; }
