/* ============================================================================
   wx_parse 运营控制台 · 组件与构成
   ----------------------------------------------------------------------------
   构成策略（技能 §5）：
     · 非对称优于居中 —— KPI 栅格按重要性分配宽度 1.35fr / 1fr / 1fr / 0.8fr
     · 重叠 —— 健康度条的汇总数字故意压住卡片的顶边框（.rail-cap）
     · 破格 —— 首张 KPI 卡带四角括号装饰并轻微上浮，逃出栅格对齐线
     · 密度可控 —— 表格行高 40px、发丝分割线，纵密但层级清晰
   ============================================================================ */

/* ==========================================================================
   1 · 骨架
   ========================================================================== */
.shell {
  display: grid;
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
  min-height: 100dvh;
}

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

.content {
  flex: 1;
  width: 100%;
  max-width: var(--maxw);
  margin: 0 auto;
  padding: var(--sp-6) var(--sp-6) var(--sp-7);
}

/* 视图切换：非当前视图整体移除出无障碍树 */
.view[hidden] { display: none; }

.view-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--sp-4);
  flex-wrap: wrap;
  margin-bottom: var(--sp-5);
}

.view-head p {
  color: var(--muted);
  font-size: var(--fs-md);
  max-width: 68ch;
  margin-top: var(--sp-1);
}

.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--faint);
  margin-bottom: var(--sp-1);
}

/* ==========================================================================
   2 · 侧边导航
   ========================================================================== */
.sidebar {
  position: sticky;
  top: 0;
  align-self: start;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  padding: var(--sp-5) var(--sp-3) var(--sp-4);
  background: var(--surface);
  border-right: 1px solid var(--line);
  z-index: 40;
  /* ⚠ 这两个属性是【手机抽屉能不能用】的关键，别删：
     .sidebar 高度锁在 100dvh，而它的内容实测约 643px（brand 51 + 导航 450 + 页脚 70
     + 上下内边距 40 + 段间距 32，9 个导航项各 40px）。只要视口高度小于 643px ——
     矮屏安卓、320×568 的小屏、【任何手机的横屏】、以及地址栏露出把 dvh 顶小的那一刻 ——
     内容就会溢出。而 position: fixed 的元素【自身不会滚动】，溢出部分既不换行也不出滚动条，
     结果是最后两项「WS 调试台」「运行配置」直接落在屏幕外，够不到也滚不到。
     overflow-y: auto → 让抽屉自己滚（宽屏下内容一般装得下，不会出现滚动条，所以不受影响）。
     overscroll-behavior: contain → 滚到抽屉头/尾时不要把滚动链甩给背后那一页。 */
  overflow-y: auto;
  overscroll-behavior: contain;
}

.brand {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: 0 var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--line);
}

.brand-mark {
  width: 34px;
  height: 34px;
  flex: 0 0 34px;
  display: grid;
  place-items: center;
  background: var(--ink);
  color: var(--paper);
  border-radius: var(--r-sm);
  font-family: var(--font-display);
  font-size: var(--fs-sm);
  font-weight: 700;
  letter-spacing: -0.04em;
}

.brand-text strong {
  display: block;
  font-size: var(--fs-md);
  font-weight: 600;
  letter-spacing: -0.01em;
}

.brand-text span {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--faint);
  letter-spacing: 0.04em;
}

.nav { display: flex; flex-direction: column; gap: 2px; }

.nav-label {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--faint);
  padding: var(--sp-3) var(--sp-3) var(--sp-2);
}

.nav-item {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  min-height: 40px;
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-sm);
  color: var(--ink-2);
  font-size: var(--fs-md);
  font-weight: 500;
  text-align: left;
  transition: background-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}

/* 强调色左轨"擦过"——不是简单变亮 */
.nav-item::before {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  width: 2px;
  height: 0;
  background: var(--accent);
  border-radius: var(--r-full);
  transform: translateY(-50%);
  transition: height var(--dur-fast) var(--ease-out);
}

.nav-item:hover { background: var(--surface-2); color: var(--ink); }
.nav-item:hover::before { height: 16px; }

.nav-item[aria-current="page"] {
  background: var(--accent-soft);
  color: var(--accent-ink);
  font-weight: 600;
}
.nav-item[aria-current="page"]::before { height: 20px; }

.nav-item .nav-ico {
  flex: 0 0 16px;
  width: 16px;
  height: 16px;
  color: currentColor;
  opacity: 0.75;
}
.nav-item[aria-current="page"] .nav-ico { opacity: 1; }

.nav-item .nav-count {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--faint);
  background: var(--surface-3);
  border-radius: var(--r-full);
  padding: 1px 7px;
  font-variant-numeric: tabular-nums;
}
.nav-item[aria-current="page"] .nav-count {
  background: var(--surface);
  color: var(--accent-ink);
}

.sidebar-foot {
  margin-top: auto;
  padding: var(--sp-3) var(--sp-3) 0;
  border-top: 1px solid var(--line);
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--faint);
  line-height: 1.9;
}
.sidebar-foot b { color: var(--muted); font-weight: 500; }

/* 移动端抽屉遮罩 */
.scrim {
  position: fixed;
  inset: 0;
  background: var(--overlay);
  z-index: 35;
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur-base) var(--ease-out), visibility var(--dur-base);
}
.scrim.is-open { opacity: 1; visibility: visible; }

/* ==========================================================================
   3 · 顶栏
   ========================================================================== */
.topbar {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: var(--topbar-h);
  padding: var(--sp-2) var(--sp-6);
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  backdrop-filter: saturate(1.4) blur(10px);
  -webkit-backdrop-filter: saturate(1.4) blur(10px);
  border-bottom: 1px solid var(--line);
}

.icon-btn {
  display: grid;
  place-items: center;
  width: 44px;               /* 触摸目标 ≥44px */
  height: 44px;
  border-radius: var(--r-sm);
  color: var(--ink-2);
  transition: background-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.icon-btn:hover { background: var(--surface-3); color: var(--ink); }
.icon-btn svg { width: 18px; height: 18px; }

.menu-btn { display: none; }

.crumb {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-md);
  font-weight: 600;
  letter-spacing: -0.01em;
  min-width: 0;
}
.crumb .sep { color: var(--line-3); font-weight: 400; }
.crumb .muted { color: var(--muted); font-weight: 500; }

.topbar-right {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

/* 实时状态胶囊 */
.live {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  height: 30px;
  padding: 0 var(--sp-3);
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  background: var(--surface);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--muted);
  white-space: nowrap;
}
.live .dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--ok);
  box-shadow: 0 0 0 3px var(--ok-soft);
}
@media (prefers-reduced-motion: no-preference) {
  .live .dot { animation: pulse 2.4s var(--ease-out) infinite; }
}
@keyframes pulse {
  0%, 100% { box-shadow: 0 0 0 3px var(--ok-soft); }
  50%      { box-shadow: 0 0 0 6px transparent; }
}

/* 分段控件（时间范围） */
.seg {
  display: inline-flex;
  padding: 3px;
  gap: 2px;
  background: var(--surface-3);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
}
.seg button {
  min-height: 30px;
  padding: 0 var(--sp-3);
  border-radius: var(--r-xs);
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--muted);
  transition: background-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.seg button:hover { color: var(--ink); }
.seg button[aria-pressed="true"] {
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--sh-1);
  font-weight: 600;
}

/* 主题切换 */
.theme-toggle {
  position: relative;
  display: grid;
  place-items: center;
  width: 44px; height: 44px;
  border-radius: var(--r-sm);
  color: var(--ink-2);
}
.theme-toggle:hover { background: var(--surface-3); }
.theme-toggle svg { width: 18px; height: 18px; }
[data-theme="light"] .theme-toggle .ico-moon { display: block; }
[data-theme="light"] .theme-toggle .ico-sun  { display: none; }
[data-theme="dark"]  .theme-toggle .ico-moon { display: none; }
[data-theme="dark"]  .theme-toggle .ico-sun  { display: block; }

