/* ---------- Design tokens ---------- */
:root {
  --color-bg: #f3f5f8;
  --color-surface: #ffffff;
  --color-border: #dde3ea;
  --color-text: #1f2933;
  --color-text-muted: #667085;
  --color-primary: #1d4e89;
  --color-primary-dark: #143a66;
  --color-primary-contrast: #ffffff;
  --color-danger: #b3261e;
  --color-danger-bg: #fdecea;
  --color-success: #2e7d32;
  --color-success-bg: #eaf6ec;
  --radius: 6px;
  --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06);
  --shadow-md: 0 2px 10px rgba(16, 24, 40, 0.08);
  --nav-height: 56px;

  /* 2026-07-17 UI 색상/배경 디자인 확정 사양 — 카테고리별 확정 팔레트로 교체.
     -bg 값은 각 색을 흰색과 약 10% 비율로 섞은 톤(옅은 배경/비활성 탭 톤)으로
     통일해서 계산함. */
  --color-account: #F17C55;      /* 가입정보 */
  --color-account-bg: #FEF2EE;
  --color-device: #9683DB;       /* 기기정보 */
  --color-device-bg: #F5F3FB;
  --color-product: #7F621F;      /* 제품정보 (카레골드) */
  --color-product-bg: #F2EFE9;
  --color-address: #30A48A;      /* 주소정보 */
  --color-address-bg: #EAF6F3;
  --color-personal: #E05A78;     /* 개인정보 */
  --color-personal-bg: #FCEFF2;
  --color-admin: #8C8296;        /* 권한관리 (중립톤) */
  --color-admin-bg: #F4F3F5;
  --color-chip-border: #D1D5DB;
  --color-chip-text: #374151;
}

* {
  box-sizing: border-box;
}

/* The [hidden] attribute is only display:none via the browser's UA stylesheet,
   which any author rule that sets display (e.g. .btn's display:inline-flex,
   .modal-overlay's display:flex) silently overrides regardless of selector
   specificity — author rules always beat UA rules. Confirmed twice now
   (#create-new-btn stayed visible via .btn's display:inline-flex even with
   hidden set; .modal-overlay had the same issue earlier). One global rule
   here instead of fixing each individual class as it's discovered. */
[hidden] {
  display: none !important;
}

html, body {
  margin: 0;
  padding: 0;
}

/* body를 세로 flex 컨테이너로 만들어 .app-header(자식1)+<main>(자식2) 구조에서
   .page--list가 "헤더 높이를 뺀 나머지 전체"를 항상 정확히 채우게 함
   (flex:1, 아래 .page--list 참고) — 예전엔 .page--list가 calc(100vh -
   var(--nav-height))로 헤더의 실제 높이를 56px로 "추정"해서 빼는 방식이었는데,
   네비게이션 바 디자인을 조금만 바꿔도(예: 알약 탭 패딩/보더) 실제 헤더 높이가
   그 추정치와 어긋나 PC 조회화면에 원치 않는 스크롤바가 계속 생기는 문제가
   반복됐음(2026-07-18) — flex 방식은 헤더의 실제 렌더링 높이를 몰라도 항상
   "남은 공간 전체"를 정확히 계산해주므로 이 사양 종류의 버그 자체가 구조적으로
   재발할 수 없음.
   반드시 min-height가 아니라 height여야 함(2026-07-18, 실기기에서 발견된
   회귀) — min-height는 바닥값일 뿐이라 body가 콘텐츠에 맞춰 자유롭게
   늘어날 수 있고, 그러면 .page--list의 flex:1이 "뷰포트 안에서 남는 공간"으로
   강제되지 못해 내부 overflow-y:auto(.list-shell__scroll)가 전혀 발동하지
   않음 — 그 결과 조회화면 전체가 그냥 늘어나 버려서 계정칩/버튼(.list-shell__
   bottom)이 화면 아래로 밀려나고 스크롤로도 닿을 수 없게 됨(가입정보/기기정보
   실기기에서 재현됨). height로 body 자체를 뷰포트 높이에 정확히 고정해야
   .page--list의 flex:1이 진짜로 "그 안에서" 압축되어 내부 스크롤이 살아남 —
   .page(목록 아닌 상세/권한관리 등)처럼 원래도 고정 높이가 아니었던 화면은
   콘텐츠가 이 높이를 넘으면 브라우저가 항상 그렇듯 문서 자체를 자연스럽게
   스크롤하므로 이 변경으로 영향받지 않음. */
body {
  font-family: "Pretendard", "Apple SD Gothic Neo", "Malgun Gothic", -apple-system, BlinkMacSystemFont, sans-serif;
  background: var(--color-bg);
  color: var(--color-text);
  line-height: 1.5;
  font-size: 15px;
  display: flex;
  flex-direction: column;
  height: 100vh;
  height: 100dvh;
}

a {
  color: var(--color-primary);
  text-decoration: none;
}
a:hover {
  text-decoration: underline;
}

h1, h2, h3 {
  margin: 0 0 0.5em;
  font-weight: 600;
}

/* ---------- Header / Nav ---------- */
/* 2026-07-17 UI 디자인 확정 사양 — 흰 배경 + 하단 얇은 회색 구분선(기존엔
   --color-primary 진한 파랑 배경 + 흰 글씨였음). 카테고리 탭은 아래
   .app-nav__link--* 규칙에서 각자 고유 색(알약 모양)으로 표시. */
.app-header {
  position: sticky;
  top: 0;
  z-index: 20;
  background: #ffffff;
  color: var(--color-text);
  border-bottom: 1px solid #E6E4E0;
}

/* 모바일 전용 1번째 줄(사이트명, 세이지그린 배너) — 데스크톱/태블릿은 이
   미디어쿼리 밖이라 숨김, 기존 1줄 구조(브랜드+탭+계정정보) 그대로 유지됨.
   실제 스타일(배경색/글자색/패딩)은 responsive.css의 768px 블록 참고. */
.app-header__top-mobile {
  display: none;
}

.app-header__inner {
  max-width: 1120px;
  margin: 0 auto;
  padding: 0 20px;
  min-height: var(--nav-height);
  display: flex;
  align-items: center;
  gap: 24px;
}

.app-header__brand {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--color-text);
  font-weight: 700;
  font-size: 16px;
  white-space: nowrap;
  flex: 0 0 auto;
}
.app-header__brand:hover {
  text-decoration: none;
  opacity: 0.9;
}
/* Desktop/tablet: text only, no icon — the icon only replaces the text on
   mobile (responsive.css), where "Hwang Je Family" doesn't fit alongside
   5 category tabs + hamburger on one line. */
.app-header__brand-icon {
  display: none;
  flex: 0 0 auto;
}

.app-nav {
  display: flex;
  align-items: center;
  gap: 4px;
  flex: 1;
}

/* Wraps just the category links (nav.ejs) so mobile can always show them in
   one row (responsive.css) without also having to unhide .app-nav__user at
   the same time — same flex/gap as .app-nav used to apply directly to the
   links so the desktop layout is pixel-identical to before this split. */
.app-nav__links {
  display: flex;
  align-items: center;
  gap: 4px;
}

/* 알약(pill) 모양 카테고리 탭 — 비활성: 해당 카테고리 색의 옅은 배경 +
   같은 색 테두리/글씨, 활성: 해당 색으로 진하게 채움 + 흰 글씨(2026-07-17
   UI 디자인 확정 사양). 각 탭의 "고유 색"은 .app-nav__link--account 등
   카테고리별 클래스(nav.ejs)로 지정 — 현재 보고 있는 페이지의 카테고리가
   아니라 탭 자신의 정체성 색이라는 점에 유의(예: 가입정보 탭은 기기정보
   페이지를 보고 있어도 항상 주황색). */
/* box-shadow(inset)로 "테두리"를 표현 — 진짜 border를 쓰면 box-sizing과 무관하게
   auto-height 요소의 렌더링 높이를 실제로 늘림. .app-header가 조금이라도
   높아지면 PC 조회화면에 스크롤바가 생기는 문제가 있었는데(2026-07-18),
   근본 원인은 .page--list가 헤더 높이를 고정값(--nav-height)으로 추정해서
   빼던 방식이었고 이건 body flex 레이아웃으로 전환해 완전히 해결함("Category
   list pages only" 주석 참고). 이 box-shadow 자체는 그 문제와 별개로
   "레이아웃 박스 크기에 영향 없는 테두리 표현법"이라 그대로 유지. */
.app-nav__link {
  padding: 8px 16px;
  border-radius: 999px;
  box-shadow: inset 0 0 0 1px transparent;
  font-size: 14px;
  font-weight: 500;
  white-space: nowrap;
}
.app-nav__link:hover {
  text-decoration: none;
  filter: brightness(0.96);
}
.app-nav__link.is-active {
  color: #fff;
  font-weight: 600;
}
.app-nav__link--account { background: var(--color-account-bg); color: var(--color-account); box-shadow: inset 0 0 0 1px var(--color-account); }
.app-nav__link--account.is-active { background: var(--color-account); }
.app-nav__link--device { background: var(--color-device-bg); color: var(--color-device); box-shadow: inset 0 0 0 1px var(--color-device); }
.app-nav__link--device.is-active { background: var(--color-device); }
.app-nav__link--product { background: var(--color-product-bg); color: var(--color-product); box-shadow: inset 0 0 0 1px var(--color-product); }
.app-nav__link--product.is-active { background: var(--color-product); }
.app-nav__link--address { background: var(--color-address-bg); color: var(--color-address); box-shadow: inset 0 0 0 1px var(--color-address); }
.app-nav__link--address.is-active { background: var(--color-address); }
.app-nav__link--personal { background: var(--color-personal-bg); color: var(--color-personal); box-shadow: inset 0 0 0 1px var(--color-personal); }
.app-nav__link--personal.is-active { background: var(--color-personal); }
.app-nav__link--admin { background: var(--color-admin-bg); color: var(--color-admin); box-shadow: inset 0 0 0 1px var(--color-admin); }
.app-nav__link--admin.is-active { background: var(--color-admin); }
/* Desktop/tablet always show the full label — abbreviation
   (가입정보→가입 etc.) is mobile-only, where the full names don't fit
   (responsive.css flips which of these two spans is shown). */
