/* ============ 基础 ============ */
:root {
  --brand: #2563eb;
  --brand-d: #1d4ed8;
  --brand-l: #eff4ff;
  --bg: #f4f5f7;
  --card: #ffffff;
  --text: #14181f;
  --muted: #6b7280;
  --line: #e5e7eb;
  --ok: #15803d;
  --ok-bg: #e8f6ee;
  --warn: #b45309;
  --warn-bg: #fdf3e3;
  --danger: #b91c1c;
  --danger-bg: #fdeaea;
  --dark: #1b2130;
  --radius: 12px;
  --shadow: 0 1px 2px rgba(16, 24, 40, .06), 0 1px 3px rgba(16, 24, 40, .04);
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

html, body {
  margin: 0; padding: 0;
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB",
    "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
  font-size: 15px;
  line-height: 1.5;
}

a { color: var(--brand); text-decoration: none; }
h1, h2, h3 { margin: 0; font-weight: 600; }
h1 { font-size: 19px; }
h2 { font-size: 15px; }
h3 { font-size: 14px; }
.muted { color: var(--muted); }
.small { font-size: 12.5px; }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.hide { display: none !important; }
.right { text-align: right; }
.center { text-align: center; }
.nowrap { white-space: nowrap; }

/* ============ 顶栏 ============ */
.topbar {
  background: var(--dark);
  color: #fff;
  padding: 10px 16px;
  display: flex; align-items: center; gap: 10px;
  position: sticky; top: 0; z-index: 30;
}
.topbar .tb-title { font-weight: 600; font-size: 15.5px; flex: 1; }
.topbar .tb-sub { font-size: 12.5px; opacity: .75; }
.topbar .tb-btn {
  background: rgba(255,255,255,.12); border: 0; color: #fff;
  font-size: 12.5px; padding: 5px 10px; border-radius: 999px; cursor: pointer;
}
.topbar .tb-btn:active { background: rgba(255,255,255,.24); }
.topbar nav { display: flex; gap: 4px; flex-wrap: wrap; }
.topbar nav a {
  color: rgba(255,255,255,.72); font-size: 13px; padding: 5px 10px; border-radius: 999px;
}
.topbar nav a.on { background: rgba(255,255,255,.16); color: #fff; }

/* ============ 布局 ============ */
.wrap { max-width: 760px; margin: 0 auto; padding: 12px 12px 96px; }
/**
 * 管理台（admin.html）用 .wrap.wide。
 *
 * 2026-09-29 张先生反馈「电脑分辨率很高很宽，还是要左右拉动才看得到操作栏」——
 * 根因就在这一行：原来写的是 **1180**，而审核表 12 列要约 1340px、报表明细 11 列，
 * 超过 1180 的部分全被推到屏幕外，最后一列的「通过 / 驳回 / 修改」自然看不见。
 *
 * 直接放宽到 1560（**不加 media query**）：max-width 本来就只是上限，
 * 窄窗口仍由视口宽度决定，小屏一点不会因此变宽 —— 加断点反而会留下一个
 * 「窗口 1300 宽却还按 1180 排」的尴尬区间。
 *
 * 这是全局兜底。审核页另有一道更硬的保险：操作列钉在右边（见下面 .t-rv 那段）。
 */
.wrap.wide { max-width: 1560px; }

.card {
  background: var(--card); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 14px; margin-bottom: 12px;
  border: 1px solid rgba(16,24,40,.04);
}
.card > h2 {
  display: flex; align-items: center; gap: 8px; margin-bottom: 10px;
  color: var(--text);
}
.card > h2 .cnt { color: var(--muted); font-weight: 400; font-size: 12.5px; }
.card.flush { padding: 0; overflow: hidden; }

.row { display: flex; align-items: center; gap: 10px; }
.row.wrapx { flex-wrap: wrap; }
.spread { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.grid { display: grid; gap: 10px; }
.grid.c2 { grid-template-columns: repeat(2, 1fr); }
.grid.c3 { grid-template-columns: repeat(3, 1fr); }
.grid.c4 { grid-template-columns: repeat(4, 1fr); }
/* 报表页统计块：2026-09-26 张先生要求精简到 3 格（工序数 / 发货数量 / 计件工资）→ 走 c3。
   历史沿革：曾是 c5（5 格）→ 加「发货数量」后 c6（6 格）→ 现在 c3。
   c5 / c6 两个类已删除 —— 全站没有第二处用它，留着只会让人以为还在用。
   为什么当初用固定列数而不是 auto-fit：auto-fit 列数随窗口宽度浮动，
   窗口略窄就掉列、最后一块孤零零落单（试过，不稳）。 */
.mt8 { margin-top: 8px; } .mt12 { margin-top: 12px; } .mt16 { margin-top: 16px; }

/* ============ 统计块 ============ */
.stat { background: #fff; border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; }
.stat .k { font-size: 12px; color: var(--muted); }
.stat .v { font-size: 20px; font-weight: 700; margin-top: 2px; letter-spacing: -.02em; }
.stat .v small { font-size: 12px; font-weight: 500; color: var(--muted); margin-left: 2px; }
/* 可点的统计块（工单页顶部汇总）：点一下按该口径筛下面的列表。
   <button> 默认居中 + 不继承字体，这里掰回左对齐的卡片样子。 */
.stat.devstat {
  display: block; width: 100%; text-align: left; font-family: inherit;
  cursor: pointer; appearance: none; -webkit-appearance: none;
  transition: border-color .12s, box-shadow .12s, transform .08s;
}
.stat.devstat:active { transform: scale(.985); }
.stat.devstat.on { border-color: var(--brand); box-shadow: 0 0 0 2px rgba(37, 99, 235, .16); }

/* ============ 按钮 ============ */
.btn {
  appearance: none; border: 1px solid var(--line); background: #fff; color: var(--text);
  padding: 9px 14px; border-radius: 10px; font-size: 14px; font-weight: 500;
  cursor: pointer; font-family: inherit; transition: .12s;
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
}
.btn:active { transform: scale(.955); filter: brightness(.92); }
.btn:disabled { opacity: .45; cursor: not-allowed; }
.btn.primary { background: var(--brand); border-color: var(--brand); color: #fff; }
.btn.primary:active { background: var(--brand-d); }
.btn.ok { background: var(--ok); border-color: var(--ok); color: #fff; }
.btn.warn { background: var(--warn); border-color: var(--warn); color: #fff; }
.btn.ghost { background: transparent; }
.btn.danger { color: var(--danger); border-color: #f2c9c9; background: #fff; }
.btn.sm { padding: 6px 10px; font-size: 13px; border-radius: 8px; }
.btn.xs { padding: 4px 8px; font-size: 12px; border-radius: 7px; }
.btn.block { width: 100%; }
.btn.grow { flex: 1; }

/* ============ 表单 ============ */
label.f { display: block; margin-bottom: 10px; }
label.f > span { display: block; font-size: 12.5px; color: var(--muted); margin-bottom: 4px; }
/* ⚠️ 新增 input type 时务必补进下面这个列表，否则宽度会退回浏览器默认值（约 170px）。
   不要改用 input:not([type=...]) 的写法 —— :not() 会抬高优先级，覆盖掉 .qty input 的窄宽。 */
input[type=text], input[type=email], input[type=password], input[type=number],
input[type=tel], input[type=date], input[type=search], select, textarea {
  width: 100%; padding: 10px 11px; border: 1px solid var(--line); border-radius: 9px;
  font-size: 15px; font-family: inherit; background: #fff; color: var(--text);
  -webkit-appearance: none; appearance: none;
}
select { background-image: none; }
input:focus, select:focus, textarea:focus { outline: 2px solid #c7d8ff; border-color: var(--brand); }
textarea { min-height: 72px; resize: vertical; }
.checkline { display: flex; align-items: center; gap: 8px; font-size: 14px; }
.checkline input { width: 18px; height: 18px; }

/* ============ 状态徽章 ============ */
.badge {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 8px; border-radius: 999px; font-size: 12px; font-weight: 500;
  background: #eef0f4; color: #4b5563; white-space: nowrap;
}
.badge.ok { background: var(--ok-bg); color: var(--ok); }
.badge.warn { background: var(--warn-bg); color: var(--warn); }
.badge.danger { background: var(--danger-bg); color: var(--danger); }
.badge.brand { background: var(--brand-l); color: var(--brand-d); }
.badge.dark { background: #2a3242; color: #cbd5e1; }

/* ============ 工单头 ============ */
.dev-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.dev-code { font-family: ui-monospace, Menlo, monospace; font-weight: 600; color: var(--brand-d); }
.bar { height: 7px; background: #eceef2; border-radius: 999px; overflow: hidden; margin-top: 10px; }
.bar > i { display: block; height: 100%; background: var(--brand); border-radius: 999px; transition: width .3s; }

/* ============ 工艺卡 ============ */
.pcard {
  border: 1px solid var(--line); border-radius: 11px; padding: 12px; margin-bottom: 10px;
  background: #fff;
}
.pcard.taken { background: #fbfbfc; }
.pcard.done { background: #f8faf9; border-color: #d9e9df; }
.pcard.mine { border-color: #bcd0ff; background: var(--brand-l); }
.pcard .pname { font-weight: 600; font-size: 15.5px; }
.pcard .meta { color: var(--muted); font-size: 12.5px; margin-top: 2px; }
.pcard .acts { display: flex; gap: 8px; margin-top: 10px; align-items: stretch; }

.qty { display: inline-flex; align-items: center; border: 1px solid var(--line); border-radius: 9px; overflow: hidden; background: #fff; height: 38px; }
.qty button { width: 36px; height: 100%; border: 0; background: #f7f8fa; font-size: 18px; color: #374151; cursor: pointer; }
.qty button:active { background: #eceef2; }
.qty input { width: 46px; height: 100%; border: 0; text-align: center; font-size: 15px; font-weight: 600; padding: 0; }
.qty input:focus { outline: none; }

.timer { font-family: ui-monospace, Menlo, monospace; font-weight: 700; font-size: 17px; }
/* 只有「计时中」一种状态（蓝色）。原来还有一个「超时」用的红色态，
   随超时功能一起在 2026-09-23 删掉了 —— 现在只记耗时，不设标准。 */
.timer.run { color: var(--brand-d); }

/* 置顶星标：点击区域按手指放大到 34px，视觉上保持克制（不抢「接单」按钮的注意力）。
   ★ 用琥珀色 —— 全站只有它用这个颜色，技术员一眼能认出「这是我钉住的」。 */
.pinwrap { display: flex; justify-content: flex-end; gap: 4px; margin-bottom: 2px; }
.pinfav, .pinup {
  width: 34px; height: 34px; padding: 0; border: 0; border-radius: 9px;
  background: transparent; color: #c2c9d4; font-size: 19px; line-height: 1; cursor: pointer;
}
.pinfav:active, .pinup:active { background: #eceef2; }
.pinfav.on { color: #f59e0b; }
.pinup { width: 30px; height: 30px; font-size: 15px; font-weight: 700; color: #6b7280; background: #f3f4f6; }

/* ============ 列表 ============ */
.line { display: flex; gap: 10px; padding: 10px 0; border-bottom: 1px dashed var(--line); }
.line:last-child { border-bottom: 0; }
.line .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--brand); margin-top: 6px; flex: none; }
.line .dot.ok { background: var(--ok); }
.line .dot.warn { background: var(--warn); }
.line .dot.grey { background: #c3c8d1; }
.line .body { flex: 1; min-width: 0; }
.line .body .t1 { font-size: 14px; }
.line .body .t2 { font-size: 12px; color: var(--muted); margin-top: 1px; }

/* ============ 表格 ============ */
.tw { overflow-x: auto; -webkit-overflow-scrolling: touch; }
table.t { width: 100%; border-collapse: collapse; font-size: 13.5px; }
table.t th, table.t td { padding: 8px 10px; text-align: left; border-bottom: 1px solid var(--line); white-space: nowrap; }
table.t th { background: #fafbfc; color: var(--muted); font-weight: 500; font-size: 12.5px; position: sticky; top: 0; }
table.t td.num, table.t th.num { text-align: right; font-family: ui-monospace, Menlo, monospace; }
table.t tbody tr:active { background: #f7f9fc; }
table.t tr.tot td { font-weight: 700; background: #fafbfc; }

/* 宽表横向滚动时，首列钉在左边 —— 手机上（报表、工序明细都有 6~11 列）
   滚到右边看金额时，还能认出这一行是谁 / 哪台设备。
   三个必须注意的点：
   1) sticky 单元格必须自带**不透明**背景，否则会透出下面滚过去的字；
   2) 合计行 / 头行 / 按下态各有自己的底色，要逐条覆盖（特异性比默认规则高）；
   3) border-collapse: collapse 下 sticky 格子的边框会跟着滚走，用 inset 阴影补分隔线。 */
.tw table.t td:first-child,
.tw table.t th:first-child {
  position: sticky; left: 0; background: #fff;
  box-shadow: inset -1px 0 0 var(--line);
}
.tw table.t td:first-child { z-index: 1; }
.tw table.t th:first-child { z-index: 3; }   /* 表头同时 sticky top，要盖住首列数据格 */
.tw table.t th:first-child { background: #fafbfc; }
.tw table.t tr.tot td:first-child { background: #fafbfc; }
.tw table.t tbody tr:active td:first-child { background: #f7f9fc; }
/* 空态行是跨全表的 <td colspan="N" class="muted center">，它同样是 :first-child ——
   若一起 sticky，会在表格右边缘多画一条竖线（宽度=整表，left:0 无从钉起）。
   排除掉：跨列单元格本来就不该钉。 */
.tw table.t td[colspan]:first-child { position: static; box-shadow: none; }

/* ---------- 审核表（.t-rv，2026-09-29）----------
   12 列的长表：不处理时在 1180 容器里必然横向滚动，而「通过 / 驳回 / 修改」在最后一列 ——
   不看滚动条根本找不到按钮（张先生的反馈）。两条保障：

   ① 操作列钉在右边。横向滚不滚、滚到哪，它都在视野里。
      注意 th.rvop 是**双轴 sticky**：全局规则给了 th{top:0}，这里再加 right:0，
      sticky 会同时生效，所以 top 必须显式写回，否则表头会跟着竖滚跑掉。
      还要给不透明底色 —— 否则横滚时下面的字会从按钮背后透出来。

   ② 两列内容收紧：「完成时间」拆成日期 + 时间两行、「工艺」允许换行
      （自定义工艺徽章「自定义工序 · 需定价」很长，不换行会独占 250px）。
      这两列原来吃掉约 360px，是挤出操作列的元凶。 */
.tw table.t.t-rv th, .tw table.t.t-rv td { padding: 8px 8px; }
.tw table.t.t-rv td.proccell { white-space: normal; max-width: 190px; }
.tw table.t.t-rv td.rvnote { white-space: normal; max-width: 150px; }
.tw table.t.t-rv td.rvtime { line-height: 1.35; }

/* 工单列表的「备注」列（2026-10-01 张先生）：
   备注记的是批次（「9月2日那批」），可能比别的列长得多。表格单元格是全局 nowrap，
   这里单独放开换行 + 限宽，免得一条长备注把整张表撑出横向滚动。
   完整内容仍在 title 上（鼠标停一下能看全）。 */
.tw table.t td.devnote { white-space: normal; max-width: 190px; word-break: break-word; }

.tw table.t.t-rv th.rvop,
.tw table.t.t-rv td.rvop {
  position: sticky; right: 0; background: #fff;
  box-shadow: inset 1px 0 0 var(--line);
}
.tw table.t.t-rv td.rvop { z-index: 2; }
.tw table.t.t-rv th.rvop { top: 0; z-index: 4; background: #fafbfc; }
.tw table.t.t-rv tbody tr:active td.rvop { background: #f7f9fc; }

/* 「项目汇总 / 当天项目」列（报表按人/按天汇总、人员页按人汇总三处共用）：
   列出做过的每道工序 × 数量。这列内容的长度不可控（有人做过十几道），
   所以**覆盖上面那条 nowrap**，改成可换行的横向标签流 ——
   一道工序一个小胶囊，一行放不下自动折行，整体由 .tw 容器横向滚动兜底。 */
table.t td.proclist { white-space: normal; text-align: left; }
table.t td.proclist .pl-item {
  display: inline-flex; align-items: baseline; gap: 3px;
  margin: 2px 5px 2px 0; padding: 1px 7px;
  background: var(--brand-l); border-radius: 6px;
  font-size: 12.5px; line-height: 1.7; white-space: nowrap;
}
table.t td.proclist .pl-q { color: var(--brand-d); font-weight: 600;
  font-family: ui-monospace, Menlo, monospace; font-size: 12px; }

/* ============ 报表 · 工序明细分页 ============ */
/* 一页 50 条（2026-09-26 张先生要求）。只有超过一页时才出现（JS 控制 .hide）。 */
.pager {
  display: flex; align-items: center; justify-content: center; gap: 12px;
  padding: 12px 12px 4px; flex-wrap: wrap;
}
.pager .pgnow { font-size: 13px; color: #6b7280; }
.pager .btn[disabled] { opacity: .4; cursor: default; }
/* 盘点清单的「跳到第 N 页」（2026-10-08 晚）：1164 台 = 59 页，只能一颗颗点下一页太累 */
.pager .pgjump {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 13px; color: #6b7280;
}
.pager .pgjump input {
  width: 58px; padding: 4px 6px; font-size: 13px; font-family: inherit;
  border: 1px solid var(--line, #d7dae0); border-radius: 6px;
  background: #fff; color: inherit; text-align: center;
}
/* 数字框去掉上下箭头 —— 手机上反正也点不到，桌面上还占宽度 */
.pager .pgjump input::-webkit-outer-spin-button,
.pager .pgjump input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.pager .pgjump input[type=number] { -moz-appearance: textfield; appearance: textfield; }

/* ============ Tab ============ */
.tabs { display: flex; gap: 2px; background: #eceef2; padding: 3px; border-radius: 10px; margin-bottom: 12px; overflow-x: auto; }
.tabs button {
  flex: 1; min-width: 68px; border: 0; background: transparent; padding: 8px 10px;
  border-radius: 8px; font-size: 13.5px; color: #4b5563; cursor: pointer; font-family: inherit;
  white-space: nowrap;
}
.tabs button.on { background: #fff; color: var(--text); font-weight: 600; box-shadow: var(--shadow); }

/* ============ 按压反馈（App 手感）============
 * 网页默认「点了没反应」是它不像 App 的最大来源。
 * 统一规则：按下去轻微缩小 + 变暗，松手弹回 —— 手指「真的按上去」的感觉。
 * touch-action: manipulation 顺带去掉双击缩放延迟。 */
.btn, .tabs button, .topbar .tb-btn { touch-action: manipulation; transition: transform .08s ease, filter .08s ease, background .12s, box-shadow .12s; }
.tabs button:active { transform: scale(.95); filter: brightness(.93); }
.topbar .tb-btn:active { transform: scale(.9); }

/* ============ 扫码接单（相机取景弹层）============ */
.btnrow { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; justify-content: flex-end; }
.scanbox {
  position: relative; background: #000; border-radius: 12px; overflow: hidden;
  aspect-ratio: 3 / 4; max-height: 52vh;
}
.scanbox video { width: 100%; height: 100%; object-fit: cover; display: block; }
/* 引导框：2026-09-25 晚由 76%×34% 的扁矩形改成居中的小正方形。
   理由不是好看 —— 人的本能是「把二维码塞满这个框」，所以框在画面里多大，
   就决定了他会站多近。旧框（高 34%）逼得人贴到 ~9cm，而手机主摄在 10cm 内
   景深极浅、自动对焦最难锁，于是「一开始总是糊、要试半天」。
   框高 28% 时「塞满框」≈ 10.7cm、「大概对准」≈ 15cm，正好落进实测算出的
   可用合焦带（解码边长 480 时 9–18cm，见 core.js scanOutputSide）。 */
.scanframe {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  height: 28%; aspect-ratio: 1;
  border: 2px solid rgba(255,255,255,.9); border-radius: 12px;
  box-shadow: 0 0 0 2000px rgba(0,0,0,.38);
}

/* 扫码兜底 UI（2026-09-25）：
   iOS「添加到主屏幕」形态下相机可能黑屏（WebKit #252465，无可靠代码级修法）。
   ① #scanMirror 是该形态下的预览：直接把解码用的那帧画出来（有「元素黑屏但后台
      解码照常」的机型，这样至少能看见画面对准）；
   ② .scanstart 是「轻点画面开始扫描」覆盖层（自动播放被拦时）；
   ③ .scanfail 是自救都失败后的出口面板（含跳 Safari + 一行诊断，方便技术员截图）。 */
.scanbox #scanMirror { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.scanbox.mirror .scanframe { display: none; }   /* 整幅画面就是扫描区，不用引导框 */
.scanstart {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  color: #fff; font-size: 14px; background: rgba(0,0,0,.38);
  text-shadow: 0 1px 3px rgba(0,0,0,.6);
}
.scanfail {
  background: #fff7ed; border: 1px solid #fed7aa; border-radius: 10px;
  padding: 10px 12px; margin-top: 10px;
}
.scanfail p { margin: 0; color: #7c2d12; line-height: 1.5; }
.scanfail #scanFailDiag { margin-top: 5px; font-size: 11px; color: #9a3412; word-break: break-all; }

/* ============ 空状态 ============ */
.empty { text-align: center; color: var(--muted); padding: 26px 10px; font-size: 13.5px; }
.empty .big { font-size: 28px; margin-bottom: 6px; opacity: .5; }

/* ============ Toast ============ */
#toast { position: fixed; left: 50%; bottom: 88px; transform: translateX(-50%); z-index: 999; width: max-content; max-width: 88vw; }
.toast-item {
  background: #1f2635; color: #fff; padding: 10px 16px; border-radius: 10px;
  font-size: 14px; margin-top: 8px; box-shadow: 0 6px 20px rgba(0,0,0,.22);
  animation: tin .18s ease-out;
}
.toast-item.err { background: #a52020; }
.toast-item.ok { background: #146c37; }
@keyframes tin { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* ============ 遮罩 / 弹层 ============ */
.mask {
  position: fixed; inset: 0; background: rgba(17,22,32,.5); z-index: 300;
  display: flex; align-items: center; justify-content: center; padding: 16px;
}
.sheet {
  background: #fff; border-radius: 14px; width: 100%; max-width: 460px;
  max-height: 86vh; overflow: auto; padding: 16px;
}
.sheet h3 { margin-bottom: 12px; font-size: 16px; }
.sheet .foot { display: flex; gap: 8px; margin-top: 14px; }

/* ============ 底部操作条 ============ */
.bottombar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
  background: rgba(255,255,255,.94); backdrop-filter: blur(8px);
  border-top: 1px solid var(--line); padding: 8px 12px calc(8px + env(safe-area-inset-bottom));
  display: flex; gap: 8px; justify-content: center;
}
.bottombar .inner { width: 100%; max-width: 736px; display: flex; gap: 8px; }

/* ============ 工单打印清单（勾选式，零 canvas） ============
   打印页屏幕上只显示这一行行清单；二维码是点「生成并打印」时才现生成的，
   所以清单再长也不会因为二维码 canvas 把内存吃满。 */
.qrlist { display: flex; flex-direction: column; }
.qrrow {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px; border-bottom: 1px solid var(--line);
  cursor: pointer; font-size: 14px;
}
.qrrow:last-child { border-bottom: 0; }
.qrrow:hover { background: #f8fafc; }
.qrrow.on { background: var(--brand-l); }
.qrrow input { width: 18px; height: 18px; flex: none; margin: 0; }
.qrrow .rc {
  font-family: ui-monospace, Menlo, monospace; font-weight: 600;
  color: var(--brand-d); flex: none;
}
.qrrow .rn {
  flex: 1; min-width: 0; color: var(--muted); font-size: 13px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.qrrow .rp { flex: none; font-size: 12.5px; color: var(--muted); }
.qrrow .rs { flex: none; }
/* 从后台「二维码」按钮深链进来时，闪一下定位到那一行 */
.qrrow.flash { animation: qrflash 1.8s ease-out; }
@keyframes qrflash { 0%, 35% { background: #fff3cd; } 100% { background: transparent; } }

/* ============ 标签打印 ============
   热敏标签纸 40×30mm（横向：40 宽 × 30 高），一张一签。
   尺寸全部用绝对 mm（含 @page），浏览器直接打印即得正确版式。
   ⚠️ 打印机驱动里的纸张也要设成 40×30，两边一致才不会缩小/转方向。 */
.label-grid { display: grid; grid-template-columns: repeat(auto-fill, 40mm); gap: 4mm; }

/* 打印区：屏幕上完全不渲染。id 选择器特异度高于 .label-grid，压得住它。 */
#printArea { display: none; }

.labelsheet {
  width: 40mm; height: 30mm; box-sizing: border-box; overflow: hidden;
  border: 1px dashed #cfd4dc; border-radius: 4px; padding: 1mm;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  /* gap 0.35mm（原 0.5mm）：省下的 0.3mm 全部让给二维码，好让它刚好跨过
     「0.50mm/模块」那道轻松可扫门槛。再小就分不清二维码与编号了。 */
  gap: 0.35mm; break-inside: avoid;
}
/* 二维码尺寸分档（档位由 core.js 的 labelLineCount 决定）：
   40×30mm 的高度是硬约束，二维码下方每多一行字，二维码就得让一行出来。
   可扫门槛（203dpi 热敏）：≥0.50mm/模块扫得轻松、≥0.33mm/模块勉强可扫。
   本二维码是 V4（33 模块 + 两侧静默区 = 41 模块），所以「边长 ÷ 41」就是模块尺寸：
     lbl-l0（无配置无提示）24mm   ÷ 41 = 0.585mm → 4.68 dots  ← 故意不缩，见下
     lbl-l1（配置 1 行）  21mm   ÷ 41 = 0.512mm → 4.10 dots（主力场景，明确达标「轻松」）
     lbl-l2（配置 2 行）  19.5mm ÷ 41 = 0.476mm → 3.80 dots（已顶到 28mm 高度上限，再大就裁切）
     lbl-l3（配置 2 行 + 提示）17.5mm ÷ 41 = 0.427mm → 3.41 dots（同上）

   ⚠️ l0 档**故意不缩**（仍是 24mm）：这一档根本没有配置行，「字太小」的矛盾不存在，
     缩它只会净亏可扫性。要缩的是下面三档 —— 它们才有配置行要腾地方。
   ⚠️ l2/l3 已是物理极限：40×30 减去上下各 1mm 留白只有 28mm，两行 6pt 配置(4.86mm)
     加上编号(2.59mm)和三个 gap(1.05mm)后，二维码最大只能到这。

   ⚠️ 2026-09-23 这次「缩码 + 放大字」是一笔**拿冗余换清晰度的取舍**：
     现场技术员反馈「配置打印出来字太小」。配置从 5pt 升到 6pt（每行多占约 0.4mm），
     40×30 的高度又动不了，只能从二维码里抠。但单独抠码会把可扫性拖垮
     （l1 档 22.5→21mm 若还是 M 级便是 0.489mm，跌破 0.50 的「轻松」线），
     所以同时把纠错等级 M 降到 L —— 37 模块省回 33 模块，正好把抠掉的补回来。
     逐档对照降级前（45 模块 / 配置 5pt）：
       0.533 / 0.500 / 0.456 / 0.411  →  0.585 / 0.512 / 0.476 / 0.427
     即：字号大了 20%，可扫性**每一档都持平或更好**，没有一档是变差的。
     代价是纠错冗余从 15% 降到 7%（标签被污损时容错变小），这是有意接受的。

   —— 这四档的毫米数是「内容高 ≤ 28mm」倒推出来的，改任一处都要跑
   label-verify.cjs 复核（它会真的量出来对不对）。
   ⚠️ 同步改 core.js 的 LABEL_QR_MM，否则档位表两边对不上。 */
.labelsheet .qrbox { width: 24mm;   height: 24mm;   flex: none; }
.labelsheet.lbl-l1 .qrbox { width: 21mm;   height: 21mm; }
.labelsheet.lbl-l2 .qrbox { width: 19.5mm; height: 19.5mm; }
.labelsheet.lbl-l3 .qrbox { width: 17.5mm; height: 17.5mm; }
.labelsheet .qrbox svg { width: 100%; height: 100%; display: block; }
/* 编号 NO-260921001 共 13 字符，7pt 等宽约 20.6mm，40mm 宽的横版上很宽裕。
   注：加入配置行后由 8pt 降到 7pt，省下的约 0.35mm 让给配置行。 */
.labelsheet .cd {
  flex: none;   /* flex:none 是必须的：否则内容超出时 flex 会先把这一行压扁，
                   溢出被「压」掉而不是暴露出来，而 .labelsheet 又是 overflow:hidden，
                   结果就是静默裁切、没人发现。 */
  font-family: ui-monospace, Menlo, monospace; font-size: 7pt; font-weight: 700;
  letter-spacing: 0; white-space: nowrap; line-height: 1.05;
}
/* 配置（型号/内存/硬盘等，管理员自定义输入）：
   6pt 字号下 38mm 宽约 17 个全角字位。默认压一行、超出打省略号；
   确实要两行时由 JS 加 .cfg-lg（同时把二维码降到 lbl-l2 档）。
   ⚠️ 17 这个阈值与 core.js cfgLabelLines() 是一对，改字号必须同时改两边。
   ⚠️ 5pt → 6pt（2026-09-23）：技术员反馈打印出来看不清。字号一大，
      单行能放的字就从 21 个掉到 17 个，所以阈值和二维码档位都得跟着调。 */
.labelsheet .cfg {
  flex: none;   /* 同 .cd：不允许被 flex 压扁，超出就要能被验证脚本抓到 */
  box-sizing: border-box; max-width: 100%;
  font-size: 6pt; line-height: 1.15; text-align: center; color: #111;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.labelsheet.cfg-lg .cfg {
  white-space: normal; word-break: break-all;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
/* 扫码提示（可选）：微信扫一扫会被发布网关拦成 403，勾选后印在标签上。
   ⚠️ 原为 5pt 两行（约 4.4mm）；加上配置行后 40×30 放不下，已压成 4pt 单行（约 1.7mm）。 */
.labelsheet .tip {
  flex: none;   /* 同上 */
  font-size: 4pt; color: #b3261e; line-height: 1.2; text-align: center; white-space: nowrap;
}

/* 后台工单表「配置」列：内容可能很长，先截断，完整内容用 title 悬浮看 */
.cfgtext {
  display: inline-block; max-width: 190px; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom;
}

@media print {
  .no-print { display: none !important; }
  /* 提示浮层（#toast）是 position:fixed，不在 .no-print 里。
     不隐藏的话，它会被印在每张标签的顶部、盖住二维码上半部分 —— 标签直接作废。
     「生成并打印」点下去后提示要 3 秒才消失，而打印对话框是同步阻塞的，
     所以打印时它一定还在，必须靠样式挡住。 */
  #toast { display: none !important; }
  body { background: #fff; margin: 0; }
  .wrap { max-width: none; padding: 0; margin: 0; }
  .card { border: 0; border-radius: 0; box-shadow: none; background: #fff; padding: 0; margin: 0; }
  /* 打印时才放出打印区（里面只装勾选的工单） */
  #printArea { display: block; }
  /* 一张标签一页纸：每张后面断页，最后一张不打标记否则会多出一页空白 */
  .labelsheet.brk { break-after: page; }
  .labelsheet { border: 0; border-radius: 0; }
  /* ⚠️ 故意不声明 @page size！
     Chromium 系浏览器（Edge/Chrome）见到 size 声明后会自己再映射一次纸型，
     与驱动的横竖向标志叠加 → 内容被旋转 90°、偏向一角（Windows 实测）。
     去掉 size 后浏览器直接使用打印时选中的纸张坐标系，纸型交给驱动。
     代价：打印时必须手动选对 40×30 纸张（页面说明里已写明）。 */
  @page { margin: 0; }
}

/* ============ 响应式 ============ */
@media (max-width: 560px) {
  /* 工单页汇总两行（状态 3 格 / 手工单 1 格）跟着转 2 列，比缩成小方块好读 */
  .grid.c3, .grid.c4 { grid-template-columns: repeat(2, 1fr); }
  /* 审核表：操作列是钉住的（见上面 .t-rv），在手机上它会一直占着屏幕宽度，
     所以按钮要比桌面更瘦，别把左边的可见区挤没。 */
  .tw table.t.t-rv th.rvop, .tw table.t.t-rv td.rvop { padding: 6px 5px; }
  .tw table.t.t-rv td.rvop .btn { padding: 4px 7px; font-size: 12px; }
}
/* 标签网格用 auto-fill 30mm，窄屏自动变 1 列，无需额外覆盖 */

/* ============ 严重错误 ============ */
.fatal { margin: 20px; padding: 14px; border-radius: 10px; background: #fdeaea; color: #b91c1c; font-size: 14px; }
.skeleton { background: linear-gradient(90deg,#eef0f4,#f7f8fa,#eef0f4); border-radius: 8px; height: 14px; margin: 8px 0; animation: sk 1.2s infinite; }
@keyframes sk { 0%{opacity:.6} 50%{opacity:1} 100%{opacity:.6} }

/* ============ 顶栏：可点击的姓名（点开「我的」） ============ */
.topbar .tb-sub.clickable {
  opacity: .95; cursor: pointer;
  background: rgba(255,255,255,.12);
  padding: 4px 9px; border-radius: 999px;
  white-space: nowrap; max-width: 36vw;
  overflow: hidden; text-overflow: ellipsis;
}
.topbar .tb-sub.clickable:active { background: rgba(255,255,255,.26); }

/* ============ 「我的」弹层 ============ */
.mehead { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
.mehead .meava {
  width: 44px; height: 44px; flex: none; border-radius: 50%;
  background: var(--brand-l); color: var(--brand-d);
  display: flex; align-items: center; justify-content: center;
  font-size: 19px; font-weight: 700;
}
.mehead .menm { font-size: 16px; font-weight: 600; line-height: 1.2; }
.melinks { margin-top: 14px; border-top: 1px solid var(--line); padding-top: 2px; }
.melinks a {
  display: block; padding: 11px 2px; color: var(--text);
  border-bottom: 1px solid var(--line); font-size: 14px;
}
.melinks a:last-child { border-bottom: 0; }
.melinks a:active { background: var(--brand-l); }

/* ============ 致命错误横幅（避免「点了没反应」） ============ */
#fatalBar {
  position: fixed; left: 8px; right: 8px; bottom: 8px; z-index: 90;
  background: #fff7ed; border: 1px solid #fed7aa; color: #7c2d12;
  border-radius: 12px; padding: 10px 12px;
  box-shadow: 0 6px 20px rgba(16,24,40,.14); font-size: 13px;
}
#fatalBar .fb-msg { font-weight: 600; }
#fatalBar .fb-detail {
  margin-top: 4px; font-family: ui-monospace, Menlo, monospace; font-size: 11px;
  color: #9a3412; word-break: break-all; max-height: 66px; overflow: auto;
}
#fatalBar .fb-acts { display: flex; gap: 6px; margin-top: 8px; }

/* ============ 首屏兜底提示 ============ */
.boot-tip {
  position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: calc(100% - 40px); max-width: 340px; z-index: 99;
  background: #fff; border: 1px solid var(--line); border-radius: 14px;
  padding: 18px 16px; text-align: center;
  box-shadow: 0 10px 30px rgba(16, 24, 40, .14);
}
.boot-tip .bt-title { font-weight: 600; font-size: 15px; }
.boot-tip .bt-detail { font-size: 13px; color: var(--muted); margin-top: 6px; }
.boot-tip .bt-hint {
  font-size: 12.5px; color: var(--warn); background: var(--warn-bg);
  border-radius: 8px; padding: 8px; margin-top: 10px; text-align: left;
}
.boot-tip .bt-acts { display: flex; gap: 8px; justify-content: center; margin-top: 14px; }

/* ============ 小间距工具类 ============ */
.mt4 { margin-top: 4px; }
.mb8 { margin-bottom: 8px; }

/* ============ 工单行展开：逐道工艺明细 ============ */
tr.devsub > td {
  background: #f8fafc;
  padding: 10px 12px 14px;
  border-top: 1px dashed var(--line);
}
.dev-detail { font-size: 13px; }
.dev-proc { display: flex; flex-direction: column; gap: 6px; }
.dp-row {
  display: grid;
  grid-template-columns: minmax(84px, 1fr) minmax(96px, 1.1fr) minmax(0, 2fr);
  gap: 8px; align-items: start;
  padding: 8px 10px; background: #fff;
  border: 1px solid var(--line); border-radius: 10px;
}
.dp-name { font-weight: 600; font-size: 13.5px; }
.dp-mid { display: flex; flex-direction: column; align-items: flex-start; }
.dp-meta { text-align: right; line-height: 1.55; }
@media (max-width: 620px) {
  .dp-row { grid-template-columns: 1fr; gap: 4px; }
  .dp-meta { text-align: left; }
}

/* ============ 首页工作台（登录后仪表盘） ============ */
.dash-hero { padding: 18px 16px 16px; }
.dash-hello { font-size: 13px; color: var(--muted); margin-top: 10px; }

.qgrid {
  display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;
  margin-top: 10px;
}
.qcard {
  display: flex; align-items: center; gap: 12px;
  background: #fff; border: 1px solid var(--line); border-radius: 14px;
  padding: 16px 14px; text-decoration: none; color: inherit;
  transition: box-shadow .15s ease, transform .15s ease, border-color .15s ease;
}
.qcard:hover {
  border-color: #c7d2fe; box-shadow: 0 8px 20px rgba(37, 99, 235, .10);
  transform: translateY(-1px);
}
.qicon {
  flex: none; width: 44px; height: 44px; border-radius: 12px;
  display: flex; align-items: center; justify-content: center;
  font-size: 19px; font-weight: 700;
}
.qbody { min-width: 0; flex: 1; }
.qcard .qtitle { font-weight: 600; font-size: 15px; line-height: 1.25; }
.qcard .qdesc {
  font-size: 12.5px; color: var(--muted); margin-top: 3px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.qcard .qarrow {
  flex: none; font-size: 22px; color: #94a3b8; line-height: 1;
  padding: 0 2px;
}
@media (max-width: 560px) {
  .qgrid { grid-template-columns: 1fr; }
}

/* ============ 手机端交互（2026-09-26） ============ */
/* 关掉手机浏览器自带的整页下拉刷新与橡皮筋：我们自己实现了下拉刷新，
   两套一起动会打架（Chrome 安卓会整页重载，把我们的手势吞掉）。 */
html, body { overscroll-behavior-y: contain; }

/* 顶部网络活动条：任一请求持续超过 150ms 才出现（快请求不闪，免得更烦） */
#netbar {
  position: fixed; top: 0; left: 0; right: 0; height: 3px;
  z-index: 9999; pointer-events: none; opacity: 0; transition: opacity .2s;
}
#netbar.on { opacity: 1; }
#netbar > i {
  display: block; width: 38%; height: 100%; border-radius: 999px;
  background: var(--brand); box-shadow: 0 0 8px rgba(37, 99, 235, .5);
  animation: nbrun 1.1s ease-in-out infinite;
}
@keyframes nbrun { 0% { transform: translateX(-40%); } 100% { transform: translateX(265%); } }

/* 下拉刷新指示器（仅触摸设备，core.js 里装配） */
#ptr {
  position: fixed; top: 0; left: 50%; transform: translate(-50%, -60px);
  z-index: 9998; display: flex; align-items: center; gap: 8px;
  padding: 8px 14px; border-radius: 0 0 14px 14px;
  background: #fff; box-shadow: 0 8px 20px rgba(15, 23, 42, .18);
  color: var(--muted); font-size: 13px; white-space: nowrap;
  opacity: 0; pointer-events: none;
}
#ptr.armed { color: var(--brand); font-weight: 600; }
.spinner {
  width: 14px; height: 14px; flex: none; border-radius: 50%;
  border: 2px solid rgba(37, 99, 235, .2); border-top-color: var(--brand);
  animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* 按钮「等待中」：点了之后**真的发了请求**才会出现（判定在 core.js） */
.btn.waiting { pointer-events: none; opacity: .8; }
.btn.waiting::after {
  content: ''; width: 12px; height: 12px; flex: none; border-radius: 50%;
  border: 2px solid transparent; border-top-color: currentColor; border-right-color: currentColor;
  animation: spin .7s linear infinite; opacity: .7;
}

/* 按压反馈：涟漪要有个绝对定位的参照，所以这两个可点元素加 relative + 裁边 */
.btn, .qcard { position: relative; overflow: hidden; }
.ripple {
  position: absolute; border-radius: 50%; pointer-events: none;
  background: rgba(37, 99, 235, .22); transform: scale(0);
  animation: rip .46s ease-out forwards;
}
.btn.primary .ripple, .btn.ok .ripple, .btn.warn .ripple { background: rgba(255, 255, 255, .38); }
@keyframes rip { to { transform: scale(1); opacity: 0; } }

/* 只在没有 hover 的设备上加强按压反馈（桌面鼠标不需要，键盘/触控板反而会误触发） */
@media (hover: none) {
  .qcard:active { transform: scale(.975); box-shadow: 0 4px 12px rgba(37, 99, 235, .14); }
  select:active, input[type="date"]:active, input[type="search"]:active { background: #eef2f7; }
  .pinfav:active, .pinup:active { transform: scale(.92); }
}

/* 表格骨架行 */
tbody tr.skrow td { padding: 10px; }
.skrow .skeleton { height: 12px; margin: 4px 0; }

/* 报表页快捷档位高亮（2026-10-02）：加了「按人记忆上次档位」之后，必须让人一眼看出
   当前看的到底是「今天」还是「近 30 天」，否则很容易把数字读错。 */
.btn.xs.on { border-color: var(--brand); color: var(--brand); background: var(--brand-l); font-weight: 600; }

/* 2026-10-04：键盘导航、手机输入与弹窗滚动 */
:focus-visible { outline: 3px solid var(--brand); outline-offset: 3px; }
.sheet { overscroll-behavior: contain; }
@media (pointer: coarse) {
  input:not([type=checkbox]):not([type=radio]), select, textarea { font-size: 16px; }
  .btn { min-height: 42px; }
  .sheet { max-height: 86dvh; }
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; scroll-behavior: auto !important; }
  .ripple { display: none; }
}

/* ============================================================================
 * 页内相机层（assets/camera.js · 2026-10-08 库存盘点）
 *
 * 样式来自「送修登记」项目（那边真机跑过）。背景：拍照走 `<input capture>` 打开的
 * iOS 系统相机，网页层拿不到预览、也不知道有没有合焦。换成 getUserMedia 之后才可能
 * 有「实时锐度提示 + 抓帧选优」。
 * 视觉上刻意用纯黑 + 大按钮 —— 仓库里光线杂、可能戴手套，别做成精细控件。
 *
 * ⚠️ z-index 取 500：高于页面内容（最高 40）和弹层（300），但**低于 toast（999）**——
 *    抓帧失败、识别结果之类的提示得能浮在相机上面，不然用户什么都看不到。
 * ========================================================================== */
body.cam-open { overflow: hidden; }

.camlayer {
  position: fixed; inset: 0; z-index: 500;
  background: #000; color: #fff;
  display: flex; flex-direction: column;
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
}

.cam-top {
  flex: none; display: flex; align-items: center; gap: 10px;
  padding: 10px 12px;
}
.cam-x {
  flex: none; width: 38px; height: 38px; border-radius: 50%; border: 0;
  background: rgba(255, 255, 255, .15); color: #fff;
  font-size: 16px; line-height: 1; cursor: pointer;
}
/* 标题块要有 min-width:0 —— 不然长标题 + 徽标会把右边的「系统相机」挤出屏幕 */
.cam-titlebox { flex: 1; min-width: 0; text-align: center; }
.cam-title { font-size: 15px; font-weight: 600; }
/* 顶栏标题下面那行小字：已扫 N · 命中 M（由 camera.js 的 onReady().badge 填） */
.cam-badge { font-size: 11.5px; color: rgba(255, 255, 255, .6); margin-top: 1px; min-height: 1em; }
.cam-tx {
  flex: none; border: 0; border-radius: 999px; padding: 9px 12px;
  background: rgba(255, 255, 255, .15); color: #fff;
  font-size: 12.5px; cursor: pointer; white-space: nowrap;
}

/* 预览区。视频用 contain 完整显示（不裁切）—— 用户看到的必须就是拍到的，
   否则会误以为「标签已经占满画面了」，实际抓下来的图里它只占中间一小条。
   多出来的黑边正好留给上下两条控件。 */
.cam-stage {
  flex: 1; min-height: 0; position: relative; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
}
/* .cam-frame 的宽高由 camera.js 按视频真实比例算好写死，这样取景框和画面对得齐 */
.cam-frame { position: relative; }
.cam-v { display: block; width: 100%; height: 100%; }
.cam-guide {
  position: absolute; left: 15%; top: 15%; width: 70%; height: 70%;
  border: 1.5px dashed rgba(255, 255, 255, .5); border-radius: 10px;
  pointer-events: none;
}
.cam-wait {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  color: #cfcfcf; font-size: 14px;
}

.cam-panel { flex: none; padding: 10px 16px 16px; }

/* HUD —— 盘点的「当场告诉用户结果」那一条。
   放这么大是有意的：拍完之后用户抬头就得看见「260706015 ✓ 已盘点」，
   而不是先拍完一排再回列表里核对。 */
.cam-hud {
  font-size: 15px; font-weight: 600; line-height: 1.45;
  min-height: 2.6em; display: flex; align-items: center;
  margin-bottom: 6px; word-break: break-all;
}
.cam-hud .muted { color: rgba(255, 255, 255, .55); font-weight: 400; font-size: 13px; }
.cam-hud .mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.cam-hud.ok { color: #6ee7b7; }
.cam-hud.warn { color: #fbbf24; }
.cam-hud.bad { color: #fca5a5; }

.cam-status { display: flex; align-items: center; gap: 8px; font-size: 13.5px; color: rgba(255, 255, 255, .8); }
.cam-dot { flex: none; width: 9px; height: 9px; border-radius: 50%; background: #fbbf24; }
.cam-dot.ok { background: #34d399; }
.cam-dot.warn { background: #fbbf24; }
.cam-dot.bad { background: #f87171; }
.cam-text { min-width: 0; }

.cam-bar {
  margin-top: 8px; height: 4px; border-radius: 999px;
  background: rgba(255, 255, 255, .18); overflow: hidden;
}
.cam-bar i {
  display: block; height: 100%; width: 0; border-radius: 999px;
  background: #34d399; transition: width .18s ease-out;
}

.cam-tip {
  margin-top: 9px; font-size: 12px; line-height: 1.55;
  color: rgba(255, 255, 255, .58);
}

.cam-foot {
  margin-top: 12px;
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 10px;
}
.cam-lens {
  justify-self: start; height: 40px; min-width: 56px; padding: 0 12px;
  border: 0; border-radius: 999px; cursor: pointer;
  background: rgba(255, 255, 255, .15); color: #fff;
  font-size: 13px; font-weight: 600;
}
/* ⚠️ 用 visibility 而不是 display:none 来藏 —— 见 camera.js 里那段说明：
   display:none 会让这颗按钮不占网格位，快门和「完成」一起左移错位。 */
.cam-lens.is-off { visibility: hidden; }
.cam-lens:disabled { opacity: .5; }
.cam-done {
  justify-self: end; height: 40px; padding: 0 14px;
  border: 0; border-radius: 999px; cursor: pointer;
  background: rgba(255, 255, 255, .15); color: #fff; font-size: 13px;
}
.cam-done b { color: #6ee7b7; margin-left: 2px; }

.cam-shot {
  justify-self: center; width: 70px; height: 70px; padding: 0;
  border: 4px solid rgba(255, 255, 255, .85); border-radius: 50%;
  background: transparent; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.cam-shot i {
  display: block; width: 52px; height: 52px; border-radius: 50%;
  background: #fff; transition: transform .1s, background .1s;
}
.cam-shot:active i { transform: scale(.86); }
/* 抓帧要几百毫秒（连抓几帧挑最清晰的），按钮必须当场变色，否则像点空了 */
.cam-shot.busy i { background: #fbbf24; }
.cam-shot:disabled { opacity: .6; }

.cam-flash {
  position: absolute; inset: 0; background: #fff; pointer-events: none;
  animation: camflash .22s ease-out forwards;
}
@keyframes camflash { from { opacity: .75; } to { opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .cam-flash { animation-duration: .01s; } }

/* 诊断面板（连点标题 5 次才出来）。小字、深底，不干扰正常拍照 ——
   它的用途是「iPhone 上万一不对劲，截一张图就能定位」。 */
.cam-diag {
  position: absolute; left: 10px; right: 10px; bottom: 196px; z-index: 5;
  max-height: 55%; overflow: auto;
  background: rgba(0, 0, 0, .87); border: 1px solid rgba(255, 255, 255, .22);
  border-radius: 10px; padding: 10px 12px; color: #cfeee7;
}
.cam-diag pre {
  margin: 0 0 8px; font-size: 11px; line-height: 1.55;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  white-space: pre-wrap; word-break: break-all;
}
.cam-diag button {
  border: 0; border-radius: 999px; padding: 6px 12px;
  background: rgba(255, 255, 255, .18); color: #fff; font-size: 12px; cursor: pointer;
}

/* ============================================================================
 * 库存盘点页（admin.js 的 renderStock）
 * ========================================================================== */

/* 进度条：一眼看出「盘了多少了」 */
.stk-bar { height: 8px; border-radius: 999px; background: #e9ecf1; overflow: hidden; margin-top: 8px; }
.stk-bar i { display: block; height: 100%; background: var(--brand); border-radius: 999px; transition: width .25s ease-out; }
.stk-bar i.done { background: #16a34a; }

/* 编号那格：等宽字体 + 略大，扫一眼就能和纸上核对 */
.stk-code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 15px; font-weight: 600; letter-spacing: .02em; }

/* 机型那格：跟编号同一套等宽字体，但小一号、不加粗 —— 它只是辅助核对，
   不能抢了编号这个唯一核对键的视觉重心 */
.stk-model { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; font-weight: 600; letter-spacing: .02em; color: #0f766e; }

/* ---------- 盘点场次（分次盘点，2026-10-08 财务要的） ---------- */
/* 顶部那条：没开场是灰的，开场了转绿底 —— 「正在进行」要一眼看出来，
   否则财务会重复点「开始盘点」，点几次都只算一场（幂等），她会以为坏了 */
.sessbar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 10px 12px; border-radius: 10px;
  background: #f7f8fa; border: 1px solid var(--line);
}
.sessbar.on { background: var(--ok-bg); border-color: #c7e6d2; }
.sessbar > .dot { width: 9px; height: 9px; border-radius: 50%; background: #b6bcc6; flex: none; }
.sessbar.on > .dot { background: var(--ok); box-shadow: 0 0 0 4px rgba(21, 128, 61, .14); }
.sessbar .sessgrow { flex: 1 1 220px; min-width: 0; }

/* 「本场 47 台」—— 这一场最要紧的数字，等宽加粗放大 */
.sessbar .sessnum { font-size: 14px; color: var(--muted); white-space: nowrap; }
.sessbar .sessnum b {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 24px; line-height: 1; color: var(--ok); margin: 0 2px;
}

/* 「结束盘点」之后那个弹窗里的大数字 —— 她点完就要看到这个 */
.sessresult { text-align: center; padding: 8px 0 2px; }
.sessresult .big {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 44px; line-height: 1.05; font-weight: 700; color: var(--ok);
}
.sessresult .big small { font-size: 15px; font-weight: 500; color: var(--muted); margin-left: 4px; }

/* 扫描记录里的每一条 */
.stk-log { max-height: 260px; overflow: auto; }
.stk-logrow {
  display: flex; align-items: flex-start; gap: 8px;
  padding: 7px 0; border-bottom: 1px solid var(--line);
}
.stk-logrow:last-child { border-bottom: 0; }
.stk-logrow .t { flex: 1; min-width: 0; word-break: break-all; }

/* 近似匹配的提示条：不自动勾，给一个「就是这个」的按钮 */
.stk-near { background: var(--warn-bg); border: 1px solid #f0d9a8; border-radius: 10px; padding: 8px 10px; margin-top: 6px; }