/* ==========================================================================
   4 · 运行健康度条（记忆点）
   7 段对应项目真实子系统；数字徽标故意压住卡片顶边框（重叠手法）
   ========================================================================== */
.rail-card {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-2);
  padding: var(--sp-5);
  overflow: hidden;
}

/* 氛围层：极淡的径向渐变，避免纯色平底（技能 §6） */
.rail-card::before {
  content: "";
  position: absolute;
  inset: -40% -10% auto -10%;
  height: 220px;
  background:
    radial-gradient(38rem 16rem at 18% 0%, color-mix(in srgb, var(--accent) 12%, transparent), transparent 62%),
    radial-gradient(30rem 14rem at 92% 10%, color-mix(in srgb, var(--ok) 10%, transparent), transparent 60%);
  pointer-events: none;
}

.rail-top {
  position: relative;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-5);
  flex-wrap: wrap;
}

.rail-head h2 { font-size: var(--fs-lg); }
.rail-head p {
  color: var(--muted);
  font-size: var(--fs-md);
  margin-top: var(--sp-1);
  max-width: 46ch;
}

/* 汇总徽标：负外边距让它压住卡片内边距线 → 重叠破格 */
.rail-cap {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  margin-top: calc(var(--sp-2) * -1);
  padding: var(--sp-2) var(--sp-4);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-full);
}
.rail-cap b {
  font-family: var(--font-display);
  font-size: var(--fs-2xl);
  font-weight: 700;
  letter-spacing: var(--ls-display);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.rail-cap span { font-size: var(--fs-sm); color: var(--muted); }

.rail {
  position: relative;
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: var(--sp-2);
  margin-top: var(--sp-5);
}

.rail-seg {
  position: relative;
  height: 6px;
  border-radius: var(--r-full);
  background: var(--surface-3);
}
.rail-seg::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--seg-color, var(--line-3));
}
.rail-seg[data-state="ok"]   { --seg-color: var(--ok); }
.rail-seg[data-state="warn"] { --seg-color: var(--warn); }
.rail-seg[data-state="err"]  { --seg-color: var(--err); }
.rail-seg[data-state="info"] { --seg-color: var(--line-3); }

.rail-legend {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: var(--sp-2);
  margin-top: var(--sp-3);
}

.rail-legend div {
  min-width: 0;
  font-size: var(--fs-xs);
  line-height: 1.5;
}
.rail-legend .k {
  display: flex;
  align-items: center;
  gap: 5px;
  color: var(--ink-2);
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.rail-legend .k i {
  flex: 0 0 6px;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--seg-color, var(--line-3));
}
.rail-legend div[data-state="ok"]   { --seg-color: var(--ok); }
.rail-legend div[data-state="warn"] { --seg-color: var(--warn); }
.rail-legend div[data-state="err"]  { --seg-color: var(--err); }
.rail-legend div[data-state="info"] { --seg-color: var(--line-3); }
.rail-legend .v {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--faint);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* 顶栏里的紧凑版健康度条 */
.rail-mini {
  display: flex;
  align-items: center;
  gap: 3px;
  height: 30px;
  padding: 0 var(--sp-3);
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  background: var(--surface);
}
.rail-mini i {
  width: 12px; height: 5px;
  border-radius: var(--r-full);
  background: var(--seg-color, var(--line-3));
}
.rail-mini i[data-state="ok"]   { --seg-color: var(--ok); }
.rail-mini i[data-state="warn"] { --seg-color: var(--warn); }
.rail-mini i[data-state="err"]  { --seg-color: var(--err); }
.rail-mini i[data-state="info"] { --seg-color: var(--line-3); }

/* ==========================================================================
   5 · 面板
   ========================================================================== */
.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-1);
  overflow: hidden;
}

.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  flex-wrap: wrap;
  padding: var(--sp-4) var(--sp-5);
  border-bottom: 1px solid var(--line);
  background: var(--surface-2);
}

.panel-head h2 {
  font-size: var(--fs-base);
  letter-spacing: -0.01em;
}
.panel-head .hint {
  font-size: var(--fs-sm);
  color: var(--muted);
  margin-top: 2px;
}
.panel-body { padding: var(--sp-5); }
.panel-body.flush { padding: 0; }

/* ==========================================================================
   6 · KPI 卡（非对称栅格 · 首卡破格）
   ========================================================================== */
.kpi-grid {
  display: grid;
  grid-template-columns: 1.35fr 1fr 1fr 0.82fr;
  gap: var(--sp-4);
  margin-top: var(--sp-4);
}

.kpi {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-1);
  padding: var(--sp-5);
  transition: transform var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.kpi:hover {
  transform: translateY(-2px);
  box-shadow: var(--sh-3);
  border-color: var(--line-2);
}

/* 破格：首卡带四角括号并轻微上浮，逃出栅格对齐线 */
.kpi.hero {
  margin-top: calc(var(--sp-3) * -1);
  padding: var(--sp-6) var(--sp-5) var(--sp-5);
  background: linear-gradient(180deg,
              color-mix(in srgb, var(--accent) 5%, var(--surface)) 0%,
              var(--surface) 62%);
}
.kpi.hero::before,
.kpi.hero::after {
  content: "";
  position: absolute;
  width: 14px; height: 14px;
  border: 2px solid var(--accent);
  opacity: 0.55;
}
.kpi.hero::before { top: 9px; left: 9px; border-right: 0; border-bottom: 0; border-radius: 5px 0 0 0; }
.kpi.hero::after  { bottom: 9px; right: 9px; border-left: 0; border-top: 0; border-radius: 0 0 5px 0; }

.kpi-label {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--muted);
}
.kpi-label .tag {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--faint);
  border: 1px solid var(--line);
  border-radius: var(--r-xs);
  padding: 0 5px;
}

.kpi-value {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  margin-top: var(--sp-3);
  font-family: var(--font-display);
  font-size: var(--fs-3xl);
  font-weight: 700;
  letter-spacing: var(--ls-display);
  line-height: var(--lh-tight);
  font-variant-numeric: tabular-nums;
}
.kpi.hero .kpi-value { font-size: var(--fs-4xl); }
.kpi-value .unit { font-size: var(--fs-base); font-weight: 500; color: var(--muted); letter-spacing: 0; }

.kpi-foot {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-top: var(--sp-3);
  font-size: var(--fs-xs);
  color: var(--faint);
  flex-wrap: wrap;
}
.kpi-foot .delta { display: inline-flex; align-items: center; gap: 3px; font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.kpi-foot .up   { color: var(--err); }   /* 国内习惯：涨=红 */
.kpi-foot .down { color: var(--ok);  }

/* 迷你火花线 */
.spark { display: flex; align-items: flex-end; gap: 2px; height: 26px; margin-top: var(--sp-3); }
.spark i {
  flex: 1;
  border-radius: 1px;
  background: var(--accent);
  opacity: 0.35;
  min-height: 2px;
}
.spark i:last-child { opacity: 1; }

/* ==========================================================================
   7 · 条形列表（接口热度 / 级别分布）
   ========================================================================== */
.bar-list { display: flex; flex-direction: column; gap: var(--sp-3); }

.bar-row { display: grid; grid-template-columns: 1fr auto; gap: var(--sp-3) var(--sp-4); align-items: center; }

.bar-row .name {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--ink-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.bar-row .name em { font-style: normal; color: var(--faint); font-size: var(--fs-2xs); margin-left: var(--sp-2); }

.bar-row .val {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

.bar-track {
  grid-column: 1 / -1;
  height: 6px;
  background: var(--surface-3);
  border-radius: var(--r-full);
  overflow: hidden;
}
.bar-fill {
  height: 100%;
  border-radius: inherit;
  background: var(--bar-color, var(--accent));
  width: var(--bar-w, 0%);
}

/* ==========================================================================
   8 · 数据表
   ========================================================================== */
.table-wrap { overflow-x: auto; }

table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-md);
}

thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface-2);
  text-align: left;
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}