.app-nav__link-short {
  display: none;
}

/* Mobile-only dropdown (hamburger) — hidden outside the mobile breakpoint
   regardless of load order, since its actual layout/toggle rules live in
   responsive.css. On desktop/tablet this duplicate 계정/권한관리 정보 would
   otherwise sit hidden-but-still-in-the-document right under the header. */
.app-nav__dropdown-menu {
  display: none;
}

.app-nav__user {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 10px;
  padding-left: 12px;
  white-space: nowrap;
}
.app-nav__user-name {
  font-size: 13px;
  color: var(--color-text-muted);
}

.app-nav__toggle-input {
  display: none;
}
.app-nav__toggle-label {
  display: none;
  color: var(--color-text);
  font-size: 20px;
  cursor: pointer;
  padding: 4px 8px;
}

/* 알림센터 종 아이콘(2026-07-19) — PC/모바일 공통, 별도 미디어쿼리로 숨기지
   않음(어느 화면폭에서도 항상 노출). */
.app-header__bell {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  border: none;
  background: none;
  color: var(--color-text);
  cursor: pointer;
  padding: 6px;
  border-radius: var(--radius);
}
.app-header__bell:hover {
  background: var(--color-bg);
}
.app-header__bell-badge {
  position: absolute;
  top: 0;
  right: 0;
  min-width: 16px;
  height: 16px;
  padding: 0 3px;
  border-radius: 999px;
  background: var(--color-danger);
  color: #fff;
  font-size: 10px;
  line-height: 16px;
  text-align: center;
}

/* 알림 목록 팝업(modal.js의 openModalWithContent 재사용) */
.notification-panel__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
  /* .modal-close(X 버튼)가 .modal-box 우상단에 position:absolute(top:12px;
     right:12px)로 떠 있는데, 이 헤더가 justify-content:space-between으로
     "모두 읽음" 버튼을 헤더 콘텐츠 폭 끝까지 밀어붙이면서 X 버튼과 겹치는
     문제가 있었음(2026-07-21, 베타 피드백) — X 버튼이 차지하는 폭(아이콘+
     패딩 약 40px)만큼 헤더 오른쪽에 여백을 줘서 겹치지 않게 함. */
  padding-right: 40px;
}
.notification-panel__header h2 {
  font-size: 16px;
}
.notification-list {
  list-style: none;
  padding: 0;
  margin: 0;
  max-height: 400px;
  overflow-y: auto;
}
.notification-item {
  padding: 10px 8px;
  border-bottom: 1px solid var(--color-border);
  cursor: pointer;
}
.notification-item:hover {
  background: var(--color-bg);
}
.notification-item.is-unread {
  background: var(--color-account-bg, var(--color-bg));
  font-weight: 600;
}
.notification-item__message {
  font-size: 13.5px;
  margin-bottom: 4px;
}
.notification-item__date {
  font-size: 11.5px;
  color: var(--color-text-muted);
  font-weight: 400;
}
.notification-empty {
  padding: 20px 8px;
  text-align: center;
  color: var(--color-text-muted);
  font-size: 13.5px;
}
.notification-item__actions {
  margin-top: 6px;
}

/* 만료/갱신 알림 설정(2026-07-19, expiry-reminder-fields.ejs — 5개 카테고리
   신규입력/수정 화면 공통) */
.expiry-reminder-dismissed-notice {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 6px;
}

/* ---------- Page background (카테고리별, 2026-07-17 UI 디자인 확정 사양) ---------- */
/* body에 배경을 걸어서 여백(표/카드/입력폼이 없는 빈 공간)에만 보이게 함 —
   .table-wrap/.device-card/.auth-card 등 실제 데이터 영역은 전부 자체
   불투명 흰 배경(--color-surface)을 갖고 있어 그 위에 그대로 덮어써짐(별도
   z-index/opacity 처리 불필요). views/partials/head.ejs가 <body
   data-bg-category="..."> 속성을 렌더링(각 라우트가 bgCategory로 전달).
   PC 기본, 모바일은 responsive.css에서 세로형 이미지로 교체. */
body[data-bg-category] {
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}
body[data-bg-category="account"] { background-image: url('/images/backgrounds/spring-pc.png'); }
body[data-bg-category="device"] { background-image: url('/images/backgrounds/summer-pc.png'); }
body[data-bg-category="product"] { background-image: url('/images/backgrounds/autumn-pc.png'); }
body[data-bg-category="address"] { background-image: url('/images/backgrounds/winter-pc.png'); }
body[data-bg-category="personal"],
body[data-bg-category="admin"] { background-image: url('/images/backgrounds/lake-afternoon-pc.png?v=2'); }
/* 대시보드는 5개 카테고리 중 어느 하나에도 속하지 않는 화면이라 로그인/
   회원가입(.auth-page)과 같은 중립적인 배경(style-background)을 재사용 —
   새 이미지 에셋을 만들지 않음. */
body[data-bg-category="dashboard"] { background-image: url('/images/backgrounds/style-background-pc.png?v=2'); }

/* ---------- Page layout ---------- */
.page {
  max-width: 1120px;
  margin: 0 auto;
  padding: 28px 20px 60px;
}

/* Category list pages only: fixed-height 3-tier layout (top block / scrolling
   middle / bottom block) built with CSS Grid instead of position:sticky — a
   sticky table header inside a normally-scrolling page is less predictable
   across browsers than giving the page itself a fixed height and letting only
   the middle grid row scroll.
   높이는 body(flex column)의 남은 공간을 flex:1로 채우는 방식(위 body 규칙
   참고) — 예전엔 calc(100dvh - var(--nav-height))로 헤더 높이를 56px로
   고정 추정해서 뺐었는데, 헤더 디자인이 바뀔 때마다 실제 높이와 어긋나
   PC에서 원치 않는 스크롤바가 반복 발생했음(2026-07-18) — flex:1은 헤더의
   실제 렌더링 높이를 몰라도 항상 정확히 남은 공간만큼만 채워서 이 버그
   자체가 재발할 수 없음. */
.page--list {
  flex: 1;
  min-height: 0;
  /* Safety net: if some other future change still makes this grid's content
     taller than the calculated height, let the page itself scroll instead of
     silently clipping .list-shell__bottom off-screen with no way to reach it. */
  overflow-y: auto;
  padding-top: 0;
  padding-bottom: 0;
  display: flex;
  flex-direction: column;
  /* 2026-07-27 — 주소정보 재설계 3차 피드백("출입번호 오른쪽 여백")으로
     발견된, .page--dashboard가 2026-07-20에 이미 겪었던 것과 정확히 동일한
     잠재 버그를 여기서도 고침: body가 flex-direction:column인 상태에서
     <main>(.page, margin:0 auto로 가운데 정렬 — cross-axis auto margin)은
     명시적 width가 없으면 align-items:stretch가 무효화되어 콘텐츠 크기로
     쪼그라듦. 지금까지 가입정보/제품정보는 .data-table에 큰 min-width
     (821~1390px)가 있어 "콘텐츠 크기" 자체가 이미 넓어서 이 버그가 가려져
     있었을 뿐 — 주소정보(min-width 없음, %기반 컬럼이라 항상 컨테이너
     폭만큼만 계산됨)에서 실제로 드러남. width:100%를 명시해 stretch 여부와
     무관하게 항상 부모(body) 폭을 확실히 채우도록 함 — 다른 4개 카테고리는
     이미 min-width로 넓게 렌더링되고 있었으니 이 값을 추가해도 그 폭은
     그대로 유지되고(안전, 회귀 없음), 주소정보만 실질적으로 넓어짐. */
  width: 100%;
}
.list-shell {
  flex: 1;
  min-height: 0; /* let the grid actually shrink inside the flex parent instead of overflowing it */
  display: grid;
  grid-template-rows: auto auto 1fr auto;
}
.list-shell__top {
  /* 원래 여기 있던 카테고리 이름/설명(.page__header)이 삭제되면서(2026-07-15,
     "조회화면 좌상단 카테고리명/설명 삭제") .page--list 자체의 padding-top:0과
     겹쳐 검색창 위에 불필요하게 큰 빈 줄이 남았던 걸 한 차례 0으로 지웠으나
     (2026-07-16), 그러면 검색줄이 카테고리 네비게이션 바에 바로 붙어버려
     오히려 어색함 — 검색줄 아래(.list-toolbar의 margin-bottom:16px, 항목
     표시줄과의 간격)와 대칭이 되도록 위쪽도 16px로 맞춤(2026-07-16 후속). */
  padding-top: 16px;
  /* .list-shell__scroll(아래)의 scrollbar-gutter:stable과 반드시 동일하게
     맞출 것 — 이 섹션(검색줄/헤더행)은 스크롤이 없어 세로 스크롤바 자리를
     안 남기는데, 아래 스크롤 섹션은 항상 그 자리를 예약해두므로, 여기에도
     똑같이 예약해두지 않으면 위쪽(검색줄/헤더 테이블/정렬버튼)이 아래쪽
     (데이터 테이블/카드그리드/게시판)보다 스크롤바 폭(약 15px)만큼 더
     오른쪽으로 삐져나와 우측면이 안 맞아 보임(2026-07-18, 실기기 스크린샷
     5개 카테고리 전부에서 동일한 패턴으로 재현됨). scrollbar-gutter는
     overflow:visible인 요소에는 아무 효과가 없어서(.table-wrap--head가
     이미 이 조합을 쓰고 있음, 아래쪽 참고) overflow-y:hidden도 같이 필요함
     — 실제로 스크롤할 필요는 없는 섹션이라 hidden으로 충분함. */
  overflow-y: hidden;
  scrollbar-gutter: stable;
}
.list-shell__scroll {
  overflow-y: auto;
  min-height: 0; /* same reason as .list-shell: without this a grid row won't shrink below its content size */
  scrollbar-gutter: stable;
}
.list-shell__bottom {
  padding: 12px 0 24px;
}
.list-shell__bottom:empty {
  padding: 0;
}

