/* op-bridge.css — 우리 앱이 퍼블 common.css 위에 얹는 오버라이드 전용 레이어.
   퍼블 vendored 파일은 무수정. 조정은 여기에만. */

/* ── 레이아웃 풀폭 — 임시 선(先)반영 블록은 2026-08-05 제거됨 ──────────────
   퍼블이 저장소에 푸시(`bb6f205`)했고 common.css 를 재-vendoring 했으므로
   `.layout-side{max-width:none;margin:0}` · `.layout-side>.side{left:0}` ·
   `.header-op .header-inner{max-width:none}` 는 이제 **vendored common.css 안에 있다**.
   제거 전후 실측(뷰포트 1600, base-layout 전 화면): layout-side `none|0px` · side `left:0` ·
   header-inner `none|1585` · content `270+1315` — 전부 동일. 되살리지 말 것(중복 선언이 된다). */

/* 검색 필터 필드 폭 — 위 풀폭 전환의 후속(같이 움직인다).

   퍼블 `.search-field` 는 `flex-basis: 30%` 라 필드 박스가 컨테이너 폭에 비례한다. 그런데 내용은
   고정폭이다(라벨 `--search-label-w` 120 + gap 8 + 입력 `--search-input-w` 300 = 420px).
   비율 박스 + 고정 내용이라 컨테이너가 **1400px 를 넘는 순간부터 필드마다 빈 꼬리**가 생기고,
   넓어질수록 커진다. 실측(뷰포트 2560, 카드 필터 3개): 박스 637 vs 내용 420 → 필드 사이 여백이
   퍼블이 선언한 `gap: 16px 30px` 의 30px 이 아니라 **247px** 이 된다.

   ★퍼블 탓이 아니라 우리가 만든 조건이다 — 퍼블은 "PC 전용 / 지원 1920"(common.css:281) 기준이고
     1920 이하에서는 이 값이 자연스럽다. 위 풀폭 전환으로 본문이 뷰포트 끝까지 늘어나면서
     퍼블이 상정하지 않은 구간에 들어간 것이다. 그 구간을 여기서 메운다.

   flex-basis 를 auto 로 되돌리면 박스가 내용 폭(420)이 되어 필드 사이가 선언값 30px 로 돌아온다.
   ※ 컨테이너가 1400px 미만일 때는 이미 `min-width:auto` 때문에 박스가 420 이었다 —
     즉 이 규칙은 **좁은 화면의 기존 동작을 넓은 화면까지 일관되게 넓힌 것**이고, 1400 미만에서는
     아무 변화도 없다.
   ※ 모든 필드가 같은 폭(420)이라 줄바꿈돼도 라벨·입력 시작점의 세로 정렬은 그대로 유지된다.
   ※ 상세 화면의 `.cd-left-col > .search-filter .search-field`(2/1 열 배치)는 명시도가 더 높아
     여기 영향을 받지 않는다.

   ★퍼블이 울트라와이드 필터 사양을 정의하면 걷어낼 것. 짝이던 풀폭 블록은 2026-08-05
     재-vendoring 으로 common.css 에 흡수됐지만, 이 규칙은 퍼블이 아직 정의하지 않은 구간을
     메우는 것이라 **남는다**. (docs/퍼블_이관_작업기준.md 참조) */
.search-field {
  flex-basis: auto;
}

/* 페이저 이전·다음 화살표의 비활성 상태.

   퍼블 목업은 1페이지에서도 양쪽 더블 화살표를 그리지만 정적이라 "갈 곳 없음" 상태가 없어
   `.pagination` 에 비활성 규칙이 없다. 실데이터에서는 첫/마지막 페이지에서 반드시 생긴다.
   표기는 퍼블 버튼 규약(.btn.is-disabled)을 그대로 따른다 — cursor:not-allowed + pointer-events:none,
   색은 비활성 텍스트 토큰 --n-400.
   마크업(href·data-page 제거, tabindex=-1)은 js/common/common.js publishHtml 과
   templates/fragments/pager.html 이 만든다.
   ※ 하이브리드(layout/base) 화면은 이 파일이 안 닿으므로 app.css 에 같은 규칙이 있다. */
.pagination .page-next.is-disabled {
  color: var(--n-400);
  cursor: not-allowed;
  pointer-events: none;
}

/* 모달 열린 동안 배경 스크롤 잠금.
   퍼블 common.css 에는 이 규칙이 없다(정적 목업이라 필요가 없었다). .modal-open 은 op-bridge.js 가
   퍼블 openModal 을 감싸며 붙인다.
   ★html 에도 걸어야 한다 — 이 앱의 실제 스크롤 컨테이너는 documentElement 라 body 에만 걸면
     안 먹는다. 안 걸리면 모달 안 날짜칸의 flatpickr 달력이 스크롤한 만큼 입력칸과 벌어진다
     (달력은 body 에 absolute 로 문서를 따라가고, 입력칸은 fixed 오버레이 안이라 제자리).
   ※ 하이브리드(layout/base)는 같은 규칙을 modal.css 에 갖고 있다 — 그쪽은 OP ui.js 가
     .modal-open 을 붙인다. 이관이 끝나 modal.css 가 사라지면 이 사본만 남는다. */
html:has(> body.modal-open),
body.modal-open { overflow: hidden; }

