/* 관제 UI — 셸·사이드바·맵·로스터·HUD·대시보드
 * index.html <style> 에서 분리(2026-09-15) — 원래 120~456행.
 * ⚠️ 이어붙인 순서가 캐스케이드다. <link> 순서를 바꾸지 마라(tokens → fleet → cockpit → panels).
 *    scripts/test_css_split.js 가 네 파일을 이어붙여 분리 시점 원본과 바이트 동일한지 검사한다. */

  /* 알람 벨 + 패널 */
  #alarmwrap{ position:relative; }
  #alarmbtn, #iotbtn{ background:var(--card); border:1px solid var(--line); color:var(--ink); border-radius:4px; padding:2px 7px; font-size:13px; line-height:1; cursor:pointer; }
  #alarmbtn:hover, #iotbtn:hover{ border-color:var(--accent); }
  #alarmbtn #alarmcount, #iotbtn #iotcount{ font:10px/1 ui-monospace,monospace; margin-left:3px; color:var(--dim); }
  #alarmbtn.has, #iotbtn.has{ animation:almpulse 1.1s ease-in-out infinite; }
  /* 설비(IoT) 패널 — 사이트 공통(맵 무관): 헤더에서 어느 화면에서든 동일하게 열림 */
  #iotwrap{ position:relative; }
  #iotpanel{ position:absolute; top:28px; right:0; z-index:30; width:340px; max-height:64vh; overflow:auto;
    background:var(--panel); border:1px solid var(--line2); border-radius:8px; box-shadow:0 8px 24px rgba(0,0,0,.45); padding:6px; }
  #iotpanel .ap-head{ display:flex; align-items:center; gap:8px; padding:4px 6px; border-bottom:1px solid var(--line); }
  #iotpanel .iot-sub{ font-size:10px; color:var(--dim); }
  .iot-cat{ font-size:10px; color:var(--dim); letter-spacing:.5px; margin:8px 4px 2px; }
  .iot-dev{ background:var(--card2); border:1px solid var(--line); border-radius:6px; padding:6px 8px; margin:4px 2px; }
  .iot-dev.alarm{ border-color:var(--err); }
  .iot-head{ display:flex; align-items:center; gap:6px; }
  .iot-dot{ width:7px; height:7px; border-radius:50%; flex:0 0 auto; }
  .iot-name{ font-weight:600; font-size:12px; cursor:pointer; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .iot-name:hover{ color:var(--accent); }
  .iot-maps{ font-size:10px; color:var(--dim); flex:0 0 auto; }
  .iot-noplace{ margin-left:auto; flex:0 0 auto; font-size:10px; color:var(--dim); }   /* 배치는 맵 에디터 소관 */
  .iot-place{ margin-left:auto; flex:0 0 auto; background:var(--card); border:1px solid var(--line); color:var(--ink2);
    border-radius:4px; font-size:10px; padding:1px 6px; cursor:pointer; }
  .iot-place:hover{ border-color:var(--accent); color:var(--accent); }
  .iot-io{ font-size:11px; color:var(--ink2); margin:3px 0 5px 13px; }
  .iot-acts{ display:flex; flex-wrap:wrap; gap:4px; }
  .iot-acts button{ background:var(--card); border:1px solid var(--line); color:var(--ink); border-radius:4px; font-size:11px; padding:2px 7px; cursor:pointer; }
  .iot-acts button:hover{ border-color:var(--accent); color:var(--accent); }
  .iot-acts button.stop{ color:var(--err); }
  .iot-empty{ padding:14px 8px; color:var(--dim); font-size:12px; text-align:center; }
  #alarmbtn.has #alarmcount{ color:var(--on-accent); background:var(--sev,var(--err)); border-radius:8px; padding:1px 5px; }
  @keyframes almpulse{ 0%,100%{ box-shadow:0 0 0 0 transparent; } 50%{ box-shadow:0 0 0 2px var(--sev,var(--err)); } }
  #alarmpanel{ position:absolute; top:28px; right:0; z-index:30; width:320px; max-height:60vh; overflow:auto;
    background:var(--panel); border:1px solid var(--line); border-radius:8px; box-shadow:0 8px 24px color-mix(in srgb,#000 45%,transparent); padding:8px; }
  .ap-head{ display:flex; align-items:center; gap:10px; margin-bottom:6px; font-size:12px; color:var(--ink); }
  .ap-head b{ flex:1; }
  .ap-tog{ display:flex; align-items:center; gap:3px; font-size:11px; color:var(--ink2); cursor:pointer; }
  .alm{ display:flex; gap:8px; align-items:flex-start; padding:6px 7px; border:1px solid var(--line); border-left-width:3px; border-radius:5px; margin-bottom:5px; background:var(--card); cursor:pointer; }
  .alm:hover{ border-color:var(--accent); }
  .alm .ai{ font-size:14px; }
  .alm .ab{ flex:1; min-width:0; }
  .alm .at{ font-size:12px; color:var(--ink); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  .alm .as{ font-size:10px; color:var(--dim); margin-top:1px; }
  .alm-none{ color:var(--dim); font-size:12px; padding:8px; text-align:center; }
  /* 관제 밖 알람 — 보이되 조용하다(소리·TTS 없음). 흐리게 두되 숨기지 않는다: 아무도 안 보는
     감지가 사라지면 그게 더 나쁜 실패다. */
  .alm.alm-far{ opacity:.62; }
  .alm-sep{ color:var(--dim); font-size:11px; margin:8px 2px 5px; padding-top:6px; border-top:1px dashed var(--line); }
  /* 좌측 사이드바 네비 */
  #body{ flex:1; display:flex; min-height:0; }
  #sidebar{ width:172px; flex:none; border-right:1px solid var(--line); background:var(--panel);
    padding:8px 6px; display:flex; flex-direction:column; gap:3px; overflow:hidden; transition:width .15s; }
  #app.nav-collapsed #sidebar{ width:46px; }
  .navitem{ display:flex; align-items:center; gap:10px; padding:8px 9px; border-radius:6px; color:var(--ink2);
    text-decoration:none; cursor:pointer; white-space:nowrap; font-size:13px; }
  .navitem:hover{ background:var(--card); color:var(--ink); }
  .navitem.active{ background:color-mix(in srgb, var(--accent) 16%, transparent); color:var(--accent); }
  /* 이모지는 글꼴마다 폭이 다르다 — 🗺 는 16px 에서 22px 를 먹어 18px 상자를 넘쳤다(실측 2026-09-16,
     모든 해상도에서 유일하게 넘치던 요소). 상자를 넓히는 대신 **폰트 크기로 맞춘다** — 상자를 넓히면
     레일 폭이 따라 늘고, 이모지마다 폭이 달라 또 어긋난다. line-height 고정으로 세로 정렬도 잠근다. */
  .navitem .ni-ic{ font-size:15px; line-height:18px; width:22px; text-align:center; flex:none;
    overflow:hidden; }
  #app.nav-collapsed .ni-tx{ display:none; }
  #content{ flex:1; display:flex; flex-direction:column; min-height:0; }
  /* 서버 설정 패널 */
  #configview{ flex:1; overflow:auto; padding:18px; }
  .cfgwrap{ max-width:660px; }
  .cfgwrap h2{ font-size:16px; color:var(--accent); margin:0 0 14px; }
  .cfg{ border:1px solid var(--line); border-radius:8px; padding:12px 14px; margin-bottom:12px; background:var(--card); }
  .cfg h3{ font-size:11px; color:var(--dim); text-transform:uppercase; letter-spacing:.5px; margin:0 0 10px; }
  .cf{ display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:8px; font-size:13px; color:var(--ink2); }
  .cf:has(input[type="checkbox"]){ justify-content:flex-start; }   /* 체크박스 행은 "라벨 왼쪽·입력 오른쪽" 배치가 아니다 — space-between 이 체크박스와 글자를 300px 떨어뜨렸다 */
  .cf input{ width:300px; background:var(--card2); border:1px solid var(--line); color:var(--ink); border-radius:4px; padding:5px 8px; }
  .btn{ background:var(--card); border:1px solid var(--line); color:var(--ink); border-radius:4px; padding:5px 12px; cursor:pointer; font:inherit; }
  .btn:hover{ border-color:var(--accent); }
  .btn.pri{ background:var(--accent); color:var(--on-accent); border:none; font-weight:600; }
  .cfg-status{ font-size:12px; color:var(--dim); margin-top:6px; }
  .cfg-pre{ margin:0; background:var(--card2); border:1px solid var(--line); border-radius:5px; padding:8px;
    font:11px/1.4 ui-monospace,monospace; color:var(--ink2); max-height:170px; overflow:auto; white-space:pre-wrap; }
  /* 상태 요약 칩 */
  /* KPI 스트립 — "너무 넓다"(사용자 지시 2026-09-15). 폭을 먹던 건 상태 **이름**이다.
     넓은 화면에선 그대로 두고, 좁아지면 라벨만 접어 점+숫자를 남긴다(title 에 전체 이름).
     헤더가 더는 줄바꿈하지 않으므로 여기서 물러나지 않으면 오른쪽 컨트롤이 잘린다. */
  #summarybar{ display:flex; gap:5px; align-items:center; flex:0 1 auto; min-width:0; overflow:hidden; }
  /* 좁아질수록 KPI 가 **먼저** 자리를 가져간다 — 관제에서 "지금 몇 대가 어떤 상태냐" 가
     연결 주소나 버전보다 급하다. 넘치면 가로 스크롤(잘라서 숨기지 않는다). */
  @media (max-width:760px){ #summarybar{ flex:1 1 auto; overflow-x:auto; scrollbar-width:none; }
    #summarybar::-webkit-scrollbar{ display:none; } }
  .summary-chip{ display:flex; align-items:center; gap:4px; padding:2px 7px; border:1px solid var(--line);
    border-radius:11px; background:var(--card); font-size:11px; color:var(--ink); white-space:nowrap; cursor:pointer; }
  .summary-chip .n{ font-variant-numeric:tabular-nums; font-weight:600; }   /* 숫자 폭 고정 — 값이 바뀌어도 칩이 안 흔들린다 */
  @media (max-width:1700px){ .summary-chip .lb{ display:none; } }           /* 점 + 숫자만 */
  @media (max-width:860px){  .summary-chip.zero{ display:none; } }          /* 0 인 상태는 접는다 */
  .summary-chip .dot{ width:8px; height:8px; border-radius:50%; }
  .summary-chip.active{ border-color:var(--accent); box-shadow:0 0 0 1px var(--accent); }
  .summary-chip.zero{ opacity:.4; }
  /* UNKNOWN(로봇이 base_state 를 안 말한 상태)은 **있을 때만** 칸을 차지한다 — 평소 헤더 폭은 그대로
     (헤더는 절대 두 줄이 되지 않는다는 규약이 tokens.css 머리말에 있다). */
  .summary-chip[data-s="UNKNOWN"].zero{ display:none; }

  #stage{ flex:1; display:flex; min-height:0; }
  /* 맵 */
  #map{ position:relative; flex:1; margin:8px; border:1px solid var(--line); border-radius:6px;
    background:
      radial-gradient(circle at 30% 30%, var(--surface), var(--card2) 70%),
      repeating-linear-gradient(0deg,transparent 0 39px,color-mix(in srgb, var(--surface) 50%, transparent) 39px 40px),
      repeating-linear-gradient(90deg,transparent 0 39px,color-mix(in srgb, var(--surface) 50%, transparent) 39px 40px);
    background-size:cover, auto, auto; overflow:hidden; cursor:crosshair; }
  /* ⚠️ has-map 클래스는 #maprot 에만 붙는다(app.js applyMapBackground) — 종전 `#map.has-map` 은 절대
     안 맞는 죽은 규칙이었고, 맞았어도 위 radial-gradient(불투명)가 이겼다. ⟳90°/270° 에서
     rotRefitScale<1 이면 #maprot 이 줄어 그 바깥으로 추상 그라데이션이 샌다 — 그때도 '바깥' 톤을 유지한다. */
  #map:has(#maprot.has-map){ background-color:var(--map-unknown); background-image:none; }
  #map.move-armed{ box-shadow:inset 0 0 0 2px var(--accent); }   /* 어택땅 arm: 다음 클릭=MOVE */
  #maprot{ position:absolute; inset:0; transform-origin:center center; }   /* 배경+유닛만 회전(컨트롤 제외) */
  /* path_plan 경로 레이어 — 전류 흐름 스타일: 글로우+코어+흐르는 대시+방향 셰브런(app.js renderPathLayer) */
  .pathlayer path{ fill:none; stroke-linecap:round; stroke-linejoin:round; }
  .pathlayer .glow{ stroke-width:12; opacity:.16; }   /* blur 필터 금지 — 대시 애니와 겹치면 매 프레임 재래스터화 */
  .pathlayer .core{ stroke-width:4; opacity:.4; }
  .pathlayer .flow{ stroke-width:4; opacity:.95; stroke-dasharray:7 11; animation:nxflow 1.3s linear infinite; }
  .pathlayer .flow.fast{ stroke-dasharray:6 12; animation-duration:.85s; }
  .pathlayer .chevs path{ stroke-width:2; opacity:.85; }
  @keyframes nxflow{ to{ stroke-dashoffset:-18; } }   /* 대시합(7+11=6+12=18)과 일치 → 이음새 없는 흐름 */
  @media (prefers-reduced-motion: reduce){ .pathlayer .flow{ animation:none; } }
  #maprot.has-map{ background-color:var(--map-unknown); background-position:center; background-repeat:no-repeat; }
  #maprotbtn{ position:absolute; top:8px; right:8px; z-index:6; background:color-mix(in srgb, var(--bg) 85%, transparent);
    color:var(--ink); border:1px solid var(--line); border-radius:5px; padding:3px 8px; font-size:11px; cursor:pointer; }
  #maprotbtn:hover{ border-color:var(--accent); }
  #map3dbtn{ position:absolute; top:8px; right:86px; z-index:6; background:color-mix(in srgb, var(--bg) 85%, transparent);
    border:1px solid var(--line); color:var(--ink2); padding:4px 10px; border-radius:6px; font-size:12px; cursor:pointer; }
  #map3dbtn:hover{ border-color:var(--accent); }
  #map3dbtn.on{ background:var(--accent); color:var(--on-accent); border-color:var(--accent); }
  #mapsel{ position:absolute; top:8px; left:8px; z-index:6; max-width:230px;
    background:color-mix(in srgb, var(--bg) 85%, transparent); color:var(--ink); border:1px solid var(--line);
    border-radius:5px; padding:3px 6px; font:11px/1.3 ui-sans-serif,system-ui; }
  #mapsel:hover{ border-color:var(--accent); }
  #mapsel{ left:86px; }                              /* 뒤로가기 버튼 옆 */
  /* ⚠️ 맵 위 컨트롤은 3D 캔버스(#map3d, z-index:5)보다 **위**여야 한다.
     종전엔 이 셋이 z-index:4 라 3D 를 켜는 순간 캔버스가 덮어써서, 화면에는 보이는데 **클릭이 안 됐다**
     — 3D 상태에서 유일하게 눌리는 버튼이 z-index:6 인 #map3dbtn 뿐이라 대시보드로 못 돌아갔다. */
  #tooverview{ position:absolute; top:8px; left:8px; z-index:6; background:color-mix(in srgb, var(--bg) 85%, transparent);
    color:var(--ink); border:1px solid var(--line); border-radius:5px; padding:3px 8px; font-size:11px; cursor:pointer; }
  #tooverview:hover{ border-color:var(--accent); }
  /* L0 사이트 전체 (overview) */
  #overview{ flex:1; overflow:auto; padding:16px; }
  .ovhead{ display:flex; align-items:baseline; gap:10px; margin-bottom:14px; }
  .ovhead h2{ font-size:16px; color:var(--accent); margin:0; }
  .ovsub{ font-size:12px; color:var(--dim); }
  /* 카드마다 ▾ 를 누르는 대신 한 번에(사용자 지시 2026-09-15: "로봇 목록도 한번에 열고 닫을 수 있는 체크"). */
  .ovallcol{ margin-left:auto; display:flex; align-items:center; gap:5px; font-size:11px;
    color:var(--dim); cursor:pointer; user-select:none; white-space:nowrap; }
  .ovallcol input{ accent-color:var(--accent); cursor:pointer; margin:0; }
  /* 줄당 3개 고정(사용자 지시 2026-09-15). auto-fill 은 화면이 넓어지면 4·5개로 늘어나 카드가 잘아졌다.
     좁아질 땐 2 → 1 로 내려간다 — 관제는 폰·태블릿에서도 열린다. */
  #overviewgrid{ display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:14px; }
  @media (max-width:1100px){ #overviewgrid{ grid-template-columns:repeat(2,minmax(0,1fr)); } }
  @media (max-width:680px){  #overviewgrid{ grid-template-columns:1fr; } }
  /* 사이트 스코프 칩 — 다중 사이트 서버에서 교차 모니터링 방지(선택 사이트만) */
  #sitechips{ display:inline-flex; gap:5px; margin:0 10px; }
  .sitechip{ background:var(--card); border:1px solid var(--line); color:var(--ink2); border-radius:12px;
    padding:2px 10px; font-size:11px; cursor:pointer; }
  .sitechip:hover{ border-color:var(--accent); }
  .sitechip.on{ background:var(--surface); border-color:var(--accent); color:var(--accent); font-weight:600; }
  .ovsite{ grid-column:1/-1; font-size:15px; font-weight:700; color:var(--accent); margin:10px 0 0; padding-bottom:5px; border-bottom:1px solid var(--line); }
  .ovsite:first-child{ margin-top:0; }
  .ovbuilding{ grid-column:1/-1; font-size:12px; color:var(--dim); margin-top:2px; }
  .ovcard{ border:1px solid var(--line); border-radius:8px; background:var(--card); overflow:hidden; cursor:pointer; transition:border-color .1s; }
  .ovcard:hover{ border-color:var(--accent); }
  /* 고정 높이 상자 — 카드 높이를 도면 종횡비가 좌우하지 않게 한다(사용자 지시 2026-09-15).
     ⚠️ 2026-09-16: 처음엔 높이만 맞췄더니 **가로로 긴 도면이 잘렸다**(robot_tower 1F = 2040×1040).
     이제 폭을 `min(상자 폭, 높이에서 나온 폭)` 으로 잡아 **양쪽 다** 안 넘는다. 상자에
     container-type 을 주고 100cqw 로 상자 폭을 읽는다 — 카드 폭이 3열/2열/1열로 바뀌어도 따라간다.
     안쪽 .ovthumb 은 원본 종횡비를 그대로 유지한다(로봇 dot 이 이 기준 %% 라 비율이 틀어지면 어긋난다). */
  .ovthumbbox{ --thumb-h:210px; height:var(--thumb-h); container-type:inline-size;
    display:grid; place-items:center; overflow:hidden;
    background:var(--card2); border-bottom:1px solid var(--line); }
  .ovthumb{ position:relative; background:var(--map-unknown) center/contain no-repeat;
    width:min(100cqw, calc(var(--thumb-h) * var(--arw) / var(--arh)));
    aspect-ratio:var(--arw) / var(--arh); height:auto; }
  .ovthumb.nomap{ --arw:3; --arh:2; display:flex; align-items:center; justify-content:center; color:var(--dim); font-size:12px; }
  .ovcard{ display:flex; flex-direction:column; }                 /* 메타가 상자 아래로 자연스럽게 붙는다 */
  .ovdot{ position:absolute; width:9px; height:9px; border-radius:50%; transform:translate(-50%,-50%); border:1px solid var(--bg); }
  .ovmeta{ padding:8px 10px; }
  .ovname{ font-size:13px; color:var(--ink); font-weight:600; }
  .ovpath{ font-size:11px; color:var(--dim); margin-top:1px; }
  .ovstats{ display:flex; align-items:center; gap:8px; margin-top:7px; font-size:11px; color:var(--ink2); flex-wrap:wrap; }
  .ovchip{ display:inline-flex; align-items:center; gap:3px; }
  .ovchip .d{ width:8px; height:8px; border-radius:50%; }
  /* 도면에 점을 못 찍은 로봇 수 — 카드가 '한가하다'고 거짓말하지 않게 이유를 적는다(테두리는 점선: 값이 아니라 결손) */
  .ovchip.nodot{ color:var(--dim); border:1px dashed var(--line2); border-radius:999px; padding:0 6px; }
  .ovdot.off{ opacity:.4; }                                   /* 오프라인 로봇 점 흐리게 */
  .ovrobots{ margin-top:8px; border-top:1px solid var(--line); padding-top:6px; }
  .ovrobots-h{ display:flex; align-items:center; gap:5px; font-size:11px; color:var(--dim); cursor:pointer; user-select:none; }
  .ovrobots-h:hover{ color:var(--ink2); }
  .ovrobots-tg{ font-size:9px; width:9px; display:inline-block; }
  .ovrobots-list{ margin-top:5px; display:flex; flex-direction:column; gap:2px; max-height:168px; overflow:auto; }
  .ovrobots.collapsed .ovrobots-list{ display:none; }
  .ovrow{ display:flex; align-items:center; gap:6px; font-size:11px; color:var(--ink2); padding:2px 5px; border-radius:4px; cursor:pointer; }
  .ovrow:hover{ background:var(--card2); color:var(--ink); }
  .ovrow.off{ color:var(--dim); }
  .ovrow-d{ width:8px; height:8px; border-radius:50%; flex:none; }
  .ovrow-ic{ flex:none; font-size:12px; line-height:1; }   /* 종류 아이콘(category 파생) */
  .ovrow-id{ flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .ovrow-s{ font-size:10px; color:var(--dim); }
  .ovrow-n{margin-left:6px;font-size:10px;color:var(--ink2);white-space:nowrap}   /* 👥N 접속 인원 */
  .selpeople{font-size:11px;color:var(--ink2);margin-top:2px}
  .ovrow.empty{ color:var(--dim); cursor:default; } .ovrow.empty:hover{ background:none; }
  .unit.onmap .tag{ opacity:.85; }
  .unit:not(.onmap)::after{ content:"~"; position:absolute; top:-3px; left:-3px; font-size:9px; color:var(--dim); }
  .unit{ position:absolute; width:40px; height:40px; margin:-20px 0 0 -20px; border-radius:8px;
    display:flex; align-items:center; justify-content:center; font-size:20px; cursor:pointer;
    background:var(--card); border:2px solid var(--line); transition:transform .08s, left .3s linear, top .3s linear; }
  .unit:hover{ transform:scale(1.12); }
  /* 표준맵 위 로봇 = 실측 풋프린트 박스(.fp, 통째 yaw 회전) + 전진 삼각형(.fpdir). 색=base_state. */
  .unit.onmap{ background:transparent; border-color:transparent; font-size:0; }
  .fp{ position:absolute; left:50%; top:50%; box-sizing:border-box; border:2px solid var(--off);
    border-radius:3px; transform-origin:center center; }
  .fpdir{ position:absolute; left:50%; top:0; transform:translate(-50%,-100%); width:0; height:0;
    border-left:5px solid transparent; border-right:5px solid transparent; border-bottom:7px solid var(--off); }
  /* 축척상 실측 박스가 안 읽히는 줌에서 쓰는 점 — 박스인 척하지 않는다(사용자 지시 2026-09-16).
     크기가 **고정 px** 이라 줌아웃해도 안 사라진다. 확대하면 같은 자리에서 .fp 실측 박스로 바뀐다. */
  /* 점은 화면에서 **항상 같은 크기**다 — `#maprot` 의 scale(zoom) 을 역으로 상쇄한다(`--lblcr`,
     라벨이 쓰는 것과 같은 수법). 상쇄하지 않으면 줌 따라 점이 같이 커져서 '박스 대신 쓰는 점' 이
     아니라 그냥 큰 원이 된다(실측 2026-09-16: 줌 3.5 에서 9px → 32px). */
  .unit.onmap .fpdot{ position:absolute; left:50%; top:50%; width:9px; height:9px; border-radius:50%;
    box-sizing:border-box; border:1px solid; transform:translate(-50%,-50%) var(--lblcr,);
    box-shadow:0 0 0 1.5px var(--bg); z-index:1; pointer-events:none; }
  /* 방향 삼각형은 로봇 yaw 로 돌아야 하므로 역회전(--lblcr 의 rotate)을 쓰면 안 된다 — 스케일만 상쇄. */
  .unit.onmap .fpdotdir{ position:absolute; left:50%; top:50%; width:0; height:0;
    border-left:3.5px solid transparent; border-right:3.5px solid transparent; border-bottom:5px solid;
    transform-origin:50% 0; z-index:1; pointer-events:none; }
  .unit.sel .fpdot{ width:11px; height:11px; }
  .unit.onmap .fpico{ position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); font-size:13px; line-height:1; z-index:1; pointer-events:none; filter:drop-shadow(0 0 2px var(--bg)); }  /* 정합 로봇도 카테고리 식별 */
  .unit.offmap{ opacity:.5; }   /* 이미지 영역 밖으로 클램프된 로봇(정합 이상/매핑 범위 밖) 흐리게 */

  /* ===== 설비(IoT) 마커 — 이모지 대신 '지금 무슨 일이 벌어지는지'를 그림으로 (STANDARD_iot §4 io)
     몸통은 div + CSS transform 이다(SVG viewBox 는 이 중첩·회전 조합에서 스케일이 예측 안 돼
     문짝이 아예 안 움직였다 — 실측). 외부 에셋 0, 맵 줌은 #maprot 의 scale 이 그대로 먹는다.
     색은 --iotcol(=base_state 토큰) 상속 → 다크/라이트·상태색 자동. */
  .unit.iot .fp{ border-color:var(--iotcol); background:color-mix(in srgb, var(--iotcol) 8%, transparent); }
  /* 문틀 테두리는 1px — 2px 면 얇은 문(6px)에서 위아래 테두리가 만나 문짝 자리가 0px 이 된다(실측). */
  .unit.iot-door .fp{ overflow:hidden; border-width:1px; border-radius:1px; }
  /* 문짝 채움은 **상태와 무관하게 일정한 밝기**로 둔다. 예전엔 --iotcol 을 62% 로 칠해서
     BUSY(앰버)로 바뀌는 순간 확 밝아졌고, 어두운 배경에서 밝은 면은 더 커 보인다
     (실측: 기하는 6×18px 로 전 상태 동일한데도 '커진다'고 읽혔다). 상태는 테두리가 말한다. */
  /* 문짝에 테두리를 주면 두 짝이 만나는 가운데서 테두리 2개가 겹쳐 **지운 이음매가 그대로 다시 생긴다**
     (닫혔을 때 '막힌 창살' 느낌의 정체). 채움만 쓰고, 상태색은 문틀(.fp 테두리)이 맡는다.
     두 짝이라는 정보는 열릴 때 갈라지며 저절로 드러난다 — 의미가 있을 때만 선이 보이는 셈. */
  /* 채움은 **불투명**이고 두 짝이 1px 겹친다. 반투명이면 맞닿는 경계에 스케일 헤어라인이 남고,
     반투명인 채로 겹치면 알파가 겹쳐 더 짙은 띠가 생긴다 — 어느 쪽이든 지운 이음매가 되살아난다.
     문은 실제로 불투명한 물체이기도 하다(밑의 맵을 가리는 게 맞다). */
  .unit.iot .leaf{ position:absolute; top:0; height:100%; width:calc(50% + 1px); box-sizing:border-box;
    background:color-mix(in srgb, var(--ink) 42%, var(--bg));
    transition:transform 1.1s cubic-bezier(.4,0,.2,1); }   /* 실물 개폐가 2~3초 — 눈으로 따라갈 속도 */
  .unit.iot .leaf.l{ left:0; }
  .unit.iot .leaf.r{ right:0; }
  /* 엘리베이터: 칸(.fp 자체) + 층수 + 방향. 층수/화살표는 .fp 밖(비회전)이라 맵을 돌려도 안 뒤집힌다. */
  .unit.iot-elev .fp{ border-radius:22%; transition:background .3s ease; }
  .unit.iot-elvopen .fp{ background:color-mix(in srgb, var(--iotcol) 38%, transparent); }  /* 문 열림=탑승 가능 */
  .unit.iot .iotlbl{ position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
    display:flex; flex-direction:column; align-items:center; line-height:1; pointer-events:none; z-index:1; }
  .unit.iot .iotlbl .flr{ font:600 12px/1 ui-monospace,monospace; color:var(--ink);
    text-shadow:0 0 3px var(--bg), 0 0 3px var(--bg); }
  .unit.iot .iotlbl .arw{ width:0; height:0; border-left:5px solid transparent; border-right:5px solid transparent; }
  .unit.iot .iotlbl .arw.up{ border-bottom:7px solid var(--iotcol); animation:arwUp 1.05s linear infinite; }
  .unit.iot .iotlbl .arw.down{ border-top:7px solid var(--iotcol); animation:arwDown 1.05s linear infinite; }
  /* 화살표가 카 위로 '흘러' 올라간다(내려갈 땐 반대) — 정지 중엔 아예 안 그린다 */
  @keyframes arwUp  { 0%{ opacity:.15; transform:translateY(4px) } 55%{ opacity:1 } 100%{ opacity:.15; transform:translateY(-4px) } }
  @keyframes arwDown{ 0%{ opacity:.15; transform:translateY(-4px) } 55%{ opacity:1 } 100%{ opacity:.15; transform:translateY(4px) } }
  /* ERROR 점멸 — 셀렉터가 .iotsvg(SVG 시절 잔재)를 가리켜 실제론 아무것도 안 깜빡이고 있었다. */
  .unit.iot-error .leaf, .unit.iot-error .fp{ animation:iotErr 1.2s ease-in-out infinite; }
  @keyframes iotErr{ 0%,100%{ opacity:1 } 50%{ opacity:.35 } }
  @media (prefers-reduced-motion: reduce){    /* 접근성 — 움직임을 끄면 최종 상태만 정적으로 */
    .unit.iot .leaf, .unit.iot .fp, .unit.iot .iotlbl .arw{ animation:none !important; transition:none !important; }
  }
  .unit .sig{ position:absolute; top:-8px; right:-8px; line-height:0; pointer-events:none; z-index:2;
    background:color-mix(in srgb, var(--bg) 78%, transparent); border-radius:3px; padding:1px; }
  .unit .tag{ position:absolute; top:42px; left:50%; transform:translateX(-50%) var(--lblcr,); white-space:nowrap;
    font-size:10px; color:var(--ink2); background:color-mix(in srgb, var(--bg) 80%, transparent); padding:0 3px; border-radius:3px; }
  .unit.sel{ box-shadow:0 0 0 2px var(--accent), 0 0 12px color-mix(in srgb, var(--accent) 40%, transparent); border-color:var(--accent); }
  .unit.off{ opacity:.45; filter:grayscale(.6); }
  .unit .st{ position:absolute; top:-4px; right:-4px; width:10px; height:10px; border-radius:50%; border:1px solid var(--bg); }
  .unit .dir{ position:absolute; width:0; height:0; border-left:4px solid transparent; border-right:4px solid transparent;
    border-bottom:7px solid color-mix(in srgb, var(--ink) 67%, transparent); top:-9px; left:50%; transform-origin:50% 24px; }
  #selbox{ position:absolute; border:1px solid var(--accent); background:color-mix(in srgb, var(--accent) 13%, transparent); display:none; pointer-events:none; }
  /* 우측 roster */
  /* 폭은 사용자가 정한다(사용자 지시 2026-09-15: "조금 폭을 줄이자(조절가능하도록), 얘도 닫을 수 있게").
     값은 --roster-w 한 곳에만 있고 localStorage 에 남는다. 접으면 폭 0 + 손잡이만 남는다. */
  #roster{ width:var(--roster-w,178px); margin:8px 8px 8px 0; border:1px solid var(--line); border-radius:6px; padding:6px;
    overflow-y:auto; display:flex; flex-direction:column; gap:5px; flex:0 0 auto; }
  #rostergrip{ flex:0 0 auto; width:6px; margin:8px 0; cursor:col-resize; border-radius:3px;
    background:transparent; transition:background .1s; }
  #rostergrip:hover,#rostergrip.drag{ background:var(--accent); }
  #stage.rosteroff #roster{ display:none; }
  #stage.rosteroff #rostergrip{ background:var(--line); }      /* 접혀 있어도 손잡이는 남는다 — 되돌릴 자리 */
  #roster #rostercollapse{ margin-left:auto; background:transparent; border:0; color:var(--dim);
    font-size:13px; line-height:1; padding:0 2px; cursor:pointer; }
  #roster #rostercollapse:hover{ color:var(--accent); }
  #roster .rtitle{ font-size:10px; color:var(--dim); text-transform:uppercase; letter-spacing:.5px; margin-bottom:2px; }
  .thumb{ display:flex; flex-direction:column; gap:3px; padding:5px 6px; border:1px solid var(--line); border-radius:5px;
    background:var(--card); cursor:pointer; font-size:11px; }
  .thumb:hover{ border-color:var(--line2); }
  .thumb.sel{ border-color:var(--accent); box-shadow:0 0 0 1px var(--accent); }
  .thumb.off{ opacity:.5; }
  /* 관제사 줄 — 붙었을 때만 생긴다. 제어권을 쥔 사람(🎮)은 색으로 구분한다. */
  .thop{ display:flex; align-items:center; gap:4px; font-size:10px; color:var(--ink2);
    border-top:1px dashed var(--line); padding-top:3px; margin-top:1px; min-width:0; }
  .thop.has{ color:var(--accent2); }
  .thop .on{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .thop .oc{ margin-left:auto; color:var(--dim); flex:0 0 auto; }
  .thumb .thr1{ display:flex; align-items:center; gap:6px; }
  .thumb .ic{ font-size:16px; }
  .thumb .nm{ flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--ink2); }
  /* ── 로스터 범위(전체/사이트/건물) — 제목 줄의 세그먼트 + 숨김·미정렬 셈. 폭 178px 이라 한 줄에 안 들어가면 접는다 ── */
  #roster .rtitle{ display:flex; flex-wrap:wrap; align-items:center; gap:2px 6px; }
  #roster .rtitle .rtl{ white-space:nowrap; }
  #roster .rhid{ color:var(--ink2); }                         /* 범위 밖이라 가린 수 — 조용히 사라지지 않게 */
  #roster .runal{ color:var(--busy); }                        /* 미정렬 수 — 어느 범위에서도 보이는 이유를 말한다 */
  .rscope{ display:inline-flex; margin-left:auto; border:1px solid var(--line); border-radius:4px; overflow:hidden; }
  .rscope button{ background:var(--card); border:0; color:var(--dim); font-family:inherit; font-size:9px; line-height:1.4;
    letter-spacing:0; padding:1px 5px; cursor:pointer; }
  .rscope button+button{ border-left:1px solid var(--line); }
  .rscope button:hover{ color:var(--ink); }
  .rscope button.on{ background:var(--accent); color:var(--on-accent); font-weight:600; }   /* 다른 .on 토글과 같은 문법 — surface 위 accent 글자는 라이트에서 4.2:1 */
  .rscope button.fb{ color:var(--busy); text-decoration:underline dotted; }   /* 골랐지만 맵이 없어 사이트로 동작 중 */
  .rscope button.eff{ background:var(--surface); color:var(--ink2); }         /* 그 대신 실제로 적용 중인 범위 */
  .thumb .unal{ font-size:9px; line-height:1.3; color:var(--busy); border:1px solid var(--busy); border-radius:3px;
    padding:0 3px; white-space:nowrap; }                       /* 미정렬 표식 — "왜 얘가 여기 있지" 방지 */

  /* 상태 배지(신호·배터리·base_state) — roster·사이트 대시보드 공용 */
  .badges{ display:flex; align-items:center; gap:7px; padding-left:1px; }
  .bdg{ display:inline-flex; align-items:center; gap:2px; font-size:10px; color:var(--ink2); line-height:1; }
  .bdg.sig{ gap:3px; } .bdg .ms{ font:9px/1 ui-monospace,monospace; }
  .bdg.csess{ font:9px/1 ui-monospace,monospace; color:var(--dim); }   /* 제어 세션 결속 칩 */
  .bdg.csess.ended{border-style:dashed;opacity:.65}     /* 서버 발급 세션 — 비활성(연결 끊김) */
  .bdg.csess.mine{ color:var(--ok); }
  .bdg svg{ display:block; }
  .bat{ gap:3px; } .bat.na{ color:var(--dim); }
  .batshell{ width:22px; height:11px; border:1px solid var(--ink2); border-radius:2px; padding:1px; position:relative; }
  .batshell::after{ content:""; position:absolute; right:-3px; top:3px; width:2px; height:5px; background:var(--ink2); border-radius:0 1px 1px 0; }
  .batfill{ display:block; height:100%; border-radius:1px; }
  .batpct{ font:9px/1 ui-monospace,monospace; color:var(--ink2); min-width:24px; }
  .bstate{ gap:3px; } .bstate .bdot{ width:7px; height:7px; border-radius:50%; }
  /* 하단 HUD */
  #hud{ height:184px; border-top:2px solid var(--line); display:flex; gap:8px; padding:8px;
    background:linear-gradient(var(--card2),var(--bg)); }
  .pane{ border:1px solid var(--line); border-radius:6px; background:var(--card); }
  #minimap{ width:210px; position:relative; }
  #minimap canvas{ width:100%; height:100%; display:block; border-radius:6px; }
  #minimap .lbl{ position:absolute; top:4px; left:6px; font-size:10px; color:var(--dim); }
  #selected{ flex:1; padding:8px; overflow:auto; }
  #selected .lbl{ font-size:10px; color:var(--dim); text-transform:uppercase; }
  #selsummary{ font-size:12px; color:var(--ink2); margin-top:2px; }
  #selgrid{ display:flex; flex-wrap:wrap; gap:6px; margin-top:6px; }
  .selicon{ width:48px; height:54px; border:1px solid var(--line); border-radius:6px; background:var(--card2);
    display:flex; flex-direction:column; align-items:center; justify-content:center; cursor:pointer; font-size:18px; position:relative; }
  .selicon .n{ font-size:9px; color:var(--ink2); margin-top:2px; max-width:46px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .selicon.focus{ border-color:var(--accent); box-shadow:0 0 0 1px var(--accent); }
  /* 스킬 카드 3×3 */
  #cmd{ width:240px; padding:8px; display:flex; flex-direction:column; }
  #cmd .lbl{ font-size:10px; color:var(--dim); text-transform:uppercase; display:flex; justify-content:space-between; align-items:center; }
  #cmd .lbl .solo{ color:var(--accent); cursor:pointer; text-transform:none; font-size:11px; }
  #cmdgrid{ display:grid; grid-template-columns:repeat(3,1fr); grid-template-rows:repeat(3,1fr); gap:5px; margin-top:6px; flex:1; min-height:120px; }
  .skill{ border:1px solid var(--line); border-radius:5px; background:var(--card2); display:flex; flex-direction:column;
    align-items:center; justify-content:center; cursor:pointer; font-size:16px; color:var(--ink); text-align:center; }
  .skill:hover{ border-color:var(--accent); background:var(--panel); }
  .skill .k{ font-size:9px; color:var(--ink2); margin-top:3px; line-height:1.1; max-width:66px; overflow:hidden; }
  .skill.empty{ background:var(--bg); border-style:dashed; cursor:default; }
  /* 빈 칸은 hover 로도 켜지지 않는다 — `.skill:hover` 와 특이도가 같아(둘 다 0,2,0) border-color 만
     hover 가 이기고 있었다. 누를 수 없는 칸이 accent 로 켜지면 '눌렀는데 무반응' 으로 읽힌다. */
  .skill.empty:hover{ border-color:var(--line); background:var(--bg); }
  .skill.folder{ color:var(--accent2); }
  .skill.back{ color:var(--ink2); }
  .skill.stop{ color:var(--err); border-color:var(--err-line); }
  /* 정지는 **맨 윗줄 2번째 칸**(기본 위치)에 그대로 두고 색만 채운다.
     2026-09-16 에 두 칸으로 눕혀 봤다가 되돌렸다 — 사용자 판단으로 패드의 근육 기억이 더 중요하다.
     남는 건 "거기에 색을 넣어" 쪽이다(2026-09-15 지시). 칸 배치는 3×3 그대로. */
  .skill.fixed.stop{ background:var(--err); border-color:var(--err); color:var(--bg); font-weight:700; }
  .skill.fixed.stop .k{ color:var(--bg); opacity:.85; margin-top:0; max-width:none; font-size:11px; }
  .skill.fixed.stop::before{ color:var(--bg); opacity:.75; }
  .skill.fixed.stop:hover{ background:var(--err); filter:brightness(1.12); border-color:var(--err); }
  .skill.fixed.stop.disabled{ background:var(--card2); color:var(--dim); border-color:var(--line); }
  .skill.fixed.stop.disabled .k{ color:var(--dim); }
  .skill.fixed{ position:relative; border-color:var(--accent); }   /* 고정 공통 스킬(이동/정지) */
  .skill.fixed::before{ content:attr(data-slot); position:absolute; top:2px; left:4px; font-size:8px; color:var(--accent); opacity:.8; }
  .skill.disabled{ opacity:.4; filter:grayscale(.7); cursor:not-allowed; border-color:var(--line); }
  .skill.disabled:hover{ border-color:var(--line); background:var(--card2); }
  .skill.fixed.disabled{ border-color:var(--line); }
  .skill.fixed.disabled::before{ color:var(--dim); }

  /* 토스트 / 로그 */
  .log{ position:fixed; right:10px; bottom:194px; max-width:46vw; font-size:11px; color:var(--ink2); opacity:.9; pointer-events:none; z-index:30; }
  #toast{ position:fixed; left:50%; bottom:200px; transform:translateX(-50%); background:color-mix(in srgb, var(--surface) 91%, transparent); border:1px solid var(--accent);
    color:var(--ink); font-size:12px; padding:8px 14px; border-radius:8px; z-index:60; display:none; max-width:70vw; }

  /* 파라미터 자동폼 모달 */
  #paramback{ position:fixed; inset:0; background:#000a; z-index:80; display:none; align-items:center; justify-content:center; }
  /* 미리보기 폼(joint_preset/joint_ctrl): 배경 차단·디밍 없이 우측 플로팅 — 시뮬 고스트를 보면서 조정 */
  #paramback.preview{ background:transparent; pointer-events:none; align-items:flex-start; justify-content:flex-end; }
  #paramback.preview #paramform{ pointer-events:auto; margin:64px 16px 0 0; box-shadow:0 8px 30px rgba(0,0,0,.6); }
  #paramform{ background:var(--card); border:1px solid var(--accent); border-radius:10px; padding:16px; width:380px; max-width:92vw; max-height:86vh; overflow:auto; }
  #paramform h3{ margin:0 0 4px; font-size:15px; color:var(--accent); }
  #paramform .sub{ font-size:11px; color:var(--dim); margin-bottom:10px; }
  .param-row{ display:flex; align-items:center; gap:8px; margin:7px 0; }
  .param-row label{ width:96px; font-size:12px; color:var(--ink2); }
  .param-row input{ flex:1; min-width:0; background:var(--card2); border:1px solid var(--line); border-radius:5px; color:var(--ink); padding:5px 7px; font:inherit; }
  .param-row .punit{ font-size:11px; color:var(--dim); width:42px; flex:none; text-align:left; }   /* 맵 마커 .unit 와 충돌 회피용 별도 클래스 */
  .param-row.req label::after{ content:" *"; color:var(--err); }
  .param-err{ color:var(--err); font-size:11px; min-height:14px; margin-top:4px; }
  .param-actions{ display:flex; gap:8px; justify-content:flex-end; margin-top:12px; }
  .param-actions button{ padding:6px 14px; border-radius:6px; border:1px solid var(--line); background:var(--card2); }
  .param-actions .go{ border-color:var(--accent); color:var(--accent); }
  .param-grid7{ display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:4px 10px; }
  .param-grid7 .param-row{ margin:2px 0; }
  .param-grid7 .param-row label{ width:26px; }

  /* 1:1 직접관제 오버레이 */
  #cockpit{ position:fixed; inset:0; background:var(--bg); z-index:50; display:flex; flex-direction:column; }
  #cockpit[hidden]{ display:none; }
  #app[hidden], #stage[hidden], #hud[hidden]{ display:none; }