tbody td {
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--line);
  color: var(--ink-2);
  vertical-align: middle;
}
tbody tr { height: 40px; }
tbody tr:last-child td { border-bottom: 0; }
tbody tr { transition: background-color var(--dur-fast) var(--ease-out); }
tbody tr:hover { background: var(--surface-2); }

td.mono, th.mono { font-family: var(--font-mono); font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }
td.num { text-align: right; font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
td .sub { display: block; font-size: var(--fs-xs); color: var(--faint); margin-top: 2px; }
td .trunc { display: block; max-width: 26ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.cell-strong { color: var(--ink); font-weight: 500; }

/* ==========================================================================
   9 · 徽标 / 胶囊
   ========================================================================== */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px var(--sp-2);
  border-radius: var(--r-full);
  border: 1px solid transparent;
  font-size: var(--fs-xs);
  font-weight: 600;
  white-space: nowrap;
  line-height: 1.7;
}
.badge::before {
  content: "";
  width: 5px; height: 5px;
  border-radius: 50%;
  background: currentColor;
}
.badge.ok   { color: var(--ok);   background: var(--ok-soft);   border-color: var(--ok-line); }
.badge.warn { color: var(--warn); background: var(--warn-soft); border-color: var(--warn-line); }
.badge.err  { color: var(--err);  background: var(--err-soft);  border-color: var(--err-line); }
.badge.info { color: var(--info); background: var(--info-soft); border-color: var(--info-line); }
.badge.acc  { color: var(--accent-ink); background: var(--accent-soft); border-color: var(--accent-line); }
/* 还没有文案的徽标不要显示成「一个小圆点 + 一圈内边距」的空胶囊。
   组头徽标（data-cfg-group-badge）是先占位、由 refreshGroupHead() 后填的，
   在配置没读回来（空态）时会保持空 —— 那时它不该出现在标题栏里。 */
.badge:empty { display: none; }

/* 日志级别徽标（无圆点，用等宽大写，和状态徽标区分开） */
.lvl {
  display: inline-block;
  min-width: 46px;
  text-align: center;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 2px 6px;
  border-radius: var(--r-xs);
  border: 1px solid transparent;
}
.lvl.debug { color: var(--faint);  background: var(--surface-3);  border-color: var(--line); }
.lvl.log   { color: var(--info);   background: var(--info-soft);  border-color: var(--info-line); }
.lvl.info  { color: var(--accent-ink); background: var(--accent-soft); border-color: var(--accent-line); }
.lvl.warn  { color: var(--warn);   background: var(--warn-soft);  border-color: var(--warn-line); }
.lvl.error { color: var(--err);    background: var(--err-soft);   border-color: var(--err-line); }

/* 平台徽标（对应 parseLog 的 PLATFORM_RULES.cls） */
.plat {
  display: inline-flex;
  align-items: center;
  padding: 2px var(--sp-2);
  border-radius: var(--r-xs);
  font-size: var(--fs-xs);
  font-weight: 600;
  color: #fff;
  background: var(--pf-other);
  white-space: nowrap;
}
.plat.douyin   { background: var(--pf-douyin); }
.plat.kuaishou { background: var(--pf-kuaishou); }
.plat.xhs      { background: var(--pf-xhs); }
.plat.bili     { background: var(--pf-bili); }
[data-theme="dark"] .plat { color: #17161a; }

/* 过滤 chips */
.chips { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: 32px;
  padding: 0 var(--sp-3);
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  background: var(--surface);
  color: var(--muted);
  font-size: var(--fs-sm);
  transition: all var(--dur-fast) var(--ease-out);
}
.chip:hover { border-color: var(--line-3); color: var(--ink); }
.chip[aria-pressed="true"] {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
  font-weight: 600;
}
.chip .n {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  font-variant-numeric: tabular-nums;
  opacity: 0.75;
}

/* ==========================================================================
   10 · 规格网格（设备信息）
   ========================================================================== */
.spec-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  overflow: hidden;
}
.spec {
  background: var(--surface);
  padding: var(--sp-3) var(--sp-4);
}
.spec dt {
  font-size: var(--fs-xs);
  color: var(--faint);
  letter-spacing: 0.02em;
}
.spec dd {
  margin: 3px 0 0;
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  word-break: break-all;
}
.spec dd.plain { font-family: var(--font-body); font-size: var(--fs-md); }

/* 统一入口三出口示意图 */
.flow { display: grid; grid-template-columns: 1fr auto 1.4fr; gap: var(--sp-4); align-items: center; }
.flow-col { display: flex; flex-direction: column; gap: var(--sp-2); }
.flow-box {
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  padding: var(--sp-2) var(--sp-3);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--ink-2);
}
.flow-box.accent { border-color: var(--accent-line); background: var(--accent-soft); color: var(--accent-ink); font-weight: 600; }
.flow-arrow { color: var(--line-3); font-family: var(--font-mono); font-size: var(--fs-lg); text-align: center; }

/* ==========================================================================
   11 · 权限矩阵
   ========================================================================== */
.matrix td.perm { text-align: center; }
.mark {
  display: inline-grid;
  place-items: center;
  width: 20px; height: 20px;
  border-radius: var(--r-xs);
  font-size: var(--fs-sm);
  font-weight: 700;
  line-height: 1;
}
.mark.yes { background: var(--ok-soft); color: var(--ok); border: 1px solid var(--ok-line); }
.mark.no  { background: transparent; color: var(--line-3); border: 1px dashed var(--line); }

/* ==========================================================================
   11.1 · 权限矩阵可编辑态 / 权限配置表单（2026-10-10）
   --------------------------------------------------------------------------
   矩阵还是那张表，只是三处变成了控件：行首的「标题 + 路径」是输入框、
   格子是单选按钮、行尾有删除；表格下方多一个「添加页面」表单。
   改的都是草稿，点「保存权限配置」才提交（POST /config）。
   ========================================================================== */

/* 行内输入框：默认透明无边框，看起来仍是纯文本；悬停 / 聚焦才显出输入框。
   为什么不做成常规表单样式：一屏十来个输入框会把矩阵读成一片栅格，
   反而看不出「页面 × 权限位」的对应关系。 */
.perm-in {
  display: block;
  width: 100%;
  min-height: 30px;
  padding: 2px var(--sp-2);
  box-sizing: border-box;
  border: 1px solid transparent;
  border-radius: var(--r-xs);
  background: transparent;
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--fs-sm);
}
.perm-in:hover { border-color: var(--line-2); background: var(--surface-2); }
.perm-in:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: transparent;
  background: var(--surface-2);
}
.perm-in-path {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--faint);
}
.perm-in-path:hover, .perm-in-path:focus-visible { color: var(--ink-2); }
.perm-in::placeholder { color: var(--faint); opacity: 0.7; }

/* 块级表单控件（权限位中文名 / 弹窗文案 / 添加页面）共用一套外观 */
.perm-in-block {
  display: block;
  width: 100%;
  min-height: 36px;
  padding: 0 var(--sp-3);
  box-sizing: border-box;
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
}
textarea.perm-in-block {
  min-height: 76px;
  padding: var(--sp-2) var(--sp-3);
  line-height: 1.6;
  resize: vertical;
  white-space: pre-wrap;
}
select.perm-in-block { font-family: var(--font-body); cursor: pointer; }
.perm-in-block:hover { border-color: var(--line-3); }
.perm-in-block:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: transparent;
}
.perm-in-block::placeholder { color: var(--faint); opacity: 0.75; }

/* 矩阵格子 = 按钮。视觉沿用原来的 .mark（✓ / ·），但本体是 button：
   可点、可 Tab、aria-pressed 反映它是不是本行选中的权限位。 */