/* ---------- 홈 대시보드(2026-07-20) ---------- */
/* .page--list와 동일한 이유로 flex:1/min-height:0(헤더 실제 높이를 몰라도
   "남은 공간 전체"를 정확히 채움) + overflow-y:auto(안전장치, 콘텐츠가
   화면보다 커지면 클리핑 대신 스크롤). 스마트폰에서는 콘텐츠 자체가 압축돼
   실제로 스크롤이 필요 없어야 한다는 요구사항이 있어(responsive.css) 이
   overflow-y:auto는 어디까지나 안전장치일 뿐, 평소엔 발동 안 하는 게 목표. */
.page--dashboard {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  padding: 16px 20px 24px;
  /* <main>이 기본 .page 클래스도 함께 갖고 있어(class="page page--dashboard"),
     .page의 max-width:1120px가 이미 <main> 자체 폭을 그 값으로 고정해버림 —
     안쪽 .dashboard-shell에 아무리 큰 max-width를 줘도 부모가 이미 1120px로
     막혀 있으면 소용없었던 것(2026-07-20, 실제로 이렇게 안 늘어나는 버그로
     발견됨). .page--dashboard 자신에도 명시적으로 override해야 함.
     max-width 값은 처음엔 회원가입 페이지(.auth-card--wide)와 동일하게
     480px였다가(2026-07-20), PC에서 만료임박/최근등록을 좌우 배치로 바꾸며
     두 카드가 나란히 들어갈 공간이 필요해져 1000px로 재확정(2026-07-20 후속,
     PC 전용 변경 — 스마트폰은 이미 확인 완료라 더 이상 손대지 말 것. 참고로
     이 max-width 값은 모바일 실제 뷰포트 폭(대부분 480px 미만)보다 항상 크므로
     모바일 렌더링에는 어차피 영향 없음, 그래서 미디어쿼리 분기 없이 이
     값 하나로 안전하게 처리됨). */
  max-width: 1000px;
  /* 진짜 원인(2026-07-20, 실브라우저 스크린샷 픽셀 분석으로 확정): body가
     flex-direction:column인 상태에서 <main>처럼 cross-axis(가로) auto margin
     (margin:0 auto로 가운데 정렬)을 가진 flex item은, 명시적 width가 없으면
     기본 align-items:stretch가 "auto margin이 있으니 늘리지 않고 콘텐츠
     크기로 줄인다"는 flexbox 스펙 규칙 때문에 stretch가 무효화되어 콘텐츠
     크기로 쪼그라듦 — .page--list가 이 문제를 안 겪는 건 안의 .data-table이
     명시적 min-width(821~1390px)를 갖고 있어 "콘텐츠 크기"가 이미 넓기
     때문일 뿐, 같은 버그가 잠재해 있었던 것. 대시보드는 이런 넓은 min-width
     콘텐츠가 없어(전부 width:100%만 있음) 부모가 줄어드는 만큼 같이 줄어드는
     연쇄가 일어나 실제로 ~376px까지 쪼그라들었음(가입정보 테이블은 1214px).
     width:100%를 명시해서 stretch에 의존하지 않고 확실하게 부모 폭을 채움. */
  width: 100%;
  margin: 0 auto;
  box-sizing: border-box;
}
.dashboard-shell {
  max-width: 1000px;
  width: 100%;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 14px;
  /* 아래 .dashboard-sections가 "남는 세로 공간 전체"를 알아야 내소유 요약을
     화면 맨 아래로 밀어낼 수 있어(2026-07-20, 사용자 요청) — .page--dashboard
     (부모)가 이미 flex:1/min-height:0으로 "남은 공간 전체"를 갖고 있으므로,
     여기서도 flex:1/min-height:0으로 그 값을 그대로 물려받아 전달함. */
  flex: 1;
  min-height: 0;
}
.dashboard-search {
  position: relative;
}
.dashboard-search input {
  width: 100%;
  padding: 10px 14px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  font-size: 15px;
  background: rgba(255, 255, 255, 0.9);
}
/* combo-menu(public/js/combobox.js)와 동일한 오버레이 패턴 재사용 —
   position:absolute라 결과가 늘어나도 아래쪽 섹션들을 밀어내지 않음
   (평상시 "스크롤 없이 한 화면" 요구사항과, 검색 중 결과가 많아질 수 있는
   상황이 서로 충돌하지 않도록 하는 핵심 장치). */
.dashboard-search-results {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 30;
  margin-top: 6px;
  max-height: 320px;
  overflow-y: auto;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-md);
  padding: 8px;
}
.dashboard-search-results__group + .dashboard-search-results__group {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--color-border);
}
.dashboard-search-results__group-title {
  margin: 0 0 6px;
  font-size: 13px;
  font-weight: 600;
  color: var(--color-text-muted);
}
.dashboard-search-results__list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.dashboard-search-results__item {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
  padding: 8px 10px;
  border-radius: var(--radius);
  cursor: pointer;
}
.dashboard-search-results__item:hover {
  background: var(--color-bg);
}
.dashboard-search-results__title {
  font-size: 14px;
}
.dashboard-search-results__subtitle {
  color: var(--color-text-muted);
  font-size: 12.5px;
  flex-shrink: 0;
}
.dashboard-search-empty {
  margin: 0;
  padding: 10px;
  font-size: 13.5px;
  color: var(--color-text-muted);
}

.dashboard-sections {
  display: flex;
  flex-direction: column;
  gap: 14px;
  /* .dashboard-shell과 동일한 이유로 flex:1/min-height:0 — 이 값이 있어야
     아래 .dashboard-card--own의 margin-top:auto가 "정확히 화면 맨 아래"까지
     밀어낼 수 있는 실제 여유 공간을 갖게 됨. */
  flex: 1;
  min-height: 0;
}
/* PC 전용(2026-07-20, 사용자 요청 — 스마트폰은 이미 확인 완료라 그대로 둠,
   responsive.css의 768px 블록에서 flex-direction:column으로 되돌려 모바일은
   기존 상하 배치를 그대로 유지) — 만료임박/최근등록을 상하가 아니라 좌우로
   배치. 두 카드가 이제 같은 높이(아래 .dashboard-card__body 8줄 고정)라
   나란히 놓아도 자연스럽게 높이가 맞음. */
.dashboard-top-row {
  display: flex;
  flex-direction: row;
  gap: 14px;
}
.dashboard-top-row > .dashboard-card {
  flex: 1;
  min-width: 0; /* flex item이 콘텐츠 크기로 안 늘어나고 정확히 반씩 나눠 갖도록 */
}
.dashboard-card {
  padding: 14px 16px;
}
.dashboard-card__title {
  margin: 0 0 10px;
  font-size: 15px;
}
/* 만료임박/최근등록 카드 전용 — 이 고정값(381px = 9줄) 자체는 이제
   `public/js/dashboard.js`가 페이지 로드/리사이즈마다 실제 사용 가능한
   높이를 재서 인라인 style로 덮어쓰므로 **PC/모바일 둘 다 이 값을 그대로
   안 씀**(2026-07-21 — 처음엔 모바일만 JS 자동계산이었다가, PC 창을 폭만
   좁혀서 테스트하면 여백이 남는 문제로 PC도 JS 자동계산 대상에 포함시킴).
   여기 남겨둔 381px(9줄)는 JS가 실행되지 않는 경우(자바스크립트 비활성화
   등)에 대한 PC 전용 안전망(fallback)일 뿐 — 모바일 전용 fallback은
   responsive.css 참고. 그 줄 수를 넘는 데이터는 카드 내부에서만
   스크롤(overflow-y:auto)로 봄. */
.dashboard-card__body {
  height: 381px; /* fallback(JS 비활성화 시): 9줄 = 37×9 + 6×8 */
  overflow-y: auto;
}
.dashboard-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.dashboard-list__item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: var(--radius);
  cursor: pointer;
  background: rgba(0, 0, 0, 0.02);
}
.dashboard-list__item:hover {
  background: rgba(0, 0, 0, 0.05);
}
.dashboard-list__label {
  font-size: 14px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dashboard-list__meta {
  font-size: 12.5px;
  color: var(--color-text-muted);
  flex-shrink: 0;
}
.dashboard-list__badge {
  font-size: 12.5px;
  font-weight: 600;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--color-bg);
  flex-shrink: 0;
}
.dashboard-list__badge.is-overdue {
  background: var(--color-danger-bg);
  color: var(--color-danger);
}
.dashboard-empty {
  margin: 0;
  font-size: 13.5px;
  color: var(--color-text-muted);
}
/* 내소유 요약은 항상 화면 맨 아래 위치(2026-07-20, 사용자 요청) —
   .dashboard-sections가 flex:1로 남는 공간 전체를 갖고 있으므로, 이 카드에만
   margin-top:auto를 줘서 그 남는 공간을 전부 이 카드 위쪽으로 흡수시킴(다른
   두 카드는 고정 높이라 밀려나지 않고, 이 카드만 항상 맨 아래로 붙음).
   PC/모바일 동일 조건(요청사항) — 미디어쿼리 없이 이 규칙 하나로 양쪽 다 적용됨. */
.dashboard-card--own {
  margin-top: auto;
}

.dashboard-own-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 10px;
}
/* PC 전용(2026-07-20, 사용자 요청) — 칩 내용(건수+라벨)을 세로 2줄이 아니라
   가로 1줄로 표시. 모바일은 responsive.css에서 flex-direction:column으로
   되돌려 기존 2줄 모양을 그대로 유지(스마트폰은 이미 확인 완료라 그대로 둠). */
