/* 生产看板 —— 大屏友好，浅色主题 */
* { margin: 0; padding: 0; box-sizing: border-box; }

:root {
  --bg: #f1f5f9;
  --card: #ffffff;
  --ink: #0f172a;
  --ink-2: #475569;
  --ink-3: #94a3b8;
  --line: #e2e8f0;
  --brand: #1d4ed8;
  --ok: #16a34a;
}

html, body { height: 100%; }

body {
  font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Helvetica Neue", sans-serif;
  background: var(--bg); color: var(--ink);
  font-size: 14px; line-height: 1.5;
}

/* ===== 顶栏 ===== */
.topbar {
  background: linear-gradient(135deg, #0f2b5b, #1d4ed8);
  color: #fff; padding: 16px 22px;
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  position: sticky; top: 0; z-index: 30;
}
.topbar .brand { font-size: 19px; font-weight: 800; letter-spacing: .5px; }
.topbar .brand small { display: block; font-size: 12px; font-weight: 400; opacity: .8; margin-top: 1px; }
.topbar .spacer { flex: 1; }

/* ===== 时钟（仅展示模式可见）===== */
.clock {
  display: none; align-items: baseline; gap: 14px;
  margin-left: 20px; padding-left: 20px;
  border-left: 1px solid rgba(255,255,255,.25);
}
.clock .ct {
  font-size: 34px; font-weight: 800; letter-spacing: 2px;
  font-variant-numeric: tabular-nums; line-height: 1;
  text-shadow: 0 2px 10px rgba(0,0,0,.25);
}
.clock .cd { font-size: 14px; opacity: .85; font-weight: 600; }
/* 顶栏里的公司名展示态：只显示时间和公司名 */
body.display-mode .clock { display: flex; }
body.display-mode .ctrl { display: none; }
body.display-mode .topbar { padding: 22px 30px; }
body.display-mode .topbar .brand { font-size: 24px; }
body.display-mode .topbar .brand small { font-size: 15px; opacity: .9; margin-top: 3px; }
body.display-mode .clock .ct { font-size: 44px; }
body.display-mode .clock .cd { font-size: 17px; }
body.display-mode .display-exit { display: block; }

/* 退出展示的浮动小按钮：默认隐藏，右下角低调显示 */
.display-exit {
  display: none; position: fixed; right: 14px; bottom: 14px; z-index: 60;
  background: rgba(15,23,42,.6); color: #fff;
  border: 1px solid rgba(255,255,255,.28); border-radius: 8px;
  padding: 7px 13px; font-size: 12.5px; cursor: pointer; font-family: inherit;
  opacity: .35; transition: opacity .2s;
}
.display-exit:hover { opacity: 1; }

/* 展示模式按钮 —— 与其它按钮区分 */
.ctrl button.btn-display {
  display: inline-flex; align-items: center; gap: 6px;
  background: rgba(255,255,255,.2);
  border-color: rgba(255,255,255,.42);
  color: #fff; font-weight: 700;
}
.ctrl button.btn-display svg { width: 15px; height: 15px; flex: none; }
.ctrl button.btn-display:hover { background: rgba(255,255,255,.32); }

.ctrl { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.ctrl select, .ctrl button {
  border: 1px solid rgba(255,255,255,.3); background: rgba(255,255,255,.14);
  color: #fff; padding: 8px 12px; border-radius: 8px; font-size: 14px;
  font-family: inherit; cursor: pointer;
}
.ctrl select option { color: var(--ink); }
.ctrl button:hover { background: rgba(255,255,255,.24); }
.ctrl button.on { background: #fff; color: var(--brand); font-weight: 700; }
/* 添加员工 —— 青色实心，与参考图一致 */
.ctrl button.btn-add-staff {
  display: inline-flex; align-items: center; gap: 6px;
  background: linear-gradient(135deg, #22d3ee, #06b6d4);
  border-color: rgba(255,255,255,.55);
  color: #04323a; font-weight: 800; letter-spacing: .5px;
  padding: 9px 16px; border-radius: 8px;
  box-shadow: 0 3px 12px rgba(6,182,212,.45);
}
.ctrl button.btn-add-staff svg { width: 16px; height: 16px; flex: none; }
.ctrl button.btn-add-staff:hover { background: linear-gradient(135deg, #67e8f9, #22d3ee); }
.ctrl button.btn-add-staff:active { transform: translateY(1px); }
.ctrl button.btn-add-staff[hidden] { display: none; }
/* 打开生产线日报表 */
.ctrl button.btn-line-daily {
  background: rgba(255,255,255,.2);
  border-color: rgba(255,255,255,.42);
  color: #fff; font-weight: 700;
}
.ctrl button.btn-line-daily:hover { background: rgba(255,255,255,.32); }
/* 顶栏当前登录人 */
.topbar .whoami {
  font-size: 12.5px; font-weight: 600;
  padding: 5px 11px; border-radius: 20px;
  background: rgba(255,255,255,.14);
  border: 1px solid rgba(255,255,255,.24);
  color: rgba(255,255,255,.92);
  display: inline-flex; align-items: center; gap: 6px;
  white-space: nowrap;
}
.topbar .whoami i {
  width: 6px; height: 6px; border-radius: 50%; background: #34d399; display: block;
}

/* ===== 布局 ===== */
.wrap { padding: 18px 22px 40px; max-width: 1560px; margin: 0 auto; }

/* KPI 行 */
.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 14px; margin-bottom: 18px; }
.kpi {
  background: var(--card); border-radius: 12px; padding: 15px 16px 14px 17px;
  box-shadow: 0 1px 3px rgba(15,23,42,.06);
  position: relative; overflow: hidden;
  display: flex; align-items: flex-start; gap: 11px;
}
/* 左侧色条：用色语义区分好坏，扫一眼就懂 */
.kpi::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
  background: var(--kpi-c, var(--brand));
}
.kpi .ic {
  flex: none; width: 34px; height: 34px; border-radius: 9px;
  display: flex; align-items: center; justify-content: center;
  background: var(--kpi-bg, #eff6ff); color: var(--kpi-c, var(--brand));
}
.kpi .ic svg { width: 18px; height: 18px; }
.kpi .tx { min-width: 0; flex: 1; }
.kpi .v {
  font-size: 27px; font-weight: 800; line-height: 1.1; letter-spacing: -.5px;
  font-variant-numeric: tabular-nums; color: var(--kpi-c, var(--ink));
}
.kpi .k { font-size: 12.5px; color: var(--ink-3); margin-top: 3px; font-weight: 500; }
.kpi .v small { font-size: 13px; font-weight: 600; color: var(--ink-3); margin-left: 3px; }

.grid { display: grid; grid-template-columns: 1fr 340px; gap: 18px; align-items: start; }
@media (max-width: 1080px) { .grid { grid-template-columns: 1fr; } }

.card {
  background: var(--card); border-radius: 12px; padding: 18px;
  box-shadow: 0 1px 3px rgba(15,23,42,.06); margin-bottom: 18px;
}
.card > h2 {
  font-size: 15px; font-weight: 700; margin-bottom: 14px;
  display: flex; align-items: center; gap: 8px;
}
.card > h2::before { content: ''; width: 4px; height: 16px; background: var(--brand); border-radius: 2px; }
.card > h2 .right { margin-left: auto; font-size: 12px; font-weight: 400; color: var(--ink-3); }

/* 空状态：给出下一步动作，不只是一句"暂无数据" */
.empty { text-align: center; padding: 34px 16px; color: var(--ink-3); font-size: 13px; line-height: 1.9; }
.empty .big { font-size: 30px; display: block; margin-bottom: 6px; opacity: .5; }
.empty b { color: var(--ink-2); }
.empty .link { color: var(--brand); font-weight: 600; }

/* ===== 机台网格 ===== */
.mgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(112px, 1fr)); gap: 10px; }
.mcell {
  border-radius: 10px; padding: 10px 9px 9px; color: #fff; cursor: pointer;
  transition: transform .1s, box-shadow .12s; position: relative; overflow: hidden;
  min-height: 88px; display: flex; flex-direction: column;
  box-shadow: 0 1px 2px rgba(15,23,42,.1);
}
.mcell:hover { box-shadow: 0 4px 12px rgba(15,23,42,.16); }
.mcell:active { transform: scale(.96); }
/* 顶部高光条：状态色再强化一次，远处扫一眼就能分辨 */
.mcell::before {
  content: ''; position: absolute; left: 0; right: 0; top: 0; height: 3px;
  background: rgba(255,255,255,.45);
}
.mcell .no { font-size: 15.5px; font-weight: 800; letter-spacing: .3px; }
/* 状态图标：与文字一起构成状态标识，不单独依赖颜色 */
.mcell .st .ico {
  width: 12px; height: 12px; flex: none; display: inline-block;
  vertical-align: -1px;
}
.mcell .st .ico svg {
  width: 100%; height: 100%; display: block;
}
/* 状态图标底衬：白字在浅色状态底上（如待料 #0ea5e9）也能看清 */
.mcell .st .ico {
  background: rgba(255,255,255,.22);
  border-radius: 3px; padding: 1.5px;
  box-sizing: content-box;
}

.mcell .st {
  font-size: 11.5px; font-weight: 700; margin-top: 3px;
  display: inline-flex; align-items: center; gap: 4px;
}
.mcell .st::before {
  content: ''; width: 5px; height: 5px; border-radius: 50%;
  background: currentColor; flex: none;
}
.mcell .pn {
  font-size: 11px; margin-top: 4px; opacity: .92;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.mcell .qt {
  margin-top: auto; font-size: 15px; font-weight: 800; padding-top: 5px;
  font-variant-numeric: tabular-nums;
}
.mcell .qt small { font-size: 10px; font-weight: 500; opacity: .85; }
/* 未报工：虚线边框，和已报工的实色块区分开 */
.mcell.idle {
  background: #f1f5f9; color: #64748b;
  border: 1.5px dashed #94a3b8; box-shadow: none;
}
.mcell.idle::before { display: none; }
.mcell.idle .st { color: #64748b; font-weight: 600; }
.mcell.idle .st .ico { background: rgba(100,116,139,.14); }
.mcell.idle .pn, .mcell.idle .qt { opacity: .75; }

/* ===== 状态分布条 ===== */
.dist { display: flex; flex-direction: column; gap: 9px; }
.dist-row { display: flex; align-items: center; gap: 10px; font-size: 13px; }
.dist-row .lb { width: 62px; flex: none; font-weight: 600; color: var(--ink-2); }
.dist-row .bar { flex: 1; height: 20px; background: var(--bg); border-radius: 5px; overflow: hidden; }
.dist-row .bar i { display: block; height: 100%; border-radius: 5px; transition: width .3s; }
.dist-row .nm { width: 46px; text-align: right; flex: none; font-weight: 700; font-variant-numeric: tabular-nums; }
.dist-row .pc { width: 40px; text-align: right; flex: none; color: var(--ink-3); font-size: 12px; }

/* ===== 排行榜 ===== */
.rank { display: flex; flex-direction: column; gap: 2px; }
.rank-row {
  display: flex; align-items: center; gap: 10px; padding: 9px 0;
  border-bottom: 1px solid var(--line); font-size: 13px;
}
.rank-row:last-child { border-bottom: none; }
.rank-row .no {
  width: 21px; height: 21px; border-radius: 5px; flex: none;
  display: flex; align-items: center; justify-content: center;
  font-size: 11px; font-weight: 800; background: var(--bg); color: var(--ink-3);
}
.rank-row.top1 .no { background: #fbbf24; color: #78350f; }
.rank-row.top2 .no { background: #cbd5e1; color: #334155; }
.rank-row.top3 .no { background: #fdba74; color: #7c2d12; }
.rank-row .nm { flex: 1; font-weight: 600; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rank-row .mc { color: var(--ink-3); font-size: 12px; }
.rank-row .qt { font-weight: 800; font-variant-numeric: tabular-nums; }
.rank-row .qt small { font-weight: 500; color: var(--ink-3); font-size: 11px; }

/* ===== 排行自动滚动（溢出才启用）=====
   做法：把列表整体放进 .rank-track，用 transform + @keyframes 上移，
   而不是改 scrollTop —— 改 scrollTop 每帧都要重排，在大屏上会明显掉帧。
   列表尾部接一份副本，滚完一份正好回到起点，视觉上无缝。
   只有数据超过容器高度时才加 .rolling 类，数据少时保持静态。 */
.rank {
  display: flex; flex-direction: column; gap: 2px;
  overflow: hidden;          /* 滚动由内层 track 负责，这里必须裁掉溢出 */
  position: relative;
}
/* 未启用滚动时的可视高度上限：超出部分隐藏但仍在 DOM 里，
   保证切换日期/数据变化时能重新测高判断 */
.rank { max-height: 460px; }
.rank.rolling .rank-track {
  animation: rankScroll var(--rank-dur, 18s) linear infinite;
  will-change: transform;
}
.rank.rolling.paused .rank-track,
.rank.rolling:hover .rank-track { animation-play-state: paused; }
.rank.rolling.locked .rank-track { animation-play-state: paused; }

/* 无缝循环的关键：轨道高一份内容，-50% 即正好上移一份的高度 */
@keyframes rankScroll {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(0, -50%, 0); }
}
/* 渐隐提示下方还有内容，不改变布局 */
.rank-wrap { position: relative; }
.rank-wrap::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 26px;
  background: linear-gradient(180deg, rgba(255,255,255,0), var(--card) 78%);
  opacity: 0; transition: opacity .2s; pointer-events: none;
}
.rank-wrap.rolling::after { opacity: 1; }

/* 暂停提示按钮：仅在启用滚动时出现，鼠标移到卡片上才显示，不常驻占位 */
.rank-toggle {
  display: none; align-items: center; gap: 5px;
  margin-top: 10px; padding: 6px 12px; border-radius: 8px;
  border: 1px solid var(--line); background: #fff; color: var(--ink-3);
  font-size: 12px; font-family: inherit; cursor: pointer; align-self: flex-start;
}
.rank.rolling + .rank-toggle, .rank-wrap.rolling .rank-toggle { display: inline-flex; }
.rank-toggle:hover { border-color: var(--brand); color: var(--brand); }
.rank-toggle svg { flex: none; }
.rank-toggle .locki { display: none; }
.rank-toggle.locked .locki { display: block; }
.rank-toggle.locked .playi { display: none; }

/* ===== 建筑分组标题 ===== */
.bgroup { margin-bottom: 20px; }
.bgroup:last-child { margin-bottom: 0; }
.bgroup > h3 {
  font-size: 14px; font-weight: 700; color: var(--ink-2);
  margin-bottom: 10px; display: flex; align-items: center; gap: 8px;
}
.bgroup > h3 .c { font-size: 12px; font-weight: 400; color: var(--ink-3); }

/* ===== 详情弹层 ===== */
#modal {
  position: fixed; inset: 0; background: rgba(15,23,42,.55); z-index: 60;
  display: none; align-items: center; justify-content: center; padding: 20px;
}
#modal.show { display: flex; }
#modal .box {
  background: #fff; border-radius: 14px; padding: 24px;
  max-width: 620px; width: 100%; max-height: 86vh; overflow-y: auto;
}
#modal .box h3 { font-size: 18px; margin-bottom: 4px; }
#modal .box .msub { font-size: 13px; color: var(--ink-3); margin-bottom: 16px; }
.kv { display: grid; grid-template-columns: 82px 1fr; gap: 9px 12px; font-size: 14px; margin-bottom: 16px; }
.kv dt { color: var(--ink-3); }
.kv dd { font-weight: 600; }
/* 汇总卡片：把技术岗填的工艺参数集中呈现，浅底以便和上面区分 */
.kv.sum {
  grid-template-columns: 1fr; gap: 0; padding: 12px 14px; margin-bottom: 18px;
  background: #f8fafc; border: 1px solid var(--line); border-radius: 10px;
}
.kv.sum > .sum-h {
  display: block; font-size: 12px; font-weight: 700; color: var(--ink-2);
  letter-spacing: .5px; margin-bottom: 8px;
}
.kv.sum > .row {
  display: flex; align-items: baseline; gap: 10px; padding: 3px 0; font-size: 13px;
}
.kv.sum > .row > .k { color: var(--ink-3); flex: none; width: 92px; }
.kv.sum > .row > .v { font-weight: 600; color: var(--ink); word-break: break-all; }
.kv.sum > .row > .v small { font-weight: 400; color: var(--ink-3); margin-left: 6px; font-size: 11px; }
.kv.sum > .none { font-size: 13px; color: var(--ink-3); }

/* 报工明细条目 —— 基础样式原来只在 report.css 里，
   大屏弹窗只命中了两条 display-mode 覆盖，等于完全没样式。
   这里补齐，白天与展示模式共用。 */
.rec-item {
  border-left: 3px solid var(--line); border-radius: 8px;
  background: #f8fafc; padding: 9px 11px; margin-bottom: 7px;
}
.rec-item .r1 { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.rec-item .tag {
  font-size: 11px; font-weight: 700; color: #fff;
  padding: 1px 7px; border-radius: 4px; flex: none;
}
.rec-item .pname { font-size: 14px; font-weight: 600; color: var(--ink); }
.rec-item .qty { font-size: 14px; font-weight: 700; color: var(--ink); margin-left: auto; }
/* 第 3 行：把这条记录里所有非空字段都列出来，
   技术岗的料号/夹位/标准产能、操作员的实际产能都在这里 */
.rec-item .r3 {
  display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px;
}
.rec-item .r3 .f {
  font-size: 11px; color: var(--ink-2); background: #e2e8f0;
  padding: 2px 7px; border-radius: 4px; white-space: nowrap;
}
.rec-item .r3 .f.tech { background: #cffafe; color: #0e7490; }
.rec-item .r3 .f.op { background: #dcfce7; color: #15803d; }
.rec-item .r2 { font-size: 12px; color: var(--ink-3); margin-top: 4px; }
#modalClose {
  width: 100%; padding: 12px; border: none; border-radius: 10px;
  background: var(--bg); color: var(--ink-2); font-size: 15px;
  font-weight: 600; cursor: pointer; font-family: inherit;
}

.empty { text-align: center; padding: 40px 0; color: var(--ink-3); font-size: 14px; }

/* ===== 微信内置浏览器提示 ===== */
#wxTip {
  position: fixed; inset: 0; z-index: 300;
  background: rgba(15,23,42,.92);
  display: flex; align-items: center; justify-content: center;
  padding: 22px;
}
.wxcard {
  background: #fff; border-radius: 16px; padding: 28px 24px;
  max-width: 400px; width: 100%; text-align: center;
}
.wxic { font-size: 44px; line-height: 1; margin-bottom: 12px; }
.wxcard h2 { font-size: 18px; font-weight: 800; color: var(--ink); margin: 0 0 8px; }
.wxlead { font-size: 14px; color: var(--ink-2); line-height: 1.6; margin: 0 0 16px; }
.wxsteps {
  text-align: left; font-size: 14px; color: var(--ink-2);
  line-height: 1.9; margin: 0 0 18px; padding-left: 22px;
}
.wxbtn {
  width: 100%; padding: 13px; border: none; border-radius: 10px;
  background: var(--brand); color: #fff; font-size: 16px; font-weight: 700;
  cursor: pointer; font-family: inherit;
}
.wxurl {
  margin-top: 12px; font-size: 12px; color: var(--ink-3);
  word-break: break-all; line-height: 1.5;
}

/* ===== 吐司 ===== */
#toast {
  position: fixed; left: 50%; bottom: 34px; transform: translateX(-50%) translateY(16px);
  background: rgba(15,23,42,.94); color: #fff; padding: 12px 20px;
  border-radius: 9px; font-size: 14px; font-weight: 600;
  opacity: 0; pointer-events: none; transition: all .22s; z-index: 90;
}
#toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
#toast.err { background: #b91c1c; }

/* 登录页 */
#login {
  min-height: 100vh; display: flex; align-items: center; justify-content: center;
  padding: 20px; background: var(--bg);
}
#login .box {
  background: #fff; border-radius: 14px; padding: 32px; max-width: 400px; width: 100%;
  text-align: center;
}
#login h2 { font-size: 20px; margin-bottom: 6px; }
#login p { font-size: 13px; color: var(--ink-3); margin-bottom: 20px; }
#login .ph { display: flex; gap: 10px; }
#login input {
  flex: 1; border: 1.5px solid var(--line); border-radius: 9px; padding: 11px 13px;
  font-size: 16px; outline: none;
}
#login input:focus { border-color: var(--brand); }
#login button {
  border: none; border-radius: 9px; padding: 11px 18px; background: var(--brand);
  color: #fff; font-size: 15px; font-weight: 600; cursor: pointer; font-family: inherit; flex: none;
}
#login .msg { margin-top: 12px; font-size: 13px; color: #dc2626; min-height: 18px; }
#login .tip { margin-top: 16px; font-size: 12px; color: var(--ink-3); }

/* ==========================================================
   前台大屏展示视图 —— 深色科技风（仅 body.display-mode 下生效）
   层级：整体概况 → 异常置顶 → 车间概况 → 滚动状态墙 → 排行
   ========================================================== */

/* ============================================================
   展示模式设计规范（Design Tokens）
   ------------------------------------------------------------
   为什么集中在这里：
   改造前展示模式的间距/圆角/字号散落在几十条规则里各写各的
   （面板 12px、告警项 9px、排行项 8px……），肉眼可见的不一致就是这么来的。
   改成一套 token 后，全站只有这一处定义「间距是多少、圆角多大」，
   新增组件只要引用变量就自动融入整体视觉语言，不会再各搞一套。

   token 分四层：
     间距 --dv-sp-*  间距阶梯（4 的倍数，视觉节奏统一）
     圆角 --dv-r-*   圆角阶梯
     字号 --dv-fs-*  字号层级
     阴影 --dv-sh-*  阴影层级
   所有值再乘 --dv-s（电视缩放系数），大屏上同比放大。
   ============================================================ */
body.display-mode {
  --bg: #060b18;
  --card: rgba(17, 30, 58, .72);
  --ink: #e8f0ff;
  --ink-2: #a8bde0;
  --ink-3: #6b83ad;
  --line: rgba(96, 165, 250, .18);
  --brand: #38bdf8;
  --ok: #22c55e;

  /* ---- 间距阶梯：4 的倍数，组件内外一律引用，不再写裸px ---- */
  /* 基准值整体收紧过一轮（第 8 轮）：
     原先面板内边距 16px、页面外边距 20px、KPI 数字 44px，
     在 1.36 档下变成 21.8 / 27.2 / 59.8px —— 元素撑得比容器还夸张，
     面板内部空得像没装东西。
     现在按「1920 档下正文 13.5px≈13px」的比例回调，视觉密度才正常。 */
  --dv-sp-1: calc(3px * var(--dv-s, 1));    /* 图标与文字之间 */
  --dv-sp-2: calc(6px * var(--dv-s, 1));    /* 元素内小间距 */
  --dv-sp-3: calc(9px * var(--dv-s, 1));    /* 组件之间（面板间距） */
  --dv-sp-4: calc(11px * var(--dv-s, 1));   /* 面板内边距 */
  --dv-sp-5: calc(13px * var(--dv-s, 1));   /* 页面外边距 */
  --dv-sp-6: calc(18px * var(--dv-s, 1));   /* 大区块留白 */

  /* ---- 圆角阶梯：越大越"卡片"，越小越"控件" ---- */
  --dv-r-sm: calc(4px * var(--dv-s, 1));    /* 徽章、进度条 */
  --dv-r-md: calc(7px * var(--dv-s, 1));    /* 行条目 */
  --dv-r-lg: calc(10px * var(--dv-s, 1));   /* 面板、大卡片 */

  /* ---- 字号层级：数字最大、标题次之、说明最小，共 5 级 ---- */
  --dv-fs-num: calc(33px * var(--dv-s, 1));   /* KPI 大数字 */
  --dv-fs-h2: calc(13px * var(--dv-s, 1));    /* 面板标题 */
  --dv-fs-body: calc(12.5px * var(--dv-s, 1));/* 行条目主文字 */
  --dv-fs-sub: calc(11px * var(--dv-s, 1));   /* 次要说明 */
  --dv-fs-cap: calc(9.8px * var(--dv-s, 1));  /* 极小标注 */

  /* ---- 阴影层级：统一外阴影 + 内高光，全站不再各写一坨 ---- */
  --dv-sh-1: 0 1px 6px rgba(2,8,23,.45);                       /* 条目 */
  --dv-sh-2: 0 6px 22px rgba(2,8,23,.42);                      /* 面板 */
  --dv-sh-3: 0 10px 34px rgba(2,8,23,.55);                      /* 强调卡片 */
  --dv-sh-in: inset 0 1px 0 rgba(255,255,255,.06);             /* 内高光 */

  /* ---- 面板统一外观：所有面板共用这一组值 ---- */
  --dv-panel-bg: linear-gradient(160deg, rgba(20,36,68,.8), rgba(11,20,42,.72));
  --dv-panel-bd: 1px solid var(--line);
  --dv-panel-sh: var(--dv-sh-in), var(--dv-sh-2);
  --dv-panel-pad: var(--dv-sp-4);
  --dv-panel-radius: var(--dv-r-lg);

  /* ---- 状态色：语义固定，全站不得各自发挥 ---- */
  --dv-c-run: #22c55e;    /* 生产中 */
  --dv-c-set: #fbbf24;    /* 调机/待料 */
  --dv-c-alert: #ef4444;  /* 告警 */
  --dv-c-idle: #64748b;   /* 未报工/停机 */

  /* --dv-c 是「当前卡片主色」：由 renderDvHero 逐卡内联注入
     （style="--dv-c:#4ade80"），决定数字、发光条、进度条的颜色。
     这里给出默认值 —— 之前它只在 var(--dv-c, #38bdf8) 的fallback 里
     出现过，属于「引用了但没定义」，靠 fallback 侥幸工作。 */
  --dv-c: var(--dv-c-info, #38bdf8);
  --dv-c-info: #38bdf8;

  background: var(--bg);
  color: var(--ink);
  min-height: 100vh;
}
/* 科技底纹：网格 + 径向光晕 + 扫描线，营造驾驶舱氛围 */
body.display-mode::before {
  content: ''; position: fixed; inset: 0; z-index: 0; pointer-events: none;
  background-image:
    linear-gradient(rgba(56,189,248,.055) 1px, transparent 1px),
    linear-gradient(90deg, rgba(56,189,248,.055) 1px, transparent 1px);
  background-size: 44px 44px;
}
body.display-mode::after {
  content: ''; position: fixed; inset: 0; z-index: 0; pointer-events: none;
  background:
    radial-gradient(900px 460px at 12% -6%, rgba(56,189,248,.20), transparent 62%),
    radial-gradient(760px 420px at 88% 4%, rgba(168,85,247,.16), transparent 62%),
    radial-gradient(900px 500px at 50% 108%, rgba(34,197,94,.10), transparent 60%);
}
/* 顶栏玻璃拟态 */
body.display-mode .topbar {
  background: linear-gradient(135deg, rgba(8,17,38,.94), rgba(15,32,66,.9));
  border-bottom: 1px solid rgba(96,165,250,.28);
  box-shadow: 0 6px 30px rgba(2,8,23,.6);
  backdrop-filter: blur(10px);
  position: relative; z-index: 40;
}
body.display-mode .topbar .brand {
  color: #e8f0ff;
  text-shadow: 0 0 18px rgba(56,189,248,.55);
}
body.display-mode .clock .ct {
  color: #7dd3fc;
  text-shadow: 0 0 22px rgba(56,189,248,.75);
}

/* ============================================================
   电视大屏自适应缩放
   ------------------------------------------------------------
   目标：公司前台电视（1080p / 4K）、超宽屏，横竖屏都能：
     · 内容完整铺满，不留白、不拉伸变形
     · 字号、间距、机台方块、进度条等比放大，远距离看得清
     · 任何分辨率下都不出现页面级滚动条

   做法：所有展示模式的尺寸都用 --dv-s 参与计算。
   --dv-s 由下方媒体查询按视口宽度分档给出（1 / 1.18 / 1.4 / 1.7）。
   分档而不是连续缩放，是为了让同一台电视上的观感稳定——
   连续 vw 缩放会让 55寸和65 寸的视觉大小几乎一样，反而不好区分。

   为什么不用 transform: scale() 整体缩放：
   那样会让整个画面（包括边框、圆角、阴影）一起被放大，
   在超宽屏上左右会多出两条空白边，等于没解决铺满问题。
   ============================================================ */
body.display-mode {
  /* 分档缩放系数，默认 1（1080p 及以下） */
  --dv-s: 1;
}

/* 1280 ~ 1600：小尺寸电视 / 笔记本外接 */
@media (min-width: 1280px) {
  body.display-mode { --dv-s: 1.12; }
}
/* 1600 ~ 1920：1080p 电视 */
@media (min-width: 1600px) {
  body.display-mode { --dv-s: 1.18; }
}
/* 1920 ~ 2560：2K 显示器 / 主流大屏 */
@media (min-width: 1920px) {
  body.display-mode { --dv-s: 1.36; }
}
/* 2560 ~ 3200：4K 电视 */
@media (min-width: 2560px) {
  body.display-mode { --dv-s: 1.62; }
}
/* 3200 以上：超宽屏 / 8K */
@media (min-width: 3200px) {
  body.display-mode { --dv-s: 1.9; }
}
/* 竖屏或极矮屏（电视横挂异常、竖装）：用高度兜底，缩小一档避免挤压 */
@media (max-height: 620px) {
  body.display-mode { --dv-s: .82; }
}

#displayView { display: none; }
/* app-on：看板主体显隐由 class 控制（JS 只切 class，不再写内联 style.display，
   否则内联样式会盖掉下面 display-mode #app 的 flex 布局） */
#app { display: none; }
body.app-on #app { display: block; }
/* 展示模式：#app 变成一屏高的 flex 列 —— 顶栏按内容自适应，
   displayView 吃掉剩余高度。比写死calc(100vh - 96px) 更稳，顶栏换行也不怕 */
/* 电视全屏常驻：整页锁死，绝不出现滚动条。
   原来只给 body 加了 overflow:hidden，但滚动容器其实是文档根——
   电视浏览器全屏时如果根节点还能滚，鼠标滚轮/触摸滑动会让整个画面位移，
   这是无人值守场景最不能出现的现象。
   overscroll-behavior:none 断掉滚动链，touch-action:none 断掉触摸手势，
   html 和 body 两层都要写，只写一层挡不住。 */
html:has(body.display-mode),
body.display-mode {
  overflow: hidden;
  overscroll-behavior: none;
  touch-action: none;
}
/* 兜底：:has() 在极老浏览器上不生效时，用 JS 给 html 打的 .dv-lock 同样生效 */
html.dv-lock { overflow: hidden; overscroll-behavior: none; }

body.display-mode #app {
  display: flex; flex-direction: column;
  height: 100vh; height: 100dvh;
}
body.display-mode .topbar { flex: none; }
body.display-mode .wrap { display: none; }
body.display-mode #displayView {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto; min-height: 0;
  /* 外边距与内边距都吃缩放系数：屏幕越大，页边距同比放大，
     铺满的同时不会把内容顶到屏幕边缘上。
     底部额外多留--dv-sp-3：底部提示是绝对定位的，
     不留这一条它就会压在状态墙面板的下边框上（截图里可见）。 */
  padding: var(--dv-sp-3) var(--dv-sp-5) var(--dv-sp-5);
  max-width: calc(2200px * var(--dv-s, 1));
  width: 100%; margin: 0 auto;
  position: relative; z-index: 1;
  overflow: hidden;              /* 页面级不滚动，滚动只发生在状态墙内部 */
}