.perm-pick {
  display: inline-grid;
  place-items: center;
  width: 22px; height: 22px;
  padding: 0;
  border: 1px dashed var(--line);
  border-radius: var(--r-xs);
  background: transparent;
  color: var(--line-3);
  font-size: var(--fs-sm);
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.perm-pick:hover { border-color: var(--accent); color: var(--accent); }
/* 已选中：和未选中拉开差距，并且不再是「可点」的手感（点它等于原地不动） */
.perm-pick.is-on {
  border: 1px solid var(--ok-line);
  background: var(--ok-soft);
  color: var(--ok);
  cursor: default;
}
.perm-pick.is-on:hover { border-color: var(--ok-line); color: var(--ok); }
.perm-pick:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ⚠ 必须写 .btn.perm-del（特异性 0,2,0）：只写 .perm-del 的话，同特异性、写在后面的
   .btn 会把这几个紧凑尺寸反向覆盖回 36px / 常规内边距。 */
.btn.perm-del { min-width: auto; min-height: 28px; padding: 0 var(--sp-3); font-size: var(--fs-xs); }

/* 第一列要给「标题 + 路径」两行输入留宽度，否则表格会把标题挤成省略号 */
.matrix .perm-page-cell { min-width: 26ch; }

/* 添加页面表单 */
.perm-add {
  margin-top: var(--sp-4);
  padding-top: var(--sp-4);
  border-top: 1px dashed var(--line);
}
.perm-add-grid { display: flex; align-items: flex-end; gap: var(--sp-3); flex-wrap: wrap; }
.perm-add-f { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.perm-add-f > span { font-size: var(--fs-xs); color: var(--faint); }
.perm-add-path  { width: 34ch; }
.perm-add-title { width: 20ch; }
.perm-add-bit   { width: 22ch; }
.perm-add .cfg-meta { margin-top: var(--sp-2); }

/* 权限位中文名网格 */
.perm-lbl-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: var(--sp-4);
}
.perm-lbl-head { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: 5px; min-width: 0; }
.perm-lbl-head code {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--ink-2);
  overflow: hidden;
  text-overflow: ellipsis;
}
.perm-lbl-head .badge { flex: 0 0 auto; font-size: 10px; padding: 0 6px; }
/* 未映射到任何页面的权限位：降一点存在感，但仍可编辑
   —— 小程序「权限管理」页要显示它们，不给改就等于这道需求漏了一半。 */
.perm-lbl.is-extra .perm-lbl-head code { color: var(--faint); }

/* 弹窗文案字段 */
.perm-text-f { display: flex; flex-direction: column; gap: 5px; }
.perm-text-f > span { font-size: var(--fs-xs); color: var(--faint); letter-spacing: 0.02em; }

@media (max-width: 860px) {
  .perm-add-f { width: 100%; }
  .perm-add-path, .perm-add-title, .perm-add-bit { width: 100%; }
}

/* ==========================================================================
   12 · 登录链路时间线
   ========================================================================== */
.timeline { position: relative; padding-left: var(--sp-5); }
.timeline::before {
  content: "";
  position: absolute;
  left: 5px; top: 6px; bottom: 6px;
  width: 1px;
  background: linear-gradient(180deg, var(--line-2), var(--line) 70%, transparent);
}
.tl-item { position: relative; padding-bottom: var(--sp-4); }
.tl-item:last-child { padding-bottom: 0; }
.tl-item::before {
  content: "";
  position: absolute;
  left: calc(var(--sp-5) * -1);
  top: 5px;
  width: 11px; height: 11px;
  border-radius: 50%;
  background: var(--surface);
  border: 2px solid var(--line-3);
}
.tl-item[data-state="ok"]::before   { border-color: var(--ok); }
.tl-item[data-state="err"]::before  { border-color: var(--err); background: var(--err); }
.tl-item[data-state="acc"]::before  { border-color: var(--accent); background: var(--accent); }

.tl-head { display: flex; align-items: baseline; gap: var(--sp-3); flex-wrap: wrap; }
.tl-head strong { font-size: var(--fs-md); font-weight: 600; color: var(--ink); }
.tl-head time {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--faint);
  font-variant-numeric: tabular-nums;
}
.tl-meta {
  margin-top: 3px;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--muted);
  word-break: break-all;
  line-height: 1.7;
}
.tl-meta b { color: var(--ink-2); font-weight: 500; }

/* ==========================================================================
   13 · 配置项 / 开关
   ========================================================================== */
.cfg-list { display: flex; flex-direction: column; }
/* 【2026-10-10】这一节原本还有三条规则：.cfg（横排一行 + 虚线下边）、.cfg:last-child、
   .cfg-val（只读值的灰底小标签）、以及一组 .switch。它们全部只服务于日志页那份
   写死的「上报配置」演示列表（main.js 的 LOG_CFG / renderLogCfg()）。
   那份演示列表已被注册表驱动的真表单取代（config-page.js 的 logCfgList 宿主，
   渲染的是 .cfg-item 结构），`.cfg` / `.cfg-val` / `.switch` 全站再无任何产物，
   留着只会让下一个人以为页面上真有一套「横排配置行」。故整组删除。
   ⚠ .cfg-info 保留：它的 strong/code 两条规则同时被 .cfg-item 使用。 */
.cfg-info strong { display: block; font-size: var(--fs-md); font-weight: 500; color: var(--ink); }
.cfg-info code { font-size: var(--fs-xs); background: none; border: 0; padding: 0; color: var(--faint); }

/* ==========================================================================
   14 · 空态 / 提示条
   ========================================================================== */
.note {
  display: flex;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  font-size: var(--fs-sm);
  color: var(--muted);
  line-height: 1.7;
}
.note strong { color: var(--ink-2); }
.note.warn { border-left-color: var(--warn); }

.empty {
  display: grid;
  place-items: center;
  gap: var(--sp-2);
  padding: var(--sp-8) var(--sp-5);
  text-align: center;
  color: var(--faint);
}
.empty svg { width: 34px; height: 34px; opacity: 0.5; }
.empty strong { display: block; color: var(--muted); font-size: var(--fs-base); font-weight: 500; }
.empty p { font-size: var(--fs-sm); max-width: 40ch; }

/* ==========================================================================
   15 · 页脚
   ========================================================================== */
.footer {
  border-top: 1px solid var(--line);
  padding: var(--sp-4) var(--sp-6);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--faint);
  display: flex;
  justify-content: space-between;
  gap: var(--sp-4);
  flex-wrap: wrap;
}