.dashboard-own-chip {
  display: flex;
  flex-direction: row;
  align-items: baseline;
  justify-content: center;
  gap: 6px;
  padding: 12px 8px;
  border-radius: var(--radius);
  text-decoration: none;
  font-weight: 600;
}
.dashboard-own-chip:hover {
  text-decoration: none;
  filter: brightness(0.97);
}
.dashboard-own-chip__count {
  font-size: 20px;
  font-weight: 700;
}
.dashboard-own-chip__label {
  font-size: 13px;
}
/* 알약 탭(.app-nav__link--*)과 동일한 이유로 border 대신 box-shadow:inset을
   씀(테두리가 auto-height 요소의 실제 렌더링 높이를 늘려 레이아웃이 어긋나는
   문제가 이 프로젝트에서 실제로 있었음, main.css 상단 body flex 관련 주석 참고). */
.dashboard-own-chip--account { background: var(--color-account-bg); color: var(--color-account); box-shadow: inset 0 0 0 1px var(--color-account); }
.dashboard-own-chip--device { background: var(--color-device-bg); color: var(--color-device); box-shadow: inset 0 0 0 1px var(--color-device); }
.dashboard-own-chip--product { background: var(--color-product-bg); color: var(--color-product); box-shadow: inset 0 0 0 1px var(--color-product); }
.dashboard-own-chip--personal { background: var(--color-personal-bg); color: var(--color-personal); box-shadow: inset 0 0 0 1px var(--color-personal); }

.page__header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 20px;
  flex-wrap: wrap;
}

.page__title {
  font-size: 20px;
  margin: 0;
}

.page__subtitle {
  color: var(--color-text-muted);
  font-size: 13px;
  margin-top: 4px;
}

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  padding: 9px 16px;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  background: var(--color-surface);
  color: var(--color-text);
}
.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.btn--primary {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
}
.btn--primary:hover {
  background: var(--color-primary-dark);
}

.btn--danger {
  background: var(--color-danger);
  border-color: var(--color-danger);
  color: #fff;
}
.btn--danger:hover {
  opacity: 0.9;
}

.btn--ghost {
  background: transparent;
  border-color: var(--color-border);
  color: inherit;
}
.btn--ghost:hover {
  background: var(--color-bg);
}

.btn--outline {
  background: var(--color-surface);
  border-color: var(--color-border);
  color: var(--color-text);
}
.btn--outline:hover {
  border-color: var(--color-primary);
  color: var(--color-primary);
}

/* 다중선택 취소 버튼 전용(연한 노란색) — .btn--outline은 상세 패널의 "취소"/편집
   취소, 잠금 페이지의 "목록으로" 등 다른 화면에서도 재사용되고 있어 그쪽엔 영향
   안 주도록 별도 클래스로 분리. */
.btn--warn {
  background: #FDE68A;
  border-color: #FBBF24;
  color: #92400E;
}
.btn--warn:hover {
  background: #FBBF24;
  border-color: #D97706;
}

.btn--sm {
  padding: 5px 10px;
  font-size: 13px;
}

/* ---------- Forms ---------- */
.form-group {
  margin-bottom: 16px;
}
.form-group label {
  display: block;
  font-size: 13px;
  font-weight: 600;
  margin-bottom: 6px;
  color: var(--color-text-muted);
}
.form-group input,
.form-group select,
.form-group textarea {
  width: 100%;
  padding: 9px 12px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  font-size: 14px;
  font-family: inherit;
  background: var(--color-surface);
  color: var(--color-text);
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
  outline: none;
  border-color: var(--color-primary);
}
.field-hint {
  margin-top: 6px;
  font-size: 12px;
  color: var(--color-text-muted);
}

/* ---------- Settings page (2026-07-18) ---------- */
/* .table-wrap은 원래 표를 감싸는 용도라 자체 padding이 없음(내부 <td>가
   각자 패딩을 가짐) — 여기서는 일반 콘텐츠 카드로 재사용하므로 이 스코프
   안에서만 padding을 추가. */
.settings-section {
  padding: 20px 24px;
  margin-bottom: 20px;
}
.settings-section h2 {
  font-size: 16px;
  margin-bottom: 16px;
}
.settings-section:last-child {
  margin-bottom: 0;
}
.settings-owned-links {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.form-group textarea {
  resize: vertical;
  /* 5개 카테고리 "메모" 필드(가입정보/기기정보/제품정보/주소정보/개인정보)
     전부가 공유하는 규칙 — 80px(약 3줄)에서 100% 증가(2026-07-21, 베타
     피드백 확인 후 진행)해 160px(약 6~7줄)로. */
  min-height: 160px;
}

/* ---------- Custom combobox (public/js/combobox.js) ---------- */
/* 네이티브 <input list> + <datalist>는 크롬에서만 드롭다운이 뜨고 엣지/태블릿/
   스마트폰에서는 아예 안 뜨는 것이 실기기 테스트로 확인됨(2026-07-16) — 브라우저
   구현에 기대지 않는 자체 드롭다운으로 전면 교체(로그인 계정목록 + 생성/수정폼의
   모든 콤보 필드 공용). */
.combo {
  position: relative;
}
.combo-menu {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 40;
  margin: 4px 0 0;
  max-height: 220px;
  overflow-y: auto;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-md);
  list-style: none;
  padding: 4px;
}
.combo-menu__item {
  padding: 8px 10px;
  border-radius: var(--radius);
  cursor: pointer;
  font-size: 14px;
}
.combo-menu__item:hover {
  background: var(--color-bg);
}

/* ---------- Alerts ---------- */
.alert {
  padding: 10px 14px;
  border-radius: var(--radius);
  font-size: 13.5px;
  margin-bottom: 16px;
}
.alert--error {
  background: var(--color-danger-bg);
  color: var(--color-danger);
}
.alert--success {
  background: var(--color-success-bg);
  color: var(--color-success);
}

/* ---------- Auth (login) ---------- */
/* 2026-07-17 UI 디자인 확정 사양 — 로그인/회원가입 화면 배경(여백에만
   보이고 .auth-card는 계속 완전 불투명 흰 배경). PC 기본, 모바일은
   responsive.css에서 세로형 이미지로 교체. */
.auth-page {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: var(--color-bg);
  background-image: url('/images/backgrounds/style-background-pc.png?v=2');
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  padding: 20px;
}
.auth-card {
  width: 100%;
  max-width: 360px;
  background: var(--color-surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow-md);
  padding: 32px 28px;
}
.auth-card__title {
  font-size: 18px;
  text-align: center;
  margin-bottom: 4px;
}
.auth-card__subtitle {
  text-align: center;
  color: var(--color-text-muted);
  font-size: 13px;
  margin-bottom: 24px;
}
/* 회원가입 폼(필수 3 + 기기정보 4 + 개인정보 3 항목)은 로그인 카드보다
   넓어야 여유 있게 들어감 — 로그인 자체는 이 클래스가 없어 기존 360px 그대로. */
.auth-card--wide {
  max-width: 480px;
}
/* 로그인 화면 전용(회원가입엔 영향 없음) — 카드를 화면 세로 중앙보다 위로
   올림(2026-07-25 요청: "로그인 버튼이 비밀번호 입력란 위치까지 올라오는
   수준"). .auth-page가 align-items:center로 세로 중앙정렬하는 걸 그대로
   두고 transform으로만 위치를 옮겨서, 다른 레이아웃 계산(높이/ауto margin)에
   영향을 주지 않음. 이동량(76px)은 .form-group 한 칸(label 13px+margin
   6px+input 약 38px+margin-bottom 16px ≈ 76px) — 비밀번호 입력란 한 칸
   높이만큼 카드 전체를 올리면 로그인 버튼이 정확히 그 자리로 옴. */
.auth-card--login {
  transform: translateY(-76px);
}
.auth-section-title {
  font-size: 15px;
  margin: 8px 0 4px;
  padding-top: 20px;
  border-top: 1px solid var(--color-border);
}
.auth-section-subtitle {
  font-size: 13px;
  color: var(--color-text-muted);
  margin: 16px 0 8px;
}
.auth-card__footer-link {
  display: block;
  text-align: center;
  margin-top: 16px;
  color: var(--color-primary);
  text-decoration: underline;
  font-size: 13.5px;
}

/* ---------- Category hub ---------- */
.hub-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
}
.hub-tile {
  display: block;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 20px;
  box-shadow: var(--shadow-sm);
  color: var(--color-text);
}
.hub-tile:hover {
  border-color: var(--color-primary);
  box-shadow: var(--shadow-md);
  text-decoration: none;
}
.hub-tile__label {
  font-size: 16px;
  font-weight: 600;
  margin-bottom: 6px;
}
.hub-tile__desc {
  font-size: 13px;
  color: var(--color-text-muted);
}
.hub-tile__type {
  display: inline-block;
  margin-top: 10px;
  font-size: 11.5px;
  color: var(--color-primary);
  background: rgba(29, 78, 137, 0.08);
  border-radius: 4px;
  padding: 2px 8px;
}

/* ---------- Toolbar (search / sort) ---------- */
.list-toolbar {
  display: flex;
  gap: 10px;
  margin-bottom: 16px;
  flex-wrap: wrap;
}
/* 2단계(반투명 블러, 2026-07-18 UI 디자인 확정 사양) — 검색어 입력줄은
   완전 불투명 대신 반투명+블러로 배경이 살짝 비치게 함(네비게이션 바/
   계정칩/버튼은 계속 완전 불투명 유지, 아래 .table-wrap/.device-card와
   동일한 값 사용). -webkit- 접두사는 사파리/구형 모바일 브라우저 대응. */
.list-toolbar input[type="search"],
.list-toolbar select {
  padding: 8px 12px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  font-size: 14px;
  background: rgba(255, 255, 255, 0.8);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}
.list-toolbar input[type="search"] {
  flex: 1;
  min-width: 180px;
}

