/* 机房维护系统 · 框架版样式 */

:root {
  --bg: #f4f6fa;
  --panel: #ffffff;
  --panel-soft: #f8fafc;
  --border: #e6eaf0;
  --border-strong: #d8dee8;
  --text: #1b2330;
  --text-soft: #56606f;
  --text-mute: #8b95a4;
  --accent: #2563eb;
  --accent-dark: #1d4ed8;
  --accent-soft: #eef4ff;
  --ok: #15803d;      --ok-soft: #e8f6ee;
  --info: #0369a1;    --info-soft: #e6f3fb;
  --warn: #b45309;    --warn-soft: #fdf3e4;
  --danger: #b91c1c;  --danger-soft: #fdecec;
  --mute: #64748b;    --mute-soft: #f1f4f8;
  --radius: 10px;
  --shadow: 0 1px 2px rgba(16, 24, 40, .04);
  --shadow-hover: 0 6px 18px rgba(16, 24, 40, .09);
}

* { box-sizing: border-box; }

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

body {
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei",
               "PingFang SC", "Hiragino Sans GB", sans-serif;
  font-size: 14px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--accent); }
.link { color: var(--accent); text-decoration: none; }
.link:hover { text-decoration: underline; }

/* ------------------------------------------------------------ 整体布局 */
.app {
  display: grid;
  grid-template-columns: 272px minmax(0, 1fr);
  min-height: 100vh;
}

.sidebar {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--panel);
  border-right: 1px solid var(--border);
  z-index: 30;
}

.main { min-width: 0; display: flex; flex-direction: column; }

/* ------------------------------------------------------------ 品牌区 */
.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 16px 16px 14px;
  border-bottom: 1px solid var(--border);
}

.brand-mark {
  width: 40px; height: 40px;
  flex: 0 0 40px;
  border-radius: 11px;
  background: linear-gradient(135deg, #2563eb, #38bdf8);
  color: #fff;
  font-size: 12.5px;
  font-weight: 700;
  letter-spacing: .5px;
  display: grid;
  place-items: center;
}

.brand-text strong { display: block; font-size: 14.5px; font-weight: 650; }
.brand-text span { display: block; font-size: 11px; color: var(--text-mute); letter-spacing: .05em; }

/* ------------------------------------------------------------ 侧栏搜索 */
.search-box { position: relative; display: flex; align-items: center; margin: 12px 14px 4px; }

.search-box input {
  width: 100%;
  height: 34px;
  padding: 0 10px 0 30px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--panel-soft);
  font-size: 13px;
  font-family: inherit;
  color: var(--text);
}
.search-box input:focus {
  outline: none;
  border-color: var(--accent);
  background: #fff;
  box-shadow: 0 0 0 3px rgba(37, 99, 235, .1);
}
.search-ico {
  position: absolute; left: 10px;
  color: var(--text-mute); font-size: 14px;
  pointer-events: none;
}