/* 通用布局工具 */
.grid-2 { display: grid; grid-template-columns: 1.15fr 0.85fr; gap: var(--sp-4); }
.grid-3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-4); }
.stack   { display: flex; flex-direction: column; gap: var(--sp-4); }
.row     { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.mt-4 { margin-top: var(--sp-4); }
.mt-5 { margin-top: var(--sp-5); }

/* ==========================================================================
   16 · 响应式（375 / 768 / 1440 三档明确答案，不是"自动堆叠"）
   ========================================================================== */

/* 桌面中档：KPI 变 2×2，主体两栏收窄 */
@media (max-width: 1280px) {
  .kpi-grid { grid-template-columns: 1fr 1fr; }
  .kpi.hero { margin-top: 0; }
  .grid-2 { grid-template-columns: 1fr; }
}

/* 平板：侧栏抽屉化，栅格单列 */
@media (max-width: 1023px) {
  .shell { grid-template-columns: minmax(0, 1fr); }

  .sidebar {
    position: fixed;
    inset: 0 auto 0 0;
    width: min(84vw, var(--sidebar-w));
    transform: translateX(-102%);
    transition: transform var(--dur-base) var(--ease-out);
    box-shadow: var(--sh-4);
  }
  .sidebar.is-open { transform: none; }

  .menu-btn { display: grid; }
  .topbar { padding-left: var(--sp-3); padding-right: var(--sp-4); }
  .content { padding: var(--sp-5) var(--sp-4) var(--sp-6); }
  .grid-3 { grid-template-columns: 1fr; }
  .rail-legend { grid-template-columns: repeat(4, minmax(0, 1fr)); row-gap: var(--sp-3); }
}

/* 手机：单列、KPI 全宽、表格横向滚动 */
@media (max-width: 640px) {
  :root { --fs-3xl: 30px; --fs-4xl: 36px; }

  .content { padding: var(--sp-4) var(--sp-3) var(--sp-6); }
  .kpi-grid { grid-template-columns: 1fr; }
  .kpi.hero { padding: var(--sp-5); }
  .rail { grid-template-columns: repeat(4, minmax(0, 1fr)); row-gap: var(--sp-2); }
  .rail-legend { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .rail-legend .k { white-space: normal; }
  .flow { grid-template-columns: 1fr; }
  .flow-arrow { transform: rotate(90deg); }
  .panel-head { padding: var(--sp-3) var(--sp-4); }
  .panel-body { padding: var(--sp-4); }
  .crumb .muted { display: none; }
  .seg { display: none; }          /* 手机上时间范围收进筛选区 */
  .view-head { align-items: flex-start; flex-direction: column; gap: var(--sp-3); }
  thead th, tbody td { padding: var(--sp-2) var(--sp-3); }

  /* ------------------------------------------------------------------
     顶栏：手机上必须收窄 —— 否则【整页横向溢出】，右侧控件被推出屏幕。
     ⚠ 这不是"再挤一挤"的美化，而是一条能算出来的溢出：.topbar 是【不换行】的
        flex 行，除 .crumb（有 min-width: 0，可收缩到 0）外，每个子项都是固定宽：
          内边距 28 + 菜单 44 + 间距 12 + rail-mini 126 + live 96
          + 刷新 44 + 退出 44 + 主题 44 + 间距 ≈ 482px
        而最宽的手机视口也只有 430px（iPhone Pro Max 一类）。多出来的部分会撑宽
        <body>，把右侧的刷新 / 退出 / 主题三个按钮推到屏幕外 —— 这就是"手机上显示不全"。
        处理原则：先砍【冗余】的，绝不动触摸目标（44px 是 .icon-btn 注释里写明的底线）。
     ------------------------------------------------------------------ */
  .topbar {
    gap: var(--sp-2);
    padding-left: var(--sp-2);
    padding-right: var(--sp-3);
    overflow-x: auto;
    /* 兜底：将来若又往顶栏塞了东西、或视口窄到 320px，overflow-x 保证控件还能划到
       （而不是被裁掉看不见），同时【不会】把文档撑宽。隐藏滚动条免得占掉一行高度。 */
    scrollbar-width: none;
  }
  .topbar::-webkit-scrollbar { height: 0; }
  .topbar-right { gap: 6px; }
  /* rail-mini 是 7 段健康度条，概览页顶部已有一模一样的完整版（还带图例）。
     手机上它独占 126px 却只起"把同一件事再说一遍"的作用，是这张表里最该让位的一项。 */
  .rail-mini { display: none; }
  /* live 胶囊左右各收到 8px：文案「已连接后端 / 演示数据」一个字都不能省 ——
     它是判断"现在看到的是真数据还是演示数据"的唯一入口，比那 7 段条重要得多。 */
  .live { padding: 0 var(--sp-2); }

  /* 日志流：内容列在手机上收窄，否则表格要横向划很远才能看到「时间」和「详情」两列 */
  .log-body .trunc { max-width: 26ch; }
}

/* ==========================================================================
   17 · 登录门（服务端真实校验的体验层入口）
   ========================================================================== */
.gate {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
  padding: var(--sp-4);
}
.gate[hidden] { display: none; }

.gate-backdrop {
  position: absolute;
  inset: 0;
  background: var(--overlay);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

.gate-card {
  position: relative;
  width: min(400px, 100%);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-4);
  padding: var(--sp-6);
}

.gate-field { display: block; margin-bottom: var(--sp-4); }
.gate-field span {
  display: block;
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--muted);
  margin-bottom: var(--sp-1);
}
.gate-field input {
  width: 100%;
  min-height: 44px;
  padding: 0 var(--sp-3);
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: var(--ink);
  font-size: var(--fs-md);
  transition: border-color var(--dur-fast) var(--ease-out);
}
.gate-field input:hover { border-color: var(--line-3); }
.gate-field input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent-line);
}

.gate-err {
  color: var(--err);
  background: var(--err-soft);
  border: 1px solid var(--err-line);
  border-radius: var(--r-sm);
  padding: var(--sp-2) var(--sp-3);
  font-size: var(--fs-sm);
  margin-bottom: var(--sp-3);
}

.gate-submit {
  width: 100%;
  min-height: 44px;
  border-radius: var(--r-sm);
  background: var(--accent);
  /* 与 .btn.primary 同源：实底上的字用 --accent-on（浅色主题=白 / 深色主题=近黑）。
     原来硬编码 #fff —— 深色主题下 --accent 是亮蓝紫，白字对比度只有 ~2:1。 */
  color: var(--accent-on);
  font-weight: 600;
  font-size: var(--fs-base);
  transition: background-color var(--dur-fast) var(--ease-out), opacity var(--dur-fast);
}
.gate-submit:hover:not(:disabled) { background: color-mix(in srgb, var(--accent) 85%, var(--ink)); }
.gate-submit:disabled { opacity: 0.6; cursor: default; }

.gate-demo {
  width: 100%;
  min-height: 44px;
  margin-top: var(--sp-2);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  color: var(--muted);
  font-size: var(--fs-sm);
  transition: color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
}
.gate-demo:hover { color: var(--ink); border-color: var(--line-3); }

.mode-banner { margin-bottom: var(--sp-4); }

/* ==========================================================================
   18.1 · 日志流工具条 / 在线用户权限表（main.js）
   ========================================================================== */
/* 级别 chips + 用户下拉：右对齐，窄屏折行 */
.log-tools {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
  justify-content: flex-end;
}
/* 工具条里的普通按钮跟 .pick select（30px）对齐 —— 默认 .btn 是 36px，
   并排放在标题栏里会一高一低。与 .cfg-group-save 同一套紧凑尺寸。 */
.log-tools .btn {
  min-height: 30px;
  padding: 0 var(--sp-3);
  font-size: var(--fs-xs);
}

.pick {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-xs);
  color: var(--ink-3);
}
.pick select {
  min-height: 30px;
  max-width: 34ch;
  padding: 0 var(--sp-2);
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: var(--ink);
  font-size: var(--fs-xs);
  font-family: var(--font-mono);
}
.pick select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: transparent; }

/* 日志来源说明（全局 / 按用户） */
.log-src { padding: var(--sp-3) var(--sp-4) 0; font-family: var(--font-mono); }

/* 解析记录 / 日志流：行末「详情」按钮 + 行下方展开区。
   为什么是行内展开而不是弹窗：这两页的用途都是【逐条核对】，时间顺序与上下条对比就是信息本身 ——
   弹窗一盖全看不见了；展开藏在该行下方能保住它在列表里的位置。

   ⚠ 这里的 .log-* 与 .parse-* 【共用一组声明】，而第 9 节（WS 台）、第 10 节（扫码）
     刻意各写一套不共用 —— 判据是【调参是否真的相同】：
       · 解析记录 / 日志流：都是「逐行原始日志」，行宽、字号、折行方式完全一致 → 共用，避免抄三遍；
       · WS 台：每帧 JSON，摘要 64ch、原文不折行（要保持 JSON 缩进）→ 调参不同，不共用；
       · 扫码：内容是任意文本，列更窄、原文折行 → 调参不同，不共用。
     以后若某一边要单独调，从这里拆出去即可，别在原处改到另一边。 */