/* ---------- Table (list / board) ---------- */
/* 2단계(반투명 블러) — 표/게시판(가입정보~권한관리 매트릭스까지 이 클래스
   공유) 전체를 감싸는 배경도 검색줄과 동일한 반투명+블러로 전환. */
.table-wrap {
  background: rgba(255, 255, 255, 0.8);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  overflow-x: auto;
  box-shadow: var(--shadow-sm);
}
.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
  /* Required for the split head/body tables (list-table-head.ejs / list-table.ejs)
     to line up: without this, each <table> sizes its own columns from its own
     content (auto layout) instead of from the shared colgroup, and the head
     table (short labels) and body table (real data) end up with different
     column widths even though they share the same <colgroup>. */
  table-layout: fixed;
  /* list-table-colgroup.ejs / product-list-colgroup.ejs give every single
     column an explicit width — deliberately no "auto"/flexible column.
     table-layout:fixed's behavior for an auto column when the explicit columns
     already add up to more than the table's own width is not well-defined, and
     in practice caused unpredictable column widths (신고된 버그: 화면을 좁히면
     로그인ID가 오히려 넓어짐) once the protected columns got wide enough to
     overcommit the space — making every column explicit removes that
     ambiguity entirely. (min-width itself is NOT set here — see
     [data-category="account"] .data-table below; every category shares this
     .data-table class but has a completely different column layout, so a
     shared min-width would leak account's value into every other category's
     table.) */
}
/* 가입정보 전용 min-width — 카테고리마다 컬럼 구성/폭이 전혀 다르므로
   .data-table 클래스 하나에 공용으로 걸면 이 값이 제품정보/주소정보/개인정보
   테이블에도 그대로 새어 들어가 불필요하게 넓어짐(실제로 겪을 뻔한 버그,
   구현 중 자체 점검으로 발견). 카테고리별 min-width는 항상 이렇게
   [data-category="..."]로 스코프를 좁혀서 추가할 것.
   합계: 0(col-select 기본)+76(가입)+200+250+165+170 = 861px. .col-select는
   .list-shell.is-selecting일 때만 36px이 되는데, 그 시점엔 실제 컬럼 합(897px)이
   이 floor를 이미 넘으므로 별도 is-selecting 변형이 필요 없음. responsive.css의
   동일 규칙과 항상 같이 맞출 것(1024px/768px breakpoint). */
[data-category="account"] .data-table {
  min-width: 861px;
}
/* 가입정보 목록 컬럼 폭(데스크톱 기본) — list-table-colgroup.ejs가 공유하는
   <col>들의 유일한 폭 지정 위치. 인라인 style="width:..."는 특정성 때문에
   responsive.css의 오버라이드를 절대 못 이겨서(2026-07-15 주소정보에서 실증된
   버그) 반드시 클래스로만 지정할 것 — .col-email에 인라인+클래스가 같이 있던 것도
   이번에 같이 정리함(플래그만 해뒀던 항목, 2026-07-16 실제로 정리 완료). */
.col-subject {
  /* 2026-07-16: 헤더 라벨 "가입"(정렬 아이콘 포함) 공간을 고려 안 하고 76px→60px로
     줄였다가 실기기에서 헤더 텍스트가 "가…"로 잘리는 회귀가 생겨 원래 값(76px)으로
     되돌림 — 데이터 값(JM/HDS 등)만 보고 폭을 정하면 안 되고, 정렬 아이콘이 붙는
     헤더 자신의 라벨 길이도 항상 같이 계산할 것. */
  width: 76px;
}
.col-service {
  width: 200px;
}
.col-login-id {
  width: 250px;
}
.col-password {
  width: 165px;
}
.col-email {
  width: 170px;
}
/* 제품정보 목록 컬럼 폭(데스크톱 기본) — product-list-colgroup.ejs가 공유하는
   <col>들의 유일한 폭 지정 위치. 기존엔 인라인 style="width:..."로 박혀있었는데
   (2026-07-16 계정 카테고리와 동일한 계열의 잠재 버그 — 모바일 반응형 오버라이드가
   특정성 때문에 전혀 안 먹힘), 클래스로 전환. 값 자체(76/106/408)는 실사용
   피드백으로 이미 확정된 값이라 그대로 유지. */
.col-product-subject {
  width: 76px;
}
.col-product-type {
  width: 106px;
}
.col-product-title {
  width: 408px;
}
/* 합계: 76+106+408 = 590px — 카테고리마다 컬럼 구성이 달라 min-width는 항상
   [data-category="..."]로 스코프 좁혀서 추가(가입정보 쪽 주석과 동일한 원칙). */
[data-category="product"] .data-table {
  min-width: 590px;
}
.data-table th,
.data-table td {
  text-align: left;
  padding: 12px 14px;
  border-bottom: 1px solid var(--color-border);
  white-space: nowrap;
  /* Safety net: with table-layout:fixed, content that doesn't fit a column's
     width would otherwise render outside the cell and visually overlap the
     next column instead of being contained — truncate with an ellipsis instead. */
  overflow: hidden;
  text-overflow: ellipsis;
}
.data-table th {
  color: var(--color-text-muted);
  font-weight: 600;
  font-size: 12.5px;
  background: #fafbfc;
}
.data-table tbody tr:hover {
  /* 2단계(반투명 블러) — .table-wrap 자체가 반투명이라 행 hover도 완전
     불투명 대신 반투명으로 맞춤(그렇지 않으면 hover 시에만 배경이 갑자기
     또렷해져 어색함). */
  background: rgba(248, 250, 252, 0.8);
}
/* Row 전체가 상세 모달 클릭 영역이 되는 카테고리 목록 테이블에만 스코프
   (admin-grants.ejs도 .data-table을 쓰지만 그쪽 행은 클릭해도 아무 동작이
   없어 커서만 pointer로 바뀌면 오히려 오해를 줄 수 있어 제외, 2026-07-16). */
.list-shell .data-table tbody tr:not(.js-empty-row) {
  cursor: pointer;
}
.data-table tbody tr:last-child td {
  border-bottom: none;
}
.data-table td.is-wrap {
  white-space: normal;
}
/* Like .is-wrap but keeps literal \n line breaks instead of collapsing them —
   for values that are really several "라벨: 값" lines joined together (e.g.
   주소정보's 출입번호: 공동현관/현관문/보안구역 등), so each label stays on its
   own line instead of running together as one long sentence. Applied directly
   to the element whose text needs this (e.g. .masked-value), not the <td> —
   white-space isn't overridden by the ambient .data-table td rule here since
   an element's own specified value always wins over an inherited one,
   regardless of the ancestor rule's specificity. */
.is-prewrap {
  white-space: pre-line;
}

/* The list is split into a fixed head table (list-table-head.ejs) and a
   scrolling body table (list-table.ejs) sharing one colgroup so their columns
   line up; these modifiers just make the two boxes look like one. */
.table-wrap--head {
  border-bottom: none;
  border-radius: var(--radius) var(--radius) 0 0;
  box-shadow: none;
  /* Must reserve the same space as .list-shell__scroll's scrollbar-gutter, or
     the head and body tables see different available widths and their shared
     colgroup computes to slightly different actual column widths. */
  scrollbar-gutter: stable;
  /* No visible scrollbar here — public/js/category-list.js mirrors the body
     table's horizontal scroll position onto this element via scrollLeft, which
     still works with overflow hidden (it just can't be dragged directly). Two
     independent horizontal scrollbars stacked on top of each other would look
     broken and (without the JS sync) would let the columns drift apart. */
  overflow-x: hidden;
}
.table-wrap--body {
  border-top: none;
  border-radius: 0 0 var(--radius) var(--radius);
}

.data-table th.is-sortable {
  cursor: pointer;
  user-select: none;
}
.data-table th.is-sortable:hover {
  color: var(--color-text);
}
.sort-indicator {
  display: inline-block;
  width: 1em;
  margin-left: 2px;
}

/* ---------- Row multi-select (long-press) ---------- */
/* IMPORTANT: never hide this column with display:none on the <th>/<td> — a
   display:none cell is dropped from the table's column-matching entirely, which
   shifts every <col> in the shared colgroup one slot to the left (this exact bug
   shipped once: 가입자 rendered at 32px, row-select-cell's width, because it had
   silently become "column 1" once row-select-cell's th disappeared from layout).
   The th/td stay rendered (correct column count/order) at all times; only the
   <col class="col-select"> in list-table-colgroup.ejs actually changes width
   (0 <-> 32px, toggled below), which both the head and body tables share so they
   can never drift apart the way display:none on just one of them would. */
/* .data-table .row-select-cell (2 classes) instead of plain .row-select-cell
   (1 class) — needs to outrank .data-table th, .data-table td's own padding
   rule (1 class + 1 type = same specificity tier as a bare 1-class selector,
   which would otherwise lose the tie only by source order — not something to
   rely on given how many bugs in this file have come down to specificity). */
.data-table .row-select-cell {
  padding: 0;
  overflow: hidden;
}
/* Selecting-mode padding must leave room for the 18px checkbox inside .col-select's
   36px column (9px each side = 18px left over) — the base cell padding (12px 14px,
   28px horizontal) only left a 4px content box here, clipping almost the entire
   checkbox via this same rule's overflow:hidden (checkbox visually "disappeared"
   even though it was still correctly in the DOM — a padding/column-width mismatch,
   not a missing element or a leftover display:none/opacity/visibility rule). */
.list-shell.is-selecting .data-table .row-select-cell {
  padding: 12px 9px;
}
.col-select {
  width: 0;
}
.list-shell.is-selecting .col-select {
  width: 36px;
}
.row-select-toggle {
  appearance: none;
  -webkit-appearance: none;
  width: 18px;
  height: 18px;
  margin: 0;
  border: 2px solid var(--color-border);
  border-radius: 50%;
  cursor: pointer;
  position: relative;
  pointer-events: none; /* row click (delegated) drives selection, not the native checkbox toggle */
}
.row-select-toggle:checked {
  border-color: var(--color-primary);
  background: var(--color-primary);
}
.row-select-toggle:checked::after {
  content: '';
  position: absolute;
  inset: 3px;
  border-radius: 50%;
  background: var(--color-primary-contrast);
}