/* 잠금 순간의 스크롤바 폭 보정.
   ★종전에는 `html { scrollbar-gutter: stable; }` 로 항상 자리를 비워 튐을 막았다. 그런데 그 방식은
     모달과 무관한 평상시에도 우측에 빈 띠를 남긴다 — 2026-08-04 실측 15px(뷰포트 2560 에서 헤더·본문·
     푸터가 전부 2545 에서 끝남). 세로 스크롤이 없는 화면에서도 남아서 "오른쪽 끝 빈 영역"으로 보였고,
     퍼블 라이브에는 없는 여백이었다. → 상시 예약을 버리고 잠기는 동안만 같은 폭을 돌려준다.
   --modal-sb 는 js/common/modal-close.js 가 잠금 클래스 토글에 맞춰 넣고 뺀다(미설정이면 0px).
   ※ 고정(fixed) 요소는 뷰포트 기준이라 문서 padding 으로는 안 밀린다 — 따로 보정한다.
     · .header  width:100% + border-box(common.css `* { box-sizing }`) → padding 이 내용만 밀어준다
     · .modal-overlay 는 inset:0 + padding:20px 이라 padding 을 덮으면 안 된다 → margin 으로 줄인다
     · .layout-side > .side 는 left:0 + 고정폭이라 뷰포트 폭 변화의 영향을 안 받는다(보정 불필요) */
html:has(> body.modal-open) {
  padding-right: var(--modal-sb, 0px);
}
html:has(> body.modal-open) .header {
  padding-right: var(--modal-sb, 0px);
}
html:has(> body.modal-open) .modal-overlay {
  margin-right: var(--modal-sb, 0px);
}

/* [DEMO] 닫힌 LNB 플라이아웃 서브메뉴 — 퍼블은 닫힘 시 .lnb-sub{display:none}이라 하위 탐색 불가.
   lnb-flyout.js가 아이콘 hover 시 .is-flyout 부여 + top/left(fixed) 지정 → 오른쪽에 팝업.

   🔴 !important 필수 (2026-08-03) — 퍼블이 접힘 규칙을 일반 선언에서
      `.layout-side.is-collapsed .lnb-sub, .lnb-sub3 { display:none !important }` 로 바꿨다
      (common.css:1101-1103, LNB 아코디언 전환의 일부). !important 는 소스 순서·명시도와
      무관하게 이기므로 op-bridge.css 를 뒤에 실어도 진다. 안 붙이면 lnb-flyout.js 가
      .is-flyout 과 좌표를 정상으로 붙여도 **팝업이 화면에 안 뜬다** — 접힘 상태에서
      하위 메뉴에 도달할 방법이 사라진다(base-layout·publish-content-base 58화면). */
.layout-side.is-collapsed .lnb-sub.is-flyout {
  display: block !important;
  position: fixed;
  min-width: 220px;
  max-height: 72vh;
  overflow-y: auto;
  padding: 10px;
  background: #fff;
  border: 1px solid var(--n-100);
  border-radius: 12px;
  box-shadow: 0 8px 24px rgba(38, 40, 44, 0.14);
  z-index: 300;
}
.layout-side.is-collapsed .lnb-sub.is-flyout .lnb-sub-item,
.layout-side.is-collapsed .lnb-sub.is-flyout .lnb-sub3-item {
  white-space: nowrap;
}
/* 플라이아웃 안 3뎁스 — is-open 일 때만 펼친다(항상 펼침 금지: 예전에 그렇게 했다가
   2뎁스 확장형(+/-) 버튼이 눌러도 반응 없어 보였다).

   🔴 2026-08-03 재도입 — 퍼블이 두 가지를 동시에 바꿔 3뎁스가 접힘 상태에서 영구히 잠겼다.
      ① `.lnb-sub-item--parent.is-open + .lnb-sub3 { display:flex }` 규칙 삭제
         (이제 열림은 jQuery slideDown 의 인라인 style 로 표현된다)
      ② `.layout-side.is-collapsed .lnb-sub3 { display:none !important }` 추가
      → 인라인 style 은 !important 를 못 이기므로 publish-shell.js 의 expand() 도 무력하다.
      플라이아웃 안에서만, is-open 인 것만 되살린다. */
.layout-side.is-collapsed .lnb-sub.is-flyout .lnb-sub-item--parent.is-open + .lnb-sub3 {
  display: flex !important;
}

/* 팝업이 열린(클릭된) 아이콘 = 선택 상태.
   접힘 base 는 하이라이트 off(common.css:1104)이므로, 선택 시에만 연보라 배경(::before)+포인트색 부여.
   lnb-flyout.js 가 한 번에 하나에만 .is-flyout-open 을 준다(단일 선택). */
.layout-side.is-collapsed .lnb-item.is-flyout-open::before {
  opacity: 1;
}
.layout-side.is-collapsed .lnb-item.is-flyout-open,
.layout-side.is-collapsed .lnb-item.is-flyout-open > .ico:first-child {
  color: var(--color-point);
}

/* [myinfo] 열람 모드 권한그룹 목록 — 후보 C: 요약(총 N개) + 고정높이 스크롤 칩 박스.
   슈퍼관리자처럼 권한이 많아도 카드 높이가 일정하게 유지되도록 #miRoleMenus 를 스크롤 박스로. */