.parse-more-cell, .log-more-cell { width: 1%; text-align: right; white-space: nowrap; }
.btn.parse-more, .btn.log-more { min-height: 26px; padding: 0 var(--sp-3); font-size: var(--fs-xs); min-width: auto; }
tbody tr.is-open > .parse-more-cell .btn.parse-more,
tbody tr.is-open > .log-more-cell .btn.log-more { border-color: var(--accent); color: var(--accent-ink); }

/* ⚠ 必须放开高度：全局 tbody tr 是固定 40px（见第 8 节），
   详情行内容有好几百 px，不覆盖会被压成一条缝、内容直接溢出去。 */
.parse-detail-row, .log-detail-row { height: auto; }
.parse-detail-row > td,
.log-detail-row > td {
  background: var(--surface-2);
  padding: var(--sp-4) var(--sp-5);
  white-space: normal;
}
.parse-detail-row .spec-grid, .log-detail-row .spec-grid { margin-bottom: var(--sp-4); }

.parse-raw-head, .log-raw-head { font-size: var(--fs-xs); color: var(--faint); margin-bottom: var(--sp-2); }
.parse-raw pre, .log-raw pre {
  margin: 0;
  padding: var(--sp-3) var(--sp-4);
  max-height: 320px;
  overflow: auto;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--ink);
  /* 日志行里有很长的 URL / JSON，必须换行而不是撑出横向滚动条：
     pre-wrap 负责按容器换行，break-all 负责把没有空格的长串也掰断。 */
  white-space: pre-wrap;
  word-break: break-all;
}
.parse-raw .ln, .log-raw .ln { display: block; }
.parse-raw .ln + .ln,
.log-raw .ln + .ln { margin-top: var(--sp-2); padding-top: var(--sp-2); border-top: 1px dashed var(--line); }
.parse-raw-none, .log-raw-none { font-size: var(--fs-sm); color: var(--faint); }
/* 详情里的「复制原文」按钮：与解析页的 .parse-raw-actions 同一位置、同一用意 */
.log-raw-actions { margin-top: var(--sp-3); }

/* 日志流「内容」列。原先这个宽度是写在 JS 里的内联 style="max-width:44ch"，
   挪到这里是为了能按断点收窄（内联样式覆盖不了媒体查询，手机上只能一直 44ch，
   把表格撑得必须横向滚动才能看到时间与详情列）。 */
.log-body .trunc { max-width: 44ch; }
/* 多行日志在列表里被拼成一行，这个角标说明「这条还有下文，点详情看全文」。
   不加的话折叠是不可见的 —— 使用者会以为日志本来就这么短。 */
.log-lines {
  margin-left: var(--sp-2);
  padding: 1px 5px;
  border: 1px solid var(--line-2);
  border-radius: var(--r-xs);
  background: var(--surface-2);
  color: var(--faint);
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  white-space: nowrap;
  cursor: help;
}

/* 在线用户 × 权限位：可写表格 */
.perm-users { min-width: 720px; }
.perm-users th,
.perm-users td { white-space: nowrap; vertical-align: middle; }
.perm-users thead th { font-size: var(--fs-xs); }
.perm-users tr.is-dirty { background: var(--warn-soft); }
.perm-users tr.is-offline { opacity: 0.62; }
.perm-users .u-openid { font-family: var(--font-mono); font-size: var(--fs-xs); }
.perm-users .u-nick {
  display: inline-block;
  max-width: 14ch;
  overflow: hidden;
  text-overflow: ellipsis;
  vertical-align: middle;
}
.perm-users input[type="checkbox"] {
  inline-size: 16px;
  block-size: 16px;
  accent-color: var(--accent);
  cursor: pointer;
}
.online-dot {
  display: inline-block;
  inline-size: 8px;
  block-size: 8px;
  margin-right: 6px;
  border-radius: 50%;
  background: var(--err);
  vertical-align: middle;
}
.online-dot.off { background: var(--line-3); }
.perm-actions { display: flex; gap: var(--sp-2); }
.perm-actions .btn { min-height: 28px; padding: 0 var(--sp-3); font-size: var(--fs-xs); }

/* ==========================================================================
   18 · 运行配置页（config-page.js）
   只服务 data-view="config"：分组卡片 + 逐项控件 + 脏值工具条
   ========================================================================== */
.cfg-toolbar {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
}

/* 分组方式切换（按领域 / 按事件）。放在工具条最左侧，用 is-on 表示当前视图，
   与 .btn 的 primary 区分开 —— 它不是「动作」而是「视图开关」，不该抢保存按钮的视觉权重。 */
.cfg-modes {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  background: var(--surface-2);
}

.cfg-mode {
  min-height: 30px;
  padding: 0 var(--sp-3);
  border: 0;
  background: transparent;
  color: var(--ink-3);
  font-size: var(--fs-sm);
}

.cfg-mode.is-on {
  background: var(--surface-3);
  color: var(--ink-1);
}

/* 分组卡片标题右侧：状态徽标 + 「保存本组」。
   为什么要按组保存：按领域分组有 16 组、按事件分组也有 16 组，
   一次只调一个模块时不必连带提交别处尚未填完的编辑 —— 组内提交、组内生效，误改面更小。 */
.cfg-group-tools {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
}
/* ⚠ 必须写成 .btn.cfg-group-save（特异性 0,2,0）而不是 .cfg-group-save（0,1,0）：
   .btn 是同特异性且写在后面的一条规则，会把这几个紧凑尺寸反向覆盖回 36px/常规内边距。 */
.btn.cfg-group-save {
  min-height: 30px;
  padding: 0 var(--sp-3);
  font-size: var(--fs-xs);
  min-width: auto;
}
/* 组内有未保存改动时给标题栏一点提示色（徽标已写「待保存」，这里只是加一条左侧竖线） */
.cfg-group.has-dirty > .panel-head { box-shadow: inset 3px 0 0 var(--warn); }

.btn {
  min-height: 36px;
  padding: 0 var(--sp-4);
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  color: var(--ink-2);
  font-size: var(--fs-sm);
  background: var(--surface-2);
  transition: border-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out), background-color var(--dur-fast) var(--ease-out);
}
.btn:hover:not(:disabled) { border-color: var(--line-3); color: var(--ink); }
.btn:disabled { opacity: 0.45; cursor: default; }
/* 主按钮（保存）：实底 + --accent-on 文字。
   ⚠ 这里原本是 `color: var(--accent-ink)` —— accent-ink 是「深一档的蓝」，
     铺在 --accent 实底上就是蓝字压蓝底（对比度≈1.4:1，看起来发闷、像样式坏了）。
   ⚠ 悬停原本写 `background: var(--accent-ink)` —— 背景变深蓝、文字还是那个深蓝，
     两者同色 → 字直接“消失”。现在改成「朝主题方向再压一档」，文字与背景始终分离。 */
.btn.primary { background: var(--accent); border-color: transparent; color: var(--accent-on); font-weight: 600; }
.btn.primary:hover:not(:disabled) { background: color-mix(in srgb, var(--accent) 85%, var(--ink)); }
.btn.primary:active:not(:disabled) { background: color-mix(in srgb, var(--accent) 72%, var(--ink)); }
/* 禁用态：偏不透明度的灰蓝会让人以为“按钮坏了”。改成明确的中性灰 → 一眼看出是「还不能点」 */
.btn.primary:disabled {
  opacity: 1;
  background: var(--surface-3);
  border-color: var(--line-2);
  color: var(--faint);
}
.btn.danger { color: var(--err); border-color: var(--err-line); background: var(--err-soft); }
.btn.danger:hover:not(:disabled) { border-color: var(--err); }

.cfg-dirty {
  font-size: var(--fs-sm);
  color: var(--warn);
  font-variant-numeric: tabular-nums;
}
/* 日志与链路页的脏提示是 .panel-body 里独立的一行（运行配置页那个在 .cfg-toolbar 里，
   本来就有 flex 布局），要显式撑成块并留出下间距，否则会跟下面的列表挤在一行。 */