/* ---------- Account filter chips (public/js/account-filter.js) ---------- */
/* Deliberately separate DOM/classes from .row-select-toggle above — chips filter
   what's shown, the row-select checkboxes are for bulk edit/delete; they must
   never be confused for one another. */
.account-chip-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.account-chip {
  display: inline-block;
  background: var(--color-surface);
  border: 1px solid var(--color-chip-border);
  color: var(--color-chip-text);
  border-radius: 999px;
  padding: 6px 16px;
  font-size: 13px;
  cursor: pointer;
  transition: all 0.15s ease;
}
.account-chip.is-selected {
  border-color: transparent;
  color: #fff;
}
[data-category="account"] .account-chip.is-selected { background: var(--color-account); }
[data-category="device"] .account-chip.is-selected { background: var(--color-device); }
[data-category="product"] .account-chip.is-selected { background: var(--color-product); }
[data-category="address"] .account-chip.is-selected { background: var(--color-address); }
[data-category="personal"] .account-chip.is-selected { background: var(--color-personal); }

/* "+ 신규 데이터 입력" 버튼(#create-new-btn)도 계정선택 칩과 동일하게 페이지의
   카테고리 색으로 통일(2026-07-17 UI 디자인 확정 사양) — 기존엔 항상
   .btn--primary(고정 파랑)라서 카테고리마다 칩 색과 버튼 색이 안 맞았음. */
[data-category="account"] #create-new-btn { background: var(--color-account); border-color: var(--color-account); }
[data-category="account"] #create-new-btn:hover { background: var(--color-account); filter: brightness(0.92); }
[data-category="device"] #create-new-btn { background: var(--color-device); border-color: var(--color-device); }
[data-category="device"] #create-new-btn:hover { background: var(--color-device); filter: brightness(0.92); }
[data-category="product"] #create-new-btn { background: var(--color-product); border-color: var(--color-product); }
[data-category="product"] #create-new-btn:hover { background: var(--color-product); filter: brightness(0.92); }
[data-category="address"] #create-new-btn { background: var(--color-address); border-color: var(--color-address); }
[data-category="address"] #create-new-btn:hover { background: var(--color-address); filter: brightness(0.92); }
[data-category="personal"] #create-new-btn { background: var(--color-personal); border-color: var(--color-personal); }
[data-category="personal"] #create-new-btn:hover { background: var(--color-personal); filter: brightness(0.92); }

/* 주소정보 목록 컬럼 기본(데스크톱) 폭 — 반드시 클래스 규칙으로만 지정할 것,
   <col>에 인라인 style="width:..."을 쓰면 responsive.css의 미디어쿼리 오버라이드가
   특정성 때문에 절대 못 이겨서 반응형 자체가 동작하지 않게 됨(2026-07-15 실기기
   테스트로 발견된 버그 — 우편번호 컬럼이 모바일에서 안 사라지고 출입번호가 밀려
   잘리던 원인).
   2026-07-27 후속(실기기 피드백): 고정 px였을 때 출입번호 오른쪽에 큰 빈 여백이
   남는 문제가 있었음(px 합계가 실제 컨테이너 폭보다 항상 작아서) — %로 전환해
   4개 컬럼 합이 항상 정확히 100%가 되도록 함(percent도 "명시적 폭"이라 이
   프로젝트의 "auto 컬럼 금지" 원칙과는 무관 — 그 버그는 항상 "auto"였지 %가
   아니었음). 우편번호(8%)/건물명(18%)/출입번호(25%)는 비교적 좁게, 주소(49%)가
   가장 넓게 — 주소가 가장 중요한 내용이고 이미 줄바꿈 처리(아래 참고)가 있어
   좁아져도 안전하기 때문. */
.col-address-postal {
  width: 8%;
}
.col-address-building {
  width: 18%;
}
.col-address-street {
  width: 49%;
}
.col-address-entrance {
  width: 25%;
}
/* 항목명(헤더 행)은 PC/태블릿/모바일 전 구간에서 스크롤해도 화면에 계속
   보이도록 고정(2026-07-27 요구사항 명시) — 컬럼폭을 %로 바꿔 테이블이 항상
   컨테이너 폭에 정확히 맞아떨어지게 된 덕분에(가로 스크롤이 필요 없어짐)
   position:sticky 하나로 간단히 구현 가능해짐(기존에 가입정보 등에서
   position:sticky 대신 head/body 테이블을 아예 분리했던 건, 그쪽은 컬럼폭이
   고정 px라 가로 스크롤이 필요해서 "헤더가 가로 스크롤은 따라가되 세로
   스크롤은 안 따라가야" 하는 요구사항 때문이었음 — 주소정보는 가로 스크롤
   자체가 없어져 이 문제가 아예 발생하지 않음). 배경색을 명시해야 밑에서
   스크롤되는 데이터 행이 비쳐 보이지 않음.
   ⚠️ #data-table는 personal-list.ejs(개인정보)도 <thead>를 포함한 채로
   동일 id를 쓰고 있어서(가입정보/제품정보는 head가 별도 테이블이라 그
   안에 thead 자체가 없어 안전하지만, 개인정보는 위험) 반드시
   [data-category="address"]로 범위를 좁힐 것 — 스코프 없이 걸면
   개인정보 목록의 헤더까지 의도치 않게 sticky로 바뀜. */
/* ⚠️ 위 position:sticky만으로는 실제로 안 붙는 버그가 있었음(2026-07-27
   재확인) — 원인은 .table-wrap의 overflow-x:auto. CSS 스펙상 overflow-x/
   overflow-y 둘 중 하나만 visible이 아니면 나머지(visible로 안 적어도 기본값)
   도 강제로 auto로 승격됨 — 즉 .table-wrap이 (실제로 스크롤은 안 하면서도)
   자체 스크롤 컨테이너로 취급되어, position:sticky의 기준이 진짜 스크롤이
   일어나는 .list-shell__scroll이 아니라 절대 안 움직이는 .table-wrap이
   되어버림(그 안에서 thead는 늘 "맨 위"라 보기엔 sticky가 아무 효과도 없는
   것처럼 보임). 주소정보는 %컬럼이라 애초에 가로 스크롤이 필요 없으므로
   overflow-x도 visible로 되돌려 이 승격 자체를 원천 차단. */
[data-category="address"] .table-wrap {
  overflow-x: visible;
  overflow-y: visible;
}
[data-category="address"] #data-table thead {
  position: sticky;
  top: 0;
  z-index: 2;
  background: #fafbfc;
}
/* 2026-07-27 실기기 피드백 — 건물명/주소/출입번호 3개 컬럼은 폭이 부족해도
   내용이 잘리지 않고 항상 줄바꿈으로 전체가 보여야 함(PC/태블릿/모바일 전
   구간 공통 요구사항이라 미디어쿼리 없이 여기 base에 둠 — 이전엔 모바일
   768px 구간에만 이 처리가 있어서 PC/태블릿에서는 여전히 말줄임표로 잘렸음).
   .data-table th/td 기본값(white-space:nowrap + text-overflow:ellipsis)을
   이 3개 셀에서만 덮어씀 — 우편번호는 원래부터 짧은 고정 형식(5자리 숫자)이라
   줄바꿈이 필요 없어 대상에서 제외. */
.address-cell-building,
.address-cell-street-cell,
.address-cell-entrance-cell {
  white-space: normal;
}
/* 주소 텍스트 자체(버튼)도 길이 제한 없이 전부 보여줌 — 이전엔
  -webkit-line-clamp:4로 4줄까지만 보여주고 나머지는 잘랐는데(모바일 전용
  규칙이었음), 이제는 PC/태블릿/모바일 어디서든 길이 제한 없이 필요한 만큼
  줄바꿈되도록 통일(2026-07-27 요구사항 명시: "주소가 모두 표시되도록"). */
.address-cell-link {
  display: block;
  width: 100%;
  white-space: normal;
  word-break: break-word;
  text-align: left;
}

/* 계정칩 + "신규 데이터 입력" 버튼 한 줄(2026-07-16) — 버튼은 항상
   margin-left:auto로 맨 오른쪽에 고정(계정칩이 있는 카테고리든 아예 없는
   카테고리(주소/개인정보)든 상관없이 동일하게 동작). */
.list-footer-row {
  display: flex;
  align-items: center;
  gap: 12px;
}
.list-footer-row .account-chip-bar {
  flex: 1;
  min-width: 0;
}
.list-footer-row .btn--icon-only {
  margin-left: auto;
  flex: 0 0 auto;
}
/* 아이콘화된 "신규 데이터 입력" 버튼 — PC/태블릿은 아이콘+텍스트 둘 다 표시,
   768px 이하 모바일은 아이콘만(반응형은 responsive.css에서 라벨을 숨김).
   글자 크기가 아니라 span 자체를 감춰서, 좁은 화면에서 계정명(칩)과 버튼
   텍스트가 같이 있을 때 줄바꿈/잘림이 생기는 문제를 원천적으로 피함. */
.btn--icon-only span:first-child {
  font-size: 18px;
  line-height: 1;
  font-weight: 700;
}
#bulk-actions {
  margin-top: 16px;
  display: flex;
  gap: 10px;
}
.bulk-result-list {
  margin: 0 0 16px;
  padding-left: 20px;
  color: var(--color-text-muted);
  font-size: 13.5px;
}
.empty-row {
  text-align: center;
  color: var(--color-text-muted);
  padding: 32px 0;
}