#myinfoView .auth-count {
  margin-left: 9px;
  color: var(--n-600);
  font-weight: 600;
}
#myinfoView .auth-menus {
  margin-top: 9px;
  max-height: 108px;
  overflow-y: auto;
  display: flex;
  flex-wrap: wrap;
  gap: 6px 8px;
  padding: 11px;
  border: 1px solid var(--n-100);
  border-radius: 12px;
  background: var(--n-40);
}
#myinfoView .auth-menus span {
  font-size: 12px;
  line-height: 1;
  padding: 6px 11px;
  border-radius: 50px;
  background: var(--p-50);
  color: var(--p-600);
  white-space: nowrap;
}

/* 접힘 상태 구분선 복구: 퍼블은 활성 섹션 부모에 is-open 을 자동 부여하고
   .lnb-item.is-open{border-bottom:0}(펼침 시 서브와 연결)이라, 접힘 상태에선
   서브가 인라인으로 안 열리는데 구분선만 사라져 보인다. 접힘에선 구분선 유지. */
.layout-side.is-collapsed .lnb-item.is-open:not(:last-child) {
  border-bottom: 1px solid var(--n-100);
}

  /* 공지 상세 본문: 서버가 준 평문을 textContent 로 넣는다(HTML 주입 방지) — 줄바꿈이
     살아야 하므로 pre-wrap 이 필요하다. 퍼블 목업은 <p> 로 조판된 정적 텍스트라 이 규칙이
     없다(구 contents/notice.css 의 .notice-content-view 가 갖고 있던 동작). */
  .notice-view-body {
    white-space: pre-wrap;
  }

  /* 12열 사용자 표(관리자·파트너): 내용 폭이 본문 컨테이너보다 넓다. 퍼블 .table{width:100%} 만
     걸리면 열이 눌려 날짜(YYYY-MM-DD hh:mm:ss)가 두세 줄로 접힌다 — .table-scroll(overflow-x:auto)이
     실제로 스크롤되도록 최소 폭을 준다. 퍼블 목업은 더 넓은 화면 기준이라 이 문제가 안 보였다. */
  .admin-table,
  .partner-table {
    min-width: 1480px;
  }

  /* 폼 보조 문구(중복확인 결과·접근권한 안내 등). 퍼블 목업은 정적이라 이런 상태 문구가 없어
     대응 클래스도 없다 — 실동작에 필요한 최소 규칙만 여기 둔다.
     op-bridge 가 부풀면 퍼블에 피드백할 신호다(작업기준 1장). */
  .field-note {
    margin-top: 6px;
    font-size: 13px;
    line-height: 18px;
    color: var(--n-600);
  }

  .field-note.error {
    color: var(--color-error, #e5484d);
  }

  /* 공통 다이얼로그(#portalCommonModal) 전용 규칙.
     퍼블 common.css 에는 이 개념이 없다(퍼블은 화면별 모달만 있고 공용 alert/confirm 이 없다).
     종전엔 하이브리드용 modal.css 에만 있었는데 승격 레이아웃은 그 파일을 싣지 않아
     승격화면 전체에서 세 규칙이 빠져 있었다 — 2026-07-30 실측:
       z-index 1200 → 1000(페이지 모달과 동일) · 폭 420 → 440px ·
       white-space pre-line → normal(다중행 메시지가 한 줄로 붙는다, 높이 45 → 22px)
     ★z-index 가 페이지 모달과 같으면 모달 위에 뜬 confirm 이 문서 순서에 따라 뒤로 깔릴 수 있다.
     ★메시지는 서버 문구를 그대로 넣는다(CAMS/PP 오류 메시지에 개행이 온다) — pre-line 이 필요하다.
     색은 하드코딩 대신 퍼블 토큰으로 옮겼다(modal.css 의 --color-text 는 퍼블에 없는 변수다). */
  .portal-common-modal {
    z-index: 1200;
  }

  .portal-common-modal-box {
    width: 420px;
  }

  .portal-common-modal-message {
    margin: 0;
    color: var(--n-900);
    font-size: var(--fs-14);
    line-height: 1.6;
    white-space: pre-line;
    word-break: keep-all;
    overflow-wrap: break-word;
  }

  /* 검색줄이 화면 밖으로 2px 삐져나가 페이지 가로 스크롤바가 생기는 것 방지.
     .search-box/.search-bar 는 flex 인데 자식들이 min-width:auto(=min-content) 기본값이라
     줄이 꽉 차면 더 못 줄고 넘친다. 퍼블 목업의 검색줄은 [셀렉트3 + 기간 + 검색어 + 아이콘]
     이고 구분선 뒤에도 아이콘 버튼 하나뿐이라 여유가 있었다.
     OP 는 같은 줄에 '표시 건수' 셀렉트와 '초기화'·'새로고침' 텍스트 버튼이 더 붙는다
     (user/partner 에서 2px 초과 발생 — 실측 docW 1464 vs viewport 1462).
     고칠 때 실측으로 걸러낸 오답 둘:
       · min-width:0 만 주면 검색어 칸이 26px 까지 짜부라진다 — 2px 넘침보다 나쁘다.
       · .search-bar 까지 flex-wrap 을 주면 검색줄이 92px → 212px(3줄)로 커진다.
     그래서 검색어 칸만 하한까지 줄어들게 하고, 그래도 모자랄 때만
     .search-box 수준에서 줄바꿈시킨다. 여유가 있을 땐 아무 변화도 없다.

     ★2026-08-05 하한 200 → 180 으로 내림 (재-vendoring 회귀 보정).
       새 퍼블 common.css 가 `.search-bar` 에 `overflow:hidden` 을 넣었다
       ("창이 좁아져도 카드 밖으로 튀어나가지 않는다"). 퍼블 목업 기준으론 맞지만,
       컨트롤이 더 많은 OP 줄에서는 넘치는 20px 이 **잘린다** — 종전 `overflow:visible`
       시절에는 카드 패딩 위로 삐져나온 채 보이기라도 했다.
       실측(뷰포트 1600): /user/admin · /logs/access-log · /logs/error-log 3화면에서
       `.search-bar` clientWidth 1203 vs scrollWidth 1223 → **검색(돋보기) 버튼이 반쯤 잘림**.
       하한 200 이 그 20px 을 막고 있었다. 180 으로 내리면 1223 → 1203 이 되어 딱 들어맞는다.
       ※ 넘치지 않는 화면(파트너·FAQ·공지 등)은 줄어들 이유가 없어 변화 0(실측 확인).
       ※ 26px 짜부라짐은 하한이 있는 한 재발하지 않는다 — 하한을 없애지 말 것. */
  .search-box {
    flex-wrap: wrap;
  }

  .search-bar {
    min-width: 0;
  }

  .search-bar .f-keyword {
    min-width: 180px;
  }

  /* 목록 표 셀 세로 가운데 정렬.
     퍼블 common.css 는 `.info-table th,td` 에는 vertical-align:middle 을 주면서
     목록 표 `.table th,td` 에는 안 준다 — reset 의 vertical-align:baseline 이 그대로 남는다.
     퍼블 목업은 모든 셀이 한 줄이라 baseline 과 middle 이 눈으로 같아 안 드러났을 뿐이다.
     실데이터에선 한 칸(담당 서비스·주소·TRID 목록)이 접히는 순간 행이 커지고
     나머지 짧은 칸의 글자가 첫 줄 baseline 에 매달려 위로 붙는다.
     실측: 한 칸이 두 줄이 되어 행이 163px 이 될 때 짧은 칸 글자가 행 중심에서 55px 위.
     ※ .info-table 의 `td:has(.input){vertical-align:top}`(입력칸 든 셀) 은 퍼블 의도라 안 건드린다. */
  .table th,
  .table td {
    vertical-align: middle;
  }

  /* 카드 하단 액션줄 버튼의 고정폭 해제.
     퍼블 common.css 는 `.info-actions .btn { min-width:80px; width:80px }` 로 폭을 못박는다.
     퍼블 목업의 라벨이 전부 4글자 이하(수정·삭제·탈퇴처리)라 문제가 없었을 뿐이다.
     OP 는 같은 자리에 퍼블에 없는 동작이 더 붙는다 — '비밀번호 초기화'(user/admin-detail),
     '대표파트너 지정'(user/partner-detail) 이 80px 에 눌려 글자가 잘린다.
     ★새 규칙이 아니라 퍼블 자신의 해법이다: pages/user/partner-detail-general.css 가
       긴 라벨 대응으로 똑같이 width:auto 를 준다. 화면 한 장에만 있어 다른 화면이 못 쓸 뿐이다.
     min-width:80px 은 그대로 살아 있어 짧은 라벨의 모양은 퍼블과 픽셀 단위로 동일하다. */
  .info-actions .btn {
    width: auto;
  }

  /* 파괴적 동작(만료 처리·삭제) 버튼.
     퍼블 common.css 에는 위험 버튼 변형이 없다 — 유일한 선례가 pages/user/partner-detail.css 의
     .btn-reject(흰 배경 + --color-error 테두리/글자)인데 그 화면 CSS 안에만 있어 다른 화면이 못 쓴다.
     화면마다 style="color:#E03131;border-color:#E03131" 을 박아 두던 걸 여기로 모은다.
     ★색은 하드코딩 대신 퍼블 토큰(--color-error)을 쓴다. 퍼블이 위험 버튼을 정식으로 내놓으면 이건 지운다. */
  .btn.btn-danger {
    background: #fff;
    border-color: var(--color-error);
    color: var(--color-error);
  }

  .btn.btn-danger:hover {
    background: var(--r-50, #fdf2f2);
  }

  /* 채움형(만료 확정 등 최종 확인 버튼) */
  .btn.btn-danger-fill {
    background: var(--color-error);
    border-color: var(--color-error);
    color: #fff;
  }

/* ── 시스템관리 트리: 미사용·비노출 항목 식별 ──────────────────────────────
   ★OP 로컬 규칙(퍼블에 대응 표시 없음). 이관 전에는 트리 행에 "사용/미사용"·"노출/숨김"
   배지를 달았는데, 퍼블 mtree 행에는 배지 자리가 없어 제거했다. 그 대신 클래스를 붙였으나
   스타일이 없어 **미사용 항목을 구분할 방법이 0** 이었다(상태 필터도 함께 사라졌다).
   퍼블이 상태 표시를 정의하면 그쪽으로 갈아탄다. */
.mtree-text.is-unused-menu .mtree-name,
.mtree-text.is-unused-code .mtree-name,
.mtree-text.is-hidden-menu .mtree-name {
  color: var(--n-400);
}
.mtree-text.is-unused-menu .mtree-name,
.mtree-text.is-unused-code .mtree-name {
  text-decoration: line-through;
}

/* ── 시스템관리 트리: 드래그 중/드롭 위치 표시 ─────────────────────────────
   ★OP 로컬 규칙. 퍼블은 `.mtree-row.is-drag-over` 하나(점선 아웃라인)만 제공하는데,
   OP 드래그는 행 상단/중앙/하단에 따라 before·inside·after 로 부모가 달라진다(menu.js placement).
   어디에 놓일지 안 보이면 눈감고 드래그하는 셈이라 3분기를 구분해 표시한다. */
.mtree-row.is-drop-before { box-shadow: inset 0 2px 0 0 var(--color-point); }
.mtree-row.is-drop-after  { box-shadow: inset 0 -2px 0 0 var(--color-point); }
.mtree-row.is-drop-inside { outline: 1px dashed var(--color-point); outline-offset: -1px; }
.mtree-row.is-drop-invalid { outline: 1px dashed var(--color-error); outline-offset: -1px; cursor: not-allowed; }

/* ── 문제조치 이미지 미리보기: 실제 사진 비율 유지 ────────────────────────
   ★OP 로컬 규칙. 퍼블 `.trouble-preview .ico-img` 는 178×178 **고정**인데,
   그건 자리표시 아이콘(정사각 svg)을 띄우는 목업 전제다. OP 는 실제 첨부 이미지를
   넣으므로 그대로 두면 세로·가로 사진이 눌려 찌그러진다.
   미리보기가 원본과 다른 비율로 보이면 "미리보기"라는 말 자체가 성립하지 않는다. */
.trouble-preview #problemActionPreviewImage {
  width: auto;
  height: auto;
  max-width: 288px;    /* .trouble-preview 320px 안에서 여백 16px 씩 */
  max-height: 288px;
  object-fit: contain;
}

/* ── 상품관리 트리: 선택 노드 배경 (product/card · product/issuer) ────────
   ★OP 보정. 퍼블 pages/cams/product/{card,issuer}.css 는
   `.prod-node--product.is-selected > .prod-row { border-radius: 6px }` 로 라운드만 주고
   **배경색을 빼먹었다**(주석에는 "선택 하이라이트"라고 적혀 있다). 라운드만으로는
   아무것도 안 보여서 "지금 어느 카드상품의 상세를 보고 있는지"가 화면에서 사라진다.
   값은 퍼블이 자기 다른 트리에 쓰는 선택 배경(common.css `.tree-node.is-selected > .tree-head`)
   과 같은 --p-50 을 그대로 쓴다(새 값 발명 아님).
   또 편집 드롭다운으로 신규등록/복사/Export/Import 를 연 프로파일(제휴사) 행도 같은 방식으로
   표시하는데, 퍼블에는 `.prod-issuer.is-selected` 규칙 자체가 없다.
   ★퍼블이 두 규칙을 채우면 이 블록은 걷어낸다. */
.prod-node.is-selected > .prod-row,
.prod-issuer.is-selected > .prod-row--issuer {
  background: var(--p-50);
  border-radius: 6px;
}

/* ── 발급사상품 빈 상태: [hidden] 숨김 가드 (퍼블 결함 보정) ──────────────
   ★퍼블 pages/cams/product/issuer.css 는 `.product-issuer-page .cd-placeholder { display:flex }`
   만 주고 `[hidden]` 가드를 빠뜨렸다. display 선언이 hidden 의 기본 display:none 을 이기므로
   상세를 열어도 좌측 안내 박스가 그대로 남는다(실측: 상세 패널 위에 겹쳐 보였다).
   같은 파일의 짝인 card.css 에는 `.product-card-page .cd-placeholder[hidden]{display:none}` 이
   **있다** — 즉 퍼블도 필요하다고 알고 있으면서 issuer 쪽만 누락한 것이다.
   ★퍼블 issuer.css 에 가드가 들어오면 이 블록은 걷어낸다. */
.product-issuer-page .cd-placeholder[hidden] {
  display: none;
}

/* ── 상품 트리 만료 표시 — OP 고유 규칙 (퍼블에 대응물 없음) ────────────────
   "만료 포함" 토글은 OP 기능이다. 퍼블 목업에는 만료 데이터가 없어서
   `pages/cams/product/{card,issuer}.html` 이 `expired` 를 **한 번도 쓰지 않고**,
   새 common.css 에도 `.expired` 규칙이 없다. 즉 퍼블 갱신으로 들어올 성질이 아니라
   우리가 계속 들고 갈 규칙이므로 임시 보정이 아닌 OP 고유 구획에 둔다.

   종전엔 app.css 가 옛 트리 구조에 걸어 줬는데(`.tree-node.expired > .tree-head .tree-label`,
   `.tree-app.expired`), CAMS 상품관리 퍼블 이관에서 마크업이 `.prod-*` 로 바뀌며 규칙만 사라졌다.
   클래스는 지금도 붙는다 — templates/product/{card,issuer}.html(th:classappend),
   js/product/card.js(.prod-node--product · .prod-leaf), js/product/issuer.js(.prod-node · .prod-leaf).

   실측(2026-08-05, 만료 포함 ON): 보정 전에는 만료/비만료가 **완전히 같은 색**이었다 —
   `.prod-issuer.expired .prod-title` rgb(26,26,27) = `:not(.expired)` rgb(26,26,27),
   `.prod-leaf.expired` rgb(89,92,97) = `:not(.expired)` rgb(89,92,97).
   `.prod-leaf`(애플리케이션)에는 만료 배지도 없어 구분 수단이 0이었다.
   색은 퍼블 비활성 텍스트 토큰 --n-400 (op-bridge 의 페이저 비활성과 같은 값).
   ※ `.badge-expire`(만료 배지)는 새 common.css 가 칠하므로 여기서 건드리지 않는다.

   ★주석 안에 CSS 주석 종료 기호(별표+슬래시)를 쓰지 말 것. 예시로라도 적으면 거기서
   주석이 조기 종료되고, 남은 설명문이 셀렉터 목록으로 흘러들어가 쉼표마다 잘린다.
   그러면 scope-css.js 가 스코프 접두를 문장 중간에 박고 진짜 셀렉터는 접두를 못 받아
   파생본에 전역 규칙이 새어나간다(2026-08-05 실제로 밟음).
   같은 이유로 셀렉터 목록 사이에도 주석을 넣지 않는다 — 설명은 이 블록 위에만 둔다.

   `.prod-node.expired > .prod-row > .prod-title` 이 따로 있는 이유 — 발급사 트리의 카드상품
   행만 `.prod-title` 이 `.prod-row` 직속이다. 퍼블이 그 행에서는 `.prod-label` 로 감싸지
   않기 때문(js/product/issuer.js:169 주석). 라벨 경유 셀렉터만 두면 이 행이 통째로 빠져
   만료 표시가 안 걸리고, 이 행에는 `.badge-expire` 도 안 붙어 구분 수단이 0 이 된다. */
.prod-issuer.expired > .prod-row .prod-label,
.prod-issuer.expired > .prod-row--issuer .prod-label,
.prod-node.expired > .prod-row .prod-label,
.prod-node.expired > .prod-row > .prod-title,
.prod-leaf.expired {
  color: var(--n-400);
}
/* 라벨 안 조각들이 자기 색을 갖고 있어 위 선언만으로는 안 먹는다 — 상속으로 되돌린다. */
.prod-issuer.expired > .prod-row .prod-label .prod-code,
.prod-issuer.expired > .prod-row .prod-label .prod-title,
.prod-issuer.expired > .prod-row .prod-label .prod-cnt,
.prod-issuer.expired > .prod-row--issuer .prod-label .prod-code,
.prod-issuer.expired > .prod-row--issuer .prod-label .prod-title,
.prod-issuer.expired > .prod-row--issuer .prod-label .prod-cnt,
.prod-node.expired > .prod-row .prod-label .prod-code,
.prod-node.expired > .prod-row .prod-label .prod-title,
.prod-node.expired > .prod-row .prod-label .prod-cnt {
  color: inherit;
}
/* 상세 안 "등록된 애플리케이션/카드상품" 표의 만료 행도 같은 규칙을 쓴다.
   한때 `.recovery`(퍼블 페이지 CSS 의 `color: red !important`)를 빌려 썼는데 —
   퍼블 상품관리 원본은 `.recovery` 를 한 번도 쓰지 않는 다른 화면용 클래스이고,
   만료는 위험이 아니라 비활성이라 종전 app.css 처럼 회색이 맞다.
   js/product/{card-detail,issuer}.js 가 `tr.classList.add('expired')` 로 붙인다. */
.cd-app-table tr.expired td {
  color: var(--n-400);
}

/* ── 표 안 클릭 셀(.cd-app-link) 색이 안 먹는다 (퍼블 결함 보정) ─────────────
   퍼블 common.css 는 `.cd-app-link { color: var(--color-point); cursor: pointer }` +
   `:hover { text-decoration: underline }` 로 이 클래스의 의도를 명시한다. 그런데
   OP·퍼블 모두 이 클래스를 `<td>` **자신**에 붙이므로 `.table tbody td`(0,1,2)가
   `.cd-app-link`(0,1,0)를 특정도로 이겨서 **색만 조용히 본문색이 된다**(실측 rgb(26,26,27)).
   퍼블 자기 화면도 마찬가지다 — 퍼블 목업은 그 자리를
   `style="cursor:pointer; color: var(--color-primary); text-decoration: underline"` 인라인으로
   덮어 가리고 있는데, **`--color-primary` 는 퍼블 CSS 어디에도 정의가 없다**(퍼블 card.html 이
   싣는 common.css·card.css 전체 grep 0건). 즉 퍼블도 죽은 토큰이라 본문색으로 뜬다.
   목업의 실패한 인라인이 아니라 스타일시트가 선언한 의도를 따른다 — 인라인으로 색을 박지
   않는다는 이 문서의 기존 판단(`readonly` 인풋 건)과 같은 처리다.
   ★퍼블이 `.cd-app-link` 특정도를 올리거나 `.table .cell-link` 로 갈아타면 걷어낸다. */
.table tbody td.cd-app-link {
  color: var(--color-point);
}
/* 만료 행에서는 만료 회색이 이긴다(비활성이 링크색보다 우선). */
.cd-app-table tr.expired td.cd-app-link {
  color: var(--n-400);
}

/* ── 신규등록/복사/Export/Import 체크박스 표 (퍼블 결함 보정) ───────────────
   ★퍼블 `pages/cams/product/issuer.html` 스스로 `<table class="table cd-check-table">` 를 쓰는데
   `.cd-check-table` 규칙이 퍼블 어디에도 없다(새 common.css·페이지 CSS 모두 grep 0건).
   즉 퍼블이 클래스만 쓰고 스타일을 안 낸 누락이다. 종전엔 app.css 가 갖고 있었고
   CAMS 상품관리 이관에서 app.css 를 걷어내며 같이 사라졌다.
   OP 사용처: js/product/{card-export,card-import,issuer-export,issuer-import}.js
   (`table cd-app-table cd-check-table` + 헤더 전체선택 `th:first-child` + 행 `td.num`).
   값은 app.css 에 있던 것을 그대로 옮긴 것이다(새 값 발명 아님).
   ★퍼블이 `.cd-check-table` 을 채우면 이 블록은 걷어낸다. */
.cd-check-table th:first-child,
.cd-check-table td.num {
  width: 36px;
  text-align: center;
}
.cd-check-table input[type="checkbox"] {
  cursor: pointer;
}

/* ── 카드상품 신규등록 액션줄 간격 — OP 고유 규칙 ──────────────────────────
   2026-08-05 재-vendoring 전까지 vendored common.css 안에 손으로 넣어 둔 블록이었다
   ("★OP 로컬 추가(퍼블 원본에 없음)"). vendored 파일은 퍼블 원본과 바이트 동일해야 하므로
   교체와 함께 이리로 옮긴다 — 그냥 덮었으면 조용히 사라졌을 자리다.
   퍼블 `pages/cams/product/card.css` 의 `.cd-newreg-actions{padding:20px}` 와는 다른 속성이고,
   `padding:0` 을 인라인으로 덮는 쪽(issuer.html 의 Import 팝업 등)에서는 이 margin 만 남는다
   (issuer.css 에는 `.cd-newreg-actions` 규칙 자체가 없다 → 여기가 유일한 간격 공급원).
   ※ card.html 의 Import 팝업 쪽 사용처는 2026-08-06 레거시 정합([업로드] 버튼 제거)으로 사라졌다.
      신규등록 패널이 계속 쓰므로 규칙 자체는 살아 있다. */
.cd-newreg-actions {
  margin-top: 16px;
}

/* ── Import 미리보기 검색 강조 — 레거시 importSearch() 이관 (2026-08-06) ────────
   레거시 cm/card/import.jsp 는 이름이 포함된 행을 `bgcolor=#c28a8a` 로 칠하고 그 위치로
   스크롤했다. 그 하드코딩 색 대신 퍼블 토큰 `--p-50` 을 쓴다 — 퍼블이 자기 트리 선택
   하이라이트(`.tree-node.is-selected`)에 쓰는 값이라 같은 화면 안에서 어휘가 어긋나지 않는다.
   ★퍼블에 "검색 일치 행" 변형이 생기면 이 블록은 걷어낸다. */
.cd-check-table tbody tr.is-match > td {
  background: var(--p-50);
}

/* ── 코드 관리 모달 '코드 명' 행 — 상세명이 숨는 분기의 그룹명 폭 (2026-08-06) ──
   퍼블 `pages/cams/system/code.css` 는 `.code-name-split .gname { flex: 0 0 40% }` 로
   '그룹명 _ 상세명' 두 칸 배치를 전제한다. 그런데 코드 그룹 신규 / 그룹 코드 수정
   두 분기는 코드명이 그룹명 단일값이라 상세명 칸과 구분자(_)를 숨긴다
   (js/cams/code.js 의 setDetailNameVisible(false)). 그러면 남은 그룹명 칸이 행의
   40% 만 차지하고 오른쪽 60% 가 빈 채로 남는다 — 그룹명은 코드명 전체를 받는 칸인데
   두 칸으로 쪼갤 때의 폭을 그대로 쓰는 셈이다.
   퍼블 원본 code.css 는 diff 0(퍼블과 바이트 동일)으로 유지해야 하고 마크업도 퍼블
   원본 그대로여야 하므로, 원본을 고치는 대신 code.js 가 얹는 상태 클래스를 여기서 받는다.
   특정도 (0,4,0) 이라 나중에 실리는 page-css(code.css, (0,3,0))에 지지 않는다.
   ★퍼블이 상세명 숨김 변형을 자체 제공하면 이 블록과 code.js 의 클래스 토글을 함께 걷어낸다. */
.code-modal .code-name-split.no-detail .gname {
  flex: 1 1 auto;
}

/* ── KMS 키 생성 — 단계 토글용 `.hidden` (2026-08-10) ──
   `.hidden` 은 이 프로젝트 어디에도 전역 정의가 없다. 화면 전용 CSS 가 각자 스코프로
   두는 것이 관행이고(`pages/cams/lifecycle/lifecycle.css` 의 `.lifecycle-page .hidden`),
   KMS 화면들의 종전 정의처는 `masterkey/masterkey.css`(하이브리드 전용)였고, 승격과 함께
   로드 대상에서 빠졌다 — 그대로 뒀다면 키 생성의 4단계 카드가 한꺼번에 보인다
   (에러가 안 나서 눈으로만 드러나는 유형이다).
   퍼블 원본 `pages/kms/keygen.css` 는 무수정 유지해야 하므로 오버라이드 레이어인 여기에 둔다.
   ★퍼블이 단계 전환 유틸리티를 자체 제공하면 이 블록을 걷어낸다.
   (KMS 4화면이 모두 승격하면서 masterkey.css 는 삭제됐다.) */
.kg-page .hidden,
.bk-page .hidden,
.iq-page .hidden {
  display: none;
}

/* ── KMS 화면 — 하이브리드에서 넘어오며 잃은 규칙 복구 (2026-08-10) ──
   `.kms-warn`(KMS 연결 불가 배너)과 `.error`(폼 오류 문구)는 `masterkey/masterkey.css`
   정의였는데, 그 파일은 하이브리드(layout/base) 전용이라 승격과 함께 빠졌다(이후 삭제됨).
   퍼블 common.css 에는 대응이 없다 — `.input.is-error` 는 입력칸 테두리용이라
   문구 스타일을 대신하지 못한다. 그대로 두면 빨간 테두리 경고박스가 맨 검은 글씨가 된다.

   ★승격 검증에서 놓쳤던 항목이다. 죽은 클래스 스윕을 "app.css 가 스타일하던 것"으로만
     걸렀더니, 화면 전용 CSS 가 함께 빠지는 이 경우를 못 잡았다. 화면 전용 CSS 를 가진
     화면을 승격할 때는 그 파일이 정의하던 클래스도 반드시 교차 확인할 것.

   radius 는 퍼블 스택에 `--radius-sm` 토큰 자체가 없어 tokens.css 의 값(6px)을 직접 쓴다.
   `.kms-warn` 은 원본대로 스코프 없이 둔다 — injection/lookup/backup 3화면 공용이라
   나머지 둘을 승격할 때 그대로 받는다. `.error` 는 흔한 이름이라 `.kg-page` 로 한정한다. */
.kms-warn {
  margin: 0 0 16px;
  padding: 12px 16px;
  font-size: var(--fs-13, 13px);
  color: #b42318;
  background: #fef3f2;
  border: 1px solid #fda29b;
  border-radius: 6px;
}
.kg-page .error {
  margin-top: 8px;
  font-size: var(--fs-13, 13px);
  color: var(--color-error, #e03131);
}

/* ── KMS 백업/복원 — 하이브리드에서 넘어오며 잃은 규칙 (2026-08-10) ──
   삭제된 `masterkey/masterkey.css` 의 `.mk-backup-page` 규칙 중 퍼블 common.css 에 대응이 **없는** 것만
   옮긴다. 퍼블이 제공하는 `.table-box`·`.table-scroll`·`.search-box`·`.check-box`·`.badge-state`
   (기본형)·`.pagination` 등은 그대로 쓰므로 가져오지 않는다.
   종전 값은 tokens.css 토큰을 썼는데 퍼블 스택에는 그 토큰들이 없어(`--color-bg`,
   `--color-border-strong`, `--color-text-muted`, `--radius-md` 등 전부 부재) 퍼블 토큰으로 바꾼다.
   ★KMS 4화면 승격 완료로 masterkey.css 는 삭제됐다(2026-08-10). */

/* 파일 선택 input 숨김 — 퍼블 [파일 추가] 버튼(label for)이 대신 받는다.
   display:none 이 아니라 클리핑이어야 키보드 포커스·폼 제출이 살아 있다. */
.bk-page .file-native {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* 업로드 전 안내 박스 */
.bk-page .empty-box {
  padding: 36px 16px;
  border: 1px dashed var(--n-300);
  border-radius: 10px;
  background: var(--n-50);
  color: var(--n-500);
  font-size: var(--fs-13, 13px);
  text-align: center;
}

/* 백업·복원 결과 메시지(성공 톤). 퍼블에 대응 컴포넌트가 없다. */
.bk-page .result {
  margin-top: 16px;
  padding: 12px 16px;
  border: 1px solid #b6e0c4;
  border-radius: 6px;
  background: #f2fbf5;
  color: #2b8a3e;
  font-size: var(--fs-13, 13px);
  font-weight: var(--fw-semibold);
}

/* 선택 불가 행(백업 불가 키 / 검증 실패 파일) — 체크박스 disabled 와 짝이다. */
.bk-page .bk-table tbody tr.off td,
.bk-page .bk-restore-table tbody tr.off td {
  color: var(--n-400);
}

/* 복원 파일 검증 상태 배지 — 퍼블 `.badge-state` 기본형(모양)에 의미색만 얹는다.
   퍼블 변형(.issued/.connected/.standby 등)은 발급·연결 상태용이라 어휘가 달라 재사용하지 않는다.
   ★4종을 유지하는 이유: `unknown`(KMS 다운으로 검증 못 함)과 `bad`(손상)를 합치면
     멀쩡한 백업 파일을 손상으로 오인하게 된다(backup.js STATE_TEXT 주석). */
.bk-page .badge-state.ok {
  background: #e8f6ec;
  color: #2b8a3e;
}
.bk-page .badge-state.warn {
  background: #fdf3dd;
  color: #b45309;
}
.bk-page .badge-state.bad {
  background: #fcedec;
  color: var(--color-error, #e03131);
}
.bk-page .badge-state.unknown {
  background: var(--n-50);
  color: var(--n-600);
}

/* ── KMS 컴포넌트 입력 표 — Component/KCV 라벨 세로 중앙 정렬 복구 (2026-08-10) ──
   퍼블 common.css 의 `.info-table tr:has(.input) th { vertical-align:top }` 은
   "입력칸이 있는 행은 라벨을 상단 정렬"이 의도다(라벨-값 폼 공통 관례). 그런데 이 표는
   한 행에 Component(입력칸 있음)·KCV(입력칸 없음) 두 라벨-값 쌍이 나란히 있다 — :has() 는
   행 전체를 스코프로 보므로 KCV 라벨까지 함께 위로 밀려 어색하게 뜬다.
   keygen.css 는 `.kg-comp-table th,td { vertical-align:middle }` 로 이미 중앙 정렬을
   의도했지만 특이도가 낮아(0,1,1) common.css 쪽(0,2,2)에 진다 — 같은 형태로 되돌린다. */
.kg-comp-table tr:has(.input) th {
  vertical-align: middle;
}