.panel-body > .cfg-dirty { display: block; margin-bottom: var(--sp-3); }

/* 分组卡片 */
.cfg-group { padding: var(--sp-5); }
.cfg-group > .panel-head { margin-bottom: var(--sp-3); }
/* 日志与链路页那块「上报配置」面板的骨架写在 index.html 里（.panel + .panel-head +
   .panel-body，三者各自已经带内边距和标题栏分隔线）。它同时是 .cfg-group
   —— 为了让 refreshGroupHead() 能复用组头逻辑 —— 但不能再吃上面那两条：
   叠加 padding 会双倍留白、给标题栏加 margin 会把分隔线和内容撑开一道缝。
   ⚠ 与 .cfg-group 同为 0,1,0，靠书写顺序取胜，所以必须写在这两条之后。 */
.cfg-group.cfg-group-shell { padding: 0; }
.cfg-group.cfg-group-shell > .panel-head { margin-bottom: 0; }

.cfg-item {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  gap: var(--sp-4);
  padding: var(--sp-4) 0;
  border-bottom: 1px dashed var(--line);
}
.cfg-item:last-child { border-bottom: 0; }
.cfg-item.is-dirty { background: var(--warn-soft); border-radius: var(--r-sm); }

.cfg-meta {
  display: block;
  margin-top: 2px;
  font-size: var(--fs-xs);
  color: var(--faint);
  font-family: var(--font-mono);
  word-break: break-all;
}

.cfg-ctrl {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}
.cfg-ctrl input[type="text"],
.cfg-ctrl input[type="number"],
.cfg-ctrl input[type="password"],
.cfg-ctrl select,
.cfg-ctrl textarea {
  min-height: 36px;
  min-width: 240px;
  max-width: 420px;
  padding: 0 var(--sp-3);
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: var(--ink);
  font-size: var(--fs-sm);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}
.cfg-ctrl textarea { min-height: 84px; padding: var(--sp-2) var(--sp-3); line-height: 1.6; resize: vertical; }
.cfg-ctrl input:focus-visible,
.cfg-ctrl select:focus-visible,
.cfg-ctrl textarea:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: transparent;
}
.cfg-ctrl input[type="number"] { min-width: 120px; }
.cfg-ctrl .btn { min-width: 92px; }

.cfg-badge { margin-left: var(--sp-2); vertical-align: middle; }

/* 布尔开关：刻意不复用 .switch（那个类是已删除的「假开关」演示样式）。
   两边各翻一次 aria-checked 等于没翻，而且从 DOM 上看不出是谁翻的。 */
.cfg-switch {
  position: relative;
  flex: 0 0 42px;
  width: 42px; height: 24px;
  border-radius: var(--r-full);
  background: var(--line-2);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out);
}
.cfg-switch::after {
  content: "";
  position: absolute;
  top: 3px; left: 3px;
  width: 18px; height: 18px;
  border-radius: 50%;
  background: #fff;
  box-shadow: var(--sh-1);
  transition: transform var(--dur-fast) var(--ease-out);
}
.cfg-switch[aria-checked="true"] { background: var(--accent); }
.cfg-switch[aria-checked="true"]::after { transform: translateX(18px); }
.cfg-switch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

@media (max-width: 860px) {
  .cfg-item { grid-template-columns: minmax(0, 1fr); }
  .cfg-ctrl { justify-content: flex-start; flex-wrap: wrap; }
  .cfg-ctrl input[type="text"],
  .cfg-ctrl input[type="number"],
  .cfg-ctrl input[type="password"],
  .cfg-ctrl select,
  .cfg-ctrl textarea { min-width: 0; width: 100%; max-width: 100%; }
}

/* 顶部故障/提示条 */
.cfg-status {
  margin-bottom: var(--sp-4);
  white-space: pre-line;
}
.cfg-status.ok { border-left-color: var(--ok); }
.cfg-status.err { border-left-color: var(--err); }

/* ---------------------------------------------------------------------------
   用户悬停卡（main.js 6.1.1）：鼠标放到「用户 / 昵称」单元格上弹出。
   用 position:fixed + JS 喂视口坐标，而不是 absolute 挂在单元格里 ——
   表格是 .table-wrap 溢出滚动的，absolute 会被 overflow 裁掉，也会被 sticky 表头盖住。
   ⚠ pointer-events:none 是必须的：卡片本身不能参与命中测试。
     否则鼠标一往卡片上移，锚点就收到 mouseout → 卡片消失 → 又 mouseover → 又出现，
     表现为疯狂闪烁。让鼠标"穿过去"，卡片就只是纯展示。
   ⚠ z-index 300：高于 base.css 的 200（跳转链接），也高于侧栏 40 / 顶栏 30。 */
.uhc {
  position: fixed;
  z-index: 300;
  min-width: 236px;
  max-width: 340px;
  padding: var(--sp-3);
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-radius: var(--r-md);
  box-shadow: var(--sh-3);
  color: var(--ink);
  font-size: var(--fs-sm);
  line-height: var(--lh-snug);
  pointer-events: none;
  animation: uhc-in var(--dur-fast) var(--ease-out);
}
@keyframes uhc-in {
  from { opacity: 0; transform: translateY(-3px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) { .uhc { animation: none; } }

/* 头部：头像 + 昵称 + 完整 openid */
.uhc-head { display: flex; align-items: center; gap: var(--sp-3); }
.uhc-av {
  position: relative;
  flex: 0 0 40px;
  width: 40px; height: 40px;
  border-radius: var(--r-full);
  overflow: hidden;
  background: var(--surface-3);
  border: 1px solid var(--line);
  display: flex; align-items: center; justify-content: center;
}
/* 没有头像时的首字兜底方块（别让左上角空一块） */
.uhc-av-fb {
  font-family: var(--font-display);
  font-size: var(--fs-lg);
  color: var(--muted);
  user-select: none;
}
.uhc-av img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

.uhc-id { min-width: 0; }
.uhc-nick {
  font-weight: 600;
  color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.uhc-openid {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  color: var(--faint);
  word-break: break-all;   /* openid 28 位连续无空格，必须允许断行，否则撑破卡片 */
  line-height: var(--lh-code);
}

.uhc-state { margin-top: var(--sp-2); }

/* 标签 / 值两列。标签定宽右对齐；值吃掉剩余空间并允许断行（邮箱、省市区串都可能很长） */
.uhc-row {
  display: grid;
  grid-template-columns: 62px minmax(0, 1fr);
  gap: var(--sp-2);
  margin-top: 5px;
  align-items: baseline;
}
.uhc-k { color: var(--muted); font-size: var(--fs-xs); }
.uhc-v { color: var(--ink-2); word-break: break-all; }
/* 没填 → 斜体灰字。管理员最需要一眼分清「这人没填过」和「后端没这个字段」，
   两者都显示斜体灰字，但卡片底部的说明会交代字段确实存在、只是空 */
.uhc-empty { color: var(--faint); font-style: italic; }
.uhc-ago { margin-left: 5px; color: var(--faint); font-size: var(--fs-2xs); font-style: normal; }

.uhc-ctx {
  margin-top: var(--sp-3);
  padding-top: var(--sp-2);
  border-top: 1px solid var(--line);
  color: var(--faint);
  font-size: var(--fs-2xs);
  line-height: var(--lh-base);
}

/* ==========================================================================
   9 · WS 调试台（scripts/ws-console.js）
   ========================================================================== */

/* 标题栏右侧的操作组（状态徽标 + 连接/断开），与 .view-head 的 flex 对齐 */
.ws-toolbar {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
}

/* 连接设置 / 主动发送 两块并排；窄屏塌成单列 */
.ws-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--sp-4);
  align-items: start;
}

.ws-fields {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--sp-3);
}
.ws-f { display: block; min-width: 0; }
.ws-fl {
  display: block;
  margin-bottom: 4px;
  font-size: var(--fs-xs);
  color: var(--muted);
}