/* ---------- Card grid (device) ---------- */
.card-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
}
/* 2단계(반투명 블러) — 기기정보 카드도 표/게시판과 동일한 반투명+블러로 전환. */
.device-card {
  background: rgba(255, 255, 255, 0.8);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 18px;
  box-shadow: var(--shadow-sm);
  cursor: pointer;
}
.device-card:hover {
  background: rgba(248, 250, 252, 0.8);
}
.device-card__title {
  font-size: 15px;
  font-weight: 600;
  margin-bottom: 10px;
}
/* dt/dd rows are grouped under a real <dl> for semantics (valid HTML5: <div>
   wrapping <dt>/<dd> pairs directly under <dl> is allowed) — reset the
   browser's default <dl> margin so this wrapper doesn't add extra spacing
   beyond what .device-card__row already controls. */
.device-card dl {
  margin: 0;
}
.device-card__row {
  display: flex;
  justify-content: space-between;
  font-size: 13px;
  padding: 4px 0;
  border-bottom: 1px dashed var(--color-border);
}
.device-card__row:last-of-type {
  border-bottom: none;
}
.device-card__row dt {
  color: var(--color-text-muted);
}
.device-card__row dd {
  margin: 0;
  text-align: right;
}

/* ---------- Detail panel ---------- */
.detail-panel {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: 24px;
}
.detail-field {
  display: grid;
  grid-template-columns: 160px 1fr;
  gap: 16px;
  padding: 12px 0;
  border-bottom: 1px solid var(--color-border);
  align-items: center;
}
.detail-field:last-child {
  border-bottom: none;
}
.detail-field__label {
  color: var(--color-text-muted);
  font-size: 13px;
  font-weight: 600;
}
.detail-field__value {
  font-size: 14px;
  word-break: break-word;
}

.masked-value {
  font-family: monospace;
  letter-spacing: 1px;
}
.btn-toggle-mask {
  margin-left: 10px;
  border: none;
  background: none;
  color: var(--color-primary);
  font-size: 12.5px;
  cursor: pointer;
  text-decoration: underline;
  padding: 0;
}

.attachment-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.attachment-list li {
  padding: 6px 0;
  border-bottom: 1px dashed var(--color-border);
  display: flex;
  align-items: center;
  gap: 10px;
}
.attachment-list li:last-child {
  border-bottom: none;
}
.attachment-list li a {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* 사진/영상 첨부파일 썸네일(public/js/attachments.js, detail-content.ejs) —
   li가 space-between 대신 자연스러운 순서(썸네일→링크→삭제버튼)로 배치되고,
   삭제 버튼은 자기 자신의 margin-left:auto로 항상 맨 오른쪽에 붙음(아래 참고)
   — 자식 개수가 조회용(썸네일+링크)/수정용(썸네일+링크+삭제버튼)으로 서로
   달라도 항상 같은 방식으로 정렬되도록. */
.attachment-thumb {
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  object-fit: cover;
  border-radius: var(--radius);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
}

/* ---------- 첨부파일 업로드(드롭존) — public/js/attachments.js ---------- */
.dropzone {
  margin-top: 8px;
  border: 2px dashed var(--color-border);
  border-radius: var(--radius);
  padding: 16px;
  text-align: center;
  background: var(--color-bg);
}
.dropzone.is-dragover {
  border-color: var(--color-primary);
  background: var(--color-account-bg);
}
.dropzone__label {
  display: block;
  cursor: pointer;
  color: var(--color-text-muted);
  font-size: 13.5px;
}
.attachment-list--staged li {
  color: var(--color-text-muted);
}
.attachment-list .btn-remove-attachment {
  flex: 0 0 auto;
  margin-left: auto;
  border: none;
  background: none;
  color: var(--color-danger);
  cursor: pointer;
  font-size: 12.5px;
  text-decoration: underline;
  padding: 0;
}

.detail-actions {
  display: flex;
  gap: 10px;
  margin-top: 20px;
}

/* ---------- 권한관리 매트릭스(public/js/admin-grants.js) ---------- */
.grant-matrix-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 16px;
}
.grant-matrix-tab {
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text);
  border-radius: 999px;
  padding: 8px 16px;
  font-size: 13.5px;
  cursor: pointer;
}
.grant-matrix-tab.is-active {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
}
/* 탭이 아니라 액션 버튼이라, 나란히 흘러가는 탭들과 구분되도록 왼쪽에
   여백+구분선을 둠(2026-07-18, "계정 관리" 칩 우측에 배치). */
.grant-matrix-tab-action {
  margin-left: 8px;
  padding-left: 16px;
  border-left: 1px solid var(--color-border);
}
#accounts-wrap .field-hint {
  margin-bottom: 12px;
}
/* 탈퇴요청 목록(2026-07-18부터 대기중+처리완료 전체 누적 표시) — 컬럼 수가
   항상 고정(6개)이라 .data-table의 기본 table-layout:fixed를 그대로 쓰되,
   모든 col에 명시적 width를 줌(auto 컬럼 금지 원칙, CLAUDE.md 참고). */
.withdrawal-requests-table .wr-col-loginid { width: 70px; }
.withdrawal-requests-table .wr-col-name { width: 70px; }
.withdrawal-requests-table .wr-col-requested { width: 130px; }
.withdrawal-requests-table .wr-col-result { width: 280px; }
.withdrawal-requests-table .wr-col-processed { width: 130px; }
.withdrawal-requests-table .wr-col-note { width: 220px; }
.withdrawal-requests-table .wr-note-input {
  width: 100%;
  border: 1px solid var(--color-border);
  border-radius: 4px;
  padding: 4px 6px;
  font-size: 13px;
}
/* 목록이 길어지면 페이지 전체가 늘어나는 대신 이 영역 안에서만 스크롤 —
   thead를 sticky로 고정해 스크롤 중에도 헤더가 보이게 함. */
.withdrawal-requests-scroll {
  max-height: 480px;
  overflow-y: auto;
}
.withdrawal-requests-table thead th {
  position: sticky;
  top: 0;
  background: var(--color-surface);
  z-index: 1;
}
/* 변경이력(휴지통) 화면(2026-07-22) — 컬럼 수 고정(7개), auto 컬럼 금지 원칙
   그대로 적용(CLAUDE.md 참고). */
.history-table .history-col-date { width: 100px; }
.history-table .history-col-item { width: 260px; }
.history-table .history-col-owner { width: 80px; }
.history-table .history-col-deletedby { width: 90px; }
.history-table .history-col-attachments { width: 80px; }
.history-table .history-col-elapsed { width: 90px; }
.history-table .history-col-actions { width: 170px; }
.grant-matrix-legend {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 12px;
  font-size: 13.5px;
  color: var(--color-text-muted);
}
/* 괄호 안 아이콘(.grant-cell--legend, 22px 높이 박스)이 일반 텍스트와 섞여
   있으면 기본 vertical-align:baseline 때문에 텍스트 기준선에 박스 밑변이
   맞춰져 위아래 중앙이 안 맞아 보임(2026-07-17 발견) — 이 span 자체를
   inline-flex로 바꿔 내부 요소(텍스트/아이콘 박스)를 전부 상하 중앙정렬. */