/* ------------------------------------------------------------ 左侧导航树 */
.nav { flex: 1; overflow-y: auto; padding: 6px 10px 18px; }
.nav::-webkit-scrollbar { width: 8px; }
.nav::-webkit-scrollbar-thumb { background: #e2e7ee; border-radius: 4px; }

.nav-title {
  font-size: 11px;
  letter-spacing: .08em;
  color: var(--text-mute);
  padding: 14px 12px 6px;
}

.nav-link {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 5px 10px;
  border-radius: 8px;
  color: var(--text-soft);
  font-size: 13.5px;
  text-decoration: none;
  cursor: pointer;
  transition: background .12s, color .12s;
}
.nav-link:hover { background: #f2f5f9; color: var(--text); }
.nav-link.is-active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.nav-link.is-active .badge { background: #dbe8ff; color: var(--accent); }

.nav-home .ico { color: var(--accent); font-size: 13px; }

.nav-link.lv1 { font-weight: 600; color: var(--text); padding-left: 6px; }
.nav-link.lv2 { padding-left: 22px; }
.nav-link.lv3 { padding-left: 58px; font-size: 13px; }

/* 展开/收起箭头:26px 方块热区 + 18px 实心三角,是原来的两倍大 */
.caret {
  width: 26px; flex: 0 0 26px;
  height: 26px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 6px;
  color: #9aa6b6;
  transition: transform .15s ease, background .15s ease, color .15s ease;
}
.caret svg {
  width: 18px; height: 18px;
  display: block;
  fill: currentColor;
}
.nav-link.lv1:hover > .caret,
.nav-link.lv2:hover > .caret,
.caret:hover { color: var(--accent); background: #e6ecf6; }
.nav-node.is-expanded > .nav-link > .caret { transform: rotate(90deg); }

.nav-children { display: none; }
.nav-node.is-expanded > .nav-children { display: block; }

.dot {
  width: 4px; height: 4px; flex: 0 0 4px;
  border-radius: 50%;
  background: #cbd3df;
}
.nav-link.lv3.is-active .dot { background: var(--accent); }

.label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.badge {
  margin-left: auto;
  font-size: 11px;
  color: var(--text-mute);
  background: var(--mute-soft);
  border-radius: 999px;
  padding: 1px 7px;
  font-weight: 500;
}

.sidebar-foot {
  padding: 10px 16px;
  border-top: 1px solid var(--border);
  font-size: 11.5px;
  color: var(--text-mute);
}

/* ------------------------------------------------------------ 顶栏 */
.topbar {
  position: sticky; top: 0; z-index: 20;
  height: 54px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 0 24px;
  background: rgba(255, 255, 255, .92);
  backdrop-filter: blur(6px);
  border-bottom: 1px solid var(--border);
}

.crumb { display: flex; align-items: center; gap: 6px; font-size: 13px; }
.crumb a { color: var(--text-soft); text-decoration: none; }
.crumb a:hover { color: var(--accent); }
.crumb .sep { color: #c3cad6; }
.crumb .cur { color: var(--text); font-weight: 600; }

/* ------------------------------------------------------------ 页面 */
.page { padding: 22px 24px 64px; }

.page-head { margin-bottom: 18px; }
.page-head h1 {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 0 0 6px;
  font-size: 21px;
  font-weight: 650;
}
.page-head .sub { margin: 0; color: var(--text-mute); font-size: 13px; }

.section-head {
  display: flex; align-items: baseline; justify-content: space-between;
  margin: 26px 0 12px;
}
.section-head h2 { margin: 0; font-size: 15px; font-weight: 650; }
.section-head .hint { font-size: 12.5px; color: var(--text-mute); }

/* ------------------------------------------------------------ 统计条 */
.stat-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
  gap: 12px;
}

.stat {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 12px 14px;
}
.stat-label { font-size: 12px; color: var(--text-mute); }
.stat-value { font-size: 22px; font-weight: 650; margin-top: 2px; line-height: 1.25; }
.stat.is-danger { background: var(--danger-soft); border-color: #f7cccc; }
.stat.is-danger .stat-value { color: var(--danger); }
.stat.is-warn { background: var(--warn-soft); border-color: #f5ddb8; }
.stat.is-warn .stat-value { color: var(--warn); }
/* 「历史台账」= 机柜编号已经对不上平面图现存对象的那些记录 */
.stat.is-mute { border-style: dashed; }
.stat.is-mute .stat-value { color: var(--text-mute); }
.stat-sub { font-size: 11.5px; color: var(--text-mute); margin-top: 1px; }

/* ------------------------------------------------------------ 卡片 */
.grid {
  display: grid;
  gap: 14px;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
.grid-wide { grid-template-columns: repeat(auto-fill, minmax(258px, 1fr)); }
.grid-floors { grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); }

.card {
  display: block;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 14px 16px;
  color: inherit;
  text-decoration: none;
  box-shadow: var(--shadow);
  transition: border-color .15s, box-shadow .15s, transform .15s;
}
.card:hover {
  border-color: var(--accent);
  box-shadow: var(--shadow-hover);
  transform: translateY(-1px);
}

.card h3 { margin: 8px 0 3px; font-size: 16px; font-weight: 650; }
.card-sub { margin: 0; font-size: 12.5px; color: var(--text-mute); }

.card-top { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

.card-foot {
  display: flex; align-items: center; gap: 8px;
  margin-top: 12px; padding-top: 10px;
  border-top: 1px solid #f0f3f8;
  font-size: 12.5px;
  color: var(--text-mute);
}
.card-foot .go { margin-left: auto; color: var(--accent); font-weight: 600; }
.card-foot .strong { font-weight: 650; color: var(--text); font-size: 14px; }

.card-building { border-top: 3px solid #dbe6fb; }
.card-building:hover { border-top-color: var(--accent); }

.card-floor {
  display: grid;
  grid-template-columns: 52px minmax(0, 1fr);
  gap: 12px;
  align-items: center;
}
.card-floor .card-foot { grid-column: 1 / -1; margin-top: 4px; }
.card-floor .card-body h3 { margin: 0 0 2px; }
.card-floor .card-body { min-width: 0; }
.clamp { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.floor-badge {
  width: 52px; height: 52px;
  justify-self: start;
  border-radius: 12px;
  background: linear-gradient(135deg, #2563eb, #38bdf8);
  color: #fff;
  font-size: 14px;
  font-weight: 700;
  display: grid;
  place-items: center;
}

.card-room { display: flex; flex-direction: column; }
.card-room h3 { margin: 10px 0 3px; }
.card-room .card-foot { margin-top: auto; }

.mini-row { display: flex; gap: 20px; margin-top: 10px; }
.mini-num { display: block; font-size: 17px; font-weight: 650; line-height: 1.2; }
.mini-label { font-size: 11.5px; color: var(--text-mute); }
.text-danger { color: var(--danger); }

/* ------------------------------------------------------------ 标签 */
.code-tag {
  display: inline-block;
  font-size: 11.5px;
  font-family: ui-monospace, Consolas, "Courier New", monospace;
  padding: 1px 7px;
  border-radius: 6px;
  background: var(--mute-soft);
  border: 1px solid var(--border);
  color: var(--text-soft);
  font-weight: 500;
  vertical-align: middle;
}

.pill {
  display: inline-block;
  font-size: 11.5px;
  font-weight: 500;
  padding: 1px 8px;
  border-radius: 999px;
  white-space: nowrap;
}
.pill.ok { background: var(--ok-soft); color: var(--ok); }
.pill.info { background: var(--info-soft); color: var(--info); }
.pill.warn { background: var(--warn-soft); color: var(--warn); }
.pill.danger { background: var(--danger-soft); color: var(--danger); }
.pill.mute { background: var(--mute-soft); color: var(--mute); }

.chip-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
.chip {
  font-size: 12.5px;
  padding: 4px 11px;
  border-radius: 999px;
  background: var(--panel);
  border: 1px solid var(--border);
  color: var(--text-soft);
}
.chip b { color: var(--text); margin-left: 3px; }

/* ------------------------------------------------------------ 面板 / 表格 */
.panel {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px 18px;
}
.panel-flush { padding: 0; overflow: hidden; }
.table-wrap { overflow-x: auto; }

.table { width: 100%; border-collapse: collapse; font-size: 13px; min-width: 940px; }
.table thead th {
  text-align: left;
  padding: 10px 12px;
  background: var(--panel-soft);
  border-bottom: 1px solid var(--border);
  color: var(--text-soft);
  font-weight: 600;
  font-size: 12.5px;
  white-space: nowrap;
}
.table td { padding: 9px 12px; border-bottom: 1px solid #f1f4f8; vertical-align: middle; }
.table tbody tr:last-child td { border-bottom: none; }
.table tbody tr:hover { background: #fafcff; }
/* ---- 设备清单的「类别」分组标题行(0.28.0)-----------------------------
   山叔 2026-09-20:「设备清单做个简单的分类」。一行横跨整表(colspan=13),
   底色与表头同一档(--panel-soft),跟内容行拉开层次。
   ⚠️ hover 必须单独压掉 —— `.table tbody tr:hover` 会给它上浅蓝,
      而它不是数据行,不该有"可以点"的暗示。 */
.table tbody tr.grp-row td {
  padding: 7px 12px;
  background: var(--panel-soft);
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
.table tbody tr.grp-row:hover td { background: var(--panel-soft); }
.table tbody tr.grp-row:first-child td { border-top: none; }
.table .grp-name { font-weight: 600; font-size: 12.5px; color: var(--text-soft); }
.table .grp-n { margin-left: 8px; font-size: 11.5px; color: var(--text-mute); }
/* 「机柜编号对不上现存对象」的历史台账行已不再渲染到表里(山叔 2026-09-19),
   所以 .is-orphan / .filter-check 两套样式一并删掉了 —— 别看到旧类名又用回去。 */
.mini-tag {
  display: inline-block; margin-left: 4px; padding: 0 5px;
  border-radius: 5px; font-size: 11px; line-height: 16px;
  background: var(--mute-soft); color: var(--mute);
  vertical-align: middle;
}

.col-idx { width: 46px; color: var(--text-mute); }
.col-num { text-align: right; white-space: nowrap; }
.mono {
  font-family: ui-monospace, Consolas, "Courier New", monospace;
  font-size: 12.5px;
  color: var(--text-soft);
}
.dev-name { font-weight: 550; }
.dev-code {
  font-size: 11px;
  color: var(--text-mute);
  font-family: ui-monospace, Consolas, "Courier New", monospace;
  margin-top: 1px;
}
.cell-remark { color: var(--text-mute); max-width: 220px; }
.empty-cell { text-align: center; color: var(--text-mute); padding: 30px 0; }
.strong { font-weight: 650; }

/* ------------------------------------------------------------ 工具条 */
.toolbar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-bottom: 12px;
}

.input {
  height: 34px;
  padding: 0 11px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: #fff;
  color: var(--text);
  font-size: 13px;
  font-family: inherit;
}
.input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(37, 99, 235, .12);
}
#dev-filter { flex: 1; min-width: 220px; }
.select { min-width: 128px; }
.filter-count { font-size: 12.5px; color: var(--text-mute); margin-left: auto; }

/* ------------------------------------------------------------ 楼层平面图 */
.plan-wrap {
  margin: 0 0 22px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: #fff;
  overflow: hidden;
}

.plan-img {
  display: block;
  margin: 0 auto;
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 72vh;
  object-fit: contain;
  background: #f3f5f9;
  cursor: zoom-in;
}

.plan-cap {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  border-top: 1px solid var(--line);
  background: #fbfcfe;
  font-size: 12px;
  color: var(--text-mute);
}
.plan-name {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: ui-monospace, Consolas, monospace;
}
.plan-btn {
  flex: none;
  padding: 4px 12px;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: #fff;
  color: var(--text-soft);
  font-size: 12px;
  font-family: inherit;
  cursor: pointer;
  transition: border-color .12s, color .12s, background .12s;
}
.plan-btn:hover { border-color: var(--accent); color: var(--accent); background: #f5f8ff; }

/* 未配置平面图时的占位 */
.plan-empty {
  margin: 0 0 22px;
  padding: 34px 20px;
  border: 1.5px dashed #d6dee9;
  border-radius: 12px;
  background: #fcfdff;
  text-align: center;
}
.plan-empty-ico { color: #c3cddb; margin-bottom: 10px; }
.plan-empty-ico svg { width: 42px; height: 42px; }
.plan-empty-t { margin: 0 0 6px; font-size: 14px; font-weight: 600; color: var(--text-soft); }
.plan-empty-h { margin: 0; font-size: 12.5px; color: var(--text-mute); line-height: 1.9; }
.plan-empty-h code {
  padding: 1px 6px;
  border-radius: 5px;
  background: #eef2f8;
  color: var(--accent);
  font-family: ui-monospace, Consolas, monospace;
  font-size: 12px;
}

/* 全屏看图 */
.plan-lightbox {
  position: fixed;
  inset: 0;
  z-index: 999;
  display: none;
  align-items: center;
  justify-content: center;
  background: rgba(15, 23, 42, .92);
  cursor: grab;
}
.plan-lightbox.is-open { display: flex; }
.plan-lightbox.is-panning { cursor: grabbing; }
.plan-lightbox img {
  max-width: none;
  max-height: none;
  transform-origin: center center;
  user-select: none;
  -webkit-user-drag: none;
  will-change: transform;
}
.plan-lb-bar {
  position: fixed;
  bottom: 0; left: 0; right: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 12px;
  color: #e8eef7;
  font-size: 12.5px;
}
.plan-lb-bar button {
  padding: 5px 14px;
  border: 1px solid rgba(255, 255, 255, .28);
  border-radius: 7px;
  background: rgba(255, 255, 255, .1);
  color: #fff;
  font-size: 12.5px;
  font-family: inherit;
  cursor: pointer;
}
.plan-lb-bar button:hover { background: rgba(255, 255, 255, .2); }
.plan-lb-tip { opacity: .6; margin-left: 6px; }

/* ------------------------------------------------------------ 条形分布 */
.bar-list { display: flex; flex-direction: column; gap: 4px; }
.bar-item {
  display: grid;
  grid-template-columns: 116px minmax(0, 1fr) 52px;
  gap: 14px;
  align-items: center;
  padding: 4px 0;
}
.bar-name { font-size: 13px; color: var(--text-soft); }
.bar-track { height: 8px; border-radius: 999px; background: #eef1f6; overflow: hidden; }
.bar-fill {
  display: block; height: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg, #3b82f6, #60a5fa);
}
.bar-num { font-size: 13px; text-align: right; color: var(--text-soft); font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------------ 其他 */
.btn {
  display: inline-block;
  padding: 8px 16px;
  border-radius: 8px;
  background: var(--accent);
  color: #fff;
  text-decoration: none;
  font-size: 13px;
}
.btn:hover { background: var(--accent-dark); }

/* ------------------------------------------------------------ 机柜方块(房间平面图上) */
/* 底图外层。cab-layer 由 JS 对齐到图片的实际渲染框 —— 图片受 max-height 限制时
   会留白,直接 inset:0 会让方块整体偏移。 */
.plan-stage {
  position: relative;
  display: flex;
  justify-content: center;
}
.plan-stage.is-drawing { cursor: crosshair; }
.plan-stage.is-drawing .plan-img { cursor: crosshair; }

.cab-layer { position: absolute; inset: 0; z-index: 2; user-select: none; }

/* 绘制模式下,已有方块让开鼠标 —— 否则拖框拖到方块上就被它吃掉了 */
.plan-stage.is-drawing .cab-box { pointer-events: none; }

/* 放大底图:松开 max-height,方块跟着变大,小柜子才点得准 */
.plan-stage.is-large .plan-img { max-height: none; }

/* 机柜页表格 */
.table .col-idx { white-space: nowrap; }

/* 每个方块带自己的主色 --kc（写成「R, G, B」三数,换色只改这一个变量）
   ⚠️ 机柜类一律红底(0.8.0 起,山叔定的规矩)—— 后端 app.kind_kc() 写进内联 style,
      这里的默认值与 .k-rack 只是"内联没写时的兜底",两边保持一致才不会出现
      「同一类机柜有的红有的蓝」。 */
.cab-box {
  --kc: 220, 38, 38;                    /* 默认:机柜红 */
  position: absolute;
  box-sizing: border-box;
  display: block;
  border: 1px solid rgba(var(--kc), .8);
  border-radius: 3px;
  background: rgba(var(--kc), .18);
  overflow: hidden;
  text-decoration: none;
  cursor: pointer;
  transition: background .12s, border-color .12s, box-shadow .12s;
}
.cab-box:hover {
  z-index: 3;
  border-color: rgba(var(--kc), 1);
  background: rgba(var(--kc), .34);
  box-shadow: 0 0 0 2px rgba(var(--kc), .22);
}
.cab-box.is-dragging { opacity: .72; cursor: grabbing; }

/* ---- 柜内录了设备的机柜:加深突出 ----
   有货的柜子实心 + 白描边,空柜只留一圈淡边 —— 一屏几百个方块时,
   只有靠深浅对比才能一眼看出哪些柜子装了东西。 */
.cab-box.has-dev {
  background: rgba(var(--kc), .55);
  border-color: rgba(var(--kc), 1);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .5);
}
.cab-box.has-dev .cab-tag { color: #fff; text-shadow: 0 1px 2px rgba(0, 0, 0, .45); }
.cab-box.has-dev:hover { background: rgba(var(--kc), .72); }
/* 有设备 + 故障/维修:状态色仍然压在最上面,但底色比空柜的状态色更实 */
.cab-box.has-dev.is-faulty { background: rgba(220, 38, 38, .62); border-color: #dc2626; }
.cab-box.has-dev.is-repairing { background: rgba(217, 119, 6, .62); border-color: #d97706; }

/* 状态色压在类型色之上 —— 顺序不能反,同优先级下后写的赢 */
.cab-box.is-faulty { border-color: #dc2626; background: rgba(220, 38, 38, .2); }
.cab-box.is-repairing { border-color: #d97706; background: rgba(217, 119, 6, .2); }
.cab-box.is-new { animation: cab-pop .35s ease-out; }

/* ---- 对象类型配色:机柜红 / 空调青 / 烟感橙 / 水浸紫 / 温湿度绿 / 摄像头红 / 其他灰 ----
   ⚠️ 机柜与摄像头同为红(220,38,38):机柜是 2.6×5.3 的长条,摄像头是 1.8×1.8 的点位,
      尺寸差一档不致混。若哪天觉得晃眼,在「对象类型」页把摄像头调成玫红即可。 */
.cab-box.k-rack,  .kind-tag.k-rack  { --kc: 220, 38, 38; }
.cab-box.k-ac,    .kind-tag.k-ac    { --kc: 8, 145, 178; }
.cab-box.k-smoke, .kind-tag.k-smoke { --kc: 234, 88, 12; }
.cab-box.k-water, .kind-tag.k-water { --kc: 124, 58, 237; }
.cab-box.k-th,    .kind-tag.k-th    { --kc: 5, 150, 105; }
.cab-box.k-cam,   .kind-tag.k-cam   { --kc: 220, 38, 38; }
.cab-box.k-other, .kind-tag.k-other { --kc: 100, 116, 139; }

/* ---- 类型图标 ----
   类型在 cabinet_kinds.icon 配了图(static/kinds/xxx.png)时,方块里直接画设备图,
   一眼能认出是空调还是开关电源;没配图的类型(机柜/烟感/其他)仍是纯色块 + 编号。
   底色压到 .07 近乎透明,免得彩色底把设备图染花。 */
.cab-box.has-ico { background: rgba(var(--kc), .07); }
.cab-ico {
  position: absolute;
  left: 5%; right: 5%; top: 13px; bottom: 2%;
  background-repeat: no-repeat;
  background-position: center bottom;
  background-size: contain;
  pointer-events: none;
}
.cab-box.has-ico .cab-tag { position: relative; z-index: 1; }

/* 点位类方块只有 1.8%(屏幕上 ~30px),塞不下图标 + 标签 ——
   图标铺满方块、标签让位(编号在悬停提示和对象清单里都还在)。
   再加 min-* 兜底:方块缩到十几像素时,图标仍保持 18px 居中,
   像一枚图钉立在原地,不至于糊成一个点。 */
.cab-box.has-ico.is-pt { background: rgba(var(--kc), .10); }
.cab-box.has-ico.is-pt .cab-ico {
  left: 50%; top: 50%; right: auto; bottom: auto;
  width: 92%; height: 92%;
  min-width: 18px; min-height: 18px;
  transform: translate(-50%, -50%);
  background-position: center;
}
.cab-box.has-ico.is-pt .cab-tag { display: none; }

/* 点位类(烟感/水浸/温湿度/摄像头)方块小,标签再压一号 */
.cab-box.k-smoke .cab-tag,
.cab-box.k-water .cab-tag,
.cab-box.k-th .cab-tag,
.cab-box.k-cam .cab-tag { font-size: 8px; letter-spacing: -.2px; }

/* 详情页标题/信息里的类型标签 */
.kind-tag {
  --kc: 100, 116, 139;                  /* 中性兜底:正常都有 .k-xxx 类或内联 --kc */
  display: inline-block;
  padding: 1px 9px;
  border: 1px solid rgba(var(--kc), .5);
  border-radius: 999px;
  background: rgba(var(--kc), .13);
  color: rgba(var(--kc), 1);
  font-size: 11.5px;
  font-weight: 650;
  line-height: 1.75;
  vertical-align: middle;
  white-space: nowrap;
}

/* 对象信息表(非机柜类型的详情页左栏) */
.kv-list { padding: 2px 0 6px; }
/* 「类型」那一行右侧的「管理类型 ↗」—— 对象信息里点得到类型管理页 */
.kv-manage { margin-left: 9px; font-size: 12px; white-space: nowrap; }
.kv {
  display: flex;
  gap: 12px;
  padding: 7px 14px;
  border-bottom: 1px dashed rgba(15, 23, 42, .08);
  font-size: 13px;
}
.kv:last-child { border-bottom: 0; }
.kv-k { flex: 0 0 78px; color: var(--text-mute); }
.kv-v { flex: 1 1 auto; color: var(--text); word-break: break-all; }
.kv-v.mono { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 12.5px; }
/* 「上联端口」那行 —— 未登记时的补白提示,跟值同一行但明显弱化 */
.kv-tip { margin-left: 8px; font-size: 12px; color: var(--text-mute); }

/* 统计值里放文字(非机柜详情页)时收一号 */
.stat-value.stat-text { font-size: 15px; font-weight: 620; }

/* 图注里的类型选择 */
.kind-pick-wrap {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--text-mute);
}
.kind-pick { height: 28px; padding: 0 6px; font-size: 12.5px; }

/* 面板内的一整段说明(非机柜详情页底部) */
.plan-inline-hint { margin: 0; padding: 14px 16px; line-height: 1.95; }
@keyframes cab-pop {
  from { transform: scale(.6); opacity: 0; }
  to { transform: scale(1); opacity: 1; }
}

.cab-tag {
  display: block;
  padding: 0 2px;
  font-size: 9px;
  line-height: 1.5;
  font-weight: 650;
  color: rgba(var(--kc), 1);
  text-shadow: 0 0 2px rgba(255, 255, 255, .8);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: none;
}

.cab-resize {
  position: absolute;
  right: 0; bottom: 0;
  width: 9px; height: 9px;
  background: var(--accent);
  border-radius: 2px 0 2px 0;
  cursor: nwse-resize;
  opacity: 0;
  transition: opacity .12s;
}
.cab-box:hover .cab-resize { opacity: 1; }

.cab-draft {
  position: absolute;
  z-index: 4;
  border: 1.5px dashed var(--accent);
  border-radius: 3px;
  background: rgba(37, 99, 235, .13);
  pointer-events: none;
}
.cab-draft[hidden] { display: none; }

/* ---- 楼层图上叠加的对象层 ----
   一层有好几百个方块,楼层图在页面里只有几百像素宽 —— 编号一律不画(糊成一片),
   只留色块 + 悬停提示 + 类型图标。「隐藏对象层」按钮给 .cab-layer 加 is-off。 */
.cab-box.is-mini { border-radius: 2px; }
.cab-box.is-mini .cab-tag { display: none; }
.cab-layer.is-off { display: none; }

/* 楼层图上的类型图标(山叔 2026-09-19:楼层的平面图上也显示图标)
   —— 空调 / 摄像头 / 门禁这些在类型里配了图的,直接把设备图画在方块里,跟房间页一致。
   ⚠️ 楼层图的方块比房间图小一个量级(实测 stage 1360px 时:摄像头 11×10、门禁 10×16、
      精密空调 12~36×20~51),纯按百分比铺会缩成一颗糊点 —— 所以给 13px 的 min 兜底,
      宁可图标略微溢出色块,也要让人认得出是什么(同房间页点位类那套做法)。
   ⚠️ 必须写在上面 .is-pt 那组规则**后面**:两边都是 3 个类的选择器,同级下后写的赢。
      楼层图上烟感/水浸/温湿度/摄像头**同时**是 is-mini 和 is-pt,得让 mini 这版说了算,
      否则会套用房间页的 92% 尺寸(在 10px 的方块上等于没画)。 */
.cab-box.is-mini.has-ico .cab-ico {
  left: 50%; top: 50%; right: auto; bottom: auto;
  width: 88%; height: 88%;
  min-width: 13px; min-height: 13px;
  transform: translate(-50%, -50%);
  background-position: center;
  /* ⚠️ 别在这里加 drop-shadow 描边「托一下」——试过：13px 的图标上,
     1px 白边占掉近 1/6 的尺寸，二次叠加后轮廓直接发虚，比不加还难认。
     小尺寸下**清晰**比「浮起来」重要。 */
}

/* 图注里的小图例:深色 = 已装设备,空心 = 空柜 */
.fl-legend {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  flex: none;
  font-size: 11.5px;
  color: var(--text-mute);
  white-space: nowrap;
}
.fl-swatch {
  width: 12px; height: 12px;
  border-radius: 2px;
  background: rgba(37, 99, 235, .55);
  border: 1px solid rgba(37, 99, 235, 1);
}
.fl-swatch.is-empty {
  margin-left: 6px;
  background: transparent;
  border-color: rgba(37, 99, 235, .45);
}

/* 对象清单页的「只看空柜」勾选 */
.obj-empty-pick {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-left: 10px;
  cursor: pointer;
}
.obj-empty-pick input { margin: 0; }

/* 图注里的对象计数:分类写开后文字变长,允许收缩并省略 */
.cab-count {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12px;
  color: var(--text-mute);
}
.cab-count .dev-on { color: var(--accent); }

/* 绘制模式提示条 */
.cab-tip {
  display: none;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
  padding: 9px 14px;
  border: 1px solid #bfdbfe;
  border-radius: 9px;
  background: #eff6ff;
  color: #1e40af;
  font-size: 12.5px;
}
.cab-tip.is-on { display: flex; }

/* 新建 / 编辑弹层 */
.cab-modal {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(15, 23, 42, .5);
}
.cab-modal[hidden] { display: none; }
.cab-form {
  width: min(580px, 92vw);
  max-height: 88vh;
  overflow: auto;
  padding: 20px 22px 18px;
  border-radius: 14px;
  background: #fff;
  box-shadow: 0 24px 64px rgba(15, 23, 42, .3);
}
.cab-form h3 { margin: 0 0 4px; font-size: 15px; font-weight: 650; }
.cab-form-sub { margin: 0 0 14px; font-size: 12.5px; color: var(--text-mute); }
.cab-form-row { display: grid; grid-template-columns: 2fr 1fr; gap: 12px; }
.cab-form label, .cab-form-full {
  display: flex; flex-direction: column; gap: 5px;
  font-size: 12.5px; color: var(--text-soft);
}
.cab-form-full { margin-top: 12px; }
.cab-form-foot {
  display: flex; justify-content: flex-end; gap: 8px;
  margin-top: 18px; padding-top: 14px;
  border-top: 1px solid var(--border);
}

/* ------------------------------------------------------------ 机柜页 */
.cab-grid {
  display: grid;
  grid-template-columns: 268px minmax(0, 1fr);
  gap: 16px;
  align-items: start;
  margin-bottom: 6px;
}
.panel-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; margin-bottom: 12px;
}
.panel-head h2 { margin: 0; font-size: 14px; font-weight: 650; }
.panel-head .hint { font-size: 12px; color: var(--text-mute); }

.uview { display: flex; flex-direction: column; gap: 1px; }
.urow {
  display: flex; align-items: center; gap: 6px;
  height: 15px;
  border: 1px solid transparent;
  border-radius: 3px;
  background: #f4f6fa;
}
.urow.is-used      { background: #e4edff; border-color: #c7dbff; }
.urow.is-faulty    { background: #fdecec; border-color: #f5c2c2; }
.urow.is-repairing { background: #fdf3e4; border-color: #f3ddb8; }
.urow.is-spare     { background: #f1f4f8; border-color: #dde3ec; }
/* 多 U 设备画成一个整体(山叔 2026-09-27,模板里打 is-lead / is-cont 标记):
   占 2U 以上的设备,行与行之间的 1px 间隙和上下圆角都抹掉,颜色连成一块。
   中间行(上面是续格、下面还是续格)下圆角也要平 —— 用 :has 看下一个兄弟。
   边框本身是透明的、透出的是各自的底色,行挨上后颜色自然无缝。 */
.urow.is-lead { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
.urow.is-cont {
  margin-top: -1px;   /* 吃掉 .uview 的 1px 间隙 */
  border-top-left-radius: 0; border-top-right-radius: 0;
}
.urow.is-cont:has(+ .urow.is-cont) {
  border-bottom-left-radius: 0; border-bottom-right-radius: 0;
}
.unum {
  flex: none; width: 24px; text-align: right;
  font-size: 9.5px; color: var(--text-mute);
  font-variant-numeric: tabular-nums;
}
.udev {
  font-size: 10px; color: #1e3a8a;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.form-grid > label { display: flex; flex-direction: column; gap: 5px; font-size: 12.5px; color: var(--text-soft); }

/* ⚠️ 作者样式里的 display 会盖过浏览器默认的 [hidden]：
   .form-grid > label 的 display:flex 就曾让弹层里的「总 U 数」藏不住(非机柜类型)。
   显式补一条,以后任何元素加 hidden 都能真正隐藏。 */
[hidden] { display: none !important; }
.form-grid .span2 { grid-column: 1 / -1; }
.form-foot { display: flex; justify-content: flex-end; gap: 8px; }
.req { color: #dc2626; }
.geom-details { font-size: 12.5px; color: var(--text-soft); }
.geom-details summary { cursor: pointer; padding: 3px 0; color: var(--text-mute); }
.geom-grid { margin-top: 10px; grid-template-columns: repeat(4, minmax(0, 1fr)); }

.btn-sm { padding: 6px 14px; font-size: 12.5px; border: 0; font-family: inherit; cursor: pointer; }
.btn-danger { background: #dc2626; }
.btn-danger:hover { background: #b91c1c; }
/* 次要按钮 —— 跟主按钮并排时用(0.26.0:房间页工具栏「⟳ 按对象同步」)。
   ⚠️ 必须写在 .btn:hover 后面:两者特异度一样,靠源码顺序取胜。
   .btn-sm 已经把 border 清零,所以描边改用 inset box-shadow。 */
.btn-quiet { background: var(--panel); color: var(--text-soft); box-shadow: inset 0 0 0 1px var(--border-strong); }
.btn-quiet:hover { background: var(--panel-soft); color: var(--text); }

.mini-btn {
  display: inline-block;
  padding: 3px 9px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: #fff;
  color: var(--text-soft);
  font-size: 12px;
  font-family: inherit;
  cursor: pointer;
}
.mini-btn:hover { border-color: var(--accent); color: var(--accent); }
.mini-danger:hover { border-color: #dc2626; color: #dc2626; }
.inline-form { display: inline; }
.col-act { white-space: nowrap; }

.panel-danger-zone { border-color: #f0cfcf; background: #fffbfb; }
.panel-danger-zone .hint { display: block; margin: 0 0 12px; }

.head-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }

.flash {
  margin-bottom: 14px; padding: 9px 14px;
  border: 1px solid; border-radius: 9px; font-size: 13px;
}
.flash-ok { background: var(--ok-soft); border-color: #bfe3cd; color: var(--ok); }
.flash-err { background: var(--danger-soft); border-color: #f3c8c8; color: var(--danger); }

/* ---------- 轻提示(toast) ----------
   给「不跳页的操作」一个反馈(0.29.0,删台账就地消失那次)。
   顶部那排 .flash 是服务端 redirect 带回来的 —— 不跳页就没有它了,人会以为没删掉。
   ⚠️ `pointer-events: none` 是有意的:这东西飘在右下角,绝不能挡住下面的按钮,
      否则连点两下删除会点到 toast 上(或者点不到「＋ 新增设备」)。
   ⚠️ `position: fixed` 同理 —— 不能挤动页面布局,否则「留在当前界面」白说,
      滚动位置还是会跳。 */
.toast-wrap {
  position: fixed; right: 20px; bottom: 20px; z-index: 90;
  display: flex; flex-direction: column; gap: 8px;
  pointer-events: none;
}
.toast {
  max-width: 380px; padding: 10px 14px;
  background: #1f2937; color: #f3f6fa;
  border-radius: 9px; font-size: 13px; line-height: 1.5;
  box-shadow: 0 8px 24px rgba(16, 24, 40, .18);
  animation: toast-in .18s ease-out;
  transition: opacity .5s, transform .5s;
}
.toast-err { background: var(--danger); }
.toast.is-out { opacity: 0; transform: translateY(6px); }
@keyframes toast-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ---------- 对象类型管理页(/kinds) ----------
   一个类型一张卡片,单条增删改(不再是一页大表格全量提交)。
   颜色是 <input type=color> 提交的 hex,后端转成 "r, g, b" 写进 --kc ——
   所以用户新增类型**不需要动任何 CSS**。 */
.kind-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(296px, 1fr));
  gap: 14px;
  align-items: start;      /* 展开编辑的卡片变高时,不要把同行的卡片一起扯高 */
}

.kind-card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-top: 3px solid rgb(var(--kc, 100, 116, 139));
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 12px 14px 10px;
}

.kind-card-head { display: flex; align-items: center; gap: 9px; }
/* 展开编辑的那张卡片描个边 —— 卡片多的时候一眼看出在改哪条 */
.kind-card.is-editing {
  border-left-color: var(--accent);
  border-right-color: var(--accent);
  border-bottom-color: var(--accent);
  box-shadow: var(--shadow-hover);
}
.kind-ava {
  flex: none; width: 30px; height: 30px;
  display: grid; place-items: center;
  border-radius: 8px;
  background: rgba(var(--kc, 100, 116, 139), .1);
}
.kind-ava img { width: 20px; height: 20px; object-fit: contain; }
.kind-ava-blk {
  width: 15px; height: 15px; border-radius: 4px;
  background: rgb(var(--kc, 100, 116, 139));
}
.kind-card-title { flex: 1 1 auto; min-width: 0; }
.kind-card-title b {
  font-size: 14.5px; font-weight: 650;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.kind-card-n {
  flex: none; text-align: right; line-height: 1.15;
  font-size: 15px; font-weight: 700;
  color: rgb(var(--kc, 100, 116, 139));
  font-variant-numeric: tabular-nums;
}
.kind-card-n i {
  display: block; font-style: normal;
  font-size: 10.5px; font-weight: 500; color: var(--text-mute);
}
.kind-card-n.is-zero { color: var(--text-mute); }

.kind-card-meta {
  margin-top: 9px;
  display: flex; align-items: center; gap: 5px;
  font-size: 12px; color: var(--text-mute);
}
.kind-card-meta .kind-dot { margin: 0; }
.kind-hex { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 11.5px; }
.kind-sep { color: #cbd5e1; }

.kind-card-pills { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 5px; }
.kind-card-pills .pill { font-size: 11px; }

/* 编辑区里的「颜色」—— 机柜类(带 U 位)固定红底,给只读色块代替取色器。
   不做这一步的话,用户在里面调半天颜色发现没反应,会以为功能坏了
   —— 实际是 app.kind_kc() 按 has_u 强制成红,取色器对它无效。 */
.kind-lock-row { display: flex; align-items: center; gap: 7px; }
.kind-lock-swatch {
  flex: none; width: 22px; height: 22px; border-radius: 6px;
  background: rgb(var(--kc, 220, 38, 38));
  border: 1px solid rgba(var(--kc, 220, 38, 38), .55);
  box-shadow: inset 0 0 0 3px rgba(255, 255, 255, .45);
}
.kind-lock-note { font-size: 12px; font-weight: 600; color: var(--text-soft); }

/* 新增类型时勾了「有机柜式 U 位」→ 颜色控件自动锁成红(联动见 app.js 第 9 节) */
label.is-locked > input[type=color] { opacity: .3; pointer-events: none; }
label.is-locked::after {
  content: "机柜类固定红底，无需单独配色";
  display: block; margin-top: 4px;
  font-size: 11px; color: var(--text-mute);
}

.kind-rooms {
  margin-top: 9px;
  display: flex; flex-wrap: wrap; align-items: center; gap: 5px;
  font-size: 11.5px;
}
.kind-rooms-k { color: var(--text-mute); }
.kind-rooms.is-none { color: var(--text-mute); }
.room-chip {
  padding: 1px 7px; border-radius: 999px;
  background: var(--panel-soft); border: 1px solid var(--border);
  color: var(--text-soft);
}
.room-chip b { color: var(--text); font-variant-numeric: tabular-nums; }
.room-chip.is-more { color: var(--text-mute); }

/* 编辑区:details 就地展开 —— 不用弹层,也不会「改一行提交全表」 */
.kind-edit { margin-top: 11px; border-top: 1px dashed var(--border); padding-top: 9px; }
.kind-edit > summary {
  display: inline-block;
  padding: 3px 11px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: #fff;
  color: var(--text-soft);
  font-size: 12px;
  cursor: pointer;
  list-style: none;
  user-select: none;
}
.kind-edit > summary::-webkit-details-marker { display: none; }
.kind-edit > summary:hover { border-color: var(--accent); color: var(--accent); }
.kind-edit[open] > summary {
  border-color: var(--accent); background: var(--accent-soft); color: var(--accent);
}
.kind-form { margin-top: 12px; gap: 10px; }
.kind-note {
  margin: 0; padding: 8px 10px;
  background: var(--warn-soft); border: 1px solid #f3ddb8; border-radius: 8px;
  font-size: 12px; color: var(--warn);
}
.kind-card-foot {
  margin-top: 11px; padding-top: 9px;
  border-top: 1px solid var(--border);
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; flex-wrap: wrap;    /* 卡片只有 ~268px 内容宽,转移表单挤不下就换行 */
}
.kind-card-hint { font-size: 11.5px; color: var(--text-mute); }

/* 新增类型:默认收成一颗按钮,展开才出表单 —— 进页面先看到的是卡片 */
.kind-add { margin-bottom: 14px; }
.kind-add > summary {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 15px;
  border: 1px solid var(--accent);
  border-radius: 8px;
  background: var(--accent);
  color: #fff;
  font-size: 13px; font-weight: 600;
  cursor: pointer; list-style: none; user-select: none;
}
.kind-add > summary::-webkit-details-marker { display: none; }
.kind-add > summary:hover { background: var(--accent-dark); border-color: var(--accent-dark); }
.kind-add[open] > summary { background: var(--accent-soft); color: var(--accent); }
.kind-add-plus { font-size: 15px; line-height: 1; }
.kind-add-panel { margin-top: 12px; }

/* ---------- 类型卡片上的操作条:排序 / 换图标 / 编辑 ---------- */
.kind-ops {
  margin-top: 10px;
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
}

.icon-btn {
  width: 26px; height: 26px; padding: 0;
  border: 1px solid var(--border-strong); border-radius: 6px;
  background: #fff; color: var(--text-soft);
  font-size: 13px; line-height: 1; cursor: pointer;
}
.icon-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.icon-btn:disabled { opacity: .35; cursor: not-allowed; }

/* 「换图标」是个 <label for=隐藏的 file> —— 点了直接弹文件框,不用先展开编辑区 */
.kind-up { display: inline; }
.kind-up .mini-btn { user-select: none; }

/* ---------- 图标库 ---------- */
.kind-icons { margin-bottom: 14px; }
.kind-icons > summary {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 15px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: #fff;
  color: var(--text-soft);
  font-size: 13px; font-weight: 600;
  cursor: pointer; list-style: none; user-select: none;
}
.kind-icons > summary::-webkit-details-marker { display: none; }
.kind-icons > summary:hover { border-color: var(--accent); color: var(--accent); }
.kind-icons[open] > summary {
  border-color: var(--accent); background: var(--accent-soft); color: var(--accent);
}
.kind-icons > .panel { margin-top: 12px; }

.kind-ico-grid {
  display: grid; gap: 8px; margin-bottom: 14px;
  grid-template-columns: repeat(auto-fill, minmax(78px, 1fr));
}
.kind-ico-cell {
  margin: 0; padding: 9px 4px 6px;
  border: 1px solid var(--border); border-radius: 8px;
  background: var(--panel-soft);
  display: flex; flex-direction: column; align-items: center; gap: 5px;
}
.kind-ico-cell img { width: 30px; height: 30px; object-fit: contain; }
.kind-ico-cell figcaption {
  max-width: 100%;
  font-size: 10.5px; color: var(--text-mute);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.kind-up-form {
  display: flex; align-items: center; gap: 9px; flex-wrap: wrap;
  padding-top: 12px; border-top: 1px dashed var(--border);
}

/* ---------- 名下对象清单 ---------- */
.kind-objs { margin-top: 10px; border-top: 1px dashed var(--border); padding-top: 9px; }
.kind-objs > summary {
  display: inline-block;
  padding: 3px 11px;
  border: 1px solid var(--border-strong); border-radius: 6px;
  background: #fff; color: var(--text-soft);
  font-size: 12px; cursor: pointer; list-style: none; user-select: none;
}
.kind-objs > summary::-webkit-details-marker { display: none; }
.kind-objs > summary:hover { border-color: var(--accent); color: var(--accent); }
.kind-objs[open] > summary {
  border-color: var(--accent); background: var(--accent-soft); color: var(--accent);
}
.ko-list {
  list-style: none; margin: 10px 0 0; padding: 0;
  max-height: 230px; overflow: auto;
  border: 1px solid var(--border); border-radius: 8px;
}
.ko-list li + li { border-top: 1px solid var(--border); }
.ko-list a {
  display: flex; align-items: center; gap: 8px;
  padding: 5px 9px;
  font-size: 12px; color: var(--text); text-decoration: none;
}
.ko-list a:hover { background: var(--accent-soft); }
.ko-room {
  flex: 0 0 auto; padding: 0 6px;
  border-radius: 4px; background: var(--mute-soft);
  color: var(--text-soft); font-size: 11px;
}
.ko-code {
  flex: 0 0 auto; color: var(--text-mute);
  font-family: ui-monospace, Menlo, Consolas, monospace;
}
.ko-name {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ko-list .pill { font-size: 10.5px; }
.ko-more { padding: 6px 9px; font-size: 11.5px; color: var(--text-mute); }

/* ---------- 删除:名下有对象时先选转移目标,再删 ---------- */
/* 占满整行、右对齐 —— 转移表单比「内置类型」那行提示更重要,让它自己站一行 */
.kind-del {
  width: 100%;
  display: flex; align-items: center; justify-content: flex-end; gap: 6px;
}
.kind-del-hint { font-size: 11.5px; color: var(--text-mute); white-space: nowrap; }
.kind-del .input-xs { width: 96px; }

/* 从别处带锚点跳进来(/kinds#k-3)时把那张卡片描出来 —— 一眼看到点的是哪个 */
.kind-card:target {
  border-left-color: var(--accent);
  border-right-color: var(--accent);
  border-bottom-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.kind-checks { display: flex; flex-direction: column; justify-content: center; gap: 6px; }
.form-grid .kind-checks { grid-column: 1 / -1; }
.kind-checks .chk {
  display: flex; align-items: center; gap: 7px;
  font-size: 12.5px; color: var(--text-soft); cursor: pointer;
}
.field-hint { font-size: 11px; color: var(--text-mute); }

.input-xs { width: 64px; padding: 4px 6px; font-size: 12px; }
.input-mid { width: 150px; padding: 4px 6px; font-size: 12px; }
/* 类型名旁边的小图标预览 —— 跟平图上画的是同一张图 */
.kind-ico-prev {
  width: 18px; height: 18px; object-fit: contain;
  vertical-align: -4px; margin-right: 5px;
}
/* 图注里更小一号(「共 153 个对象 · 空调 7」那行) */
.stat-ico {
  width: 13px; height: 13px; object-fit: contain;
  vertical-align: -2px; margin-right: 2px;
}
.kind-color {
  width: 100%; max-width: 74px; height: 30px; padding: 2px; cursor: pointer;
  border: 1px solid #e4e9f0; border-radius: 6px; background: #fff;
}
.kind-dot {
  display: inline-block; width: 10px; height: 10px; border-radius: 50%;
  background: rgb(var(--kc, 100, 116, 139)); margin-right: 6px; vertical-align: middle;
}
.kind-foot {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 12px 16px; border-top: 1px solid #eef2f7;
}
.kind-foot .hint { font-size: 12px; color: var(--text-mute); }
.tag-mini {
  display: inline-block; margin-left: 6px; padding: 1px 6px; border-radius: 4px;
  background: #eef2f7; color: #64748b; font-size: 11px;
}

/* ---------- 对象清单页(/room/<id>/objects) ---------- */
.obj-table { min-width: 1280px; }
.obj-table td { padding: 6px 10px; }
.obj-table .input-xs { width: 84px; }
.obj-table .input-mid { width: 160px; }
.obj-table .col-pick { width: 34px; text-align: center; }
.obj-table th.col-pick { padding-left: 10px; padding-right: 0; }
.obj-table input[type=checkbox] {
  width: 15px; height: 15px; margin: 0;
  accent-color: var(--accent); cursor: pointer; vertical-align: middle;
}
/* 位置只读、大小可改 —— 左/上灰字，宽/高是输入框 */
.obj-geom { white-space: nowrap; font-size: 12px; }
.obj-geom .obj-xy { color: var(--text-mute); margin-right: 10px; }
.obj-size { display: inline-flex; align-items: center; gap: 3px; color: var(--text-soft); }
.obj-size label { display: inline-flex; align-items: center; gap: 3px; }
.obj-size + .obj-size, .obj-size label + label { margin-left: 8px; }
.obj-size .input-xs { width: 68px; }

/* 批量统一大小的工具条 */
.obj-batch {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
  margin: 0 0 10px; padding: 9px 12px;
  background: var(--accent-soft);
  border: 1px solid #dbe7fe; border-radius: var(--radius);
  font-size: 13px;
}
.obj-batch .obj-batch-pick { color: var(--text-soft); }
.obj-batch .obj-batch-pick b { color: var(--accent-dark); font-size: 14px; }
.obj-batch-field {
  display: inline-flex; align-items: center; gap: 4px;
  color: var(--text-soft); white-space: nowrap;
}
.obj-batch-field .input-xs { width: 78px; }
.obj-batch-msg { color: var(--ok); font-size: 12.5px; }
.obj-batch-msg.is-bad { color: var(--danger); }
.obj-batch-tip { margin-left: auto; font-size: 12px; }

.empty { grid-column: 1 / -1; text-align: center; color: var(--text-mute); padding: 30px; font-size: 13px; }

@media (max-width: 900px) {
  .app { grid-template-columns: 1fr; }
  .sidebar {
    position: static;
    height: auto;
    border-right: none;
    border-bottom: 1px solid var(--border);
  }
  .nav { max-height: 46vh; }
  .topbar { padding: 0 14px; }
  .page { padding: 16px 14px 48px; }
}

/* ==================== 交换机端口面板 ==================== */
/* 前面板走「浅灰金属」拟物:所有颜色是本文件里写死的,不跟对象类型配色。 */
.port-head-tag {
  font-size: 12px; font-weight: 600;
  color: var(--info); background: var(--info-soft);
  border-radius: 999px; padding: 2px 10px; vertical-align: 3px;
}

.switch-face {
  background: linear-gradient(180deg, #fbfcfe 0%, #eef1f6 100%);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  box-shadow: inset 0 1px 0 #fff, var(--shadow);
  overflow: hidden;
}
.switch-face-bar {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 16px;
  background: linear-gradient(180deg, #e7ebf2 0%, #dde3ec 100%);
  border-bottom: 1px solid var(--border-strong);
  font-size: 12px; color: var(--text-soft);
}
.sf-brand { font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--text); }
.sf-model { font-family: ui-monospace, Menlo, Consolas, monospace; color: var(--text-soft); }
.sf-name  { margin-left: auto; font-weight: 600; color: var(--text); }
.sf-ip    { font-family: ui-monospace, Menlo, Consolas, monospace; color: var(--text-mute); }

.switch-face-body { padding: 14px 16px; display: flex; flex-direction: column; gap: 14px; }
.sf-group-label { font-size: 12px; color: var(--text-mute); margin-bottom: 7px; }
.sf-group-label b { color: var(--text-soft); }

/* 前面板一行几个口由模板按机型算好(--cols),见 app.py:port_grid_cols()
   24 口电口 → 2 行 × 12,48 口电口 → 2 行 × 24 —— 两种 1U 机型的真实排法 */
.pt-grid { display: grid; grid-template-columns: repeat(var(--cols, 24), minmax(0, 1fr)); gap: 5px; }

/* 少口设备(动环采集器 1 口、光口 4 口这种,列数 < 8):格子不许跟着 1fr 撑满整行 ——
   否则 1 口的机器一个口就铺满 1000px 宽、按 4:3 撑到 700px 高(山叔 2026-09-29)。
   封顶 64px,左对齐,观感就是真机正面那一小截。
   ⚠️ 写成 .pt-grid.pt-grid-few(两个类)是为了压过下面两个窄屏媒体查询里的 .pt-grid
      —— 窄屏收窄列数时,少口设备同样要保持 64px 封顶,不能又被 1fr 撑开。 */
.pt-grid.pt-grid-few { grid-template-columns: repeat(var(--cols, 1), minmax(0, 64px)); }

.sf-layout { margin-left: 6px; font-size: 11px; color: var(--text-mute); opacity: .85; }

/* RJ45 的卡扣凹槽 —— 0.35.1 恢复:0.34.2「编号进框」时把整条删了,连带光口椭圆
   (下面那条只留了尺寸,没留 content/left/背景)也一起不渲染 → 端口图标全没了。
   占比压到 10%+26%:48 口密排时格子才 17px 高,图标再大就跟框内编号打架。 */
.pt-slot::after {
  content: ""; position: absolute; left: 50%; top: 10%;
  width: 46%; height: 26%; transform: translateX(-50%);
  border-radius: 1px;
  background: rgba(255, 255, 255, .42);
  box-shadow: inset 0 1px 1px rgba(16, 24, 40, .14);
}

/* 光口(SFP)和电口(RJ45)要一眼分得开:口子是圆的 + 模块孔是圆的。
   宽度跟电口一致(真机上 SFP 笼 14mm vs RJ45 13.5mm,几乎同宽),只是略高一点。 */
.pt-grid-opt .pt-slot { border-radius: 6px; aspect-ratio: 4 / 3.4; }
.pt-grid-opt .pt-slot::after {
  top: 10%;
  width: 40%; height: 30%;
  border-radius: 50% / 45%;
}

.pt {
  --pc: #cbd5e1;                       /* 端口主色,按状态覆盖 */
  appearance: none; background: none; border: 0; padding: 0;
  cursor: pointer; min-width: 0;
  position: relative; display: block;
}
.pt-slot {
  position: relative;
  /* ⚠️ display:block 不能删 —— 0.34.2 把 .pt 从 flex 改成 block 之后,
     .pt-slot 这个 span 退回行内盒,width:100% 失效、格子塌成 2px 宽,
     图标伪元素也跟着 0 宽(山叔 2026-10-17:端口图标不见了)。 */
  display: block;
  width: 100%; aspect-ratio: 4 / 3;
  border-radius: 3px;
  background: var(--pc);
  border: 1px solid rgba(16, 24, 40, .12);
  box-shadow: inset 0 -2px 0 rgba(16, 24, 40, .16), inset 0 1px 0 rgba(255, 255, 255, .38);
  transition: transform .08s ease, box-shadow .12s ease;
}
/* 录了对端的口,右上角点一颗蓝珠 */
.pt.has-peer .pt-slot::before {
  content: ""; position: absolute; right: 1px; top: 1px; z-index: 2;
  width: 5px; height: 5px; border-radius: 50%;
  background: #0ea5e9; box-shadow: 0 0 0 1px rgba(255, 255, 255, .85);
}
/* 编号/端口名 —— 画进方框内,0.35.1 起沉到下半截(凹槽/椭圆占上半截,
   真机正面就是「口在上、号在下」),白描边垫底,灰底/绿底上都读得清。
   ⚠️ line-height 收到 1.05:48 口密排时格子只有 17px 高,行高一大就跟图标压上。 */
.pt-no {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 1;
  display: flex; align-items: flex-end; justify-content: center;
  padding: 0 2px 1px;
  font-size: 9px; line-height: 1.05;
  font-family: ui-monospace, Menlo, Consolas, monospace;
  color: rgba(16, 24, 40, .82);
  text-shadow: 0 1px 0 rgba(255, 255, 255, .45);
  white-space: nowrap; overflow: hidden;
}
/* 自己改过名的口:黑色加粗(山叔 2026-10-17:灰字不清楚) */
.pt.has-name .pt-no { color: #111; font-weight: 700; text-shadow: 0 1px 0 rgba(255, 255, 255, .55); }
.pt:hover .pt-slot { transform: translateY(-2px); box-shadow: 0 4px 10px rgba(16, 24, 40, .20); }
.pt:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }

/* 状态配色 —— 复用 pill 那套 ok/info/warn/danger/mute 语义 */
.pt-mute   { --pc: #cbd5e1; }   /* 空闲 */
.pt-ok     { --pc: #16a34a; }   /* 占用 */
.pt-danger { --pc: #dc2626; }   /* 故障 */
.pt-warn   { --pc: #d97706; }   /* 预留 */

/* ---------- ODF 架端口面板(0.35.0) ----------
   一张 ODF 架 = 若干「盘」,每盘 12 芯横排,像真机正面那一长条。
   并口(双芯 LC 双工)跨两格宽 —— 由模板给 .pt 内联 grid-column: span 2。 */
.odf-face { display: flex; flex-direction: column; gap: 8px; }
.odf-tray {
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  background: var(--panel-soft);
  overflow: hidden;
  width: fit-content;                 /* 面板只占需要的那点宽度,不撑满 */
  max-width: 100%;
}
.odf-tray-label {
  padding: 3px 10px;
  font-size: 11.5px; font-weight: 650; color: var(--text-soft);
  background: linear-gradient(180deg, #eef2f8, #e3e9f2);
  border-bottom: 1px solid var(--border);
}
/* 独立网格(不带 .pt-grid 类,避开窄屏那两条把列数砍到 6/12 的媒体查询):
   ODF 一排就是 12 芯,拆两排反而乱。
   ⚠️ 0.36.1 起列宽封顶 36px + 左对齐:双纤并口跨两格,按 1fr 均分整行的话
      一个口能撑到 176×130px,一屏只看得到 4 个盘(山叔 2026-10-17:太大了)。
      封顶后并口 76×57px,一屏能看 8 个盘,跟真机 ODF 正面那一小条差不多。 */
.odf-grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 36px));
  justify-content: start;
  gap: 4px; padding: 7px 9px;
}
/* 窄屏放不下 12×36px,退回 1fr 挤着排(至少还能看个大概) */
@media (max-width: 620px) {
  .odf-grid { grid-template-columns: repeat(12, minmax(0, 1fr)); }
}
.odf-grid .pt-no { font-size: 11px; }
/* 双纤开关条(0.36.0):放在「端口数量」面板最上面,一眼看出这台是不是双纤架 */
.odf-dual-bar {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin: 0 0 14px; padding: 10px 12px;
  border: 1px solid var(--border); border-radius: 8px;
  background: var(--panel-soft);
}
.odf-dual-bar .hint { flex: 1; min-width: 240px; line-height: 1.55; }
.odf-dual-switch { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.odf-dual-switch span { font-weight: 500; color: var(--text-soft); }
.odf-dual-switch input { pointer-events: none; }
/* 并口:稍深的描边 + 一颗小「双工」角标,跟单芯一眼分开 */
.pt.is-merged .pt-slot {
  box-shadow: inset 0 -2px 0 rgba(16, 24, 40, .16), inset 0 1px 0 rgba(255, 255, 255, .38),
              0 0 0 1.5px #0f766e;
}
.pt.is-merged .pt-no { color: #0f766e; font-weight: 700; }
.pt.is-merged.has-name .pt-no { color: #111; }

/* 端口明细表:靠这个类做筛选隐藏 */
.port-row.is-hide { display: none; }
.panel-head-pad { padding: 16px 16px 0; margin-bottom: 10px; }

/* 保存后回跳,把刚改的那个口圈出来 */
.pt.is-flash .pt-slot {
  outline: 3px solid var(--accent); outline-offset: 2px;
  animation: pt-flash 1.4s ease-out 2;
}
@keyframes pt-flash {
  0%, 100% { outline-color: var(--accent); }
  50%      { outline-color: transparent; }
}
/* 明细表里对应那一行也点一下 */
.port-row.is-flash { background: var(--accent-soft); }

/* ---------- 一个网口只能接一个设备(山叔 2026-09-19) ----------
   同一个平面图对象被第二个交换机口指向时,**后录的那条**标红。
   ⚠️ 用「加标记」而不是改 --pc:端口底色已经表达状态(空闲灰 / 占用绿 / 故障红 /
      预留琥珀),把颜色抢过来等于把状态藏了。所以是「槽位套一圈红 + 左上角一颗 !」。 */
.pt.is-dup { position: relative; }
.pt.is-dup .pt-slot {
  box-shadow: inset 0 -2px 0 rgba(16, 24, 40, .16), inset 0 1px 0 rgba(255, 255, 255, .38),
              0 0 0 2px var(--danger);
}
.pt-dup {
  position: absolute; top: -4px; left: -4px; z-index: 2;
  width: 12px; height: 12px; border-radius: 50%;
  background: var(--danger); color: #fff;
  font-size: 9px; line-height: 12px; font-weight: 700; text-align: center;
  box-shadow: 0 0 0 1.5px #fff;
}
.pt.is-dup .pt-no { color: var(--danger); font-weight: 600; }
.link.is-dup { color: var(--danger); }
.kv-tip.is-dup { display: block; margin: 4px 0 0; color: var(--danger); line-height: 1.55; }

/* 端口弹窗里「这个口已经被别的口占了」的当场预警 —— 文案由 JS 填,见 app.js:checkPeer */
.peer-warn {
  margin: 0 0 12px; padding: 9px 11px;
  background: var(--danger-soft); border: 1px solid #f3c8c8; border-radius: 8px;
  font-size: 12.5px; line-height: 1.6; color: var(--danger);
}
.cab-form-foot .btn:disabled { opacity: .45; cursor: not-allowed; }

/* 窄屏:列数收窄,但**不固定 12/6** —— 小端口面板(4 口光口)不该被撑成一整行 */
@media (max-width: 1200px) {
  .pt-grid { grid-template-columns: repeat(var(--cols-m, 12), minmax(0, 1fr)); }
}
@media (max-width: 700px) {
  .pt-grid { grid-template-columns: repeat(var(--cols-s, 6), minmax(0, 1fr)); }
}

/* ---- 编辑弹窗里的端口状态条(山叔 2026-09-19)----
   有网口的设备(动环采集器 / NVR)点开编辑,也要一眼看到每个口的状态。
   色块**复用端口面板那套 .pt / .pt-{状态}** —— 同一套配色,不另起炉灶。 */
.dev-ports {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}
.dev-ports-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 9px;
  font-size: 12.5px;
}
.dev-ports-title { font-weight: 650; color: var(--text); }
.dev-ports-sum { color: var(--text-mute); }
.dev-ports-go { margin-left: auto; white-space: nowrap; }
/* ⚠️ 固定 34px + 左对齐，**不要** auto-fill + 1fr —— 那样 2 口(采集器/NVR)的
   色块会各自撑到几百像素宽，看着像两个大按钮而不是两个 RJ45。 */
.dev-ports-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, 34px);
  justify-content: start;
  gap: 5px;
}
/* 弹窗里的口是 <a> 直跳端口面板,得把链接默认的下划线/颜色按掉 */
.dev-ports .pt { text-decoration: none; }
.dev-ports .pt-no { font-size: 8.5px; }
.dev-ports-note {
  margin: 9px 0 0;
  font-size: 11.5px;
  color: var(--text-mute);
}

/* ---- 机柜页:U 位图 / 设备表里的端口入口 ---- */
/* 交换机占的 U 位整格可点(透明链接铺满),点哪都能进端口面板 */
.urow { position: relative; }
.urow.is-clickable { cursor: pointer; }
.urow.is-clickable:hover {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-soft);
}

/* ---- 多U设备「真·一个整体」(山叔 2026-09-30) --------------------------
   0.31.1 抹平了行间缝和圆角,但每行自带的 1px 边框在相接处仍叠出一条横杠;
   hover 也只亮悬停的那一行,鼠标放上去还是「多个个体」。
   ① 内部相接的边框一律透明(底色本来就一致) —— 横杠消失,整块只剩一具外框。
   ② hover 整块亮:app.js 末尾的委托按 data-udev 给同一台设备的所有行统一加
      .is-grp-hover,外框只亮「顶格上边 + 末格下边 + 左右两侧」,中间不出线。
   ⚠️ 特异性都是冲着压过上面 `.urow.is-clickable:hover`(accent 全边框+外圈影)
      去的 —— 少一截就会被它翻成 accent 横线,别随手精简选择器。 */
.uview .urow.is-lead { border-bottom-color: transparent; }
.uview .urow.is-cont { border-top-color: transparent; }
.uview .urow.is-cont:has(+ .urow.is-cont) { border-bottom-color: transparent; }
.uview .urow.is-grp-hover {
  border-left-color: var(--accent); border-right-color: var(--accent); box-shadow: none;
}
.uview .urow.is-lead.is-grp-hover { border-top-color: var(--accent); border-bottom-color: transparent; }
.uview .urow.is-cont.is-grp-hover { border-top-color: transparent; }
.uview .urow.is-cont.is-grp-hover:not(:has(+ .urow.is-cont)) { border-bottom-color: var(--accent); }
.uview .urow.is-grp-hover .udev-go { opacity: 1; }
.urow-link { position: absolute; inset: 0; z-index: 1; }
/* 整格是 <button> 的那种(非交换机)——抹掉按钮自带的框和底色,只留一块可点的透明区 */
button.urow-btn {
  appearance: none; -webkit-appearance: none;
  padding: 0; border: 0; background: transparent;
  cursor: pointer;
}

.dev-link { color: var(--accent); text-decoration: none; }
.dev-link:hover { text-decoration: underline; }
/* 设备名做成按钮时(非交换机那几台)要长得和链接一模一样 */
button.dev-open {
  appearance: none; -webkit-appearance: none;
  display: inline; padding: 0; border: 0; background: transparent;
  font-family: inherit; font-size: inherit; font-weight: 550;
  text-align: left; cursor: pointer;
}
a.mini-btn { text-decoration: none; }

/* ---- 房间页「设备清单」里那个可改的名称(0.23.0) ----------------------
   名称做成按钮、长得像链接,底下加一条虚线 —— 光变个颜色没人知道这儿能点
   (山叔这次就是"看不出能改"才来问的)。点下去在原地换成 .dev-rename-input
   (见 app.js 第 13 节),存成功后闪一下底色。 */
button.dev-rename {
  appearance: none; -webkit-appearance: none;
  display: inline; padding: 0 0 1px; border: 0; background: transparent;
  font-family: inherit; font-size: inherit; font-weight: 550;
  text-align: left; cursor: pointer;
  color: var(--accent);
  border-bottom: 1px dashed var(--border-strong);
}
button.dev-rename:hover { border-bottom-color: var(--accent); }
button.dev-rename.is-flash { background: var(--accent-soft); border-radius: 3px; }
.dev-rename-input { max-width: 190px; height: 28px; font-size: 13px; padding: 0 6px; }

/* U 位图里那点「可点」的提示 —— 光靠蓝底别人不知道能点 */
.udev-go {
  margin-left: auto; padding-right: 4px;
  font-size: 9px; color: var(--accent); opacity: .7;
  white-space: nowrap;
}
.urow.is-clickable:hover .udev-go { opacity: 1; }

/* ---- 端口数量表单里的「24 口 / 48 口」快捷按钮 ---- */
.chip-btn {
  appearance: none; cursor: pointer;
  font: inherit; font-size: 11px; line-height: 1.6;
  padding: 2px 10px; margin-right: 5px;
  color: var(--accent); background: var(--accent-soft);
  border: 1px solid transparent; border-radius: 999px;
  transition: border-color .12s ease;
}
.chip-btn:hover { border-color: var(--accent); }
.chip-btn:active { transform: translateY(1px); }

/* 排法提示(→ 2 行 × 24 口) */
[data-layout-hint] {
  margin-left: 4px; color: var(--accent);
  font-family: ui-monospace, Menlo, Consolas, monospace;
}

/* ---- 设备清单「定位」列(0.30.0) --------------------------------------
   点「定位」→ 平面图上对应对象位置放一个高闪框(.loc-flash),约 3 秒后消失。
   框放进 .cab-layer(data-cab-layer),坐标用百分比 —— 与 .cab-box 同一套,
   底图缩放/换图都不飘;平面图被滚走时先 scrollIntoView 再闪。 */
.col-loc { white-space: nowrap; text-align: center; }
.dev-locate { min-width: 44px; }
.loc-none { color: var(--mute, #94a3b8); }

.loc-flash {
  position: absolute;
  z-index: 5;
  pointer-events: none;
  border: 3px solid #f59e0b;
  border-radius: 6px;
  background: rgba(245, 158, 11, .22);
  box-shadow: 0 0 0 2px rgba(255, 255, 255, .55), 0 0 18px 6px rgba(245, 158, 11, .55);
  animation: loc-blink .5s step-end 6;
}
@keyframes loc-blink {
  0%, 100% { opacity: 1; }
  50%      { opacity: .25; }
}

/* ---- 登录页 / 账号管理 / 只读提示(山叔 2026-10-01) ------------------ */
.login-page {
  min-height: 100vh; display: flex; align-items: center; justify-content: center;
  background: linear-gradient(160deg, #eef2f9 0%, #e3eaf5 100%);
}
.login-card {
  width: 340px; background: #fff; border: 1px solid var(--border);
  border-radius: 12px; box-shadow: var(--shadow);
  padding: 24px 24px 18px; display: flex; flex-direction: column; gap: 12px;
}
.login-brand {
  width: 40px; height: 40px; border-radius: 10px; background: var(--accent);
  color: #fff; font-weight: 800; font-size: 14px;
  display: flex; align-items: center; justify-content: center;
}
.login-card h1 { margin: 0; font-size: 17px; font-weight: 650; }
.login-sub { margin: 0; font-size: 12.5px; color: var(--text-mute); }
.login-card label { display: flex; flex-direction: column; gap: 5px; font-size: 12px; color: var(--text-soft); }
.btn-login { width: 100%; margin-top: 2px; }
.login-tip { margin: 4px 0 0; font-size: 11.5px; line-height: 1.6; color: var(--text-mute); }

/* 侧栏底部:当前账号 + 角色 + 退出 */
.sidebar-foot .me { margin-top: 8px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 12px; }
.me-name { font-weight: 600; color: var(--text); }
.me-role { padding: 1px 7px; border-radius: 999px; font-size: 11px; }
.me-role.is-admin { background: var(--accent-soft); color: var(--accent); }
.me-role.is-view  { background: #f1f4f8; color: var(--text-mute); border: 1px solid var(--border); }

/* 只读账号:整站顶部一条黄条 */
.readonly-bar {
  margin: 10px 16px 0; padding: 8px 12px; border-radius: 8px;
  background: #fff7e6; border: 1px solid #f3ddb8; color: #92400e; font-size: 12.5px;
}

/* 账号管理:一账号一张卡(表格里塞 form 是非法结构,所以用卡片) */
.user-list { display: flex; flex-direction: column; gap: 10px; }
.user-card {
  display: flex; align-items: flex-end; gap: 10px;
  border: 1px solid var(--border); border-radius: 10px; padding: 10px 12px;
  background: #fbfcfe;
}
.user-card.is-admin { border-color: #c7dbff; background: #f5f9ff; }
.user-form { flex: 1; }
.uc-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.uc-name { font-weight: 650; font-size: 13.5px; }
.uc-meta { margin-left: auto; font-size: 11.5px; color: var(--text-mute); }
.uc-fields { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; }
.uc-fields label { display: flex; flex-direction: column; gap: 4px; font-size: 11.5px; color: var(--text-mute); }
.input-sm { height: 30px; font-size: 12.5px; }

/* ---- 「跳到对端设备」落地页高闪(山叔 2026-10-02) --------------------
   端口页点「定位」→ 带 ?focus=<id>&fk=<cabdev|cab|dev> 跳到机柜页 / 房间页,
   app.js 找到那台设备加 .is-focus-flash:滚进视野 + 琥珀色闪 6 下(约 3 秒)。
   与平面图「定位」那套(.loc-flash)同一配色,认得出是同一个「找到了」的意思。
   ⚠️ 只加 class,不改数据;动画完自己收掉,不留痕。 */
.is-focus-flash { animation: focus-flash .5s step-end 6; border-radius: 6px; }
@keyframes focus-flash {
  0%, 100% {
    background-color: rgba(245, 158, 11, .32);
    box-shadow: 0 0 0 3px #f59e0b, 0 0 16px 4px rgba(245, 158, 11, .5);
  }
  50% { background-color: transparent; box-shadow: none; }
}
/* 端口弹窗里「对端设备」那行的「定位 ⟶」 */
.peer-go-row { display: flex; align-items: center; gap: 6px; }
.peer-go-row .input { flex: 1; min-width: 0; }
.go-peer { white-space: nowrap; }

/* ---- 拓扑图(0.37.0 首版 / 0.37.1 修版 / 0.37.2 连线横平竖直,山叔 2026-10-18)---
   按已录的端口对应关系画。实线=双向锁定,虚线=单向(对头还没回指)。
   横向机柜泳道、纵向层级 —— 跟实物对得上。 */
.topo-stat { margin-bottom: 12px; }
.topo-legend { display: inline-flex; align-items: center; gap: 10px; margin-right: 6px;
               font-size: 11.5px; color: var(--text-mute); }
.topo-lg { display: inline-flex; align-items: center; gap: 4px; }
.topo-lg::before { content: ""; width: 22px; height: 0; border-top-width: 2px; }
.topo-lg.is-line-solid::before { border-top-style: solid; border-top-color: #0ea5e9; }
.topo-lg.is-line-dash::before { border-top-style: dashed; border-top-color: #94a3b8; }
.topo-lg.is-box-sw::before, .topo-lg.is-box-odf::before,
.topo-lg.is-box-other::before, .topo-lg.is-box-ext::before {
  content: ""; width: 12px; height: 10px; border-radius: 3px; border: 0; }
.topo-lg.is-box-sw::before { background: #dbeafe; border: 1px solid #378add; }
.topo-lg.is-box-odf::before { background: #d9f2e7; border: 1px solid #1d9e75; }
.topo-lg.is-box-other::before { background: #ede9fe; border: 1px solid #7f77dd; }
.topo-lg.is-box-ext::before { background: #f1efe8; border: 1px dashed #888780; }

.topo-panel { padding: 0; overflow: hidden; }
.topo-stage { position: relative; height: 62vh; min-height: 420px; cursor: grab;
              background:
                radial-gradient(circle at 1px 1px, rgba(16,24,40,.06) 1px, transparent 0) 0 0/18px 18px,
                var(--panel-soft); }
.topo-stage.is-drag { cursor: grabbing; }
.topo-svg { width: 100%; height: 100%; display: block; }
.topo-room-box { fill: rgba(255,255,255,.55); stroke: var(--border); stroke-width: .5; }
.topo-room-label { font-size: 12px; font-weight: 600; fill: var(--text-soft); }
.topo-lane-box { fill: none; stroke: var(--border-tertiary); stroke-width: .5;
                 stroke-dasharray: 2 3; }
.topo-lane-box.is-empty { fill: rgba(16,24,40,.025); }
.topo-lane-label { font-size: 11px; font-weight: 600; fill: var(--text-soft); }
.topo-lane-rule { stroke: var(--border-tertiary); stroke-width: .5; }
.topo-lane-empty { font-size: 10.5px; fill: var(--text-mute); }
.topo-unlinked-box { fill: rgba(245,158,11,.06); stroke: rgba(217,119,6,.35);
                     stroke-width: .5; stroke-dasharray: 4 3; }
.topo-ext-box { fill: rgba(100,116,139,.07); stroke: rgba(100,116,139,.35);
                stroke-width: .5; stroke-dasharray: 4 3; }

.topo-node-box { stroke-width: 1; }
.topo-node-name { font-size: 12px; font-weight: 600; text-anchor: middle;
                  dominant-baseline: central; }
.topo-node-sub { font-size: 9.5px; text-anchor: middle; dominant-baseline: central;
                 fill: var(--text-mute); }
.topo-sw .topo-node-box { fill: #dbeafe; stroke: #378add; }
.topo-sw .topo-node-name { fill: #0c447c; }
.topo-odf .topo-node-box { fill: #d9f2e7; stroke: #1d9e75; }
.topo-odf .topo-node-name { fill: #085041; }
.topo-other .topo-node-box { fill: #ede9fe; stroke: #7f77dd; }
.topo-other .topo-node-name { fill: #3c3489; }
.topo-ext .topo-node-box { fill: #f1efe8; stroke: #888780; stroke-dasharray: 3 2; }
.topo-ext .topo-node-name { fill: #444441; }
.topo-node.is-unlinked .topo-node-box { stroke-dasharray: 3 2; }
.topo-node.is-link { cursor: pointer; }
.topo-node.is-link:hover .topo-node-box, .topo-node:focus .topo-node-box {
  filter: brightness(.97); stroke-width: 2; }
.topo-node:focus { outline: none; }

.topo-edge-line { fill: none; stroke: #0ea5e9; stroke-width: 1.6; }
.topo-edge.is-oneway .topo-edge-line { stroke: #94a3b8; stroke-width: 1.4;
                                      stroke-dasharray: 5 4; }
.topo-edge-hit { fill: none; stroke: transparent; stroke-width: 12; cursor: help; }
.topo-edge:hover .topo-edge-line { stroke-width: 2.6; }
.topo-edge-label { font-size: 9.5px; text-anchor: middle; fill: var(--text-mute);
                   paint-order: stroke; stroke: rgba(255,255,255,.85); stroke-width: 3px;
                   /* ⚠️ 标签画在最上层,不吃指针 —— 否则它正好落在两框之间的连线上,
                      hover 连线永远触发不了(实测 Playwright 卡死,人手上就是"线hover不出提示") */
                   pointer-events: none; }
.topo-room-label, .topo-lane-label, .topo-lane-rule, .topo-lane-empty {
  pointer-events: none; }
.topo-tip { position: absolute; z-index: 20; max-width: 280px; padding: 8px 10px;
            border: 1px solid var(--border-strong); border-radius: 8px;
            background: #fff; box-shadow: 0 6px 18px rgba(16,24,40,.16);
            font-size: 12px; line-height: 1.6; color: var(--text-soft);
            pointer-events: none; }
.topo-tip b { color: var(--text-primary); }
.topo-tip .warn { color: #b45309; }