.ws-opts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-5);
  margin-top: var(--sp-4);
}
.ws-ck {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fs-sm);
  color: var(--muted);
  cursor: pointer;
}
.ws-ck input { flex: 0 0 auto; }

.ws-hint {
  margin-top: var(--sp-3);
  font-size: var(--fs-sm);
  color: var(--faint);
  line-height: 1.7;
}
.ws-hint kbd, .panel-head kbd {
  display: inline-block;
  min-width: 18px;
  padding: 0 5px;
  text-align: center;
  border: 1px solid var(--line-2);
  border-bottom-width: 2px;
  border-radius: var(--r-xs);
  background: var(--surface-2);
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
}

/* 指令模板：28 个按钮，限高滚动 —— 否则「主动发送」卡会比事件流还长 */
.ws-presets {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  max-height: 132px;
  overflow-y: auto;
}
.ws-preset {
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  min-height: 28px;
  padding: 0 9px;
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink-2);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out), background-color var(--dur-fast) var(--ease-out);
}
.ws-preset:hover { border-color: var(--accent); background: var(--accent-soft); color: var(--accent-ink); }
.ws-preset:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.ws-preset-n { font-family: var(--font-body); font-size: var(--fs-2xs); color: var(--faint); }

.ws-json {
  display: block;
  width: 100%;
  min-height: 148px;
  margin-top: var(--sp-3);
  padding: var(--sp-3);
  resize: vertical;
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  background: var(--surface-3);
  color: var(--ink);
  font-size: var(--fs-sm);
  line-height: var(--lh-code);
}
.ws-json:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: transparent; }
.ws-json::placeholder { color: var(--faint); opacity: 0.75; }

.ws-send-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  margin-top: var(--sp-3);
}
.ws-json-err {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--fs-xs);
  color: var(--faint);
  line-height: 1.55;
}
.ws-json-err.is-err  { color: var(--err); }
.ws-json-err.is-warn { color: var(--warn); }

/* 事件流工具条里的行内输入框：尺寸对齐 .pick select（30px），否则与按钮一高一低 */
.ws-in {
  min-height: 30px;
  min-width: 20ch;
  padding: 0 var(--sp-2);
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
}
.ws-in:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: transparent; }

/* 事件流滚动区：500 行 × 40px 不设限高会把页面拉到两万像素 */
.ws-wrap { max-height: 560px; overflow: auto; }

.ws-t { white-space: nowrap; }
.ws-tcell { white-space: nowrap; }
.ws-dir { font-size: var(--fs-2xs); }
/* ⚠ 必须干掉 .lvl 的 text-transform: uppercase —— 帧类型是区分大小写的标识符，
   显示成 AI_DELTA 会让人照着敲错。 */
.ws-ttype { text-transform: none; letter-spacing: 0; }
.ws-ttype-unknown { border-style: dashed; color: var(--muted); }
.ws-tlabel { margin-left: 6px; }
/* 摘要列不要沿用全局 .trunc 的 26ch —— 那一列是给网址用的，这里需要更宽 */
.ws-sum .trunc { max-width: 64ch; }
.ws-sum .sub { font-family: var(--font-mono); }
.ws-more { width: 1%; text-align: right; white-space: nowrap; }

/* 发送失败的帧：整行淡淡染红，一眼挑出「点了但没发出去」 */
.ws-failed > td { background: var(--err-soft); }

/* ⚠ 必须放开高度：全局 tbody tr 是固定 40px（同 .parse-detail-row 的坑），
   详情里有 spec 网格 + 原文 <pre>，不覆盖会被压成一条缝、内容溢出去 */
.ws-detail-row { height: auto; }
.ws-detail-row > td {
  padding: var(--sp-4) var(--sp-4) var(--sp-5);
  background: var(--surface-2);
  border-top: 1px solid var(--line);
}
.ws-spec { margin-bottom: var(--sp-4); }
.ws-detail-note {
  margin-top: var(--sp-3);
  font-size: var(--fs-xs);
  color: var(--faint);
  line-height: 1.65;
}
.ws-raw {
  margin: var(--sp-2) 0 0;
  max-height: 320px;
  overflow: auto;
  padding: var(--sp-3);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink-2);
  font-size: var(--fs-xs);
  line-height: var(--lh-code);
  /* JSON 折行后更难读 —— 保留缩进、横向滚动 */
  white-space: pre;
}
.ws-detail-actions { margin-top: var(--sp-3); }

/* 顶部说明条的语义变体（setNote 的第二个参数） */
.note.ws-note-err { border-left-color: var(--err); }

@media (max-width: 1040px) {
  .ws-grid { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 860px) {
  .ws-fields { grid-template-columns: minmax(0, 1fr); }
  .ws-tlabel { display: none; }
  .ws-sum .trunc { max-width: 40ch; }
  .ws-wrap { max-height: 460px; }
}

/* ==========================================================================
   10 · 扫码记录（视图 4 · 只读）
   ==========================================================================
   与第 9 节「WS 调试台」的 .ws-detail-row 是同一类结构（行内展开），
   但刻意不共用类名：那一套的尺寸是按"每帧 JSON"调的（64ch 摘要、pre 不折行），
   扫码内容是"人扫到的文本"，折行更好读、列也更窄 —— 把两边的调参绑在一起
   改一边就会碰坏另一边。 */

.scan-more-cell { width: 1%; text-align: right; white-space: nowrap; }
.btn.scan-more { min-height: 26px; padding: 0 var(--sp-3); font-size: var(--fs-xs); min-width: auto; }
tbody tr.is-open > .scan-more-cell .btn.scan-more {
  border-color: var(--accent);
  color: var(--accent-ink);
}

/* ⚠ 必须放开高度：全局 tbody tr 固定 40px（见第 8 节），
   详情行里有 spec 网格 + 原文 <pre>，不覆盖会被压成一条缝、内容直接溢出去。 */
.scan-detail-row { height: auto; }
.scan-detail-row > td {
  background: var(--surface-2);
  padding: var(--sp-4) var(--sp-5);
  white-space: normal;
}
.scan-detail-row .spec-grid { margin-bottom: var(--sp-4); }

.scan-raw-head { font-size: var(--fs-xs); color: var(--faint); margin-bottom: var(--sp-2); }
/* 扫码内容是任意文本（URL / WIFI: / MECARD: / 纯文本），不是日志行也不是 JSON：
   一律 pre-wrap 折行，长 URL 靠 overflow-wrap 断，不横向滚动。
   为什么不是 pre + 横滚：扫到一长串 URL 时横滚会把"开头"滚出视野，
   而开头（协议 / 域名）恰恰是判断这是什么内容最关键的部分。 */
.scan-raw pre {
  margin: 0;
  padding: var(--sp-3) var(--sp-4);
  max-height: 320px;
  overflow: auto;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink-2);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  line-height: var(--lh-code);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  word-break: break-all;
}
.scan-raw-none { font-size: var(--fs-sm); color: var(--faint); font-style: italic; }
.scan-raw-actions { margin-top: var(--sp-3); }

/* 超过后端上限被截断的提示：用 --warn 而不是 --err ——
   「截断了」是如实告知，不是错误。 */
.scan-cut { color: var(--warn); font-weight: 600; }

/* 长度列上的截断角标：+N 里的 "+" 表示"这是原始长度，内容只回了一部分" */
.scan-cut-mark {
  margin-left: 3px;
  color: var(--warn);
  font-weight: 700;
  cursor: help;
}

/* 内容列比全局 .trunc 的 26ch 宽一点：扫码内容多为 URL，26ch 只能看到域名 */
.scan-body .trunc { max-width: 46ch; }

@media (max-width: 860px) {
  .scan-body .trunc { max-width: 28ch; }
}