/* ---------- 顶部整体概况 ---------- */
/* 卡片随屏幕变宽：min-width 吃缩放系数，列数变化由 auto-fit 自动决定。
   align-items:stretch 是关键 —— grid 默认 stretch，
   所有卡片会被拉到同一高度，7 张卡片的下沿才能齐平。 */
.dv-hero {
  display: grid;
  /* minmax 从 168px 降到 132px：
     1080 宽（等效 1920、档位 1.36）下，168px 只能排下 6 张，
     第 7 张「一次良率」被挤到第二行独占一行，看起来像个孤儿。
     132px×1.36≈180px，7 张刚好一行排满，且不挤压内容
     （卡片内文字已全部单行+省略，不会溢出）。 */
  grid-template-columns: repeat(auto-fit, minmax(calc(132px * var(--dv-s, 1)), 1fr));
  gap: var(--dv-sp-3);
  margin-bottom: var(--dv-sp-3);
  align-items: stretch;              /* 卡片等高 */
  flex: none;
}
/* KPI 卡片是页面第一视觉焦点，用最重的一档阴影（--dv-sh-3），
   与下方信息面板（--dv-sh-2）拉开层次 */
.dv-hcard {
  position: relative; overflow: hidden;
  display: flex; flex-direction: column;   /* 内容纵向排列，统一基线 */
  background: linear-gradient(160deg, rgba(23,42,80,.85), rgba(12,22,45,.72));
  border: var(--dv-panel-bd);
  border-radius: var(--dv-panel-radius);
  padding: var(--dv-panel-pad);
  box-shadow: var(--dv-sh-in), var(--dv-sh-3);
}
/* 顶部发光条：卡片主色 */
.dv-hcard::before {
  content: ''; position: absolute; left: 0; right: 0; top: 0;
  height: calc(3px * var(--dv-s, 1));
  background: var(--dv-c, #38bdf8);
  box-shadow: 0 0 14px var(--dv-c, #38bdf8);
}
/* KPI 大数字：允许收缩但绝不换行。
   「7,200 件」这类四位数 + 单位在窄屏会折行，把卡片撑高一截，
   导致同一排卡片下沿参差—— white-space:nowrap + min-width:0 解决。
   数字过大时用 vw 封顶（不用 cqw：容器查询单位在部分内嵌电视浏览器上不支持，
   会让整条规则失效）。基准 33px，1.9 档（8K）下约 63px，
   与面板标题 13px×1.9≈25px 仍是 2.5 倍关系，远看清晰但不喧宾夺主。 */
.dv-hcard .v {
  font-weight: 800; line-height: 1.05;
  letter-spacing: calc(-1px * var(--dv-s, 1));
  font-variant-numeric: tabular-nums;
  color: var(--dv-c, #e8f0ff);
  text-shadow: 0 0 22px rgba(56,189,248,.35);
  white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
  min-width: 0;
  font-size: min(var(--dv-fs-num), 3.4vw);
}
/* 单位随大数字等比缩放：用 em 跟随，不另设固定值 */
.dv-hcard .v small {
  font-size: .36em; font-weight: 700; color: var(--ink-3);
  margin-left: .28em; letter-spacing: 0;
}
.dv-hcard .k {
  font-size: var(--dv-fs-h2); font-weight: 700; color: var(--ink-2);
  margin-top: var(--dv-sp-2); letter-spacing: .5px;
}
.dv-hcard .s {
  font-size: var(--dv-fs-sub); color: var(--ink-3); margin-top: var(--dv-sp-1);
  /* 说明文字长度不一（「8 在线 / 81 离线」vs「无故障」），
     统一单行 + 省略，避免它把进度条顶到不同位置 */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* 进度条永远贴在卡片底部：margin-top:auto 让它吸底，
   这样无论上面文字占多高，所有卡片的进度条都在同一水平线上。 */
.dv-hcard .bar {
  height: calc(5px * var(--dv-s, 1)); border-radius: calc(3px * var(--dv-s, 1));
  background: rgba(255,255,255,.07);
  margin-top: auto;                 /* 吸底对齐的关键 */
  padding-top: 0;
  overflow: hidden;
}
.dv-hcard .bar i {
  display: block; height: 100%; border-radius: inherit;
  background: var(--dv-c, #38bdf8);
  box-shadow: 0 0 10px var(--dv-c, #38bdf8);
  transition: width .5s;
}

/* ---------- 三栏主体 ---------- */
/* 三列等高，各列内部再分配：状态墙吃掉全部剩余高度。
   列宽比例按内容实际需求重新配平（第 8 轮）：
   原 320 / 1fr / 280 在 1.36 档下左右各435px / 382px，
   而右栏三个面板的内容（百分比、订单号）实际只需要 ~240px，
   多出来的宽度把进度条拉得很长，反而让中栏状态墙被压窄、方块变小。
   现在左栏 300 / 中栏 1fr / 右栏 250，把省下的宽度还给状态墙。 */
.dv-body {
  display: grid;
  grid-template-columns:
    calc(300px * var(--dv-s, 1)) minmax(0, 1fr) calc(250px * var(--dv-s, 1));
  gap: var(--dv-sp-3);
  align-items: stretch;
  flex: 1 1 auto; min-height: 0;   /* 关键：允许被压缩，剩余高度分给子项 */
}
/* grid 子项默认 min-width:auto，长订单号会把 280px 的右列撑破。
   统一置 0，宽度完全由 grid 轨道决定，内部各行自己处理省略。 */
.dv-body > * { min-width: 0; }
/* 断点按「缩放后的等效宽度」判断，不能直接用物理宽度 ——
   否则 4K 电视（3840px 宽）会误命中 max-width:1400px 把右栏整个隐藏掉。
   等效宽度 = 物理宽度 ÷ 缩放系数。 */
@media (max-width: calc(1400px * var(--dv-s, 1))) {
  /* 中等宽度下右栏改为放到状态墙下方，而不是 display:none ——
     直接隐藏等于把订单进度、巡检率、产量排行这三块信息从大屏上抹掉，
     而这三块恰恰是老板最关心的。 */
  .dv-body { grid-template-columns: calc(300px * var(--dv-s, 1)) minmax(0, 1fr); }
  .dv-right {
    grid-column: 1 / -1;      /* 跨整行，落在状态墙下面 */
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(calc(240px * var(--dv-s, 1)), 1fr));
    gap: var(--dv-sp-3);
    align-items: start;
  }
  .dv-right .dv-panel { flex: none !important; }
  /* 状态墙不再独占整列高度，给下方右栏留出空间 */
  .dv-col > .dv-wallpanel { max-height: 62vh; }
}

/* 窄屏（平板竖屏、手机）：单列纵向排列，页面允许纵向滚动。
   这里必须解除全局的锁滚动 —— 一屏塞不下就只能滚，
   宁可滚动也不能让内容被裁掉看不见。 */
@media (max-width: calc(1000px * var(--dv-s, 1))) {
  .dv-body {
    grid-template-columns: 1fr;
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
  }
  .dv-col { min-height: auto; }
  .dv-right { grid-column: auto; grid-template-columns: 1fr; }
  /* 单列时状态墙不能 flex:1（没有确定的剩余高度），按内容高度排 */
  .dv-col > .dv-wallpanel,
  .dv-col > .dv-panel:last-child { flex: none !important; }
  /* 注意：这里不要动 .dv-wallwrap 的 overflow。
     主规则 .dv-wallwrap { overflow:hidden } 写在文件更靠后的位置，
     同特异性下后来者胜 —— 在这里写 overflow:visible 是无效代码（曾经踩过）。
     窄屏下内容超高由 fitDvWall整体缩放处理，不需要放开 overflow。 */
  .dv-hero { grid-template-columns: repeat(auto-fit, minmax(calc(150px * var(--dv-s, 1)), 1fr)); }
  /* 单列下底部提示改为静态流内元素，不再绝对定位压在内容上 */
  .dv-foot {
    position: static; margin-top: var(--dv-sp-2); opacity: .6;
  }
}

/* 矮屏（1366×768 笔记本横屏、投影仪、小尺寸电视竖挂）：整体收紧。
   收敛方式统一走 --dv-s，不再单独覆盖具体字号 ——
   .dv-wgrid / .dv-wcell .no 的主规则写在文件更靠后的位置，
   同特异性下后来者胜，这里再写一份覆盖是无效代码（曾经踩过）。
   本段位于所有宽度分档之后，源码顺序在后，因此能覆盖上面的宽度档。 */
@media (max-height: 820px) {
  body.display-mode { --dv-s: .95; }
  .dv-hcard .s { display: none; }
}
@media (max-height: 680px) {
  body.display-mode { --dv-s: .84; }
  .dv-hcard .v small { display: none; }
}
.dv-col {
  display: flex; flex-direction: column; gap: var(--dv-sp-3);
  min-width: 0; min-height: 0;    /* min-width:0 防止 grid 列被内容撑破 */
}

/* 面板：全站统一外观，值全部来自 token，不再各写一套 */
.dv-panel {
  position: relative;
  background: var(--dv-panel-bg);
  border: var(--dv-panel-bd);
  border-radius: var(--dv-panel-radius);
  padding: var(--dv-panel-pad);
  box-shadow: var(--dv-panel-sh);
  /* 默认不参与纵向拉伸，内容超出时各自内部滚动 */
  min-height: 0;
}
/* 面板标题：字号取 --dv-fs-h2，字重、字距全站一致 */
.dv-panel > h2 {
  font-size: var(--dv-fs-h2); font-weight: 700;
  margin-bottom: var(--dv-sp-3);
  display: flex; align-items: center; gap: var(--dv-sp-2);
  color: #cfe2ff; letter-spacing: 1px; flex: none;
  /* 标题过长时省略，而不是把面板撑宽 */
  min-width: 0;
}
.dv-panel > h2::before {
  content: ''; flex: none;
  width: calc(3px * var(--dv-s, 1));
  height: calc(15px * var(--dv-s, 1));
  border-radius: calc(2px * var(--dv-s, 1));
  background: var(--brand); box-shadow: 0 0 10px var(--brand);
}
/* 标题右侧的汇总说明（em）：全站统一靠右、最小宽度 0 防挤压标题 */
.dv-panel > h2 em {
  margin-left: auto; font-style: normal;
  font-size: var(--dv-fs-sub);
  font-weight: 600; color: var(--ink-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  min-width: 0; padding-left: var(--dv-sp-2);
}

/* 面板默认按内容高度（flex:none），避免把列撑破；
   需要吃满剩余高度的面板（告警 / 状态墙 / 排行 / 每列最后一块）单独设 flex:1 */
.dv-col > .dv-panel { flex: none; }
/* 每列最后一个面板撑满剩余高度：配合内部flex 布局，
   把富余空间平摊进面板内部，而不是在页面底部积出一条空白带 */
.dv-col > .dv-panel:last-child {
  flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0;
}

/* ---------- 告警置顶 ---------- */
.dv-alert {
  border-color: rgba(248,113,113,.38);
  box-shadow: var(--dv-sh-in), 0 0 26px rgba(239,68,68,.14);
  /* 高度策略（第 8 轮改动）：原来 flex:1 1 auto 会让这块吃掉左栏全部剩余高度，
     即使只有一行「全部正常」也占掉近半屏（截图里就是这样）。
     现在改为 flex:none 按内容高度，真正有告警时列表自己内部滚动；
     省下的高度交给车间概况，两块内容各占合适的比例。 */
  flex: none;
  display: flex; flex-direction: column;
  min-height: 0;
  /* 但至少给一点高度，避免只剩标题像条线 */
  min-height: calc(84px * var(--dv-s, 1));
}
.dv-alert > h2::before { background: #f87171; box-shadow: 0 0 10px #f87171; }
.dv-alert-list {
  display: flex; flex-direction: column; gap: var(--dv-sp-2);
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
  scrollbar-width: thin; scrollbar-color: rgba(248,113,113,.4) transparent;
}
.dv-alert-list::-webkit-scrollbar { width: calc(5px * var(--dv-s, 1)); }
.dv-alert-list::-webkit-scrollbar-track { background: transparent; }
.dv-alert-list::-webkit-scrollbar-thumb { background: rgba(248,113,113,.35); border-radius: var(--dv-r-sm); }
.dv-alert-item {
  display: flex; align-items: center; gap: var(--dv-sp-2);
  padding: var(--dv-sp-2) var(--dv-sp-3);
  border-radius: var(--dv-r-md);
  background: rgba(239,68,68,.12);
  border-left: calc(3px * var(--dv-s, 1)) solid var(--dv-c-alert);
  transition: background .18s, transform .12s;
}
.dv-alert-item:hover { background: rgba(239,68,68,.22); transform: translateX(2px); }
.dv-alert-item.warn { background: rgba(245,158,11,.12); border-left-color: var(--dv-c-set); }
.dv-alert-item.warn:hover { background: rgba(245,158,11,.22); }
/* 机台号、状态、数量、时间四段宽度都固定：
   否则每行内容长度不同，同一列表的行会左右跳动，视觉极不整齐 */
.dv-alert-item .no {
  font-size: var(--dv-fs-body); font-weight: 800;
  min-width: calc(46px * var(--dv-s, 1)); color: var(--ink); flex: none;
}
.dv-alert-item .st {
  font-size: var(--dv-fs-cap); font-weight: 700;
  padding: calc(2px * var(--dv-s, 1)) calc(8px * var(--dv-s, 1));
  border-radius: calc(20px * var(--dv-s, 1));
  color: #fff; flex: none; text-shadow: 0 0 8px rgba(0,0,0,.4);
}
.dv-alert-item .info {
  flex: 1 1 auto; min-width: 0;
  font-size: var(--dv-fs-sub); color: var(--ink-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dv-alert-item .qt {
  font-size: var(--dv-fs-h2); font-weight: 800;
  font-variant-numeric: tabular-nums; color: var(--ink); flex: none;
}
.dv-alert-item .ago {
  font-size: var(--dv-fs-cap); color: var(--ink-3);
  flex: none; min-width: calc(58px * var(--dv-s, 1)); text-align: right;
}
.dv-none {
  text-align: center; padding: var(--dv-sp-6) var(--dv-sp-3);
  color: var(--dv-c-run); font-size: var(--dv-fs-body); font-weight: 700;
}

/* ---------- 车间 / 区域概况 ---------- */
/* 面板已由 .dv-col > .dv-panel:last-child 撑满，这里让车间条目均分高度 */
.dv-lines {
  display: flex; flex-direction: column; gap: var(--dv-sp-3);
  flex: 1 1 auto; min-height: 0;
}
.dv-line {
  flex: 1 1 0; min-height: 0;
  border: var(--dv-panel-bd); border-radius: var(--dv-r-md);
  padding: var(--dv-sp-3);
  background: rgba(255,255,255,.022);
}
.dv-line .t {
  display: flex; align-items: baseline; gap: var(--dv-sp-2);
  margin-bottom: var(--dv-sp-2); min-width: 0;
}
.dv-line .t .n {
  font-size: var(--dv-fs-h2); font-weight: 800;
  color: #cfe2ff; letter-spacing: .5px; flex: none;
}
.dv-line .t .c {
  font-size: var(--dv-fs-cap); color: var(--ink-3);
  margin-left: auto; font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
}
/* 状态分段条：全站统一的进度条高度与圆角 */
.dv-line .seg {
  display: flex;
  height: calc(18px * var(--dv-s, 1));
  border-radius: var(--dv-r-sm);
  overflow: hidden; background: rgba(255,255,255,.05);
}
.dv-line .seg i { display: block; height: 100%; transition: width .45s; box-shadow: 0 0 10px currentColor; }
.dv-line .legend {
  display: flex; flex-wrap: wrap;
  gap: var(--dv-sp-1) var(--dv-sp-3);
  margin-top: var(--dv-sp-2);
  font-size: var(--dv-fs-cap); color: var(--ink-2);
}
.dv-line .legend span { display: inline-flex; align-items: center; gap: var(--dv-sp-1); font-weight: 600; }
.dv-line .legend i {
  width: calc(8px * var(--dv-s, 1)); height: calc(8px * var(--dv-s, 1));
  border-radius: calc(2px * var(--dv-s, 1)); display: block; flex: none;
}

/* ---------- 状态墙 ---------- */
/* ---------- 状态墙 ----------
   布局目标：89 台全部一屏可见、不重叠、不需滚动。
   做法：按楼栋分区（.dv-wgroup）+ 分区内紧凑方块网格（.dv-wgrid），
   方块只显示机台号，状态由颜色 + 图例表达。 */
/* 用 .dv-col > 前缀提高特异性，压过上面的 .dv-col > .dv-panel { flex:none } */
.dv-col > .dv-wallpanel {
  display: flex; flex-direction: column;
  /* 状态墙吃掉整列高度 */
  flex: 1 1 auto;
  overflow: hidden;
  min-height: 0;
}
.dv-wallpanel > h2 { flex: none; margin-bottom: var(--dv-sp-2); }

/* ---------- 颜色图例 ---------- */
/* 不再按状态分组后，颜色成了主要的状态信道，图例必须常驻可见 */
.dv-legend {
  flex: none;
  display: flex; flex-wrap: wrap;
  gap: var(--dv-sp-2) var(--dv-sp-4);
  padding: 0 2px var(--dv-sp-2);
  margin-bottom: var(--dv-sp-2);
  border-bottom: 1px solid rgba(96,165,250,.14);
}
.dv-lg-item {
  display: inline-flex; align-items: center;
  gap: var(--dv-sp-1);
  font-size: var(--dv-fs-sub); color: var(--ink-2); font-weight: 600;
  white-space: nowrap;
}
/* 图例色块与状态墙方块用同一套尺寸/圆角，两者的视觉语言才对得上 */
.dv-lg-item > i {
  width: calc(11px * var(--dv-s, 1)); height: calc(11px * var(--dv-s, 1));
  border-radius: var(--dv-r-sm);
  flex: none; display: block;
  box-shadow: 0 0 8px currentColor, inset 0 1px 0 rgba(255,255,255,.25);
}
.dv-lg-item .n {
  font-variant-numeric: tabular-nums; font-weight: 800; color: var(--ink);
  min-width: calc(18px * var(--dv-s, 1)); text-align: right;
}

/* 内容区：固定展示，绝不滚动。
   overflow 用 hidden 而不是 auto —— auto 会出现滚动条、
   会被滚轮/触摸拖拽/键盘/惯性带动，正是要消除的行为。
   内容真的超高时由 JS 整体 scale 缩小（fitDvWall），不靠滚动兜底。
   overscroll-behavior + touch-action 再补一道：连滚动链和触摸手势一起禁掉，
   防止在状态墙上滑动时带动外层页面。 */
.dv-wallwrap {
  position: relative; flex: 1 1 auto; min-height: 0;
  overflow: hidden;
  overscroll-behavior: none;
  touch-action: none;
  /* 竖向 flex：让 .dv-wall 拿到面板全部剩余高度，分区才能均分 */
  display: flex; flex-direction: column;
  /* 容器查询基准：让下面 .dv-wgrid 用 cqw 按「中栏实际宽度」算方块尺寸，
     而不是按整个视口宽度算。不支持 cqw 的浏览器会忽略这一行，
     下面的 @supports 块也不会命中，自动退回 vw 方案。 */
  container-type: inline-size;
}
/* 缩放基准点：顶部居中。缩放后内容顶边不动，不会上下跳。 */
.dv-wall {
  display: flex; flex-direction: column;
  flex: 1 1 auto; min-height: 0;
  transform-origin: top center;
}

/* 楼栋分区：标题 + 组内网格。
   flex-grow 由 JS 按该栋机台数写入 --dv-n（见 layoutDvWall），
   于是各栋按「机台数量比例」瓜分整片高度—— 69 台的A3栋自然分到更多高度。
   这替代了原来的 space-between：
   · space-between 会把富余空间全部变成「A3 与 A8 之间的大空档」，
     正是页面上那块黄色空白的成因（第 12 轮修掉）；
   · 按数量瓜分则每块空间都分给了实际内容，一屏铺满。
   flex-basis:0 保证「有多少台就分多少」，新增机台后高度自动重算。 */
.dv-wgroup {
  display: flex; flex-direction: column;
  flex: var(--dv-n, 1) 1 0;
  min-height: 0;
  margin-bottom: var(--dv-sp-3);
}
.dv-wgroup:last-child { margin-bottom: 0; }
.dv-wgroup .gh {
  display: flex; align-items: baseline;
  gap: var(--dv-sp-2);
  flex: none;
  padding: 0 2px var(--dv-sp-1);
  font-size: var(--dv-fs-sub); font-weight: 700;
  color: var(--ink-2); letter-spacing: .6px;
  min-width: 0;
}
.dv-wgroup .gh .gt::before {
  content: ''; display: inline-block;
  width: calc(3px * var(--dv-s, 1));
  height: calc(12px * var(--dv-s, 1));
  border-radius: calc(2px * var(--dv-s, 1));
  margin-right: var(--dv-sp-1);
  background: var(--brand); box-shadow: 0 0 8px var(--brand);
  vertical-align: -1px;
}
.dv-wgroup .gh .gt {
  font-size: var(--dv-fs-h2); color: #cfe2ff; font-weight: 800; flex: none;
}
.dv-wgroup .gh .n {
  margin-left: auto; font-size: var(--dv-fs-cap);
  color: var(--ink-3); font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap; padding-left: var(--dv-sp-2);
}

/* 紧凑网格：列数与方块尺寸都随「容器宽度」而不是视口宽度变化。
   第 8 轮改动要点：原来用 4.4vw / 3.4vw，
   但状态墙只占三栏中间一列（中栏宽度 ≈ 视口 46%），
   视口变宽时 vw 跟着涨、中栏却没同比例变宽 —— 或者反过来，
   中栏加宽后方块仍按旧 vw 计算，结果就是「面板很宽但方块很小、外围大片留白」（截图实况）。
   改用容器查询单位 cqw：以 .dv-wallwrap 为容器，1cqw = 容器宽度的 1%，
   方块就能真正跟着中栏一起缩放。

   上下限仍乘 --dv-s，且给上下限兜底：极窄容器下 cqw 会算出过小的值，
   min() 保证不会缩到认不出机台号。 */
/* ---------- 状态墙方块网格 ----------
   ⚠ 历史踩过的坑（都已修，勿回退）：
   · 第 9 轮：写成 fit-content(minmax(min(...), 1fr))。规范里 fit-content()
     只接受 <length-percentage> 一个参数，嵌套 minmax() 属非法语法 ——
     浏览器**静默丢弃整条声明**，grid-template-columns 退回 none，
     89 台方块竖排成一条。表现是「界面炸了」而非「样式没生效」，极难定位。
     现由 check-grid-syntax.cjs 守住语法合法性。
   · 第 10 轮：改用 flex + flex:0 0 auto 想让方块贴合编号宽度，
     结果每行能装几个取决于内容总宽 → 行长参差、居中留白，
     用户反馈「不是一排一排的，中间空太多」。
   · 第 11 轮：用固定边长 + aspect-ratio:1 保证方块是正方，
     但行高固定、整体按内容高度堆叠 → 分区底部留出大片空白
     （用户截图里的黄色区域）。
   · 第 12 轮：改为「行容器 + 行内均分」两层结构，彻底零空白（详见下）。

   ★ 为什么是两层（row > cell）而不是单层 grid：
     单层 grid 铺满宽度靠 `repeat(cols, 1fr)`，但**末行不满时，
     右侧剩下的格子位就是空的** —— 而机台数是 89（质数），
     任何列数都除不尽，缺口必然存在。
     改成两层后：外层只管「有几行」，内层每行用 flex 均分，
     末行哪怕只有 1 台，也会把整行宽度拉满 → 宽高双向都不留空白。
     代价是末行格子比满行略宽，这是「填满」必须付的、也是可接受的代价。 */
.dv-wgrid {
  /* 吃满所属分区的全部高度：行数写进 --dv-rows，
     行高由 .dv-wrow 的 flex:1 均分，纵向不留空档。
     flex:1 + min-height:0 是前提 —— 没有它，网格只按内容高度撑开，
     分区底部就会空出来（第 12 轮修掉的黄色空白就是这条）。 */
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  /* 行间距；列间距在 .dv-wrow 上 */
  gap: var(--dv-sp-1);
}
/* 行：高度均分整片剩余空间，宽度吃满 */
.dv-wrow {
  flex: 1 1 0;
  min-height: 0;
  display: flex;
  gap: var(--dv-sp-1);
}

/* 紧凑方块：只显示机台号。
   纯展示：不设 cursor:pointer，不做 hover 位移/缩放 —— 大屏无人值守，
   任何「看起来能点」或「悬停会动」的样式都会误导现场人员。

   第 12 轮：方块尺寸完全由「行内 flex 均分」决定（flex:1 1 0 + min-width:0），
   方块自身不再设任何宽高 —— 否则会覆盖轨道、把格子压回内容尺寸，
   行高均分与整行铺满就同时失效了。
   min-width:0 是关键：flex item 默认 min-width:auto，
   会被「100#」这类内容顶宽，导致该行溢出、末行留白。
   编号 nowrap + overflow 保证「100#」不换行不溢出方块。 */
.dv-wcell {
  position: relative;
  /* 宽度由行内 flex 均分 → 每行必然铺满整行宽度（含末行） */
  flex: 1 1 0;
  min-width: 0; min-height: 0;
  border-radius: var(--dv-r-sm);
  padding: 0 var(--dv-sp-1);
  color: #fff; cursor: pointer;
  user-select: none;
  display: flex; align-items: center; justify-content: center;
  gap: var(--dv-sp-1);
  border: 1px solid rgba(255,255,255,.16);
  box-shadow: var(--dv-sh-1);
}
/* 点击机台号看详情。
   只加指针手势 + 边框高亮，刻意不加 transform 缩放 ——
   大屏常年无人值守，缩放动画会让整面墙一直「有反应」，反而干扰展示效果。 */
.dv-wcell:hover {
  border-color: rgba(255,255,255,.75);
  box-shadow: 0 0 0 1px rgba(255,255,255,.35), 0 6px 18px rgba(0,0,0,.35);
}
.dv-wcell .no {
  /* 字号跟着方块走：用容器单位 cqw 跟着中栏缩放，
     再用 max() 兜一个下限 —— 极窄容器下也不会缩到认不出机台号。
     第 12 轮起方块尺寸由 grid 轨道均分决定，字号改用「相对中栏」的比例，
     不再与某个固定边长公式耦合（方块边长已随容器高度变化，不再是常量）。 */
  font-size: max(calc(9px * var(--dv-s, 1)), min(calc(15px * var(--dv-s, 1)), 2.2cqw));
  font-weight: 800; letter-spacing: .2px; line-height: 1;
  text-shadow: 0 1px 3px rgba(0,0,0,.4);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  /* 极端情况（超长编号）宁可截断也不溢出方块 */
  max-width: 100%;
  overflow: hidden;
}
@supports not (width: 1cqw) {
  /* 不支持 cqw 时退回 vw 字号（上面的 cqw 版本会被整条忽略） */
  .dv-wcell .no { font-size: max(calc(9px * var(--dv-s, 1)), min(calc(15px * var(--dv-s, 1)), 2.1vw)); }
}
/* 状态图标角标：不依赖颜色的第二信道，常显可读 */
.dv-wcell .ico {
  width: calc(9px * var(--dv-s, 1)); height: calc(9px * var(--dv-s, 1));
  flex: none; display: none;
  background: rgba(255,255,255,.28);
  border-radius: calc(2px * var(--dv-s, 1));
  padding: calc(1px * var(--dv-s, 1));
  box-sizing: content-box;
}
.dv-wcell .ico svg { width: 100%; height: 100%; fill: #fff; stroke: #fff; display: block; }
.dv-wcell.idle .ico { display: none; }
/* 未报工/停机：降级显示。
   这里用 --dv-c-idle 而不是随手写一个灰，
   保证「车间概况分段条里的未报工」与「状态墙里的未报工方块」
   是同一个灰 —— 同一状态在两处必须是同一个颜色。
   边框色用 color-mix 从 --dv-c-idle 派生，
   这样改一次 --dv-c-idle，虚线边框也跟着变。
   （color-mix 在很老的浏览器上不生效，只影响这条虚线颜色，
     不影响方块本身的底色与文字色，不会导致内容不可见。） */
.dv-wcell.idle {
  background: rgba(255,255,255,.05);
  color: var(--dv-c-idle);
  border: 1px dashed rgba(148,163,184,.45);
  box-shadow: none;
}
@supports (color: color-mix(in srgb, red, blue)) {
  .dv-wcell.idle {
    border-color: color-mix(in srgb, var(--dv-c-idle) 55%, transparent);
  }
}
.dv-wcell.alert {
  border-color: #fff;
  box-shadow: 0 0 0 2px rgba(239,68,68,.95), 0 0 18px rgba(239,68,68,.6);
  animation: dvPulse 1.8s ease-in-out infinite;
}
@keyframes dvPulse {
  0%, 100% { box-shadow: 0 0 0 2px rgba(239,68,68,.95), 0 0 14px rgba(239,68,68,.45); }
  50% { box-shadow: 0 0 0 3px rgba(239,68,68,1), 0 0 24px rgba(239,68,68,.85); }
}

/* 新数据进入时的入场高亮（patchList 的 enterClass 挂上，900ms 后自动摘除） */
.enter { animation: dvEnter .85s ease; }
@keyframes dvEnter {
  0% { transform: scale(.82); filter: brightness(2.2); }
  55% { transform: scale(1.06); filter: brightness(1.5); }
  100% { transform: scale(1); filter: brightness(1); }
}
.dv-alert-item.enter { animation: dvEnterA .85s ease; }
@keyframes dvEnterA {
  0% { transform: translateX(-14px); opacity: 0; }
  60% { transform: translateX(3px); opacity: 1; }
  100% { transform: none; }
}

/* 刷新失败的轻量提示：不遮挡内容，右上角淡入淡出 */
.dv-error {
  position: fixed;
  right: calc(18px * var(--dv-s, 1)); top: calc(18px * var(--dv-s, 1));
  z-index: 120;
  background: rgba(127,29,29,.94); color: #fecaca;
  border: 1px solid rgba(248,113,113,.5);
  border-radius: var(--dv-r-md);
  padding: var(--dv-sp-3) var(--dv-sp-4);
  font-size: var(--dv-fs-body); font-weight: 600;
  box-shadow: 0 8px 28px rgba(127,29,29,.5);
  opacity: 0; transform: translateY(calc(-10px * var(--dv-s, 1)));
  pointer-events: none;
  transition: opacity .28s, transform .28s;
}
.dv-error.show { opacity: 1; transform: none; }
body:not(.display-mode) .dv-error { display: none; }

/* ---------- 排行 ---------- */
/* 右栏三个面板的高度策略统一为：
   巡检、订单 = flex:none（按内容高度）
   排行= flex:1（吃掉剩余高度）
   这样内容少的面板不会凭空多出一大块空白，内容多的排行也能充分利用纵向空间。 */
.dv-right .dv-qapanel,
.dv-right .dv-pmpanel { flex: none; }
.dv-right .dv-panel:last-child {
  flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0;
}
.dv-right .dv-rank {
  display: flex; flex-direction: column;
  gap: var(--dv-sp-1);
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
  scrollbar-width: thin; scrollbar-color: rgba(56,189,248,.35) transparent;
}
.dv-right .dv-rank::-webkit-scrollbar { width: calc(5px * var(--dv-s, 1)); }
.dv-right .dv-rank::-webkit-scrollbar-track { background: transparent; }
.dv-right .dv-rank::-webkit-scrollbar-thumb { background: rgba(56,189,248,.3); border-radius: var(--dv-r-sm); }
.dv-rank-row {
  display: flex; align-items: center;
  gap: var(--dv-sp-2);
  padding: var(--dv-sp-2) 0;
  border-bottom: 1px solid rgba(96,165,250,.12);
  font-size: var(--dv-fs-body);
  min-width: 0;
}
.dv-rank-row:last-child { border-bottom: none; }
/* 排名徽章：固定尺寸方块，三条分隔线宽度一致 */
.dv-rank-row .no {
  width: calc(22px * var(--dv-s, 1)); height: calc(22px * var(--dv-s, 1));
  border-radius: var(--dv-r-sm); flex: none;
  display: flex; align-items: center; justify-content: center;
  font-size: var(--dv-fs-cap); font-weight: 800;
  background: rgba(255,255,255,.07); color: var(--ink-3);
}
.dv-rank-row.top1 .no { background: linear-gradient(135deg,#fbbf24,#f59e0b); color: #422006; box-shadow: 0 0 12px rgba(251,191,36,.6); }
.dv-rank-row.top2 .no { background: linear-gradient(135deg,#cbd5e1,#94a3b8); color: #1e293b; }
.dv-rank-row.top3 .no { background: linear-gradient(135deg,#fdba74,#fb923c); color: #431407; }
/* 产品名拿走剩余空间，机台数与产量不参与伸缩，保证右侧数字上下对齐成列 */
.dv-rank-row .nm {
  flex: 1 1 auto; min-width: 0; font-weight: 700;
  color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dv-rank-row .mc {
  flex: none; color: var(--ink-3); font-size: var(--dv-fs-cap);
  font-variant-numeric: tabular-nums;
}
.dv-rank-row .qt {
  flex: none; min-width: calc(62px * var(--dv-s, 1)); text-align: right;
  font-weight: 800; font-variant-numeric: tabular-nums; color: #7dd3fc;
  white-space: nowrap;
}
.dv-rank-row .qt small { font-weight: 500; color: var(--ink-3); font-size: var(--dv-fs-cap); }

/* ===== 大屏排行自动滚动（与普通模式同思路）=====
   大屏是无人值守场景，不能靠人拖滚动条：必须自动循环。
   数据少于一屏时不启用，避免只有两三条还在滚、看起来像坏了。 */
.dv-right .dv-rank { overflow: hidden; position: relative; }
.dv-right .dv-rank.rolling .dv-rank-track {
  animation: rankScroll var(--rank-dur, 20s) linear infinite;
  will-change: transform;
}
.dv-right .dv-rank.rolling:hover .dv-rank-track { animation-play-state: paused; }
.dv-right .dv-rank.rolling.locked .dv-rank-track { animation-play-state: paused; }

.dv-rank-toggle {
  display: none; align-items: center; gap: var(--dv-sp-1);
  margin-top: var(--dv-sp-3);
  padding: var(--dv-sp-2) var(--dv-sp-3);
  border-radius: var(--dv-r-md);
  border: 1px solid rgba(96,165,250,.28); background: rgba(255,255,255,.06);
  color: var(--ink-3); font-size: var(--dv-fs-sub);
  font-family: inherit; cursor: pointer;
  align-self: flex-start;
}
.dv-right .dv-rank.rolling + .dv-rank-toggle { display: inline-flex; }
.dv-rank-toggle:hover { border-color: rgba(96,165,250,.6); color: #7dd3fc; }
.dv-rank-toggle svg { flex: none; }
.dv-rank-toggle .locki { display: none; }
.dv-rank-toggle.locked .locki { display: block; }
.dv-rank-toggle.locked .playi { display: none; }

/* ---------- 底部 + 搜索 ---------- */
/* 底部提示：绝对定位在displayView 内侧底部，不占用布局高度。
   之前 bottom:2px 会让文字压在状态墙面板的下边框上（截图可见），
   改成放在 padding 之内、并让 displayView 底部留出空间，彻底避开。 */
.dv-foot {
  position: absolute; left: 0; right: 0;
  bottom: calc(2px * var(--dv-s, 1));
  text-align: center;
  font-size: var(--dv-fs-cap); color: var(--ink-3); letter-spacing: .5px;
  pointer-events: none; opacity: .5;
  /* 与状态墙面板底边保持间距，压字的问题就不再出现 */
  z-index: 3;
}
.dv-search {
  position: fixed;
  right: calc(18px * var(--dv-s, 1)); top: calc(100px * var(--dv-s, 1));
  z-index: 55;
  display: flex; gap: var(--dv-sp-1);
  opacity: .1; transition: opacity .25s;
}
.dv-search:hover, .dv-search:focus-within { opacity: 1; }
.dv-search input {
  width: calc(185px * var(--dv-s, 1));
  padding: var(--dv-sp-2) var(--dv-sp-3);
  font-size: var(--dv-fs-body); font-family: inherit; outline: none;
  border: 1px solid rgba(96,165,250,.3); border-radius: var(--dv-r-md);
  background: rgba(11,20,42,.92); color: var(--ink);
}
.dv-search input::placeholder { color: var(--ink-3); }
.dv-search input:focus { border-color: var(--brand); box-shadow: 0 0 0 3px rgba(56,189,248,.16); }
.dv-search button {
  padding: var(--dv-sp-2) var(--dv-sp-3);
  font-size: var(--dv-fs-body); cursor: pointer; font-family: inherit;
  border: 1px solid rgba(96,165,250,.3); border-radius: var(--dv-r-md);
  background: rgba(11,20,42,.92); color: var(--ink-2);
}
.dv-search button:hover { color: #fff; border-color: var(--brand); }
body:not(.display-mode) .dv-search { display: none; }
/* 展示模式下退出按钮改成深色玻璃风格 */
body.display-mode .display-exit {
  background: rgba(11,20,42,.7); border: 1px solid rgba(96,165,250,.3);
  color: var(--ink-2); backdrop-filter: blur(8px);
}
body.display-mode .display-exit:hover { color: #fff; border-color: var(--brand); }
/* 展示模式下详情弹层也要适配深色 */
body.display-mode #modal { background: rgba(2,6,18,.82); backdrop-filter: blur(4px); }
body.display-mode #modal .box {
  background: linear-gradient(160deg, rgba(20,36,68,.97), rgba(11,20,42,.97));
  border: 1px solid rgba(96,165,250,.28); color: #e8f0ff;
  box-shadow: 0 24px 70px rgba(0,0,0,.7);
}
body.display-mode #modal .box h3 { color: #e8f0ff; }
body.display-mode #modal .box .msub { color: var(--ink-3); }
body.display-mode .kv dt { color: var(--ink-3); }
body.display-mode .kv dd { color: #e8f0ff; }
body.display-mode #modalClose { background: rgba(255,255,255,.08); color: var(--ink-2); }
body.display-mode #modalClose:hover { background: rgba(255,255,255,.14); }
body.display-mode .rec-item { background: rgba(255,255,255,.035); }
body.display-mode .rec-item .r2 { color: var(--ink-3); }
/* 新增的汇总卡与明细字段 chip 的深色适配 */
body.display-mode .kv.sum {
  background: rgba(255,255,255,.05); border-color: rgba(96,165,250,.24);
}
body.display-mode .kv.sum > .sum-h { color: var(--ink-2); }
body.display-mode .kv.sum > .row > .v { color: #e8f0ff; }
body.display-mode .kv.sum > .row > .v small { color: var(--ink-3); }
body.display-mode .kv.sum > .none { color: var(--ink-3); }
body.display-mode .rec-item .pname,
body.display-mode .rec-item .qty { color: #e8f0ff; }
body.display-mode .rec-item .r3 .f {
  background: rgba(255,255,255,.1); color: var(--ink-2);
}
body.display-mode .rec-item .r3 .f.tech { background: rgba(34,211,238,.16); color: #67e8f9; }
body.display-mode .rec-item .r3 .f.op { background: rgba(74,222,128,.16); color: #86efac; }
body.display-mode .empty { color: var(--ink-3); }

/* ===== 登录页深色科技风（与报工页一致） ===== */
#login {
  position: relative;
  background: #070d1c;
  overflow: hidden;
}
/* 三层结构，靠 z-index 排定层次：
   ::after 0=虚化底图 → .bgmask 1=半隐藏遮罩 → ::before 1=网格 → .box 2=卡片 */
#login::after {
  content: ''; position: absolute; inset: 0; z-index: 0;
  background-image: url('assets/cnc-bg.jpg');
  background-size: cover; background-position: center top;
  filter: blur(7px) saturate(1.25);
  transform: scale(1.1);   /* 放大避免 blur 后边缘露底色 */
  opacity: .4;
  pointer-events: none;
}
#login .bgmask {
  position: absolute; inset: 0; z-index: 1;
  background:
    linear-gradient(180deg, rgba(7,13,28,.86) 0%, rgba(7,13,28,.5) 34%, rgba(7,13,28,.9) 78%, #070d1c 100%),
    linear-gradient(90deg, rgba(7,13,28,.72) 0%, rgba(7,13,28,.1) 42%, rgba(7,13,28,.72) 100%);
  pointer-events: none;
}
/* 网格底纹做质感 */
#login::before {
  content: ''; position: absolute; left: 0; right: 0; top: 0; height: 100%;
  z-index: 1;
  background-image:
    linear-gradient(rgba(96,165,250,.07) 1px, transparent 1px),
    linear-gradient(90deg, rgba(96,165,250,.07) 1px, transparent 1px);
  background-size: 30px 30px;
  -webkit-mask-image: radial-gradient(80% 60% at 50% 40%, #000 0%, transparent 100%);
  mask-image: radial-gradient(80% 60% at 50% 40%, #000 0%, transparent 100%);
  pointer-events: none;
}
@supports not (filter: blur(1px)) {
  #login::after { filter: none; opacity: .15; }
}
#login > .box {
  position: relative; z-index: 2;
  background: rgba(255,255,255,.97);
  border-radius: 18px; padding: 34px 30px 28px;
  box-shadow: 0 20px 52px rgba(2,8,23,.46);
  border: 1px solid rgba(255,255,255,.6);
}
#login .lglogo {
  width: 50px; height: 50px; margin: 0 auto 14px; color: var(--brand);
}
#login .lglogo svg { width: 100%; height: 100%; }
#login .badge {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 12px; border-radius: 20px; margin-bottom: 16px;
  background: rgba(29,78,216,.09); border: 1px solid rgba(29,78,216,.22);
  font-size: 11.5px; font-weight: 600; color: var(--brand); letter-spacing: .4px;
}
#login .badge i {
  width: 5px; height: 5px; border-radius: 50%; background: #16a34a; display: block;
}
#login h2 {
  font-size: 21px; font-weight: 800; letter-spacing: 1px; color: var(--ink);
}
#login p { font-size: 12.5px; color: var(--ink-3); margin-bottom: 22px; }
#login .ph { margin-top: 10px; }
#login input { padding: 13px; border-radius: 11px; }
#login button { padding: 13px 22px; border-radius: 11px; letter-spacing: 1px; }
#login .msg { text-align: center; }
#login .tip { text-align: center; line-height: 1.7; }

/* ============================================================
   品质巡检完成率
   数据来自 machine_checks 表，与报工（records）完全独立。
   ============================================================ */
.qa-sum { display: flex; gap: 10px; margin-bottom: 13px; flex-wrap: wrap; }
.qa-kpi {
  flex: 1; min-width: 88px; padding: 11px 12px; border-radius: 11px;
  background: #f8fafc; border: 1px solid var(--line);
}
.qa-kpi .v {
  display: block; font-size: 22px; font-weight: 800; line-height: 1.15;
  font-variant-numeric: tabular-nums; color: var(--ink);
}
.qa-kpi .v small { font-size: 13px; font-weight: 600; color: var(--ink-3); }
.qa-kpi .k { display: block; font-size: 11.5px; color: var(--ink-3); margin-top: 3px; }
.qa-kpi.ok { background: #f0fdf4; border-color: #86efac; }
.qa-kpi.ok .v { color: #15803d; }
.qa-kpi.full { background: #f0fdf4; border-color: #16a34a; }
.qa-kpi.full .v { color: #16a34a; }
.qa-kpi.ng { background: #fef2f2; border-color: #fca5a5; }
.qa-kpi.ng .v { color: #b91c1c; }

.qa-list { display: flex; flex-direction: column; gap: 6px; }
.qa-row {
  display: flex; align-items: center; gap: 9px;
  font-size: 12.5px; padding: 6px 8px; border-radius: 8px;
}
.qa-row.ok { background: #f0fdf4; }
.qa-row.ok .nm { color: #15803d; font-weight: 600; }
.qa-row.ok .bar i { background: #16a34a; }
.qa-row.part { background: #fff7ed; }
.qa-row.part .nm { color: #c2410c; font-weight: 600; }
.qa-row.part .bar i { background: #ea580c; }
.qa-row.none { background: #fef2f2; }
.qa-row.none .nm { color: #b91c1c; font-weight: 600; }
.qa-row.none .bar i { background: #dc2626; }
.qa-row .nm {
  width: 92px; flex: none; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap;
}
.qa-row .bar {
  flex: 1; height: 7px; border-radius: 4px;
  background: rgba(0, 0, 0, .06); overflow: hidden;
}
.qa-row .bar i { display: block; height: 100%; border-radius: 4px; transition: width .3s; }
.qa-row .ct {
  width: 34px; flex: none; text-align: right;
  font-variant-numeric: tabular-nums; font-weight: 700; color: var(--ink-2);
}
.qa-row .ng {
  flex: none; font-size: 11px; font-weight: 700; color: #b91c1c;
  padding: 1px 7px; border-radius: 20px; background: #fee2e2;
}

/* ---------- 大屏版 ---------- */
/* 右栏三个面板统一策略：
   巡检 / 订单按内容高度（flex:none），排行吃掉剩余高度。
   之前三个都用 max-height:xxvh，各算各的，
   导致内容少的面板下方留白、内容多的被硬切—— 现在改成同一套flex 规则。
   注意类名必须是 dv-pmpanel：HTML 里曾经误写成 dv-pmpmanel，
   导致下面这条规则一直不生效（拼错的类名不会报错，只会静默失效）。 */
.dv-qapanel .dv-qa,
.dv-pmpanel .dv-pm {
  display: flex; flex-direction: column;
  gap: var(--dv-sp-2);
  flex: none;
  min-height: 0;
}
.dv-qa-row {
  display: flex; align-items: center; gap: var(--dv-sp-3);
  font-size: var(--dv-fs-body);
  padding: var(--dv-sp-1) 0;
  min-width: 0;
}
/* 机台名列固定宽度：两行之间才能上下对齐成一条竖线 */
.dv-qa-row .nm {
  width: calc(96px * var(--dv-s, 1)); flex: none;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--ink-2);
}
.dv-qa-row .bar {
  flex: 1 1 auto; min-width: 0;
  height: calc(9px * var(--dv-s, 1));
  border-radius: calc(5px * var(--dv-s, 1));
  background: rgba(255, 255, 255, .12); overflow: hidden;
}
.dv-qa-row .bar i { display: block; height: 100%; border-radius: inherit; }
.dv-qa-row.ok .bar i { background: #4ade80; }
.dv-qa-row.ok .ct { color: #4ade80; }
.dv-qa-row.part .bar i { background: #fbbf24; }
.dv-qa-row.part .ct { color: #fbbf24; }
.dv-qa-row .ct {
  width: calc(52px * var(--dv-s, 1)); flex: none; text-align: right;
  font-variant-numeric: tabular-nums; font-weight: 800;
  white-space: nowrap;
}
.dv-qa-row .ng {
  flex: none; font-size: var(--dv-fs-cap); font-weight: 800; color: #fff;
  background: var(--dv-c-alert);
  padding: calc(1px * var(--dv-s, 1)) calc(9px * var(--dv-s, 1));
  border-radius: calc(20px * var(--dv-s, 1));
}

/* ---------- 大屏深色下的巡检面板 ---------- */
body.display-mode .qa-row .nm { color: #e2e8f0; }
body.display-mode .qa-row .ct { color: #cbd5e1; }

/* ============================================================
   订单交付进度卡片（PMC 建单 → 仓库按订单号出库 → 看板汇总）
   ============================================================ */
.pm-sum { display: flex; gap: 10px; margin-bottom: 13px; flex-wrap: wrap; }
.pm-kpi {
  flex: 1; min-width: 88px; padding: 11px 12px; border-radius: 11px;
  background: #f8fafc; border: 1px solid var(--line);
}
.pm-kpi .v {
  display: block; font-size: 22px; font-weight: 800; line-height: 1.15;
  font-variant-numeric: tabular-nums; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pm-kpi .v small { font-size: 13px; font-weight: 600; color: var(--ink-3); }
.pm-kpi .k { display: block; font-size: 11.5px; color: var(--ink-3); margin-top: 3px; }
.pm-kpi.ok { background: #f0fdf4; border-color: #86efac; }
.pm-kpi.ok .v { color: #15803d; }
.pm-kpi.full { background: #f0fdf4; border-color: #16a34a; }
.pm-kpi.full .v { color: #16a34a; }

.pm-list { display: flex; flex-direction: column; gap: 3px; }
.pm-row {
  display: flex; align-items: center; gap: 9px;
  font-size: 12.5px; padding: 6px 8px; border-radius: 8px;
}
.pm-row.ok { background: #f0fdf4; }
.pm-row.part { background: #fff7ed; }
.pm-row.none { background: #fef2f2; }
.pm-row .no {
  width: 132px; flex: none; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; font-weight: 700;
  font-family: ui-monospace, Menlo, Consolas, monospace;
}
.pm-row.ok .no { color: #15803d; }
.pm-row.part .no { color: #c2410c; }
.pm-row.none .no { color: #b91c1c; }
.pm-row .bar {
  flex: 1; height: 7px; border-radius: 4px;
  background: rgba(0, 0, 0, .06); overflow: hidden;
}
.pm-row .bar i { display: block; height: 100%; border-radius: 4px; transition: width .3s; }
.pm-row.ok .bar i { background: #16a34a; }
.pm-row.part .bar i { background: #ea580c; }
.pm-row.none .bar i { background: #dc2626; }
.pm-row .ct {
  width: 96px; flex: none; text-align: right;
  font-variant-numeric: tabular-nums; font-weight: 700; color: var(--ink-2);
}
.pm-row .ct small { font-size: 10.5px; color: var(--ink-3); font-weight: 600; }
.pm-row .pc {
  width: 48px; flex: none; text-align: right;
  font-variant-numeric: tabular-nums; font-weight: 800; font-size: 13px;
}
.pm-row.ok .pc { color: #15803d; }
.pm-row.part .pc { color: #c2410c; }
.pm-row.none .pc { color: #b91c1c; }
/* 每单下方的补充说明：客户、出库次数、日计划完成、交期 */
.pm-sub {
  font-size: 11.5px; color: var(--ink-3);
  padding: 0 8px 5px 8px; margin-top: -2px; line-height: 1.5;
}

/* ---------- 大屏版 ---------- */
/* 右列宽度有限，订单号 + 进度条 + 百分比必须挤得下。
   关键是把宽度预算按比例分配，而不是「订单号自适应 + 进度条剩多少算多少」：
   之前订单号用 flex:1 会吃掉绝大部分空间，进度条被压到min-width 的22px，
   视觉上几乎看不见。现在订单号给固定上限，进度条拿走剩余全部空间。
   min-width:0 必须有—— flex 子项默认 min-width:auto，
   不置0 的话长订单号会撑破容器而不是走省略号。 */
.dv-pm-row {
  display: flex; align-items: center; gap: var(--dv-sp-2);
  font-size: var(--dv-fs-body);
  padding: var(--dv-sp-1) 0;
  min-width: 0;
}
.dv-pm-row .no {
  /* 订单号不参与伸缩，但有上限：
     flex:0 1 auto + max-width 让短号不撑开、长号能省略 */
  flex: 0 1 auto; min-width: 0; max-width: 58%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--ink-2);
  font-family: ui-monospace, Menlo, Consolas, monospace;
  line-height: 1.25;
}
/* 进度条吃掉剩余空间，这样进度才有视觉意义 */
.dv-pm-row .bar {
  flex: 1 1 auto; min-width: calc(34px * var(--dv-s, 1));
  height: calc(9px * var(--dv-s, 1));
  border-radius: calc(5px * var(--dv-s, 1));
  background: rgba(255, 255, 255, .12); overflow: hidden;
}
.dv-pm-row .bar i { display: block; height: 100%; border-radius: inherit; }
.dv-pm-row.ok .bar i { background: #4ade80; }
.dv-pm-row.ok .ct { color: #4ade80; }
.dv-pm-row.part .bar i { background: #fbbf24; }
.dv-pm-row.part .ct { color: #fbbf24; }
/* 百分比固定宽度：装得下「100%」和「23.1%」，右对齐 */
.dv-pm-row .ct {
  width: calc(58px * var(--dv-s, 1)); flex: none; text-align: right;
  font-variant-numeric: tabular-nums; font-weight: 800;
  font-size: var(--dv-fs-sub); line-height: 1.25;
  white-space: nowrap;
}
body.display-mode .pm-row .no { color: var(--ink-2); }
body.display-mode .pm-row .ct { color: var(--ink-3); }

/* ============================================================
   品质巡检完成率：默认折叠 / 点击展开收起
   ------------------------------------------------------------
   折叠用 max-height + opacity 而非 display:none ——
   display:none 会把元素移出渲染树，display-mode 下的
   父级flex 布局会重排、且展开时有明显跳动；
   max-height 过渡更平滑，且元素仍在渲染树里，
   innerHTML 写入照常生效（这正是「折叠不影响数据统计」的前提）。
   ============================================================ */
.collapsible .fold-h {
  display: flex; align-items: center; gap: 7px;
  cursor: pointer; user-select: none; margin-bottom: 0;
  transition: color .15s;
}
.collapsible .fold-h:hover { color: var(--brand, #1d4ed8); }
.collapsible .fold-h:focus-visible {
  outline: 2px solid var(--brand, #1d4ed8); outline-offset: 2px; border-radius: 4px;
}
.collapsible .fold-c {
  flex: none; transition: transform .2s ease; transform: rotate(0deg);
}
.collapsible .fold-h .right { margin-left: auto; }

.collapsible .fold-b {
  max-height: 1400px; opacity: 1; overflow: hidden;
  transition: max-height .26s ease, opacity .2s ease, margin-top .26s ease;
  margin-top: 12px;
}
.collapsible.collapsed .fold-b {
  max-height: 0; opacity: 0; margin-top: 0;
}
/* 折叠态时箭头朝右，展开态朝下（默认 transform: rotate(90deg)） */
.collapsible.collapsed .fold-c { transform: rotate(0deg); }
.collapsible:not(.collapsed) .fold-c { transform: rotate(90deg); }

/* 有异常时给标题上色：折起来也能看见「今天有异常」 */
.collapsible.has-ng .fold-h { color: #b91c1c; }
.collapsible.has-ng.collapsed .fold-h {
  background: #fef2f2; border-radius: 8px; padding: 2px 6px; margin: -2px -6px;
}

/* ============================================================
   全屏模式字号统一缩放（放在文件末尾以覆盖前面的固定值）
   ------------------------------------------------------------
   这一层现在只做一件事：把还没迁移到 token 的字号接上 --dv-s。
   已经引用 token 的（面板标题、行条目、图例等）在各自规则里改好了，
   这里不再重复写死数值 —— 否则这份「末尾覆盖清单」会反过来
   把 token 体系架空，两套值各说各话，正是这次要消灭的问题。

   保留的原因：这些元素（顶栏、弹窗、普通模式共用）的规则写在文件前部，
   迁移成本高且风险大，用末尾一层薄覆盖保证电视上也能看清。
   新增组件请直接用 token，不要往这里加。
   ============================================================ */

/* --- KPI：字号已由 token 接管，这里只补齐间距 --- */
body.display-mode .dv-hcard .bar { margin-top: auto; }

/* --- 状态墙：方块号已自适应，这里补分区标题 --- */
body.display-mode .dv-wgroup .gh .gt { font-size: var(--dv-fs-h2); }

/* --- 顶栏（公司名 / 时钟）：这两个属于普通模式共用组件，
   放在末尾薄覆盖一层，电视上跟着放大 --- */
body.display-mode .topbar .brand { font-size: calc(24px * var(--dv-s)); }
body.display-mode .clock .ct { font-size: calc(44px * var(--dv-s)); }
body.display-mode .clock .cd { font-size: calc(17px * var(--dv-s)); }
