/* ══════════════════════════════════════════════════════════════════════════
   KSP — ksp.css · DECK 의 ②-b 프로젝트 층 + 도메인 부품 (D 구역)
   읽는 순서: tokens.css → ui.css → **ksp.css**

   이 파일이 하는 일은 둘뿐이다.
     ① 이 프로젝트에만 있는 "역할"에 이름을 준다 — 장치/서버/침수/OTA…
        (역할 토큰은 ① 원시 램프를 참조해도 된다. 그게 이 층의 일이다.
         금지되는 건 **화면 코드**가 --p- 를 직접 쓰는 것이다.)
     ② 이 업무에만 있는 부품을 만든다 — 차단바·차량 실루엣·번호판 카드·
        정산 2행 원장·서버 판정 행·관리 화면·D3 시험 화면.

   다음 프로젝트는 tokens.css·ui.css·ui.js 를 그대로 가져가고
   **이 파일만 자기 도메인으로 새로 쓴다.**

   색 규약 (2026-07-30 사용자 지정): 장치(엣지)가 낸 값 = amber,
   서버가 낸 값 = cyan. 화면 어디서나 이 두 색이 같은 뜻을 갖는다.
   ══════════════════════════════════════════════════════════════════════════ */

/* ─────────────────────────────────────────────────────────────────────────
   ①  프로젝트 역할 토큰 (②-b 층)
   ───────────────────────────────────────────────────────────────────────── */
