/* ────────────────────────────────────────────────────────────
   HACCP 통합관제 — 다크 테마 (GUI 시안 0~4.png 기준)
   ──────────────────────────────────────────────────────────── */
:root{
  --bg:          #17181b;
  --sidebar:     #212226;
  --card:        #2a2c31;
  --card-2:      #26282c;
  --border:      #34363c;
  --border-soft: #3a3d43;
  --text:        #e7e9ec;
  --text-dim:    #9aa0a6;
  --text-mute:   #6f757b;
  --blue:        #3b9bf5;
  --blue-deep:   #2b7fd4;
  --green:       #2ecc71;
  --green-btn:   #22a95c;
  --red:         #ef4444;
  --red-btn:     #e5484d;
  --amber:       #f0b429;
  --thead:       #38495a;
  --radius:      8px;
  /* 현장 모니터링 Vision 칸 높이. 카메라가 붙든 안 붙든 이 높이로 고정되고
     영상은 이 안으로 줄여 넣는다 — 영상을 더 크게 보려면 이 값만 올리면 된다.
     화면 높이에 따라 커지되 위아래 한계를 두어 배치가 흔들리지 않게 한다. */
  --vision-cell-h: clamp(360px, 46vh, 620px);
}
*{box-sizing:border-box}
html,body{margin:0;padding:0;height:100%}
body{
  background:var(--bg); color:var(--text);
  font-family:"Malgun Gothic","Apple SD Gothic Neo","Noto Sans KR",
              -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
  font-size:14px; line-height:1.45;
}
button,input,select{font-family:inherit}
.hidden{display:none !important}

/* ── 레이아웃 ───────────────────────────────────────────── */
.layout{display:flex; min-height:100vh}
.sidebar{
  width:212px; flex:0 0 212px; background:var(--sidebar);
  border-right:1px solid var(--border); padding:22px 16px;
  display:flex; flex-direction:column; position:sticky; top:0; height:100vh;
}
.main{flex:1; min-width:0; display:flex; flex-direction:column}
.content{padding:0 18px 40px}

/* ── 브랜드 ─────────────────────────────────────────────── */
.brand{padding-bottom:16px; border-bottom:1px solid var(--border); margin-bottom:16px}
.brand-site{color:var(--blue); font-size:17px; font-weight:700; letter-spacing:-.3px}
.brand-sys {color:var(--blue); font-size:17px; font-weight:700; letter-spacing:-.3px}
.brand-ver {color:var(--text-mute); font-size:11px; font-weight:600; margin-top:6px}