.grant-matrix-legend-cycle {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
/* X/○/◉ 셀 — 매트릭스 본문(js-grant-cell, 클릭 가능)과 범례(legend, 클릭 불가)
   둘 다 재사용. 대각선/ADMIN 행처럼 항상 고정인 셀은 grant-cell--fixed로 커서만
   기본값으로 되돌려 "클릭해도 안 바뀐다"는 걸 시각적으로 구분. EDIT 등급 기호는
   이중원(◎)이 실기기에서 겹동그라미로 보여 헷갈린다는 피드백으로 안이 채워진
   원(◉, FISHEYE)으로 교체(2026-07-17). */
.grant-cell {
  font-size: 16px;
  cursor: pointer;
  user-select: none;
}
/* 컬럼 폭을 <td class="grant-cell"> 자체의 width로 지정했었는데, 이건
   table-layout:auto 아래에서 브라우저가 "힌트"로만 취급해서 실제로는 무시될
   수 있음(2026-07-17, 실기기에서 30→60px까지 여러 차례 늘려도 렌더링이 전혀
   안 바뀌는 것으로 확인됨 — 두 개의 다른 브라우저에서 캐시를 완전히 지우고
   재확인해도 동일해서 캐시 문제가 아니라 이 렌더링 방식 자체의 문제로 특정).
   대신 셀 안에 display:block인 내부 span(.grant-cell__inner)을 두고 그
   span에 width를 주면, 블록 요소의 명시적 width는 테이블 레이아웃 알고리즘의
   협상 대상이 아니라 무조건 지켜야 하는 값이라 셀이 최소 그만큼 넓어짐 —
   이 span 크기가 실제 컬럼 폭을 강제하는 유일한 소스. */
.grant-cell__inner {
  display: block;
  /* 50px는 계정이 늘어날 걸 고려하면 너무 넓다는 피드백(2026-07-17)으로
     축소 — 팀원이 계속 늘어날 걸 감안해 여유를 더 두는 방향. */
  width: 36px;
  text-align: center;
}
.grant-matrix-table th.grant-matrix-owner-label {
  text-align: center;
}
.grant-cell--legend {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: var(--radius);
  cursor: default;
}
.grant-cell--fixed {
  cursor: default;
  color: var(--color-text-muted);
}
.grant-cell--none {
  color: var(--color-danger);
}
.grant-cell--view {
  color: var(--color-primary);
}
.grant-cell--edit {
  color: var(--color-success);
  font-weight: 600;
}
.grant-matrix-table th:first-child,
.grant-matrix-table td:first-child {
  text-align: left;
  font-weight: 600;
}
/* 가로/세로축 설명을 표 자체의 1행 위 / 1열 왼쪽에 추가(2026-07-17) — 기존
   대각선 분할 코너 셀 + 별도 안내문장(<p>) 조합이 좁은 화면에서 코너 셀 텍스트가
   잘려("대상\소유자" → "대상₩소...") 안 보인다는 피드백으로 표 구조 자체로 이동.
   A1 셀(.grant-matrix-blank)은 요청에 따라 텍스트 없이 완전히 비워둠. */
.grant-matrix-blank {
  background: #fafbfc;
}
.grant-matrix-axis-caption {
  background: #fafbfc;
  color: var(--color-text-muted);
  font-size: 12px;
  font-weight: 500;
}
/* .grant-matrix-table th, td의 white-space:nowrap(아래)을 문장이 있는 이
   캡션 셀에서만 되돌리려면 th와 함께 묶어 특정성을 맞춰야 함(이 파일에서
   반복적으로 겪은 특정성 문제와 동일한 종류). */
.grant-matrix-table th.grant-matrix-axis-caption {
  white-space: normal;
}
/* .data-table th의 기본 text-align:left(specificity 0,1,1)가 단일 클래스
   선택자(0,1,0)보다 특정성이 높아 이 규칙이 계속 무시되고 있었음(2026-07-17
   발견) — th와 묶어 특정성을 올림(바로 위 white-space 규칙과 동일한 이유). */
.grant-matrix-table th.grant-matrix-axis-caption--top {
  text-align: center;
  padding: 6px 10px;
}
.grant-matrix-axis-caption--left {
  writing-mode: vertical-rl;
  text-orientation: mixed;
  text-align: center;
  padding: 10px 4px;
  vertical-align: middle;
}
/* .grant-cell__inner와 동일한 이유(위 주석 참고) — th 자체의 width 대신
   내부 span에 명시적 width를 줘서 테이블 자동 레이아웃이 무시할 수 없게 함. */
.grant-matrix-axis-caption__inner {
  display: inline-block;
  width: 30px;
}
/* .grant-matrix-table th:first-child가 text-align:left/font-weight:600을
   강제하는데(로그인ID 헤더용 규칙), 세로축 캡션은 rowspan 때문에 그 규칙과
   같은 :first-child 자리에 놓여 특정성으로 밀림 — 이 셀에서만 다시 되돌림. */
.grant-matrix-table th:first-child.grant-matrix-axis-caption {
  text-align: center;
  font-weight: 500;
}
/* 세로축 계정 라벨(JM/JO/... th) 전용 — 좁고 가운데 정렬(2026-07-17, 표 왼쪽에
   불필요한 여백이 크게 남아 보인다는 피드백으로 폭을 좁힘 → 이후 오른쪽 정렬이
   어색하다는 후속 피드백으로 가운데 정렬로 재변경).
   idx===0 행에서는 이 th가 2번째 자식(1번째는 세로축 캡션 rowspan 셀)이라
   :first-child 규칙과 안 겹치지만, idx>0 행에서는 이 th가 1번째 자식이 되어
   위 :first-child 규칙(text-align:left)과 특정성이 같아짐 — 이 경우까지
   커버하려면 :first-child를 붙인 선택자를 별도로 둬야 함. */
.grant-matrix-table th.grant-matrix-grantee-label {
  text-align: center;
}
.grant-matrix-table th:first-child.grant-matrix-grantee-label {
  text-align: center;
}
/* .grant-cell__inner와 동일한 이유 — th 자체의 width 대신 내부 span에
   명시적 width를 줘서 테이블 자동 레이아웃이 무시할 수 없게 함. */
.grant-matrix-grantee-label__inner {
  display: inline-block;
  width: 48px;
}

/* 좁은 화면에서 계정명(예: "JM")과 X/○/◉ 아이콘이 "..."로 잘려 보이는 문제
   (2026-07-17, 실기기 스크린샷으로 확인) — .data-table의 기본
   table-layout:fixed는 컨테이너 폭에 맞춰 모든 컬럼을 균등 압축하는데, 이
   매트릭스는 컬럼 수가 팀원 수에 따라 가변이라 고정폭 colgroup을 둘 수 없음.
   대신 이 표만 예외적으로 auto 레이아웃으로 풀어서 내용 기준 최소폭을
   보장하고, 넘치는 부분은 .table-wrap의 기존 overflow-x:auto로 가로
   스크롤 처리(컬럼 압축 대신 스크롤 — 가입정보/제품정보 목록과 동일한
   이 프로젝트의 기존 원칙, main.css 587행 부근 주석 참고).
   width:auto(2026-07-17 추가) — .data-table의 기본 width:100%를 그대로 두면
   실제 컬럼들의 내용 기준 합계가 컨테이너 폭보다 작을 때 남는 여백을 모든
   컬럼(좁아야 할 세로축 라벨/캡션 컬럼 포함)에 비례 배분해버려, 라벨 컬럼이
   불필요하게 넓어지는 원인이 됐음 — width:auto로 표 자체가 내용만큼만
   차지하도록 바꿔서 해결. */
.grant-matrix-table {
  table-layout: auto;
  width: auto;
}
/* 컬럼 폭을 줄인 뒤 표가 좁아지면서, 감싸는 .table-wrap(기본적으로 부모
   폭에 꽉 차는 block 요소)만 원래 폭 그대로 남아 표 오른쪽에 보기 안 좋은
   흰 여백이 생김(2026-07-17) — 이 표를 감싸는 wrap만 예외적으로
   inline-block으로 바꿔 표 내용만큼만 폭을 차지하도록 함. */
#grant-matrix-wrap {
  display: inline-block;
  max-width: 100%;
}
.grant-matrix-table th,
.grant-matrix-table td {
  white-space: nowrap;
  overflow: visible;
  text-overflow: clip;
  /* 기본 .data-table th/td 패딩(12px 14px)이 계정 컬럼 수가 늘어날 걸
     고려하면 너무 넉넉하다는 피드백(2026-07-17)으로 데스크톱에서도 축소. */
  padding: 8px 8px;
}

/* 아이콘 설명 문장 안 화살표(X제한 → ○열람 → ◉열람+수정+삭제, 2026-07-17) */
.grant-matrix-legend-arrow {
  color: var(--color-text-muted);
  margin: 0 2px;
}

/* ---------- Badges ---------- */
.badge {
  display: inline-block;
  font-size: 11.5px;
  padding: 2px 8px;
  border-radius: 999px;
  background: #eef1f5;
  color: var(--color-text-muted);
}
.badge--admin {
  background: rgba(29, 78, 137, 0.1);
  color: var(--color-primary);
}

.text-muted {
  color: var(--color-text-muted);
}

/* ---------- Error page ---------- */
.error-page {
  min-height: 60vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: 12px;
}
.error-page__code {
  font-size: 48px;
  font-weight: 700;
  color: var(--color-primary);
}

.locked-page {
  min-height: 50vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: 10px;
}
.locked-page__heading {
  color: var(--color-text-muted);
  margin-bottom: 4px;
}
.locked-page__error {
  color: var(--color-danger);
  font-size: 13px;
}
.locked-page__form {
  display: flex;
  gap: 8px;
}
.locked-page__form input {
  padding: 9px 12px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  font-size: 14px;
}
.locked-page__back {
  margin-top: 12px;
}

/* ---------- Modal ---------- */
body.modal-open {
  overflow: hidden;
}
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(16, 24, 40, 0.5);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 40px 16px;
  overflow-y: auto;
  z-index: 1000;
}
.modal-overlay[hidden] {
  display: none;
}
.modal-box {
  position: relative;
  background: var(--color-surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow-md);
  width: 100%;
  max-width: 720px;
  padding: 24px;
}
.modal-close {
  position: absolute;
  top: 12px;
  right: 12px;
  border: none;
  background: none;
  font-size: 22px;
  line-height: 1;
  color: var(--color-text-muted);
  cursor: pointer;
  padding: 4px 8px;
}
.modal-close:hover {
  color: var(--color-text);
}
.modal-content .page__header {
  margin-top: 0;
}
.modal-content .detail-panel {
  border: none;
  box-shadow: none;
  padding: 0;
}
.modal-loading,
.modal-error {
  color: var(--color-text-muted);
  text-align: center;
  padding: 32px 0;
}
.modal-error p {
  margin-bottom: 12px;
}
.modal-locked {
  text-align: center;
  padding: 24px 0;
}
/* 인라인 토스트(.alert 배너)를 대체하는 범용 확인 알림 팝업(2026-07-19,
   modal.js의 showAlertModal() 참고). */
.modal-alert {
  text-align: center;
  padding: 24px 0;
}
.modal-alert__text {
  margin-bottom: 16px;
  white-space: pre-line;
}
.modal-alert--error .modal-alert__text {
  color: var(--color-danger);
}
.modal-locked__heading {
  font-weight: 600;
  margin-bottom: 16px;
}
.modal-locked__error {
  color: var(--color-danger);
  font-size: 13px;
  margin-bottom: 12px;
}
.modal-locked__form {
  display: flex;
  gap: 8px;
  justify-content: center;
}
.modal-locked__input {
  padding: 9px 12px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  font-size: 14px;
  max-width: 220px;
}
/* DB 다운로드 비밀번호 입력 모달 안의 다운로드 이력(2026-07-18) — 모달 본문
   .modal-locked은 text-align:center라 이 영역만 좌측 정렬로 되돌림. */
.backup-history {
  margin-top: 20px;
  text-align: left;
}
.backup-history__scroll {
  max-height: 200px;
  overflow-y: auto;
}
.backup-history__table {
  font-size: 12.5px;
}
/* .data-table th/td의 기본 text-align:left(요소+클래스 결합 선택자, 특정성
   0,1,1)를 이기려면 이쪽도 동일하게 요소+클래스로 묶어야 함(CLAUDE.md
   참고 — 단일 클래스 선택자는 조용히 무시될 수 있음). */
.backup-history__table th,
.backup-history__table td {
  text-align: center;
}
.backup-history__table thead th {
  position: sticky;
  top: 0;
  background: var(--color-surface);
}
.link-button {
  border: none;
  background: none;
  padding: 0;
  color: var(--color-primary);
  text-decoration: underline;
  cursor: pointer;
  font: inherit;
}