:root{
  --c-device:var(--c-warn);   --c-device-bg:var(--c-warn-bg);  --c-device-line:var(--c-warn-line);
  --c-server:var(--c-accent); --c-server-bg:var(--c-accent-bg);--c-server-line:var(--c-accent-line);
  --c-water:var(--p-bl-500);       /* 침수 수위 — 정보색 계열, 경보 시 crit 로 전환 */
  --c-ota:var(--p-vi-500);         /* 펌웨어 업데이트 — 업무색과 겹치지 않는 보라 */
  --c-ota-bg:#1a1030;
  /* 영상·썸네일 뒤 바탕. 레터박스는 관례상 두 테마 모두 검정에 가깝다. */
  --c-video:#05080f;
  --c-well:var(--c-surface-3);     /* 진행바 트랙·크롭 바탕 등 "파인 면" */

  /* 도메인 치수 — 폭에 따라 토큰 자체가 반응한다(미디어 쿼리는 구조 전환에만) */
  --pad-page:clamp(10px,1.2vw,20px);
  --rcol-w:clamp(300px,25vw,440px);      /* 우측 패널 폭 */
  --devcard-min:min(100%,380px);         /* 장치 카드 최소폭 — 모바일 넘침 방지 */
  --lpr-w:clamp(104px,8vw,138px);        /* 번호판 인식 카드 폭 (세로형 §86) */
  /* 관제는 정보 밀도가 높아 DECK 최소치(10px)보다 작은 표식이 필요하다 */
  --fs-nano:10px;
}
:root[data-density="compact"]{ --fs-nano:9px }
:root[data-density="touch"]{   --fs-nano:11px }
@media (prefers-color-scheme: light){
  :root{ --c-water:var(--p-bl-700); --c-ota:var(--p-vi-700); --c-ota-bg:#efeaff }
}
:root[data-theme="light"]{ --c-water:var(--p-bl-700); --c-ota:var(--p-vi-700); --c-ota-bg:#efeaff }
:root[data-theme="dark"]{  --c-water:var(--p-bl-500); --c-ota:var(--p-vi-500); --c-ota-bg:#1a1030 }
/* DECK 부품(.panel/.kpi)에 도메인 톤을 추가한다 — 부품 규칙 자체는 손대지 않는다 */
.panel[data-tone="device"],.kpi[data-tone="device"]{--tone:var(--c-device)}
.panel[data-tone="server"],.kpi[data-tone="server"]{--tone:var(--c-server)}

/* ─────────────────────────────────────────────────────────────────────────
   ②  페이지 뼈대 — 전체폭 (2026-07-31 사용자 지정: 고정 상한 없음)
   ───────────────────────────────────────────────────────────────────────── */
.wrap{width:100%;padding:var(--pad-page) var(--pad-page) var(--sp-7);
      display:flex;flex-direction:column;gap:var(--sp-4)}
.ic{width:1em;height:1em;fill:currentColor;vertical-align:-0.125em;flex:none}
.ic.lg{width:1.25em;height:1.25em}
.mono{font-family:var(--ff-mono)}
/* 평범한 표면 상자. DECK 의 .panel(제목 바+본문 구조)과 달리 껍데기만 필요할 때.
   구분해 두지 않으면 .panel 의 flex-column 이 인라인 배치를 조용히 무너뜨린다. */
.card{background:var(--c-surface);border:1px solid var(--c-line);border-radius:var(--r-3)}
/* 목록이 빌 때 패널 한가운데 뜨는 안내. DECK 의 .empty(블록형 빈 상태)와
   쓰임이 달라 이름을 따로 둔다 — 같은 이름으로 덮으면 나중에 반드시 헷갈린다. */
.overlay-empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  pointer-events:none;color:var(--c-text-3);font:var(--fs-micro)/1 var(--ff-mono)}
/* 장치 카드의 잠금 버튼 줄 — 4개가 폭을 고르게 나눠 갖는다 */
.btns{display:flex;gap:6px}
.btns > .btn{flex:1;padding-inline:0;font-size:var(--fs-nano)}
/* 헤더의 테마 스위치 */
.axisbar{display:inline-flex;gap:2px;padding:2px;border-radius:var(--r-2);
  border:1px solid var(--c-line);background:var(--c-surface-2)}
.axisbar .btn{padding-inline:var(--sp-3);font-size:var(--fs-micro)}
.axisbar .btn[aria-pressed="true"]{background:var(--c-accent-bg);color:var(--c-accent)}

/* 헤더 3구역 (§90): [정체성] | [지표] | [상태·조작]
   구역 사이에만 큰 간격을 둬 "무엇이 한 묶음인지"가 보이게 한다. */
.appbar{display:flex;align-items:center;flex-wrap:wrap;gap:var(--sp-3) var(--sp-6);
        padding:var(--sp-3) var(--sp-4);min-height:62px;
        background:var(--c-surface);border:1px solid var(--c-line);border-radius:var(--r-3)}
.hgroup{display:flex;align-items:center;gap:var(--sp-3);flex-wrap:wrap}
.hgroup.idt{gap:0;flex-direction:column;align-items:flex-start;line-height:1.25}
.logo{font:var(--p-fw-bold) var(--fs-micro)/1.4 var(--ff-mono);letter-spacing:.1em;
      color:var(--c-accent)}
.lot{font-size:var(--fs-lg);font-weight:var(--p-fw-black)}
.sp{flex:1}
#clock{font:var(--fs-xl)/1 var(--ff-mono);color:var(--c-accent)}

/* KPI 줄 — 헤더 안. 넘치면 줄바꿈(스크롤 아님) */
.kpis{display:flex;flex-wrap:wrap;gap:var(--sp-2);flex:1 1 320px;min-width:0}
/* max-width 가 없으면 줄바꿈된 마지막 하나가 줄 전체로 늘어나 "고장난 칸"처럼 보인다
   (2026-08-01 조밀 밀도에서 실측 — 7개 중 1개만 아랫줄로 넘어갔을 때) */
.kpis > .kpi{min-width:70px;flex:1 1 70px;max-width:180px;align-items:center;
             text-align:center;padding:var(--sp-3) var(--sp-2)}
.kpis > .kpi .label{letter-spacing:.02em;text-transform:none;white-space:nowrap}
.kpis > .kpi b{font:var(--p-fw-bold) var(--fs-num)/1.1 var(--ff-mono)}

/* ─────────────────────────────────────────────────────────────────────────
   ③  번호판 인식 스트립 — 세로형 카드 (§86)
   썸네일(위) → 차번(중, 크게) → 시각·신뢰도·출처(아래) 순의 읽기 흐름.
   테두리 전체가 출처색이라 멀리서도 장치/서버가 구분된다.
   ───────────────────────────────────────────────────────────────────────── */
.recog{position:relative;min-height:150px;padding:var(--sp-3)}
.recog > h2{font:var(--p-fw-bold) var(--fs-sm)/1.4 var(--ff-mono);color:var(--c-accent);
            display:inline}
.recog-list{display:flex;gap:var(--sp-3);overflow-x:auto;margin-top:var(--sp-3);
            padding-bottom:2px;overscroll-behavior-x:contain}
.legend{display:inline-flex;gap:var(--sp-4);align-items:center;margin-left:var(--sp-4);
        font:var(--p-fw-bold) var(--fs-micro)/1 var(--ff-mono);color:var(--c-text-2);
        vertical-align:2px}
.legend .sw{display:inline-block;width:10px;height:10px;border-radius:3px;margin-right:4px;
            vertical-align:-1px}
.legend .sw.src-dev{background:var(--c-device)} .legend .sw.src-srv{background:var(--c-server)}

/* <deck-plate src="device|server" plate conf time thumb fresh> */
.plate-card{flex:0 0 var(--lpr-w);width:var(--lpr-w);display:flex;flex-direction:column;
  border-radius:var(--r-2);background:var(--c-surface-2);overflow:hidden;
  border:2px solid var(--c-line)}
.plate-card[data-src="device"]{border-color:var(--c-device)}
.plate-card[data-src="server"]{border-color:var(--c-server)}
@keyframes plate-blink{50%{border-color:var(--c-text)}}
.plate-card[data-fresh]{animation:plate-blink .8s ease-in-out 6}
.plate-card > .crop{width:100%;height:46px;flex:none;background:var(--c-video);
  border-bottom:1px solid var(--c-line);display:flex;align-items:center;justify-content:center;
  color:var(--c-text-3);font:var(--fs-nano)/1 var(--ff-mono)}
.plate-card > .crop img{width:100%;height:100%;object-fit:contain;
  image-rendering:pixelated;display:block}
.plate-card > .who{display:flex;flex-direction:column;align-items:center;gap:1px;
  padding:var(--sp-2) 4px 5px;min-width:0}
.plate-card > .who > b{font:var(--p-fw-bold) var(--fs-base)/1.15 var(--ff-mono);
  color:var(--c-text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
.plate-card .meta{display:flex;align-items:center;justify-content:center;flex-wrap:wrap;
  gap:3px 5px;font:var(--fs-nano)/1.4 var(--ff-mono);color:var(--c-text-3)}
.plate-card .meta .conf{color:var(--c-text-2)}
/* 출처 뱃지 — 좁은 카드에서 글자가 쪼개지지 않게 nowrap */
.plate-card .meta .src{font-weight:var(--p-fw-bold);padding:0 4px;border-radius:3px;
  white-space:nowrap;border:1px solid currentColor;
  color:var(--c-device);background:var(--c-device-bg)}
.plate-card[data-src="server"] .meta .src{color:var(--c-server);background:var(--c-server-bg)}

/* ─────────────────────────────────────────────────────────────────────────
   ④  장치 카드 — 상태·잠금 시각화·영상
   ───────────────────────────────────────────────────────────────────────── */
/* ⚠ 이름을 .main 으로 두면 안 된다 — DECK 의 `.shell > .main`, 스타일 가이드의
   `.doc .main` 과 겹쳐 남의 화면을 무너뜨린다(2026-08-01 실측: 가이드의 패널이
   글자 한 자 폭으로 찌그러졌다). 프로젝트 층 이름에는 접두를 붙인다. */
.ops-main{display:flex;gap:var(--sp-4);align-items:flex-start}
.devPanel{flex:1;min-width:0;position:relative;padding:var(--sp-7) var(--sp-4) var(--sp-4);
          min-height:260px}
.devgrid{display:grid;gap:var(--sp-4);
         grid-template-columns:repeat(auto-fit,minmax(var(--devcard-min),1fr))}

.devcard{position:relative;background:var(--c-surface-2);border:1px solid var(--c-line);
  border-radius:var(--r-2);padding:var(--sp-4);display:flex;flex-direction:column;gap:var(--sp-3)}
.devcard.off{opacity:.55}
.devcard .stripe{position:absolute;left:10px;right:10px;top:0;height:2px;border-radius:2px;
  background:var(--c-accent)}
.devcard.off .stripe{background:var(--c-line-strong)}
.devcard .hd{display:flex;align-items:center;gap:var(--sp-3)}
.devcard .id{font:var(--p-fw-bold) var(--fs-base)/1.3 var(--ff-sans)}
.devcard .zone{font:var(--p-fw-bold) var(--fs-nano)/1.6 var(--ff-mono);color:var(--c-accent);
  background:var(--c-accent-bg);border-radius:8px;padding:1px 8px}
.devcard .model{font:var(--fs-nano)/1.6 var(--ff-mono);color:var(--c-text-3);
  border:1px solid var(--c-line);border-radius:8px;padding:1px 7px}
/* 상태 뱃지는 **아이디 바로 앞**(2026-08-03 사용자 지정) — 예전엔 margin-left:auto 로
   우측 끝이라 넓은 카드에서 "무엇이" 와 "어떤 상태인지" 가 멀어졌다.
   우측 끝은 이제 앞/뒤 선택(.camsel)이 차지한다. */
.devcard .st{font:var(--p-fw-bold) var(--fs-nano)/1.6 var(--ff-mono);
  padding:2px 9px;border-radius:9px;background:var(--c-ok-bg);color:var(--c-ok)}
.devcard.off .st{background:var(--c-crit-bg);color:var(--c-crit)}
/* ── ⑦ 이벤트 강조 (2026-08-03 사용자 지정) ────────────────────────────────
   어떤 일이 **어느 주차면에서** 일어났는지 눈으로 잡히게 카드를 잠깐 물들인다.
   종류별로 색이 다르다: 인식=서버색 / 입차=장치색 / 출차=정상 / 차단기=강조 / 정산=장치.
   테두리+외곽선만 건드려 레이아웃이 흔들리지 않는다(폭·높이 불변).
   prefers-reduced-motion 은 ui.css 가 이미 애니메이션을 죽인다 —
   그때도 `border-color` 는 남으므로 "무슨 일이 났다"는 사실 자체는 보인다. */
.devcard.ev-lpr,.devcard.ev-entry,.devcard.ev-exit,.devcard.ev-lock,.devcard.ev-pay{
  animation:ev-pulse 1.3s ease-out 2}
.devcard.ev-lpr{--ev:var(--c-server)}
.devcard.ev-entry{--ev:var(--c-device)}
.devcard.ev-exit{--ev:var(--c-ok)}
.devcard.ev-lock{--ev:var(--c-accent)}
.devcard.ev-pay{--ev:var(--c-device)}
@keyframes ev-pulse{
  0%  {box-shadow:0 0 0 0 color-mix(in srgb,var(--ev) 70%,transparent);
       border-color:var(--ev)}
  70% {box-shadow:0 0 0 9px color-mix(in srgb,var(--ev) 0%,transparent);
       border-color:var(--ev)}
  100%{box-shadow:0 0 0 0 transparent}
}
/* 인식 스트립·정산 행에도 같은 어휘를 쓴다(새로 들어온 항목) */
.ledger.ev-new,.prow.ev-new{animation:ev-row .9s ease-out 2}
@keyframes ev-row{0%{background:color-mix(in srgb,var(--c-device) 22%,transparent)}
                  100%{background:transparent}}

/* 장치 자율 복귀 경고 뱃지 (제조사 요청서 3번 — 실측 누적) */
.devcard .revert{font:var(--fs-nano)/1.6 var(--ff-mono);color:var(--c-crit);
  border:1px solid var(--c-crit);border-radius:8px;padding:1px 7px}

/* 탭 줄 (현황 / 영상) */
.ztabs{display:flex;gap:4px;align-items:center}
.ztab{padding:2px 10px;border-radius:4px;border:1px solid var(--c-line);
  font:var(--fs-micro)/1.6 var(--ff-mono);color:var(--c-text-3);cursor:pointer;user-select:none}
.ztab.on{background:var(--c-accent-bg);border-color:var(--c-accent-line);
  color:var(--c-accent);font-weight:var(--p-fw-bold)}

/* 잠금 베이 — 지면 + 차단판 + 차량 실루엣(정면) */
.lockbay{position:relative;height:104px;border-radius:6px;background:var(--c-video);
  overflow:hidden;display:flex;align-items:flex-end;justify-content:center}
.ground{position:absolute;left:0;right:0;bottom:14px;height:2px;background:var(--c-line-strong)}
/* ── 실물 형상 (2026-08-03 사용자 제공 제품 사진 기준) ──────────────────────
   SUNPARKING AI LPR FLAP BARRIER: 검은 베이스 플레이트(양끝 경고 빗금) 위에
   가운데 LPR 유닛이 앉고, **노란 아치형 프레임**이 서거나(UP=차단) 접힌다(DOWN=개방).
   이전에는 청록 사다리꼴이라 실물과 연결이 안 됐다.
   실물 색이므로 테마 토큰을 쓰지 않는다(.scar 와 같은 규칙 — 실제 사물의 색은
   라이트/다크로 바뀌지 않는다). 치수는 Qt 원본 비율 유지(96×47, 차량 150px 대비 §89). */
.lockbase{position:absolute;bottom:14px;left:50%;transform:translateX(-50%);
  width:112px;height:13px;z-index:2;border-radius:3px;box-sizing:border-box;
  background:#15181d;border:1px solid #2b3038;
  display:flex;align-items:center;justify-content:space-between;padding:0 3px}
.lockbase .haz{width:20px;height:7px;border-radius:2px;   /* 양끝 경고 빗금 */
  background:repeating-linear-gradient(115deg,#e0b400 0 4px,#b8362b 4px 8px)}
.lockbase .unit{width:34px;height:9px;border-radius:2px;   /* 가운데 LPR 유닛 */
  background:#0a0c10;border:1px solid #333a44}
/* 아치 프레임 — 테두리로 그려 가운데가 저절로 비어 있다(실물의 뚫린 프레임). */
.lockarm{position:absolute;bottom:26px;left:50%;transform:translateX(-50%);
  width:96px;height:7px;z-index:3;box-sizing:border-box;background:transparent;
  border:7px solid #f2c31a;border-bottom:0;border-radius:13px 13px 0 0;
  box-shadow:0 0 10px rgba(242,195,26,.30);
  transition:height .9s cubic-bezier(.3,1.3,.4,1),border-color .3s,box-shadow .3s}
.lockarm.up{height:47px}
.lockarm.down{height:7px;border-color:#454b54;box-shadow:none}   /* 접혀 누움 */
.lockarm.unk{height:21px;border-color:#454b54;border-style:dashed;box-shadow:none}
/* 윗면 표시등 3개 — 서 있을 때만 보인다 */
.lockarm .led{position:absolute;left:50%;top:-5px;transform:translateX(-50%);
  width:26px;height:3px;opacity:0;transition:opacity .5s;
  background:radial-gradient(circle 1.5px at 3px 50%,#6b4e00 99%,transparent),
             radial-gradient(circle 1.5px at 13px 50%,#6b4e00 99%,transparent),
             radial-gradient(circle 1.5px at 23px 50%,#6b4e00 99%,transparent)}
.lockarm.up .led{opacity:1}
.lockstate{position:absolute;left:8px;top:8px;z-index:3;
  font:var(--p-fw-bold) var(--fs-micro)/1 var(--ff-mono)}
.lockstate.up{color:var(--c-accent)}
.lockstate.down,.lockstate.unk{color:var(--c-text-3)}
.carchip{position:absolute;right:8px;top:8px;z-index:3;
  font:var(--p-fw-bold) var(--fs-micro)/1 var(--ff-mono);color:var(--c-text-3)}
.carchip.on{color:var(--c-device)}
.carchip.move{animation:carchip-blink 1.1s infinite}
@keyframes carchip-blink{0%,100%{opacity:1}50%{opacity:.3}}

/* 차량 정면 실루엣 — 좌표·색은 Qt 원본 그대로(150×64). 실물 색이라 테마 불변. */
.scar{position:absolute;left:50%;bottom:14px;width:150px;height:64px;
  transform:translateX(-50%);z-index:1;display:none}
.scar.show{display:block}
.scar .scabin{position:absolute;left:22px;right:22px;top:0;height:28px;
  border-radius:11px;background:#2f8896}                 /* 캐빈(지붕) */
.scar .swin{position:absolute;left:30px;right:30px;top:5px;height:19px;
  border-radius:7px;background:#10222e}                  /* 앞유리 */
.scar .sbody{position:absolute;left:0;right:0;top:24px;height:36px;
  border-radius:11px;background:#3ba8b8}                 /* 본체(범퍼부) */
.scar .sl{position:absolute;top:33px;width:24px;height:11px;border-radius:5px;
  background:#d9ecb2}                                    /* 헤드라이트 */
.scar .sl.l{left:12px} .scar .sl.r{right:12px}
/* 진입·진출은 좌우 이동이 아니라 깜빡임 — 정면 뷰에 맞는 표현(Qt 동일) */
.scar.move-in,.scar.move-out{animation:car-blink 1.4s ease-in-out infinite}
@keyframes car-blink{50%{opacity:.3}}
/* 번호판 배지 — 베이스 플레이트 바로 위. 실물 번호판 색이라 테마 불변.
   (베이스가 14~27px 를 차지하므로 그 위로 띄운다 — 2026-08-03 형상 변경) */
.plateBadge{position:absolute;left:50%;bottom:31px;transform:translateX(-50%);z-index:6;
  background:#eef1e2;color:#111;font:800 11px/1 var(--ff-mono);letter-spacing:.5px;
  padding:3px 7px;border-radius:3px;border:1px solid #8f937c;
  box-shadow:0 2px 6px rgba(0,0,0,.45)}

/* 텔레메트리 — 2열 격자 */
.tele{display:grid;grid-template-columns:1fr 1fr;gap:3px var(--sp-4);
  font:var(--fs-micro)/1.5 var(--ff-mono)}
.tele .k{color:var(--c-text-3)}
.tele .v{color:var(--c-text-2)}
.tele .v.bad{color:var(--c-crit);font-weight:var(--p-fw-bold)}
.tele .v.elap{color:var(--c-device);font-weight:var(--p-fw-bold)}   /* 주차 경과 */
.tele .row{display:flex;justify-content:space-between;gap:6px}
.tele .row.full{grid-column:1 / -1}

/* 침수 수위 */
.waterrow{display:flex;align-items:center;gap:var(--sp-3);
  font:var(--fs-nano)/1.6 var(--ff-mono);color:var(--c-text-3)}
.wbar{flex:1;height:5px;border-radius:3px;background:var(--c-well);
  border:1px solid var(--c-line);overflow:hidden}
.wbar i{display:block;height:100%;background:var(--c-water)}
.waterrow.alarm{color:var(--c-crit);font-weight:var(--p-fw-bold)}
.waterrow.alarm .wbar i{background:var(--c-crit)}

/* 카메라 — 대형 단일 영상(16:9) + 앞/뒤 전환.
   높이를 자르면 박스가 넓어져 비율이 깨지므로, 높이 상한을 폭 상한으로 환산한다. */
.cams{display:flex;flex-direction:column;gap:6px}
.cambox{position:relative;aspect-ratio:16/9;width:100%;
  max-width:calc(min(52vh,560px) * 16 / 9);margin-inline:auto;
  border-radius:var(--r-1);overflow:hidden;background:var(--c-video);
  border:1px solid var(--c-line)}
.cambox video{width:100%;height:100%;object-fit:contain;display:block;background:#000}
/* 영상 위 "앞쪽/뒤쪽" 라벨은 제거했다(2026-08-03 사용자 지정) — 어느 렌즈인지는
   머리줄의 앞/뒤 선택이 이미 눌린 상태로 말해 준다. 라벨은 화면만 가렸다. */
.camsel{margin-left:auto;display:flex;gap:4px;flex:none}   /* 카드 머리줄 우측 끝 */
.cammsg{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  color:var(--c-text-3);font:var(--fs-micro)/1 var(--ff-mono);pointer-events:none}
/* 서버 ANPR 탐지 박스 오버레이 (anpr_box 델타, 4초) */
.anprbox{position:absolute;border:3px solid var(--c-server);border-radius:3px;z-index:3;
  pointer-events:none}
.anprbox i{position:absolute;bottom:100%;left:0;margin-bottom:3px;font-style:normal;
  font:var(--p-fw-bold) var(--fs-nano)/1.6 var(--ff-mono);white-space:nowrap;
  color:var(--c-server);background:var(--c-server-bg);border:1px solid var(--c-server);
  border-radius:3px;padding:1px 5px}

/* 2단 배치 (§87) — 넓은 화면: 현황(좌) + 영상(우) 동시. 좁은 화면: 탭 전환. */
.panes{display:flex;flex-direction:column;gap:var(--sp-3);min-width:0}
.panes .pane-cam{display:none;min-width:0}
.panes.camon .pane-cam{display:block}
.panes.camon .pane-st{display:none}

/* ─────────────────────────────────────────────────────────────────────────
   ⑤  우측 패널 — 주차정산 원장 · 서버 판정 · 음성 · 요금 · OTA
   ───────────────────────────────────────────────────────────────────────── */
.rcol{width:var(--rcol-w);flex:none;display:flex;flex-direction:column;gap:var(--sp-4)}
/* 패널 제목 바 (§92) — 3종의 제목 위치·여백을 하나로 통일 */
.rpanel{position:relative;padding:26px var(--sp-3) var(--sp-3);min-height:90px}
.ptitle{position:absolute;left:0;right:0;top:0;padding:var(--sp-3) var(--sp-4) 4px;
  font:var(--p-fw-bold) var(--fs-micro)/1.4 var(--ff-mono);color:var(--c-server);
  display:flex;align-items:center;gap:6px}
.ptitle::before{content:"";width:3px;height:11px;border-radius:2px;
  background:var(--c-server);opacity:.85}
/* 패널 제목 우측 합계 (2026-08-03 사용자 지정) — 주차정산 제목줄 오른쪽 끝에
   "납입금액/정산합계 <값> KRW". 제목이 absolute 라 같은 줄에서 우측 정렬된다. */
.psum{float:right;display:inline-flex;align-items:baseline;gap:5px;
  font:var(--p-fw-medium) var(--fs-nano)/1.6 var(--ff-mono);color:var(--c-text-3);
  text-transform:none;letter-spacing:0}
.psum b{font:var(--p-fw-bold) var(--fs-base)/1.2 var(--ff-mono);color:var(--c-device);
  font-variant-numeric:tabular-nums}
.psum i{font-style:normal;color:var(--c-text-3)}
.plist,.olist{display:flex;flex-direction:column;gap:var(--sp-2);
  max-height:clamp(180px,24vh,360px);overflow-y:auto}
.olist{max-height:clamp(200px,26vh,420px)}

/* ── 주차정산 원장 <deck-ledger> — 장치·서버 2행 대조 (§72) ──
   열: 태그 | 입차(시각·차번) | 출차(시각·차번) | (주차시간/요금)
   말줄임 없이 전부 보여야 한다(현장 판독) — 시간·요금은 한 셀에 상하 배치. */
.ledger{position:relative;background:var(--c-surface-2);border:1px solid var(--c-line);
  border-radius:var(--r-1);padding:var(--sp-2) var(--sp-3);
  font:var(--fs-micro)/1.5 var(--ff-mono);display:block}
.ledger .line{display:grid;grid-template-columns:28px 1.15fr 1fr 80px;gap:5px;
  align-items:center;padding:3px 5px;border-radius:5px;
  background:var(--tone-bg);border-left:3px solid var(--tone)}
.ledger .line + .line,.ledger .none,.ledger .foot{margin-top:2px}
.ledger .line[data-src="device"]{--tone:var(--c-device);--tone-bg:var(--c-device-bg)}
.ledger .line[data-src="server"]{--tone:var(--c-server);--tone-bg:var(--c-server-bg)}
.ledger .line > .who{font-weight:var(--p-fw-bold);font-size:var(--fs-nano);
  color:var(--tone);text-align:center}
.ledger .line > .cell{display:flex;flex-direction:column;line-height:1.3;min-width:0}
/* 날짜+시각은 길다("입차 2026-08-01 13:50:40"). 자르면 현장에서 못 읽으므로
   nowrap 대신 두 줄로 접는다 — 정보를 줄이지 않고 폭에 맞춘다(§72 사용자 지정). */
.ledger .line > .cell > small{color:var(--c-text-3);text-align:center;
  font-size:var(--fs-nano);line-height:1.25}
.ledger .line > .cell > b{font-size:var(--fs-sm);color:var(--c-text);text-align:center}
.ledger .line > .cell > b[data-mismatch]{color:var(--c-crit)}
.ledger .line > .amt{display:flex;flex-direction:column;line-height:1.3;text-align:right}
.ledger .line > .amt > small{color:var(--c-text-2);font-size:var(--fs-nano)}
.ledger .line > .amt > b{font-size:var(--fs-sm);color:var(--tone)}
.ledger .line > .amt > b[data-mismatch]{color:var(--c-crit)}
.ledger .none{color:var(--c-text-3);padding:3px 6px;background:var(--c-server-bg);
  border-left:3px solid var(--c-server);border-radius:5px}
.ledger .foot{display:flex;gap:var(--sp-3);flex-wrap:wrap}

/* ── 서버 판정 스트림 — 장치 주문과 별개인 서버 세션 이력 ── */
.prow{position:relative;background:var(--c-surface-2);border:1px solid var(--c-line);
  border-radius:var(--r-1);padding:var(--sp-2) var(--sp-3) var(--sp-2) var(--sp-4);
  font:var(--fs-micro)/1.5 var(--ff-mono);--tone:var(--c-server)}
.prow::before{content:"";position:absolute;left:0;top:6px;bottom:6px;width:3px;
  border-radius:2px;background:var(--tone)}
.prow.denied{--tone:var(--c-crit)}
.prow.exit{--tone:var(--c-ok)}
.prow .l1{display:flex;gap:var(--sp-3);align-items:baseline}
.prow .ev{font-weight:var(--p-fw-bold);color:var(--tone)}
.prow .p{font-size:var(--fs-sm);font-weight:var(--p-fw-bold);color:var(--c-text)}
.prow .t{color:var(--c-text-3)}
.prow .who{color:var(--c-text-2)}
/* 주차면 (2026-08-08 §175) - 판정 행의 기준 신원. 현장·중앙 두 화면이 같은 클래스를
   쓰므로 여기 한 곳에서 정한다. 장치 뱃지(.devcard .zone)와 같은 색을 쓴다 - 두 화면
   에서 같은 자리가 같은 색으로 보여야 눈으로 대조가 된다. */
.prow .where{font:var(--p-fw-bold) var(--fs-nano)/1.6 var(--ff-mono);color:var(--c-accent)}
.prow .auto{margin-left:auto;font-size:var(--fs-nano);color:var(--c-device);
  border:1px solid var(--c-device);border-radius:4px;padding:0 4px}
.prow .l2{display:flex;gap:var(--sp-4);margin-top:2px;color:var(--c-text-2);flex-wrap:wrap}
.prow .l2 .srvc{color:var(--c-server)}
.prow .l2 .devc{color:var(--c-device)}
.prow .l2 .bad{color:var(--c-crit);font-weight:var(--p-fw-bold)}
.prow .l2 .ok{color:var(--c-ok)}
.paid{color:var(--c-ok)} .unpaid{color:var(--c-crit)}

/* ── 음성 통화 ── */
.callbox{background:var(--c-device-bg);border:1px solid var(--c-device);border-radius:8px;
  padding:10px;display:flex;flex-direction:column;gap:6px;animation:ring-pulse 1.2s infinite}
@keyframes ring-pulse{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--c-device) 35%,transparent)}
                      50%{box-shadow:0 0 0 6px transparent}}
.callbox.live{animation:none;background:var(--c-ok-bg);border-color:var(--c-ok)}
.callbox .who{font:var(--p-fw-bold) var(--fs-sm)/1.4 var(--ff-mono);color:var(--c-device)}
.callbox.live .who{color:var(--c-ok)}
.callbox .why{font:var(--fs-micro)/1.5 var(--ff-mono);color:var(--c-text-2)}
.vidle{font:var(--fs-micro)/1 var(--ff-mono);color:var(--c-text-3);text-align:center;
  padding:var(--sp-3) 0}

/* ── 펌웨어 업데이트 ── */
.otarow{background:var(--c-surface-2);border:1px solid var(--c-line);border-radius:7px;
  padding:7px 10px;font:var(--fs-micro)/1.5 var(--ff-mono);
  display:flex;flex-direction:column;gap:5px}
.otarow .l1{display:flex;gap:var(--sp-3)}
.otarow .ph{color:var(--c-ota);font-weight:var(--p-fw-bold)}
.obar{height:5px;border-radius:3px;background:var(--c-well);border:1px solid var(--c-line);
  overflow:hidden}
.obar i{display:block;height:100%;background:var(--c-ota);transition:width .8s}

/* 우측 패널 안의 작은 폼 — DECK .field 를 좁은 칸에 맞춰 조인다 */
.frm{display:flex;flex-direction:column;gap:var(--sp-3)}
.frm .row2{display:flex;gap:6px}
.frm .fld{flex:1;min-width:0}
.g4note{font:var(--fs-nano)/1.5 var(--ff-mono);color:var(--c-text-3);text-align:center}

/* ─────────────────────────────────────────────────────────────────────────
   ⑥  알림 이력 · 서버 상태 — 관리 화면과 **같은 오버레이 규약**(2026-08-03)
       예전 우상단 팝오버(.pop)는 헤더 버튼에 매달린 좌표라, 진입점이 좌측 네비로
       옮겨간 뒤(§106) 누르는 자리와 뜨는 자리가 어긋났다. 이제 셋 다 .admOv 다.
   ───────────────────────────────────────────────────────────────────────── */
/* ⚠ 행 클래스가 예전엔 `.sr` 이었다 — DECK 공용 유틸 `.sr`(스크린리더 전용:
   position:absolute·1px·clip-path)과 **이름이 겹쳐** 행이 통째로 화면 밖으로 잘렸다.
   글자는 DOM 에 멀쩡히 있어서 "행 개수"로 검증하면 정상으로 보인다(2026-08-03 실측:
   행 7개·텍스트 정상인데 크기가 4x6px). 프로젝트 층 이름에는 접두를 붙인다(§11-20). */
.sysd .sysrow{display:flex;justify-content:space-between;padding:3px 2px;
  font:var(--fs-micro)/1.5 var(--ff-mono);color:var(--c-text)}
.sysd .sysrow .k{color:var(--c-text-3)}
.sysd .sysrow.bad{color:var(--c-crit);font-weight:var(--p-fw-bold)}
.nlog .ni{display:flex;gap:var(--sp-3);padding:5px 6px;border-radius:6px;margin-bottom:4px;
  font:var(--fs-sm)/1.5 var(--ff-mono);color:var(--c-text);
  border-left:4px solid var(--c-info);background:var(--c-info-bg)}
.nlog .ni.err{border-left-color:var(--c-crit);background:var(--c-crit-bg);color:var(--c-crit)}
.nlog .ni .t{color:var(--c-text-3);flex:none}
.nlog .empty2{color:var(--c-text-3);font:var(--fs-micro)/1 var(--ff-mono);padding:6px}
/* 서버 상태 칩 (§74) — 정상 dim / 경고 warn / 위험 crit */
#sysChip{display:none}
#sysChip.warn{border-color:var(--c-warn);color:var(--c-warn)}
#sysChip.crit{border-color:var(--c-crit);color:var(--c-crit)}
#bellBtn .n{color:var(--c-device);font-weight:var(--p-fw-bold)}

/* ─────────────────────────────────────────────────────────────────────────
   ⑦  관리 화면 (C-9) — 대장 · 정책 · 차량 · 조회 · 서버
   ───────────────────────────────────────────────────────────────────────── */
.admOv{position:fixed;inset:0;background:var(--c-overlay);z-index:50;
  display:none;align-items:center;justify-content:center;padding:var(--sp-4)}
.admOv.on{display:flex}
.admBox{background:var(--c-surface);border:1px solid var(--c-line-strong);border-radius:10px;
  width:min(1080px,94vw);height:min(760px,92vh);display:flex;flex-direction:column;
  overflow:hidden;box-shadow:var(--c-shadow)}
/* 작은 상자 — 알림·서버 상태처럼 내용이 짧은 오버레이. 높이는 내용만큼만.
   ⚠ `.admBody{flex:1}` 은 flex-basis:0% 이라, 상자 높이가 auto 면 본문이 **0에서
     자라지 않아** 제목줄만 남는다(2026-08-03 실측: 상자 53px·본문 12px·목록 0px —
     "서버 상태를 눌러도 아무것도 안 나온다"의 정체였다. 행은 DOM 에 다 있었기에
     개수만 세면 정상으로 보인다 — 높이를 재야 보인다).
     내용 크기로 자라도록 flex-basis 를 auto 로 돌려놓는다. */
.admBox.sm{width:min(560px,94vw);height:auto;max-height:min(680px,88vh)}
.admBox.sm > .admBody{flex:0 1 auto;min-height:0}
.admHd{display:flex;align-items:center;gap:var(--sp-3);padding:var(--sp-4) var(--sp-5);
  border-bottom:1px solid var(--c-line)}
.admHd h3{color:var(--c-accent);display:flex;align-items:center;gap:6px}
.admHd .sp{flex:1}
.admTabs{display:flex;gap:6px;padding:var(--sp-3) var(--sp-5) 0;flex-wrap:wrap}
.admTab{padding:5px 14px;border-radius:6px 6px 0 0;border:1px solid var(--c-line);
  border-bottom:none;background:var(--c-surface-2);color:var(--c-text-3);cursor:pointer;
  font-size:var(--fs-sm)}
.admTab.on{background:var(--c-accent-bg);color:var(--c-accent);font-weight:var(--p-fw-bold)}
.admBody{flex:1;overflow:auto;padding:var(--sp-4) var(--sp-5)}
.admBody table{width:100%;border-collapse:collapse;font-size:var(--fs-sm)}
.admBody th,.admBody td{border:1px solid var(--c-line);padding:5px 7px;text-align:left;
  white-space:nowrap}
.admBody th{background:var(--c-surface-2);color:var(--c-text-3);position:sticky;top:0;z-index:1}
.admBody td input,.admBody td select{width:100%;background:var(--c-surface-2);
  border:1px solid var(--c-line);color:var(--c-text);padding:3px 5px;border-radius:4px;
  font-size:var(--fs-micro)}
.admRow{display:flex;gap:var(--sp-3);align-items:flex-end;flex-wrap:wrap;margin-bottom:var(--sp-4)}
.admRow .fld{display:flex;flex-direction:column;gap:3px}
.admRow label{font-size:var(--fs-micro);color:var(--c-text-3)}
.admRow input,.admRow select{background:var(--c-surface-2);border:1px solid var(--c-line);
  color:var(--c-text);padding:4px 7px;border-radius:5px;font-size:var(--fs-sm)}
.admMuted{color:var(--c-text-3);font-size:var(--fs-sm)}
.admTot{margin-top:var(--sp-3);font-size:var(--fs-sm);color:var(--c-accent)}
/* 서버 관리 탭 — 되돌릴 수 없는 조작이라 설명을 크게 두고 버튼을 작게 둔다 */
.admOps{display:flex;flex-direction:column;gap:var(--sp-5);max-width:640px}
.admOps .op{display:flex;gap:var(--sp-4);align-items:flex-start;
  background:var(--c-surface-2);border:1px solid var(--c-line);border-radius:var(--r-2);
  padding:var(--sp-4)}
.admOps .op .tx{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}
.admOps .op .tx b{font-size:var(--fs-base)}
.admOps .op .tx span{font-size:var(--fs-sm);color:var(--c-text-2)}

/* ─────────────────────────────────────────────────────────────────────────
   ⑧  D3 자체 OCR 시험 화면 (§83) — 검출·OCR **원시** 결과(투표 이전)
   운영 화면(LPR 인식)과 수치가 다를 수 있다 — 시험 전용.
   ───────────────────────────────────────────────────────────────────────── */
.d3ov{display:none;position:fixed;inset:0;z-index:60;background:var(--c-canvas);
  padding:var(--pad-page);overflow-y:auto}
.d3ov.on{display:flex;flex-direction:column;gap:var(--sp-4)}
.d3hd{display:flex;align-items:center;gap:var(--sp-4);flex-wrap:wrap}
.d3hd h3{color:var(--c-ota);display:flex;align-items:center;gap:6px}
.d3hd .stat{font:var(--fs-sm)/1.5 var(--ff-mono);color:var(--c-text-2)}
.d3hd .x{margin-left:auto;cursor:pointer;color:var(--c-text-3);font-size:var(--fs-xl);
  padding:0 var(--sp-3);background:none;border:0}
.d3body{display:flex;gap:var(--sp-4);align-items:flex-start;flex-wrap:wrap}
.d3vid{position:relative;flex:1 1 520px;min-width:0;aspect-ratio:16/9;
  max-width:calc(min(60vh,720px) * 16 / 9);background:var(--c-video);
  border:1px solid var(--c-line-strong);border-radius:var(--r-2);overflow:hidden}
.d3vid video{width:100%;height:100%;object-fit:contain;display:block}
.d3vid .msg{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  color:var(--c-text-3);font:var(--fs-sm)/1 var(--ff-mono)}
.d3sel{position:absolute;right:var(--sp-3);top:var(--sp-3);display:flex;gap:var(--sp-2);z-index:3}
/* 원시 검출 박스 — 색으로 판정 사유를 구분(Qt판과 동일 규칙) */
.d3box{position:absolute;border-radius:3px;z-index:2;pointer-events:none;
  border:3px solid var(--tone,var(--c-ota))}
.d3box.edge{--tone:var(--c-warn)}                    /* 경계 잘림 — 투표 제외(§59) */
.d3box.fail{--tone:var(--c-crit)}                    /* 문자 인식 실패 */
.d3box.shape{--tone:var(--c-text-3);border-width:2px;opacity:.55}   /* 형상외(§60) */
.d3box i{position:absolute;left:0;bottom:calc(100% + 4px);white-space:nowrap;font-style:normal;
  font:var(--p-fw-bold) var(--fs-sm)/1.4 var(--ff-mono);color:var(--tone,var(--c-text));
  background:var(--c-ota-bg);border:1px solid var(--tone,var(--c-ota));
  border-radius:4px;padding:2px 7px}
.d3box:not(.edge):not(.fail):not(.shape) i{color:var(--c-text)}
.d3cols{flex:1 1 320px;min-width:0;display:flex;flex-direction:column;gap:var(--sp-4)}
.d3list{background:var(--c-surface);border:1px solid var(--c-line);border-radius:var(--r-3);
  padding:var(--sp-3);max-height:38vh;overflow-y:auto}
.d3list h4{margin-bottom:var(--sp-3);color:var(--c-server);
  font:var(--p-fw-bold) var(--fs-micro)/1.4 var(--ff-mono)}
.d3row{display:flex;gap:var(--sp-3);padding:3px 4px;margin-bottom:3px;border-radius:var(--r-1);
  font:var(--fs-micro)/1.5 var(--ff-mono);background:var(--c-surface-2);
  border-left:3px solid var(--tone,var(--c-ota))}
.d3row.edge{--tone:var(--c-warn)}
.d3row.fail{--tone:var(--c-crit);color:var(--c-crit)}
.d3row.shape{--tone:var(--c-text-3);color:var(--c-text-3)}
.d3row .t{color:var(--c-text-3);flex:none}
.d3row .p{font-weight:var(--p-fw-bold);color:var(--c-text)}
.d3row.fail .p,.d3row.shape .p{color:inherit}
.d3row .m{margin-left:auto;color:var(--c-text-3);flex:none}

/* ─────────────────────────────────────────────────────────────────────────
   ⑨  반응형 — 토큰이 폭을 따라가므로 여기선 **구조 변경**만 다룬다
   1280px↑ : 장치 카드 2단(현황 좌 / 영상 우)
   ~1280px : 장치 카드 1열 (카드 폭 확보 우선)
   ~980px  : 본문 2단 → 세로 1단
   ~640px  : 모바일 세로 — 헤더 축약, 정산 행 2줄 접기, 터치 타깃 확대
   ───────────────────────────────────────────────────────────────────────── */
@media (min-width:1280px){
  /* 조작(잠금 버튼)이 왼쪽, 관찰(영상)이 오른쪽 — 2026-07-31 사용자 지정 */
  .panes{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.45fr);
         gap:var(--sp-4);align-items:start}
  .panes .pane-st{order:1}
  .panes .pane-cam{order:2}
  .panes .pane-cam,.panes.camon .pane-cam{display:block}
  .panes .pane-st,.panes.camon .pane-st{display:block}
  /* 2단에선 탭 줄 자체를 없앤다(§93 A안) — 둘 다 보이므로 전환이 할 일이 없다.
     앞/뒤 선택은 폭과 무관하게 카드 머리줄에 있다(2026-08-03). */
  .devcard .ztabs{display:none}
  .pane-cam .cambox{max-width:none}
  /* 우측 열은 화면 폭에 따라 좁아지므로 **열 자체의 폭**으로 판단한다 —
     뷰포트 기준이면 좁은 열에서 값이 세로로 쪼개진다(1400px 실측). */
  .pane-st{container-type:inline-size}
  .pane-st .tele{grid-template-columns:1fr;gap:3px var(--sp-4)}
  @container (min-width:420px){ .pane-st .tele{grid-template-columns:1fr 1fr} }
  .pane-st .tele .row{justify-content:flex-start;gap:var(--sp-3)}
  .pane-st .tele .k{flex:0 0 62px}
  .pane-st .tele .v{color:var(--c-text)}
  .pane-st .lockbay{height:clamp(120px,16vh,180px)}
  .pane-st .waterrow,.pane-st .btns{margin-top:var(--sp-3)}
}
@media (max-width:1280px){
  .devgrid{grid-template-columns:1fr}
}
@media (max-width:980px){
  .ops-main{flex-direction:column;align-items:stretch}
  .rcol{width:100%}
  .devgrid{grid-template-columns:repeat(auto-fit,minmax(var(--devcard-min),1fr))}
  .appbar{gap:var(--sp-3)}
  #clock{order:-1}
}
@media (max-width:640px){
  :root{--fs-num:15px}
  .wrap{gap:var(--sp-3)}
  .legend{display:none}                 /* 색 범례 — 좁은 화면에선 생략 */
  .kpis{order:2;flex:1 1 100%}
  .kpis > .kpi{min-width:0;flex:1 1 20%;padding:var(--sp-2)}
  .recog-list{gap:var(--sp-2)}
  /* 정산 행: 태그·입차·출차 / 시간·요금 2줄로 접어 글자 잘림 방지 */
  .ledger .line{grid-template-columns:26px 1fr 1fr;row-gap:2px}
  .ledger .line > .amt{grid-column:2 / -1;flex-direction:row;justify-content:flex-end;
    gap:var(--sp-3);align-items:baseline}
}
@media (prefers-reduced-motion:reduce){
  .lockarm{transition:none}
  .callbox,.carchip.move,.scar.move-in,.scar.move-out{animation:none}
}
/* 주차면 표기 (2026-08-08 §174) — 스트림 행이 어느 주차면 것인지.
   ⚠ 장치가 1대일 때는 없어도 됐다. 2대부터는 이게 없으면 어느 주차면 인식·정산인지
   알 수 없다 — 정산·증적에 직결되는 표기다. */
.plate-card .meta .where,
ksp-ledger .where {
  font-weight: 600;
  color: var(--fg);
  background: var(--bg-2, rgba(127,127,127,.14));
  border-radius: 4px;
  padding: 0 .35em;
}
ksp-ledger .where { display: block; margin-bottom: .25rem; font-size: .85em; }