/* ── 네비게이션 ─────────────────────────────────────────── */
.nav{display:flex; flex-direction:column; gap:6px}
.nav-item{
  display:flex; align-items:center; gap:10px;
  background:transparent; border:0; color:var(--text-dim);
  padding:0 10px; height:44px;                 /* 현장 터치 패널 최소 타깃 */
  border-radius:6px; cursor:pointer;
  font-size:13.5px; text-align:left; width:100%; transition:background .12s,color .12s;
}
.nav-item .ico{width:18px; height:18px; flex:0 0 18px}
/* 배지가 붙어도 메뉴명이 두 줄로 접히지 않게 한다 */
.nav-item > span{white-space:nowrap; overflow:hidden; text-overflow:ellipsis}
.nav-alert{
  margin-left:auto; flex:0 0 auto; font-size:10.5px; font-weight:700; color:var(--red);
  background:#3d1d1d; border:1px solid #6b3030; border-radius:999px;
  padding:0 6px; line-height:16px;
}
.nav-item:hover{background:#2c2e33; color:var(--text)}
.nav-item.active{background:var(--blue); color:#fff; font-weight:600}
.conn{margin-top:auto; display:flex; align-items:center; gap:7px;
      font-size:11px; color:var(--text-mute); padding-top:12px}
.conn .dot{width:7px;height:7px;border-radius:50%;background:var(--text-mute)}
.conn.ok  .dot{background:var(--green); box-shadow:0 0 6px var(--green)}
.conn.err .dot{background:var(--red)}

/* ── 상단바 ─────────────────────────────────────────────── */
.topbar{
  margin:16px 18px 14px; padding:14px 18px;
  background:var(--card); border:1px solid var(--border); border-radius:var(--radius);
  display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:10px;
}
.topbar-left{display:flex; align-items:center; gap:9px}
.fish{color:var(--blue); font-size:16px}
.line-name{font-size:16px; font-weight:700}
.topbar-right{display:flex; align-items:center; gap:20px; font-size:13px; color:var(--text-dim)}
.haccp-wrap{display:flex; align-items:center; gap:7px}
.pill{
  border:1px solid #2f6b45; background:#1d3d2a; color:var(--green);
  border-radius:999px; padding:2px 13px; font-size:11.5px; font-weight:700; cursor:pointer;
}
.pill.off{border-color:#6b3030; background:#3d1d1d; color:var(--red)}
.metric b{color:var(--text)}
.clock{color:var(--text-dim); font-variant-numeric:tabular-nums; min-width:78px; text-align:right}

/* ── 카드 / 섹션 ───────────────────────────────────────── */
.card{
  background:var(--card); border:1px solid var(--border);
  border-radius:var(--radius); padding:16px; margin-bottom:14px;
}
.card-title{margin:0 0 12px; font-size:14px; font-weight:700}
.card-title.blue{color:var(--blue)}
.section-title{
  font-size:14.5px; font-weight:700; color:var(--blue);
  margin:18px 0 10px; padding-left:9px; border-left:3px solid var(--blue);
  display:flex; align-items:center; gap:10px;
  /* 설명이 길어도 제목을 짓누르지 않고 아랫줄로 내려간다 */
  flex-wrap:wrap;
}
.badge-warn{
  margin-left:auto; font-size:11px; font-weight:700; color:var(--amber);
  border:1px solid #6b5520; background:#3a2f14; border-radius:5px; padding:3px 8px;
}
.hint{color:var(--text-dim); font-size:12px; margin:0 0 12px}

/* ── KPI / 제어 ─────────────────────────────────────────── */
.row-top{display:grid; grid-template-columns:1.35fr 1fr; gap:14px; margin-top:16px}
.kpi-card{display:flex; align-items:center; justify-content:space-around; padding:24px 16px; margin:0}
.kpi{text-align:center}
.kpi-value{font-size:34px; font-weight:800; letter-spacing:-1px}
.kpi-value.green{color:var(--green)}
.kpi-label{color:var(--text-dim); font-size:12px; margin-top:4px}
.control-card{margin:0}
.control-buttons{display:grid; grid-template-columns:1fr 1fr; gap:12px}
.btn{
  border:0; border-radius:6px; cursor:pointer; color:#fff;
  font-size:13.5px; font-weight:700; padding:13px 10px; transition:filter .12s;
}
.btn:hover{filter:brightness(1.1)}
.btn:active{filter:brightness(.92)}
.btn-start{background:var(--green-btn)}
.btn-stop {background:var(--red-btn)}
.btn-start.running{background:#8a8f96; cursor:default}
.btn-add{background:var(--blue); padding:9px 16px; font-size:13px}
.btn-del{background:var(--red-btn); padding:5px 12px; font-size:12px; border-radius:5px}
.btn-save{background:var(--blue-deep); padding:5px 12px; font-size:12px; border-radius:5px}
.btn-report{background:var(--green-btn); padding:9px 16px; font-size:12.5px; margin-left:auto}
.btn-ghost{background:#3a3d43}
.btn-act{background:var(--blue-deep); padding:4px 10px; font-size:11.5px; border-radius:5px}

/* ── 공정 흐름 ─────────────────────────────────────────── */
.flow{display:flex; align-items:stretch; flex-wrap:wrap; gap:0}
.flow-step{
  background:var(--card-2); border:1px solid var(--border-soft); border-radius:7px;
  padding:12px 20px; text-align:center; min-width:118px;
}
.flow-step.run{border-color:#2f6b45}
.flow-step .nm{font-weight:700; font-size:13px}
.flow-step .mg{color:var(--blue); font-size:11.5px; margin-top:3px}
.flow-arrow{display:flex; align-items:center; padding:0 9px; color:var(--text-mute)}

/* ── 상태 그리드 ───────────────────────────────────────── */
.grid{display:grid; gap:12px}
.grid-4{grid-template-columns:repeat(4,minmax(0,1fr))}
.grid-3{grid-template-columns:repeat(3,minmax(0,1fr))}
@media (max-width:1300px){.grid-4{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (max-width:1100px){.grid-3{grid-template-columns:repeat(2,minmax(0,1fr))}
                          .row-top{grid-template-columns:1fr}}
@media (max-width:760px) {.grid-4,.grid-3{grid-template-columns:1fr}}

.tile{
  background:var(--card); border:1px solid var(--border);
  border-radius:var(--radius); padding:14px 15px; position:relative;
}
.tile.alarm{border-color:var(--red); box-shadow:0 0 0 1px rgba(239,68,68,.35)}
.tile.offline{opacity:.55}
.tile-head{display:flex; align-items:center; justify-content:space-between; margin-bottom:9px}
.tile-name{font-weight:700; font-size:13.5px}
.tile-tag{
  font-size:10.5px; color:var(--blue); border:1px solid #2f5f8a;
  background:#1b2c3d; border-radius:4px; padding:1px 7px;
}
.tile-tag.plain{color:var(--text-dim); border-color:var(--border-soft); background:transparent}
.tile-main{display:flex; align-items:flex-end; justify-content:space-between; gap:8px}
.big{font-size:26px; font-weight:800; letter-spacing:-.6px}
.big.ok{color:var(--green)}
.big.bad{color:var(--red)}
.big.blue{color:var(--blue)}
.unit-label{font-size:11px; color:var(--text-dim); margin-top:2px}
.tile-sub{display:flex; justify-content:space-between; margin-top:10px; font-size:11.5px; color:var(--text-dim)}
.tile-sub .v{color:var(--text)}
.right{text-align:right}

/* ── 로그 ──────────────────────────────────────────────── */
.log-card{margin-top:16px; min-height:190px}
.log-list{display:flex; flex-direction:column}
.log-row{
  display:flex; gap:14px; align-items:baseline;
  padding:9px 2px; border-bottom:1px solid #303237; font-size:12.5px;
}
.log-row:last-child{border-bottom:0}
.log-time{color:var(--text-mute); min-width:62px; font-variant-numeric:tabular-nums}
.log-tag{font-weight:700; min-width:44px}
.log-tag.DEVIATION{color:var(--red)}
.log-tag.DONE{color:var(--green)}
.log-tag.CONTROL{color:var(--blue)}
.log-tag.INFO{color:var(--text-dim)}
.log-msg{color:var(--text-dim)}

/* ── Vision ────────────────────────────────────────────── */
/* 칸 높이는 카메라가 붙기 전과 붙은 뒤가 같아야 한다. min-height 만 두면
   영상이 들어온 칸만 원본 해상도(640x480)만큼 늘어나 줄 전체가 따라 커진다.
   높이를 고정하고 영상을 그 안으로 줄여 넣는다 (아래 .vision-shot). */
.vision-cell{background:#000; border-radius:6px; padding:12px;
             height:var(--vision-cell-h);
             display:flex; flex-direction:column; overflow:hidden}
.vision-live{font-size:11.5px; font-weight:700; color:var(--green)}
.vision-live.off{color:var(--red)}
/* min-height:0 이 없으면 flex 항목이 내용보다 작아지지 못해, 결국 영상 원본
   크기가 칸을 밀어 올린다 (flex 기본값 min-height:auto). */
.vision-body{flex:1; min-height:0;
             display:flex; flex-direction:column; align-items:center; justify-content:center; gap:6px}
.vision-icon{font-size:34px; color:#4a4d52}
.vision-title{font-weight:700; font-size:14px}
.vision-manager{color:var(--text-dim); font-size:12px}
.vision-meta{display:flex; justify-content:space-between; font-size:11px; color:var(--text-mute);
             border-top:1px solid #1e1e1e; padding-top:8px; margin-top:8px}
.vision-meta .bad{color:var(--red)}
.vision-live .dim{font-weight:400; color:var(--text-mute)}
.vision-manager .bad{color:var(--red)}
/* 카메라 정지영상 미리보기 — RTSP 는 브라우저가 직접 재생하지 못한다 */
.vision-live.wait{color:var(--amber)}
/* 남은 자리만큼만 차지하고, 비율은 지킨 채 그 안으로 줄여 넣는다.
   flex-basis 를 0 으로 두어야 원본 해상도가 칸 크기 계산에 끼어들지 않는다. */
.vision-shot{
  flex:1 1 0; width:100%; min-height:0; max-width:100%; max-height:100%;
  object-fit:contain; border-radius:4px; background:#000;
}

/* ── 카메라 설정 표 ─────────────────────────────────────── */
.badge{
  display:inline-block; font-size:11px; font-weight:700; border-radius:3px;
  padding:2px 8px; border:1px solid transparent;
}
.badge.ok  {background:#0f3d24; color:#5ee29a; border-color:#1c6b41}
.badge.bad {background:#4a1d1d; color:#ff9b9b; border-color:#7d2f2f}
.badge.wait{background:#3a3320; color:#e6c778; border-color:#6b5a2f}
.badge.off {background:#2f3238; color:var(--text-mute); border-color:#43464d}
.badge.warn{background:#3a3320; color:#e6c778; border-color:#6b5a2f}
.badge.dim {background:#2b2f36; color:var(--text-mute); border-color:var(--border-soft)}
.mono{font-family:ui-monospace, SFMono-Regular, Menlo, monospace; font-size:11.5px}
.nowrap{white-space:nowrap}

/* ── 장비 통합 카드 위젯 ─────────────────────────────────
   장비 한 대의 담당자·가동상태·작업정보·계측값을 한 장으로 읽는 카드.
   위에서 아래로 [머리말] → [가동상태] → [썸네일+작업정보] → [계측 표] */
/* 위젯 본문은 밝은 바탕(#fff·#111)이므로 밝은 테마 색을 쓴다.
   .widget.dark 인 경우만 아래에서 따로 덮는다. */
.ec-box{display:flex; flex-direction:column; height:100%; gap:4px; min-height:0}
.ec-head{display:flex; align-items:baseline; gap:8px; flex:0 0 auto}
.ec-name{font-size:13px; font-weight:800; color:#1864ab; letter-spacing:-.2px;
         overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.ec-mgr{margin-left:auto; font-size:11px; color:#868e96; white-space:nowrap}
.ec-state{
  flex:0 0 auto; text-align:center; font-size:12px; font-weight:800;
  letter-spacing:1px; padding:1px 0;
}
.ec-state.run  {color:#2b8a3e}
.ec-state.stop {color:#868e96}
.ec-state.fault{color:#c92a2a}

.ec-body{
  flex:1 1 auto; min-height:0; display:flex; gap:8px; align-items:stretch;
  border:2px solid #37b24d; border-radius:6px; padding:6px; overflow:hidden;
}
.ec-box.bad .ec-body{border-color:#e03131}
.ec-thumb{
  flex:0 0 34%; min-width:0; border-radius:4px; overflow:hidden;
  background:#e9ecef; display:flex; align-items:center; justify-content:center;
}
.ec-shot{width:100%; height:100%; max-width:100%; max-height:100%; object-fit:cover}
.ec-thumb-ph{font-size:26px; color:#adb5bd}
.ec-info{flex:1 1 auto; min-width:0; display:flex; flex-direction:column;
         justify-content:center; gap:3px}
.ec-info-row{display:flex; gap:6px; font-size:11.5px; line-height:1.35}
.ec-info-row .k{flex:0 0 52px; color:#868e96}
.ec-info-row .k::after{content:' :'}
.ec-info-row .v{
  flex:1 1 auto; min-width:0; font-weight:600;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.ec-info-row .dim{color:#adb5bd; font-style:normal}

.ec-table{width:100%; border-collapse:collapse; table-layout:fixed; flex:0 0 auto}
.ec-table th{
  font-size:10.5px; font-weight:700; color:#868e96;
  padding:3px 2px; border:1px solid #ced4da; background:#f1f3f5;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.ec-table td{
  font-size:12px; font-weight:700; text-align:center; padding:4px 2px;
  border:1px solid #ced4da;
}
.ec-table td small{font-size:9px; font-weight:600; opacity:.65; margin-left:1px}
.ec-table td.bad{color:#e03131}
.ec-run{font-variant-numeric:tabular-nums; color:#1864ab}
.ec-gear{
  border:0; background:transparent; cursor:pointer; font-size:14px;
  line-height:1; padding:0; color:#868e96;
}
.ec-gear:hover{color:#111}

.widget.dark .ec-name{color:#74c0fc}
.widget.dark .ec-mgr,
.widget.dark .ec-info-row .k,
.widget.dark .ec-table th{color:var(--text-dim)}
.widget.dark .ec-thumb{background:#11161d}
.widget.dark .ec-table th{background:#33363c; border-color:#4a4d53}
.widget.dark .ec-table td{border-color:#4a4d53}
.widget.dark .ec-gear:hover{color:var(--text)}

/* 작업 정보 입력 (위젯 설정 모달) */
.job-list{display:flex; flex-direction:column; gap:6px; margin-bottom:6px}
.job-row{display:flex; align-items:center; gap:8px}
.job-row .job-key{flex:0 0 62px; font-size:12px; color:var(--text-dim)}
.job-row .input{flex:1 1 auto; min-width:0}

/* ── 작업자 배정 칩 ─────────────────────────────────────── */
.sub-title{
  font-size:13px; font-weight:700; color:var(--text-dim);
  margin:18px 0 8px; padding-top:14px; border-top:1px solid var(--border-soft);
}

/* ── 장비 통합 카드: 썸네일 자리에 무엇을 띄울지 고르기 ── */
.btn-sm{font-size:12px; font-weight:600; padding:7px 10px}
.thumb-modes{display:flex; flex-direction:column; gap:6px; font-size:13px}
.thumb-modes label{display:flex; align-items:center; gap:7px; cursor:pointer}
.thumb-pick{display:flex; gap:12px; align-items:flex-start; margin-top:10px}
.thumb-prev{
  flex:0 0 132px; height:99px; border-radius:5px; overflow:hidden;
  background:#11161d; border:1px solid var(--border-soft);
  display:flex; align-items:center; justify-content:center;
}
.thumb-prev img{width:100%; height:100%; object-fit:cover}
.thumb-prev .dim{font-size:11.5px; color:var(--text-mute)}
.thumb-acts{flex:1 1 auto; min-width:0; display:flex; flex-direction:column; gap:6px}
.chip{
  display:inline-flex; align-items:center; gap:6px; font-size:12px;
  background:#2b3440; color:#cfe0f0; border:1px solid #3c4b5c;
  border-radius:12px; padding:2px 4px 2px 10px; margin:2px 4px 2px 0;
}
.chip.bad{background:#4a1d1d; color:#ffc9c9; border-color:#7d2f2f}
.chip-x{
  border:0; background:transparent; color:inherit; cursor:pointer;
  font-size:14px; line-height:1; padding:0 5px; opacity:.65;
}
.chip-x:hover{opacity:1}

/* ── 표 ────────────────────────────────────────────────── */
.filter-bar{display:flex; align-items:center; gap:9px; flex-wrap:wrap; margin-bottom:14px}
.filter-label{color:var(--text-dim); font-size:12.5px}
.tilde{color:var(--text-mute)}
/* 상단 전역 경고 — 어느 화면에 있든 이탈·수집중단이 보여야 한다 */
.gl-alert{
  border:none; border-radius:6px; padding:6px 12px; font-size:12.5px;
  font-weight:bold; color:#fff; cursor:pointer; letter-spacing:-.2px;
  background:var(--red); animation:glpulse 1.6s ease-in-out infinite;
}
.gl-alert.stale{background:var(--amber); color:#221a00}
@keyframes glpulse{0%,100%{opacity:1} 50%{opacity:.62}}
@media (prefers-reduced-motion:reduce){.gl-alert{animation:none}}

/* 계측 이력 · 수정이력 */
#readingTable td.rv-act{white-space:nowrap}
#readingTable td.warn-txt{color:var(--amber); font-weight:bold}
#readingTable .rv-fix{color:var(--amber); margin-left:4px; cursor:help}
#correctionTable td{white-space:nowrap}
#correctionTable .old{color:var(--text-mute); text-decoration:line-through}
#correctionTable .new{color:var(--amber); font-weight:bold}
.modal-info.warn{border-left:3px solid var(--amber); padding-left:9px}
#mnValues .mn-row{display:grid; grid-template-columns:1fr 120px 46px;
                  gap:8px; align-items:center; padding:4px 3px}
#mnValues .mn-row .k{font-size:12.5px; color:var(--text)}
#mnValues .mn-row .u{font-size:11.5px; color:var(--text-mute)}
#mnValues .mn-row .input{min-width:0; padding:6px 9px}

/* 조회 막대 안의 체크박스 (계측 이력의 '원시값·환산식' 등) */
.chk-filter{display:flex; align-items:center; gap:6px; font-size:12.5px;
            color:var(--text-dim); cursor:pointer; user-select:none}
.chk-filter input{accent-color:var(--blue)}
/* 계측 이력 표 — 항목 수만큼 열이 늘어나므로 셀을 좁게 잡는다 */
#readingTable th, #readingTable td{white-space:nowrap; font-variant-numeric:tabular-nums}
#readingTable td.dev{color:var(--red); font-weight:bold}
#readingTable .unit{color:var(--text-mute); font-size:10.5px; margin-left:2px}
.add-bar{display:flex; gap:9px; align-items:center; flex-wrap:wrap; margin-bottom:14px}
.input{
  background:#3a3d43; border:1px solid #45484e; color:var(--text);
  border-radius:5px; padding:8px 11px; font-size:13px; outline:none; min-width:120px;
}
.input:focus{border-color:var(--blue)}
.input::placeholder{color:var(--text-mute)}
.input.grow{flex:1}
.input-num{width:110px; min-width:110px}
.input-date{width:150px; min-width:150px; color-scheme:dark}
.input-select{min-width:120px}
.map-cell{width:58px; min-width:58px}
.btn-mini{padding:3px 8px; font-size:11px}
/* 카드 하단 CCP 항목 칩 (살균 유지시간·금속 검출·pH …) */
.ccp-strip{display:flex; flex-wrap:wrap; gap:6px; margin-top:9px;
  padding-top:9px; border-top:1px solid var(--border)}
.ccp-chip{font-size:10.5px; color:var(--text-dim); background:#2b2f36;
  border:1px solid var(--border); border-radius:5px; padding:2px 7px}
.ccp-chip b{color:var(--text); margin-left:2px}
.ccp-chip.bad{color:#ff9b9b; background:#4a1d1d; border-color:#7d2f2f}
.ccp-chip.bad b{color:#ff9b9b}
/* 항목별 CCP 기준 편집 */
.ccp-list{max-height:44vh; overflow-y:auto; margin:10px 0}
.ccp-row{display:flex; align-items:center; gap:8px; padding:7px 0;
  border-bottom:1px solid var(--border)}
.ccp-row .nm{flex:1; font-size:12.5px}
.ccp-row .nm .dim{font-size:11px}
.ccp-row .input{width:78px; padding:5px 7px; text-align:center}
.ccp-row select.input{width:150px; text-align:left}
.ccp-row .unit{font-size:11px; color:var(--text-mute); width:44px}
.ccp-on{width:16px; height:16px; accent-color:var(--blue)}
.input-mini{width:64px; min-width:64px; padding:5px 7px; text-align:center;
            background:#1f2125; border:1px solid #45484e}
/* 열이 많은 표는 창이 좁으면 옆으로 민다. HACCP 기록표는 열을 숨기면 근거가
   잘리므로 열은 그대로 두고, 잘리는 쪽에만 그림자를 남긴다 (끝까지 밀면 사라진다). */
.table-scroll{
  overflow-x:auto;
  background:
    linear-gradient(90deg,  var(--card) 40%, rgba(42,44,49,0)) left  / 26px 100% no-repeat local,
    linear-gradient(270deg, var(--card) 40%, rgba(42,44,49,0)) right / 26px 100% no-repeat local,
    radial-gradient(farthest-side at 0    50%, rgba(0,0,0,.45), rgba(0,0,0,0)) left  / 13px 100% no-repeat scroll,
    radial-gradient(farthest-side at 100% 50%, rgba(0,0,0,.45), rgba(0,0,0,0)) right / 13px 100% no-repeat scroll;
}
.table{width:100%; border-collapse:collapse; font-size:12.5px}
.table thead th{
  background:var(--thead); color:#dbe6f0; font-weight:700; font-size:12px;
  padding:11px 10px; text-align:center; white-space:nowrap;
}
.table thead th:first-child{border-radius:5px 0 0 5px}
.table thead th:last-child {border-radius:0 5px 5px 0}
.table tbody td{
  padding:12px 10px; border-bottom:1px solid #313339;
  text-align:center; color:var(--text-dim); vertical-align:middle;
}
.table tbody tr:hover td{background:#2e3036}
.table .tl{text-align:left}
.table td.tl{text-align:left; color:var(--text)}
.table .dim{color:var(--text-mute); font-size:11px}
.table .bad{color:var(--red); font-weight:700}
.table .ok{color:var(--green)}
.loc-list{display:flex; flex-direction:column}
.loc-row{display:flex; align-items:center; justify-content:space-between;
         padding:11px 4px; border-bottom:1px solid #313339}
.loc-row:last-child{border-bottom:0}

/* ── 모달 / 토스트 ─────────────────────────────────────── */
.modal-back{position:fixed; inset:0; background:rgba(0,0,0,.62);
            display:flex; align-items:center; justify-content:center; z-index:50}
.modal{background:var(--card); border:1px solid var(--border); border-radius:10px;
       padding:22px; width:min(430px,92vw); max-height:90vh; overflow:auto}
.modal h3{margin:0 0 6px; font-size:15px}
.modal label{display:block; font-size:12px; color:var(--text-dim); margin:12px 0 5px}
.modal .input{width:100%}
.modal-info{font-size:12px; color:var(--text-dim); background:var(--card-2);
            border:1px solid var(--border-soft); border-radius:6px; padding:10px; margin-top:8px}
.modal-buttons{display:flex; justify-content:flex-end; gap:9px; margin-top:18px}
.toast{
  position:fixed; left:50%; bottom:30px; transform:translateX(-50%);
  background:#2f3238; border:1px solid var(--border-soft); color:var(--text);
  padding:11px 20px; border-radius:7px; font-size:13px; z-index:60;
  box-shadow:0 6px 20px rgba(0,0,0,.45);
}
.toast.err{border-color:var(--red); color:#ffb4b4}

/* ── 로그인 화면 ────────────────────────────────────────── */
.login-back{
  position:fixed; inset:0; background:var(--bg); z-index:100;
  display:flex; align-items:center; justify-content:center;
}
.login-box{
  background:var(--card); border:1px solid var(--border); border-radius:10px;
  padding:32px 30px; width:min(360px,92vw);
}
.login-brand{text-align:center; margin-bottom:22px; padding-bottom:18px;
             border-bottom:1px solid var(--border)}
.login-brand .brand-site,.login-brand .brand-sys{font-size:19px}
.login-box label{display:block; font-size:12px; color:var(--text-dim); margin:14px 0 5px}
.login-box .input{width:100%}
.login-submit{width:100%; margin-top:22px; padding:12px}
.login-error{
  margin-top:14px; font-size:12.5px; color:#ffb4b4;
  background:#3a1d1d; border:1px solid #6b3030; border-radius:6px; padding:9px 11px;
}
.login-hint{margin-top:18px; font-size:11px; color:var(--text-mute); line-height:1.7;
            text-align:center}

/* ── 사이드바 사용자 영역 ───────────────────────────────── */
.user-box{margin-top:auto; padding-top:14px; border-top:1px solid var(--border)}
.user-name{font-size:12.5px; font-weight:700}
.user-role{font-size:11px; color:var(--blue); margin-top:2px}
.user-actions{display:flex; gap:10px; margin-top:8px}
.link-btn{
  background:none; border:0; padding:0; cursor:pointer;
  color:var(--text-mute); font-size:11px; text-decoration:underline;
}
.link-btn:hover{color:var(--text-dim)}
.user-box + .conn{margin-top:12px}
/* 초기 비밀번호 안내 — 로그인을 막지 않고 표시만 한다 */
.pw-notice{
  margin-top:8px; font-size:10.5px; font-weight:700; color:var(--amber);
  border:1px solid #6b5520; background:#3a2f14; border-radius:5px;
  padding:3px 7px; display:inline-block;
}
/* 설정 화면 — 내 계정 카드 */
.account-row{display:flex; align-items:center; gap:12px}
.account-name{font-size:14px; font-weight:700}
.account-role{font-size:11.5px; color:var(--blue); margin-top:2px}
.account-row .badge-warn{margin-left:0}

/* ── 전자결재 배지 ──────────────────────────────────────── */
.stamp{
  display:inline-block; font-size:10.5px; font-weight:700; border-radius:4px;
  padding:2px 7px; white-space:nowrap;
}
.stamp.none    {color:var(--text-mute); border:1px solid var(--border-soft)}
.stamp.reviewed{color:var(--amber);  border:1px solid #6b5520; background:#3a2f14}
.stamp.approved{color:var(--green);  border:1px solid #2f6b45; background:#1d3d2a}
.stamp.invalid {color:var(--red);    border:1px solid #6b3030; background:#3d1d1d}

/* ── 이탈 알람 강조 ─────────────────────────────────────
   이탈이 난 장비를 어느 화면에서 보든 1초 안에 찾을 수 있어야 한다.
   장비마다 고유색(제목줄·표식·카드)이 이미 있으므로 "빨갛게 칠하기"만으로는
   묻힌다. 그래서 화면이 달라도 같은 세 가지를 함께 쓴다.
     ① 빨간 겹테두리 + 번지는 후광   ② 맥동(움직임)   ③ "⚠ 이탈" 글자 배지
   ③ 은 색각 이상 사용자를 위한 것이다 — 색이 안 보여도 글자는 읽힌다.
   움직임에 민감한 사용자를 위해 prefers-reduced-motion 에서는 맥동을 끄고
   테두리·배지만 남긴다 (강조는 유지된다). */
@keyframes dev-glow{
  0%,100%{box-shadow:0 0 0 2px var(--red), 0 0 12px 2px rgba(239,68,68,.45)}
  50%    {box-shadow:0 0 0 3px var(--red), 0 0 26px 8px rgba(239,68,68,.85)}
}
@keyframes dev-blink{50%{opacity:.42}}
@keyframes dev-ripple{
  0%  {transform:scale(1);   opacity:.85}
  100%{transform:scale(3.6); opacity:0}
}

/* "⚠ 이탈" 배지. 매 주기 다시 그리는 카드(대시보드·모니터링)는 이탈일 때만
   이 조각을 찍고, 한 번만 그리고 갱신만 하는 위젯은 미리 박아 두고 감춰 둔다. */
.dev-badge{
  display:inline-flex; align-items:center; gap:2px; flex:0 0 auto;
  font-size:10px; font-weight:800; color:#fff; background:var(--red);
  border:1px solid #ff8f8f; border-radius:3px; padding:0 5px;
  white-space:nowrap; text-shadow:none;
  animation:dev-blink 1.1s ease-in-out infinite;
}
.widget .dev-badge{display:none}
.widget.dev-alarm .dev-badge{display:inline-flex}
/* 누르면 "무엇이 왜 이탈인가"를 설명한다 */
.dev-badge[data-target]{cursor:pointer}
.dev-badge[data-target]:hover{background:#ff6b6b}
.hotspot.alarm .tag{cursor:pointer}
/* 도면 위 표식은 배지를 따로 달지 않는다 — 이름표가 이미 "⚠ 장비명"이라
   그 자체가 누를 자리다 (표식이 여럿이면 배지가 도면을 덮는다). */
.hotspot .dev-badge{display:none}

/* 위젯 카드 — 테두리 색은 장비 색이 인라인으로 박혀 있어 못 덮는다.
   그래서 겹테두리는 box-shadow 로 두른다. */
.widget.dev-alarm{animation:dev-glow 1.2s ease-in-out infinite}
.widget.dev-alarm .widget-body{background:rgba(239,68,68,.09)}
.widget.dark.dev-alarm .widget-body{background:rgba(239,68,68,.16)}

/* 통합 대시보드 장비/보관소 카드 */
.tile.dev{
  border-color:var(--red); background:linear-gradient(180deg,#33262a,var(--card));
  animation:dev-glow 1.4s ease-in-out infinite;
}
.tile.dev .tile-name{color:#ffb4b4}
/* 공정 흐름 — 어느 공정에서 났는지 띠에서 바로 보이게 */
.flow-step.dev{background:#3a2126; box-shadow:0 0 0 1px var(--red)}
.flow-step.dev .nm{animation:dev-blink 1.2s ease-in-out infinite}

/* 현장 모니터링 Vision 칸 */
.vision-cell.dev-alarm{animation:dev-glow 1.4s ease-in-out infinite}
.vision-cell.dev-alarm .vision-title{color:#ffb4b4}

/* 이탈 배너 — 이미 빨갛지만, 새 이탈이 뜬 것을 놓치지 않게 숨을 쉬게 한다 */
.wg-alert{animation:dev-blink 1.6s ease-in-out infinite}

@media (prefers-reduced-motion: reduce){
  .dev-badge,.widget.dev-alarm,.tile.dev,.vision-cell.dev-alarm,
  .flow-step.dev .nm,.wg-alert,.hotspot.alarm .pin,.hotspot.cam.alarm .pin{
    animation:none;
  }
  .hotspot.alarm .pin::after{display:none}
  .widget.dev-alarm,.tile.dev,.vision-cell.dev-alarm{
    box-shadow:0 0 0 3px var(--red), 0 0 16px 4px rgba(239,68,68,.6);
  }
}

/* ── 이탈 설명 모달 ─────────────────────────────────────
   한 줄에 [항목] [측정값] [한계기준] [얼마나 벗어났는가] 를 나란히 둔다.
   측정값과 기준이 서로 떨어져 있으면 읽는 사람이 스스로 비교해야 한다. */
.dv-list{margin-top:10px; border:1px solid var(--border-soft); border-radius:6px;
         background:var(--card-2); overflow:hidden}
.dv-row{display:grid; grid-template-columns:92px 96px 128px 1fr; gap:10px;
        align-items:baseline; padding:9px 11px; border-bottom:1px solid #313339;
        font-size:12px}
.dv-row:last-child{border-bottom:0}
.dv-k{color:var(--text-dim); font-weight:700}
.dv-v{color:var(--red); font-weight:800; font-size:14px}
.dv-b{color:var(--text-dim)}
.dv-w{color:var(--text)}
.dv-none{padding:12px; font-size:12px; color:var(--green); font-weight:700}
@media (max-width:620px){
  .dv-row{grid-template-columns:1fr 1fr; gap:4px 10px}
  .dv-w{grid-column:1 / -1}
}

/* ── 이력: 결재 현황 띠 ─────────────────────────────────
   검토·승인은 HACCP 이 요구하는 절차인데, 남은 건수가 어디에도 안 보이면
   버튼만 있고 아무도 누르지 않는 상태가 된다. 표 위에 세워 두고, 누르면
   바로 그 상태만 걸러 볼 수 있게 한다. */
.appr-bar{
  display:flex; align-items:center; flex-wrap:wrap; gap:7px;
  margin:0 0 12px; padding:9px 11px;
  background:var(--card-2); border:1px solid var(--border-soft); border-radius:7px;
}
.appr-title{font-size:11.5px; font-weight:700; color:var(--text-dim); margin-right:3px}
.appr-chip{
  display:inline-flex; align-items:center; gap:5px; cursor:pointer;
  font-size:11.5px; font-weight:700; color:var(--text-dim);
  background:transparent; border:1px solid var(--border-soft);
  border-radius:14px; padding:3px 11px;
}
.appr-chip b{font-size:13px; color:var(--text)}
.appr-chip:hover{border-color:var(--blue); color:var(--text)}
.appr-chip.on{border-color:var(--blue); background:#1b2c3d; color:#cfe6ff}
.appr-chip.dev  b{color:#ffb4b4}
.appr-chip.warn {border-color:#6b5520; background:#3a2f14; color:#ffd88a}
.appr-chip.warn b{color:var(--amber)}
.appr-chip.ok   b{color:var(--green)}
.appr-chip.zero {opacity:.55}
.appr-chip.zero b{color:var(--text-mute)}
.appr-note{font-size:11.5px; color:var(--amber); margin-left:auto}
.appr-note.done{color:var(--green)}
.appr-none{font-size:12px; color:var(--green); font-weight:700}

/* ── 이력: 작업자(조치) / 관리자(검토·승인) 구분 ────────── */
/* 조치는 작업자가, 검토·승인은 관리자가 한다. 표에서 두 칸이 같은 색이면
   기록만 보고는 누가 한 일인지 갈리지 않으므로 머리글에 역할을 박아 둔다. */
.th-role{
  display:inline-block; margin-left:5px; font-size:9.5px; font-weight:700;
  border-radius:3px; padding:1px 5px; vertical-align:middle;
}
.th-role.worker {color:#8ec5ff; border:1px solid #2f4f6b; background:#16283a}
.th-role.manager{color:#8fe0ac; border:1px solid #2f6b45; background:#16301f}
.table .by{font-weight:700}
.table .by.worker{color:#8ec5ff}
.table .ck{color:var(--green); font-size:10.5px; font-weight:700; margin-top:3px}

/* 검토 확인 모달 — 작업자가 남긴 조치이력 */
.act-trace{
  margin-top:10px; border:1px solid var(--border-soft); border-radius:6px;
  background:var(--card-2); padding:4px 10px;
}
.at-row{display:flex; gap:10px; padding:7px 0; border-bottom:1px solid #313339; font-size:12px}
.at-row:last-child{border-bottom:0}
.at-row .k{width:96px; flex:none; color:var(--text-mute)}
.at-row .v{color:var(--text); font-weight:700; word-break:break-all}
.at-none{padding:10px 0; font-size:12px}
.at-none.bad{color:#ffb4b4; font-weight:700}
.at-none.dim{color:var(--text-mute)}
.modal-info .dim{color:var(--text-mute)}

/* ── 시스템 관리 ────────────────────────────────────────── */
.spacer{flex:1}
.section-title .btn{font-weight:700}
.stat{
  background:var(--card); border:1px solid var(--border);
  border-radius:var(--radius); padding:16px;
}
.stat .lbl{font-size:12px; color:var(--text-dim)}
.stat .val{font-size:22px; font-weight:800; margin-top:6px}
.stat .val.ok{color:var(--green)}
.stat .val.bad{color:var(--red)}
.stat .sub{font-size:11px; color:var(--text-mute); margin-top:6px; word-break:break-all}
.mono{font-family:ui-monospace,Menlo,Consolas,monospace; font-size:11px}
.kv{display:flex; justify-content:space-between; gap:14px; padding:7px 2px;
    border-bottom:1px solid #313339; font-size:12.5px}
.kv:last-child{border-bottom:0}
.kv .k{color:var(--text-dim); flex:0 0 auto}
/* 백업 파일명·아카이브 경로처럼 띄어쓰기 없는 긴 값이 화면을 밀어내지 않게 한다 */
.kv > :last-child{min-width:0; text-align:right; overflow-wrap:anywhere}

/* ── 통신 설정 ──────────────────────────────────────────── */
.hint-inline{font-size:11.5px; color:var(--text-mute); font-weight:400}
.conn-form,.dp-form{
  display:flex; flex-wrap:wrap; gap:10px; align-items:flex-end;
  padding:14px; background:var(--card-2); border:1px solid var(--border-soft);
  border-radius:7px; margin-bottom:14px;
}
.field{display:flex; flex-direction:column; gap:5px; min-width:150px; flex:1}
.field.narrow{min-width:88px; max-width:110px; flex:0 0 auto}
.field.btn-field{min-width:auto; flex:0 0 auto}
.field label{font-size:11px; color:var(--text-dim)}
.field .input{width:100%}
.profile-note{
  margin:-6px 0 14px; padding:9px 12px; border-left:3px solid var(--blue);
  background:#1e2a36; border-radius:0 5px 5px 0; font-size:12px; color:var(--text-dim);
}
.conn-status{font-size:11px; font-weight:700; border-radius:4px; padding:2px 8px}
.conn-status.OK      {color:var(--green); border:1px solid #2f6b45; background:#1d3d2a}
.conn-status.ERROR   {color:var(--red);   border:1px solid #6b3030; background:#3d1d1d}
.conn-status.DISABLED{color:var(--text-mute); border:1px solid var(--border-soft)}
.conn-status.UNKNOWN {color:var(--amber); border:1px solid #6b5520; background:#3a2f14}
.btn-group{display:flex; gap:5px; justify-content:center; flex-wrap:wrap}
.modal-wide{width:min(620px,94vw)}
.check-list{
  max-height:200px; overflow-y:auto; background:var(--card-2);
  border:1px solid var(--border-soft); border-radius:6px; padding:9px; margin-top:6px;
}
.check-list label{display:flex; align-items:center; gap:8px; padding:5px 3px;
                  font-size:12.5px; color:var(--text); cursor:pointer}
.check-inline{display:flex !important; align-items:center; gap:8px; margin-top:14px !important;
              font-size:12.5px; color:var(--text-dim); cursor:pointer}
.row-2{display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-top:6px}
.row-3{display:grid; grid-template-columns:1fr 1fr 1fr; gap:12px; margin-top:6px}
.row-2 label{display:block; font-size:12px; color:var(--text-dim); margin:12px 0 5px}
.row-2 .input{width:100%}
.val-cell{font-variant-numeric:tabular-nums}
.val-cell .raw{color:var(--text-mute); font-size:10.5px}
.toggle{cursor:pointer; font-size:11px; border-radius:4px; padding:2px 8px;
        border:1px solid var(--border-soft); background:transparent; color:var(--text-mute)}
.toggle.on{color:var(--green); border-color:#2f6b45; background:#1d3d2a}

/* ════════════════════════════════════════════════════════════
   위젯 통합 대시보드 (5.png 시안)
   ════════════════════════════════════════════════════════════ */
.wg-canvas{
  position:relative; height:560px; background:#0f1012;
  border:1px solid var(--border); border-radius:var(--radius);
  overflow:hidden; user-select:none;
}
/* 도면 + 장비 표식을 한 덩어리로 확대/이동시킨다 (핫스팟 좌표는 이 층 기준) */
.wg-stage{
  position:absolute; top:0; left:0; width:100%;
  transform-origin:50% 50%; will-change:transform;
}
.wg-stage.pannable{cursor:grab}
.wg-stage.panning{cursor:grabbing}
/* 크기 조절 중에는 도면 위 어디서든 커서 모양을 유지한다 */
.wg-canvas.resizing, .wg-canvas.resizing *{cursor:nwse-resize !important}
.wg-image{display:block; width:100%; height:auto; pointer-events:none}
.wg-hotspots{position:absolute; inset:0; pointer-events:none}
.wg-widgets{position:absolute; inset:0; pointer-events:none}
.wg-widgets > *{pointer-events:auto}

/* ── 도면 크기/위치 조절 테두리 ─────────────────────────── */
/* 도면 자체를 끌어 옮기고, 네 모서리 손잡이를 끌어 크기를 바꾼다.        */
.wg-frame{
  position:absolute; pointer-events:none;
  outline:1px dashed rgba(59,155,245,.55); outline-offset:0;
}
.wg-handles{position:absolute; inset:0; pointer-events:none; z-index:880}
.wg-handle{
  position:absolute; width:14px; height:14px; margin:-7px 0 0 -7px;
  pointer-events:auto;
  background:var(--blue); border:2px solid #fff; border-radius:3px;
  box-shadow:0 1px 4px rgba(0,0,0,.5);
}
.wg-handle.nw,.wg-handle.se{cursor:nwse-resize}
.wg-handle.ne,.wg-handle.sw{cursor:nesw-resize}

/* 확대율 표시 + 맞춤/초기화 (조절은 마우스로, 이건 되돌리기용) */
.wg-viewtools{
  position:absolute; top:8px; right:8px; z-index:900;
  display:flex; align-items:center; gap:6px;
  background:rgba(20,21,24,.82); border:1px solid var(--border);
  border-radius:6px; padding:4px 6px;
}
.wg-zoom{
  font-size:11.5px; font-weight:700; color:var(--text);
  font-variant-numeric:tabular-nums; min-width:38px; text-align:center;
}
.wg-viewtools button{
  background:#3a3d43; color:var(--text); border:0; border-radius:4px;
  padding:3px 8px; font-size:11.5px; cursor:pointer;
}
.wg-viewtools button:hover{background:#4a4e55}
.wg-viewtools button.primary{background:var(--blue-deep)}
.wg-viewtools button.primary:hover{background:var(--blue)}
/* 확정된 상태 — 손잡이 없이 자물쇠 표시만 남는다 */
.wg-viewtools.locked .wg-zoom{color:var(--text-dim)}

/* 표시 영역 높이 조절 손잡이 */
.wg-hgrip{
  height:10px; margin-top:-1px; cursor:ns-resize;
  border:1px solid var(--border); border-top:0;
  border-radius:0 0 var(--radius) var(--radius);
  background:var(--card-2);
}
.wg-hgrip::after{
  content:''; display:block; width:46px; height:2px; margin:3px auto;
  background:var(--border-soft); border-radius:2px;
}
.wg-hgrip:hover::after{background:var(--blue)}

/* ── 도면 없음 안내 ─────────────────────────────────────── */
.wg-empty{
  position:absolute; inset:0; display:flex; flex-direction:column;
  align-items:center; justify-content:center; gap:10px; text-align:center;
}
.wg-empty-icon{font-size:48px; color:#33363c}
.wg-empty-title{font-size:16px; font-weight:700}
.wg-empty-desc{font-size:12.5px; color:var(--text-dim); line-height:1.8}
.wg-empty .btn{margin-top:10px}

/* ── 이탈 알림 배너 ─────────────────────────────────────── */
.wg-alert{
  background:var(--red); color:#fff; font-weight:700; font-size:14px;
  padding:9px 16px; border-radius:6px; margin-bottom:10px; text-align:center;
}

/* ── 핫스팟 (도면 위 장비 표식) ─────────────────────────── */
.hotspot{
  position:absolute; pointer-events:auto;
  /* 도면을 축소해도 표식은 읽을 수 있는 크기를 유지한다 (--hs-scale = 1/확대율) */
  transform:translate(-50%,-50%) scale(var(--hs-scale,1));
  cursor:pointer; display:flex; flex-direction:column; align-items:center; gap:3px;
}
/* 표식 색 = 장비 색 (--tc 는 JS 가 넣는다). 같은 장비의 위젯 제목줄과 같은 색이다. */
.hotspot .pin{
  position:relative;
  width:16px; height:16px; border-radius:50%;
  background:var(--tc, #3b9bf5); border:2px solid #fff;
  box-shadow:0 0 0 3px var(--tc-ring, rgba(59,155,245,.35));
}
/* 이탈 중에는 장비 색(핀 속)을 유지한 채, 빨간 후광이 번지고 물결이 퍼진다.
   핀만 빨갛게 하면 어느 장비인지가 색에서 사라지므로 속은 그대로 두고,
   대신 이름표를 빨갛게 바꿔 "⚠ 장비명"이 도면 위에서 바로 읽히게 한다. */
.hotspot.alarm{z-index:20}
.hotspot.alarm .pin{
  width:20px; height:20px; border-color:#fff;
  box-shadow:0 0 0 5px rgba(239,68,68,.75), 0 0 16px 5px rgba(239,68,68,.55);
  animation:pulse 1.1s ease-in-out infinite;
}
@keyframes pulse{50%{box-shadow:0 0 0 11px rgba(239,68,68,.28),
                                0 0 26px 10px rgba(239,68,68,.8)}}
/* 퍼져 나가는 물결 — 움직임이 있어야 정지 화면 속에서 눈이 먼저 간다 */
.hotspot.alarm .pin::after{
  content:''; position:absolute; left:50%; top:50%;
  width:20px; height:20px; margin:-10px 0 0 -10px;
  border-radius:50%; border:2px solid var(--red); pointer-events:none;
  animation:dev-ripple 1.5s ease-out infinite;
}
.hotspot.alarm .tag{
  background:var(--red); border-color:#fff; color:#fff;
  box-shadow:0 0 10px rgba(239,68,68,.7);
}
.hotspot.alarm .tag::before{content:'⚠ '}

/* ── 비전 위젯 표식 : 원 대신 카메라 모양 ─────────────────
   연결되면 초록, 끊기면 빨강. 색은 SVG 의 currentColor 로 흘러간다. */
.hotspot.cam .pin{
  width:auto; height:auto; border:0; border-radius:0;
  background:none; box-shadow:none; display:block;
  filter:drop-shadow(0 1px 3px rgba(0,0,0,.6));
}
.hotspot.cam .pin svg{display:block}
.hotspot.cam-on  .pin{color:#2ecc71}
.hotspot.cam-off .pin{color:#ef4444}
.hotspot.cam-off .pin svg{animation:cam-blink 1.6s ease-in-out infinite}
@keyframes cam-blink{50%{opacity:.45}}
.hotspot.cam.alarm .pin{
  width:auto; height:auto; box-shadow:none;
  animation:dev-blink 1.1s ease-in-out infinite;
  filter:drop-shadow(0 0 8px rgba(239,68,68,1)) drop-shadow(0 0 16px rgba(239,68,68,.8))
         drop-shadow(0 1px 3px rgba(0,0,0,.6))}
.hotspot .tag{
  font-size:10.5px; font-weight:700; color:#fff; white-space:nowrap;
  background:var(--tc, rgba(0,0,0,.72));
  border:1px solid rgba(255,255,255,.35);
  border-radius:4px; padding:1px 6px;
  text-shadow:0 1px 2px rgba(0,0,0,.55);
}
.hotspot:hover .pin{box-shadow:0 0 0 5px rgba(59,155,245,.5)}
/* 끌어서 옮길 수 있는 표식 (구성 권한이 있을 때) */
body[data-role="ADMIN"] .hotspot,
body[data-role="HACCP_MANAGER"] .hotspot,
body[data-role="OPEN"] .hotspot{cursor:grab}
.hotspot.dragging{cursor:grabbing; z-index:30}
.hotspot.dragging .pin{box-shadow:0 0 0 6px rgba(59,155,245,.6)}
.hotspot.dragging .tag{opacity:.85}

/* 위젯을 닫아 둔 표식 — 속이 빈 원 + 닫힌 개수. 클릭하면 다시 열린다. */
.hotspot.closed .pin{background:transparent; border-color:var(--tc, #3b9bf5)}
.hotspot.closed.cam .pin{opacity:.5}
.hotspot.closed .tag{opacity:.7}
.hotspot .tag .cnt{
  display:inline-block; min-width:13px; margin-left:3px; padding:0 3px;
  background:var(--blue); color:#fff; border-radius:7px;
  font-size:9.5px; text-align:center;
}

/* ── 항목 색 범례 ───────────────────────────────────────── */
.wg-legend{
  position:absolute; left:8px; bottom:8px; z-index:900;
  display:flex; flex-wrap:wrap; gap:4px 12px; max-width:60%;
  background:rgba(20,21,24,.82); border:1px solid var(--border);
  border-radius:6px; padding:5px 9px;
  font-size:11px; color:var(--text-dim);
}
.wg-legend span{display:flex; align-items:center; gap:5px; white-space:nowrap}
.wg-legend i{width:9px; height:9px; border-radius:50%; border:1px solid #fff2}


/* ── 위젯 공통 ──────────────────────────────────────────── */
.widget{
  position:absolute; background:rgba(255,255,255,.96); color:#111;
  border:2px solid #111; border-radius:4px;
  box-shadow:0 4px 14px rgba(0,0,0,.45); display:flex; flex-direction:column;
  overflow:hidden; min-width:150px; min-height:70px;
}
.widget.dark{background:rgba(28,30,34,.97); color:var(--text); border-color:#4a4d53}
.widget.alarm{border-color:var(--red)}
.widget-head{
  display:flex; align-items:center; gap:8px; padding:5px 8px;
  background:#e9ecef; border-bottom:1px solid #c9ced4; cursor:move; flex:0 0 auto;
}
.widget.dark .widget-head{background:#33363c; border-bottom-color:#4a4d53}
/* 장비 색으로 칠한 제목줄 — 도면 표식과 같은 색이라 짝을 바로 찾을 수 있다 */
.widget-head.tinted{border-bottom-color:rgba(0,0,0,.25)}
.widget-head.tinted .widget-title{color:#fff; text-shadow:0 1px 2px rgba(0,0,0,.35)}
.widget-head.tinted .widget-sub{color:rgba(255,255,255,.8)}
.widget-head.tinted .widget-close{color:rgba(255,255,255,.85)}
.widget-head.tinted .widget-close:hover{color:#fff}
.widget-title{font-size:12.5px; font-weight:700; flex:1;
              overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.widget-sub{font-size:10.5px; color:#667; font-weight:400}
.widget.dark .widget-sub{color:var(--text-mute)}
.widget-close{
  background:none; border:0; cursor:pointer; font-size:14px; font-weight:700;
  color:#333; line-height:1; padding:0 2px;
}
.widget.dark .widget-close{color:var(--text-dim)}
.widget-close:hover{color:var(--red)}
.widget-body{flex:1; padding:8px 10px; overflow:hidden; display:flex}
.widget-resize{
  position:absolute; right:0; bottom:0; width:14px; height:14px;
  cursor:nwse-resize; background:linear-gradient(135deg,transparent 50%,#9aa0a6 50%);
}

/* ── METRIC 위젯 (온도·습도·중량) ───────────────────────── */
.metric-row{display:flex; align-items:center; justify-content:space-around;
            gap:8px; width:100%; flex-wrap:wrap}
.metric-cell{display:flex; align-items:center; gap:6px}
.metric-icon{font-size:22px; line-height:1}
.metric-icon.temp{color:#e03131}
.metric-icon.hum{color:#1c7ed6}
.metric-icon.weight{color:#343a40}
.widget.dark .metric-icon.weight{color:#adb5bd}
.metric-value{font-size:16px; font-weight:800; white-space:nowrap}
.metric-value.bad{color:var(--red)}
.metric-label{font-size:10px; color:#868e96; display:block; font-weight:400}

/* ── GAUGE 위젯 — 항목마다 눈금판 1개, 좁으면 줄바꿈 ─────── */
.gg-grid{
  width:100%; align-self:stretch; overflow:auto;
  display:grid; gap:6px 8px; align-content:center; justify-items:center;
  grid-template-columns:repeat(auto-fit, minmax(86px, 1fr));
}
.gg-item{
  --gg: 74px;
  width:100%; max-width:132px; text-align:center;
  display:flex; flex-direction:column; align-items:center; gap:1px;
}
.gg-grid.one .gg-item{--gg:94px; max-width:none}
.gg-ring{position:relative; width:var(--gg); height:var(--gg)}
.gg-ring svg{width:100%; height:100%; display:block}
.gg-track{stroke:#e9ecef}
.gg-center{
  position:absolute; inset:0; display:flex; flex-direction:column;
  align-items:center; justify-content:center; gap:0; line-height:1.05;
}
.gg-pct{font-size:16px; font-weight:800; letter-spacing:-.4px;
        font-variant-numeric:tabular-nums}
.gg-pct small{font-size:9px; font-weight:700; margin-left:1px}
.gg-grid.one .gg-pct{font-size:22px}
.gg-center.val .gg-pct{font-size:13px}
.gg-grid.one .gg-center.val .gg-pct{font-size:17px}
.gg-center.val .gg-pct small{font-size:8.5px}
.gg-grid.one .gg-pct small{font-size:12px}
.gg-now{font-size:9.5px; font-weight:700; color:#868e96; margin-top:1px;
        font-variant-numeric:tabular-nums}
.gg-grid.one .gg-now{font-size:12px}
.gg-name{font-size:11px; font-weight:700; color:#343a40; margin-top:2px}
.gg-base{font-size:9.5px; color:#adb5bd; white-space:nowrap;
         overflow:hidden; text-overflow:ellipsis; max-width:100%}
.gg-item.bad .gg-name{color:#e03131}
.widget.dark .gg-track{stroke:#3a3d43}
.widget.dark .gg-name{color:#e9ecef}
.widget.dark .gg-now{color:var(--text-dim)}
.widget.dark .gg-base{color:var(--text-mute)}

/* 위젯 만들 때 쓰는 항목별 기준값 입력 */
.gmax-list{
  background:var(--card-2); border:1px solid var(--border-soft); border-radius:6px;
  padding:8px 9px; margin-top:6px; display:flex; flex-direction:column; gap:6px;
}
.gmax-row{display:flex; align-items:center; gap:8px}
.gmax-key{flex:1; min-width:0; font-size:12.5px; color:var(--text);
          overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.gmax-row .input{flex:0 0 108px; width:108px; min-width:0; margin:0; padding:6px 9px;
                 font-size:12.5px; text-align:right}
.gmax-unit{flex:0 0 34px; font-size:11.5px; color:var(--text-mute)}
.gmax-list .hint{margin:0}

/* ── CHART 위젯 ─────────────────────────────────────────── */
.chart-box{width:100%; height:100%; display:flex; flex-direction:column}
.chart-box svg{flex:1; width:100%}
.chart-legend{display:flex; gap:10px; justify-content:center;
              font-size:10px; color:#666; padding-top:2px}
.widget.dark .chart-legend{color:var(--text-dim)}
.chart-legend i{display:inline-block; width:10px; height:2px; margin-right:3px;
                vertical-align:middle}

/* ── STATUS 위젯 ────────────────────────────────────────── */
.status-box{width:100%; font-size:11.5px}
.status-badge{
  display:inline-block; font-size:11px; font-weight:700; border-radius:3px;
  padding:2px 9px; margin-bottom:6px;
}
.status-badge.run{background:#d3f9d8; color:#2b8a3e; border:1px solid #8ce99a}
.status-badge.stop{background:#f1f3f5; color:#868e96; border:1px solid #ced4da}
.status-badge.fault{background:#ffe3e3; color:#c92a2a; border:1px solid #ffa8a8}
.status-grid{display:grid; grid-template-columns:1fr 1fr; gap:3px 10px}
.status-grid .k{color:#868e96}
.widget.dark .status-grid .k{color:var(--text-mute)}
.status-grid .v{font-weight:700; text-align:right}

/* ── VISION 위젯 ────────────────────────────────────────── */
.vision-box{width:100%; height:100%; background:#000; border-radius:3px;
            display:flex; flex-direction:column; align-items:center;
            justify-content:center; gap:4px; color:#ccc}
.vision-box .live{position:absolute; top:26px; left:14px; font-size:10px;
                  font-weight:700; color:var(--green)}
.vision-box .meta{font-size:10.5px; color:#888}

/* ── ALARM 위젯 ─────────────────────────────────────────── */
.alarm-box{width:100%; display:flex; flex-direction:column; gap:3px;
           font-size:11.5px; overflow:auto}
.alarm-box .none{color:#2b8a3e; font-weight:700}
.alarm-box .item{color:#c92a2a}

/* ── 우클릭 컨텍스트 메뉴 ───────────────────────────────── */
.ctx-menu{
  position:fixed; z-index:200; min-width:210px; padding:5px;
  background:var(--card); border:1px solid var(--border-soft);
  border-radius:7px; box-shadow:0 8px 24px rgba(0,0,0,.5);
}
.ctx-title{font-size:10.5px; color:var(--text-mute); padding:5px 9px 6px;
           border-bottom:1px solid var(--border); margin-bottom:4px}
.ctx-item{
  display:block; width:100%; text-align:left; background:none; border:0;
  color:var(--text); font-size:12.5px; padding:7px 9px; border-radius:5px;
  cursor:pointer;
}
.ctx-item:hover{background:var(--blue); color:#fff}
.ctx-item.danger:hover{background:var(--red)}
.ctx-item .dim{font-size:10.5px; color:var(--text-mute); margin-left:4px}
.ctx-item:hover .dim{color:rgba(255,255,255,.75)}
.ctx-sep{height:1px; background:var(--border); margin:4px 0}

/* ── 위젯 종류 선택 ─────────────────────────────────────── */
.wg-type-list{display:grid; grid-template-columns:repeat(3,1fr); gap:7px; margin-top:6px}
.wg-type{
  background:var(--card-2); border:1px solid var(--border-soft); border-radius:6px;
  padding:9px 7px; font-size:11.5px; color:var(--text-dim); cursor:pointer;
  text-align:center; line-height:1.5;
}
.wg-type:hover{border-color:var(--blue); color:var(--text)}
.wg-type.on{border-color:var(--blue); background:#1e2a36; color:var(--text);
            font-weight:700}
.wg-type .ico{display:block; font-size:17px; margin-bottom:3px}


/* ════════════════════════════════════════════════════════
   대시보드 개편 — 상태를 먼저 읽히게
   ════════════════════════════════════════════════════════ */

/* ── 라인 상태 요약 바 ──────────────────────────────────── */
.statusbar{
  display:flex; align-items:center; gap:22px; padding:16px 20px 16px 18px;
  background:var(--card); border:1px solid var(--border); border-radius:var(--radius);
  border-left:4px solid var(--green); margin-bottom:14px;
}
.statusbar.dev{border-left-color:var(--red)}
.statusbar.off{border-left-color:var(--amber)}
.statusbar.idle{border-left-color:var(--text-mute)}
.sb-mark{
  width:38px; height:38px; border-radius:50%; flex:0 0 38px;
  display:flex; align-items:center; justify-content:center;
  background:#12301f; border:1px solid #2f6b45;
}
.statusbar.dev .sb-mark{background:#3d1d1d; border-color:#7d2f2f}
.statusbar.off .sb-mark{background:#3a3320; border-color:#6b5a2f}
.statusbar.idle .sb-mark{background:#2b2f36; border-color:var(--border-soft)}
.sb-mark svg{width:20px; height:20px; fill:none; stroke:var(--green);
  stroke-width:2.2; stroke-linecap:round; stroke-linejoin:round}
.statusbar.dev .sb-mark svg{stroke:var(--red)}
.statusbar.off .sb-mark svg{stroke:var(--amber)}
.statusbar.idle .sb-mark svg{stroke:var(--text-dim)}
.sb-text{min-width:0}
.sb-head{font-size:19px; font-weight:800; letter-spacing:-.3px; line-height:1.2; color:var(--green)}
.statusbar.dev .sb-head{color:var(--red)}
.statusbar.off .sb-head{color:var(--amber)}
.statusbar.idle .sb-head{color:var(--text-dim)}
.sb-sub{font-size:12px; color:var(--text-dim); margin-top:3px}
.sb-sub b{color:var(--text)}
.statusbar.dev .sb-sub b{color:#ff9b9b}
.statusbar.off .sb-sub b{color:#e6c778}
.sb-counts{margin-left:auto; display:flex; gap:26px}
.sb-counts .cnt{text-align:center; min-width:52px}
.sb-counts .n{font-size:22px; font-weight:800; font-variant-numeric:tabular-nums;
  line-height:1.1; color:var(--text-mute)}
.sb-counts .n.on-ok{color:var(--green)}
.sb-counts .n.on-dev{color:var(--red)}
.sb-counts .n.on-off{color:var(--amber)}
.sb-counts .l{font-size:11px; color:var(--text-dim); margin-top:3px}
.sb-meta{padding-left:22px; border-left:1px solid var(--border); font-size:11.5px;
  color:var(--text-mute); line-height:1.7; text-align:right; white-space:nowrap}
.sb-meta b{color:var(--text-dim); font-weight:600; font-variant-numeric:tabular-nums}
@media (max-width:1180px){
  .statusbar{flex-wrap:wrap}
  .sb-meta{padding-left:0; border-left:0; text-align:left; width:100%}
}

.sec-hint{margin-left:12px; font-size:11px; font-weight:400; color:var(--text-mute)}
.kpi-div{width:1px; align-self:stretch; background:var(--border)}
.kpi-value.blue{color:var(--blue)}
.kpi-value{font-variant-numeric:tabular-nums}

/* ── 공정 흐름: 상태 색 + 처리속도 막대 ─────────────────── */
.flow-step{flex:1; min-width:0}
.flow-step{border-top:3px solid var(--text-mute)}
.flow-step.run{border-top-color:var(--green)}
.flow-step.dev{border-top-color:var(--red)}
.flow-step.dev .nm{color:#ff9b9b}
.flow-step.off{border-top-color:var(--amber); opacity:.65}
.flow-rate{display:flex; align-items:baseline; gap:5px; margin-top:8px; justify-content:center}
.flow-rate b{font-size:15px; font-weight:800; font-variant-numeric:tabular-nums}
.flow-rate span{font-size:10.5px; color:var(--text-mute)}
.flow-bar{height:4px; border-radius:2px; background:#1e2024; margin-top:7px; overflow:hidden}
.flow-bar i{display:block; height:100%; background:var(--green); transition:width .3s}
.flow-step.dev .flow-bar i{background:var(--red)}

/* ── 장비 카드 ─────────────────────────────────────────── */
.tile{border-left:4px solid var(--green); display:flex; flex-direction:column; gap:11px}
.tile.dev{border-left-color:var(--red)}
.tile.off{border-left-color:var(--amber); background:var(--card-2); opacity:1}
.tile.idle{border-left-color:var(--text-mute)}
.tile.off .tile-name{color:var(--text-dim)}
.t-head{display:flex; align-items:center; gap:8px}
.t-proc{font-size:11px; color:var(--text-mute)}
.t-primary{display:flex; gap:14px}
.pmet{flex:1; min-width:0}
.pmet .v{font-size:26px; font-weight:800; letter-spacing:-.6px; line-height:1.1;
  font-variant-numeric:tabular-nums; color:var(--green); word-break:keep-all}
.pmet .v.blue{color:var(--blue)}
.pmet .v.plain{color:var(--text)}
.pmet .v.bad{color:var(--red)}
.pmet .v.dim{color:var(--text-mute)}
.pmet .v small{font-size:14px; font-weight:700; margin-left:2px}
.pmet .l{font-size:11px; color:var(--text-dim); margin-top:3px}
.pmet .l.bad{color:#ff9b9b}
.pmet .std{font-size:10.5px; color:var(--text-mute); margin-top:2px}
.pmet .std.bad{color:#ff9b9b}
.t-ccp{display:flex; flex-direction:column; gap:6px; padding-top:10px;
  border-top:1px solid var(--border)}
.ccp-row{display:flex; align-items:center; gap:8px; font-size:11.5px}
.ccp-row .k{color:var(--text-dim)}
.ccp-row .v{margin-left:auto; color:var(--text); font-weight:700;
  font-variant-numeric:tabular-nums}
.ccp-row .v.bad{color:#ff9b9b}
.ccp-row .v.dim{color:var(--text-mute)}
.ccp-row .dot{width:6px; height:6px; border-radius:50%; background:var(--green); flex:0 0 6px}
.ccp-row .dot.bad{background:var(--red)}
.ccp-row .dot.dim{background:var(--text-mute)}
.t-foot{display:flex; align-items:center; gap:8px; font-size:10.5px; color:var(--text-mute);
  padding-top:9px; border-top:1px solid var(--border)}
.t-foot .rx, .s-foot .rx{font-variant-numeric:tabular-nums}
.t-foot .rx.bad, .s-foot .rx.bad{color:var(--amber)}
.t-foot .rx b, .s-foot .rx b{color:var(--amber)}
.t-foot .cpm{margin-left:auto; color:var(--text-dim); font-variant-numeric:tabular-nums}
.t-foot .cpm b{color:var(--text)}
.t-foot .cpm b.dim{color:var(--text-mute)}
.badge.off{background:#3a3320; color:#e6c778; border-color:#6b5a2f}
.badge.idle{background:#2b2f36; color:var(--text-dim); border-color:var(--border-soft)}

/* ── 보관소 카드: 한계기준 범위 안의 현재 위치 ──────────── */
.stile{
  background:var(--card); border:1px solid var(--border); border-radius:var(--radius);
  padding:14px 15px; border-left:4px solid var(--green);
  display:flex; flex-direction:column; gap:10px;
}
.stile.dev{border-left-color:var(--red)}
.stile.off{border-left-color:var(--amber); background:var(--card-2)}
.s-temp{display:flex; align-items:baseline; gap:9px}
.s-temp .v{font-size:26px; font-weight:800; letter-spacing:-.6px; line-height:1.1;
  color:var(--green); font-variant-numeric:tabular-nums}
.s-temp .v.bad{color:var(--red)}
.s-temp .v.dim{color:var(--text-mute)}
.s-temp .v small{font-size:14px; font-weight:700; margin-left:2px}
.s-temp .w{margin-left:auto; font-size:19px; font-weight:800; color:var(--blue);
  font-variant-numeric:tabular-nums}
.s-temp .w.dim{color:var(--text-mute)}
.gauge{position:relative; height:6px; border-radius:3px; background:#1e2024}
.gauge .band{position:absolute; top:0; bottom:0; left:0; right:0; border-radius:3px;
  background:#1d3d2a}
.gauge .now{position:absolute; top:-3px; width:2px; height:12px; border-radius:1px;
  background:var(--green)}
.gauge .now.bad{background:var(--red)}
.g-scale{display:flex; justify-content:space-between; font-size:10px; color:var(--text-mute)}
.s-foot{display:flex; gap:8px; font-size:10.5px; color:var(--text-mute)}
.s-foot .r{margin-left:auto}

/* ── 로그: 레벨을 색 글자에서 칩으로 ───────────────────── */
.log-tag{
  font-size:10px; font-weight:700; border-radius:3px; padding:1px 0;
  width:44px; min-width:44px; flex:0 0 44px; text-align:center;
  border:1px solid transparent;
}
.log-tag.INFO     {background:#232a33; color:var(--text-dim); border-color:#313a45}
.log-tag.DONE     {background:#0f3d24; color:#5ee29a;         border-color:#1c6b41}
.log-tag.CONTROL  {background:#1b2c3d; color:var(--blue);     border-color:#2f5f8a}
.log-tag.DEVIATION{background:#4a1d1d; color:#ff9b9b;         border-color:#7d2f2f}
.log-row.dev{background:#241a1a}
.log-row.dev .log-msg{color:#ff9b9b}
