/* ============================================================================
   PubgBot 面板 —— 样式
   方向: **Apple 系统界面** (macOS Sonoma / iOS Settings 路线)

   ── Apple 化具体换了什么(2026-08-16) ──

   字体   SF Pro 优先(-apple-system 在 macOS/iOS 上就是它), 大字负字距、
          正文正常字距; 数字全部 tabular-nums —— 3 秒一刷的面板, 数字宽度
          不稳就会左右跳。
   色板   换成 Apple 系统色(systemBlue/Green/Red/Orange 的深浅两套)。它们
          是按 P3 屏幕和暗适应调过的, 比之前那套 GitHub 味的更沉、更准。
   圆角   加大并统一(控件 8 / 卡片 12 / 弹层 16), 靠近 Apple 的连续曲率观感。
   材质   顶栏和弹层用 vibrancy(backdrop-filter), **滚动区一律不用** ——
          见下面第 2 条, 那个理由仍然成立。
   分隔   发丝线(0.5px 物理像素)代替 1px 实线, 分组内嵌列表的观感。

   ── 三条贯穿全文件的规矩, 改之前先读 ──

   1. **颜色是稀缺资源, 只给异常。** 一个主色(靛蓝) + 一条冷灰阶就是全部。
      正常在跑的设备, 卡上除了一颗绿点几乎没有颜色 —— 所以任何一处红/琥珀
      出现在余光里都是真信号。上一版"每张卡都花花绿绿"等于把告警色的信噪比
      稀释成 0。

   2. **深色但有层次, 靠边框不靠亮度。** 底 → 卡 → 悬停 三层只差几点亮度;
      真正把卡"抬起来"的是发丝线边框 + 顶部那道内高光。半夜眼睛是暗适应
      状态, 大面积亮块和渐变都刺眼。

      ★ **backdrop-filter 只给不滚动的东西**(顶栏、弹层、门禁遮罩)。日志区
        和图墙一律不用 —— 它在滚动时掉帧, 而日志页是 3 秒一轮询、还要能
        一直往下滚的。Apple 自己也是这么分的: 侧边栏和工具栏有 vibrancy,
        内容区没有。

   3. **状态不只靠颜色。** 四种连通状态用四种形状(实心圆/空心圈/菱形/横杠),
      告警块和危险按钮带三角前缀。把屏幕转成灰度, 信息一条不丢。
   ============================================================================ */

:root {
  color-scheme: dark;

  /* ★ 全站数字走 tabular-nums。这面板 3 秒一刷, 比例数字会让"心跳 27 秒前"
     里的数字左右跳 —— 而余光扫过去时那种抖动比数字本身更显眼。 */
  font-variant-numeric: tabular-nums;

  /* ---------- 中性阶: Apple 深色的中性灰(H≈240, S≈4%) ----------
     macOS 深色的窗口底是 #1e1e1e 一带, 但那是给窗口的; 全屏 Web 面板底色
     要再深一点才不刺眼(半夜暗适应), 所以取 #0d0d0f 起步。 */
  --c-bg:          #0d0d0f;   /* 页面底 */
  --c-bg-sunken:   #09090b;   /* 比底还深: 日志区/图片舞台/失联的卡 */
  --c-surface:     #1c1c1e;   /* 卡片 —— systemGray6 dark */
  --c-surface-2:   #2c2c2e;   /* 卡片 hover / 输入框 —— systemGray5 dark */
  --c-surface-3:   #3a3a3c;   /* 按钮 hover / 选中行 —— systemGray4 dark */
  --c-scrim:       rgba(0, 0, 0, .60);

  /* 发丝线 —— Apple 的分隔线是半透明白/黑叠在底色上, 而不是一个固定灰值。
     这样它在任何背景上都保持同样的"轻" */
  --c-border:      rgba(255, 255, 255, .10);
  --c-border-2:    rgba(255, 255, 255, .16);
  --c-border-3:    rgba(255, 255, 255, .26);

  /* 文字四级 + 一个"不用读得出"的 dim。
     ★ 每一级都过 4.5:1 —— 半夜扫一眼的面板, 层次靠字号/字重/等宽来做,
     不靠把字调灰到看不见。真正可以看不清的只有 --c-text-dim(占位/禁用)。 */
  --c-text:        #ffffff;                    /* label */
  --c-text-2:      rgba(235, 235, 245, .78);   /* secondaryLabel */
  --c-text-3:      rgba(235, 235, 245, .62);   /* tertiaryLabel */
  --c-text-4:      rgba(235, 235, 245, .48);
  --c-text-dim:    rgba(235, 235, 245, .30);   /* quaternaryLabel */
  --c-text-on-accent: #ffffff;

  /* ---------- 主色: systemBlue(dark) —— 全站只有这一个 ---------- */
  --c-accent:        #0a84ff;
  --c-accent-hover:  #3d9dff;
  --c-accent-text:   #64b5ff;
  --c-accent-bg:     rgba(10, 132, 255, .16);
  --c-accent-border: rgba(10, 132, 255, .42);
  --c-accent-ring:   rgba(10, 132, 255, .45);

  /* ---------- 语义状态 ---------- */
  /* systemGreen / systemOrange / systemRed 的 dark 变体 */
  --c-ok:          #30d158;
  --c-ok-text:     #4ade7a;
  --c-ok-bg:       rgba(48, 209, 88, .16);
  --c-ok-border:   rgba(48, 209, 88, .36);

  --c-warn:        #ff9f0a;
  --c-warn-text:   #ffb340;
  --c-warn-bg:     rgba(255, 159, 10, .16);
  --c-warn-border: rgba(255, 159, 10, .36);

  --c-danger:        #ff453a;
  --c-danger-text:   #ff6961;
  --c-danger-bg:     rgba(255, 69, 58, .16);
  --c-danger-border: rgba(255, 69, 58, .36);

  --c-neutral:        var(--c-text-3);
  --c-neutral-bg:     rgba(235, 235, 245, .12);
  --c-neutral-border: rgba(235, 235, 245, .18);

  /* vibrancy —— **只给不滚动的东西**, 见文件头第 2 条 */
  --mat-bar:   rgba(28, 28, 30, .72);
  --mat-panel: rgba(28, 28, 30, .86);
  --blur:      saturate(180%) blur(20px);

  /* ---------- 字体 ---------- */
  /* ★ -apple-system 放**最前面** —— 在 macOS/iOS 上它解析到 SF Pro,
     而 system-ui 在某些浏览器上会被主题字体劫持。中文回落到苹方。 */
  --font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui,
               "PingFang SC", "HarmonyOS Sans SC", "Microsoft YaHei", sans-serif;
  --font-mono: ui-monospace, "SF Mono", Menlo, "JetBrains Mono", Consolas,
               "Sarasa Mono SC", monospace;

  /* 10px 只给全大写的微标签(侧栏分组名/表头) —— 那种字形本身就比
     同号数的小写高, 视觉上和 11px 的正文是一档。正文不许用它。 */
  --fs-10: 10px;
  --fs-11: 11px;  --fs-12: 12px;  --fs-13: 13px;  --fs-14: 14px;
  --fs-16: 16px;  --fs-20: 20px;  --fs-28: 28px;

  --fw-normal: 400;  --fw-medium: 500;  --fw-semibold: 600;

  --lh-tight: 1.2;  --lh-snug: 1.35;  --lh-normal: 1.55;

  /* SF 在大字号上要收字距、小字号上不收 —— Apple 的 optical sizing 就是
     这么做的, 照抄它的方向 */
  --ls-tight: -.022em;
  --ls-num:   -.01em;
  --ls-wide:   .04em;

  /* ---------- 间距: 4px 栅格 ---------- */
  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px;  --sp-4: 16px;  --sp-5: 20px;
  --sp-6: 24px; --sp-8: 32px; --sp-10: 40px;

  /* ---------- 圆角 ---------- */
  /* Apple 的圆角比 Linear 那一路大一档, 而且**同一层级必须一致** ——
     控件 8 / 卡片 12 / 弹层 16, 中间不再有别的值 */
  --r-xs: 5px;  --r-sm: 6px;  --r-md: 8px;  --r-lg: 12px;  --r-xl: 16px;

  /* 侧栏宽度 —— #shell 的网格和窄屏那条媒体查询都读它 */
  --side-w: 208px;
  --r-full: 999px;

  /* ---------- 阴影: 深色下几乎只是"压边", 真正的分层靠边框 ---------- */
  --shadow-xs: 0 1px 2px rgba(0, 0, 0, .30);
  --shadow-sm: 0 2px 8px rgba(0, 0, 0, .35);
  --shadow-md: 0 10px 30px -8px rgba(0, 0, 0, .55);
  --shadow-lg: 0 28px 70px -20px rgba(0, 0, 0, .70);
  /* 顶部一根内高光: "卡片有厚度"就是这一行, 别去掉 */
  --shadow-edge: inset 0 1px 0 rgba(255, 255, 255, .06);
  /* Apple 的焦点环是**两层**: 里面一圈底色描边把控件和光晕隔开 */
  --ring: 0 0 0 3.5px var(--c-accent-ring);

  /* ---------- 动效 ---------- */
  /* Apple 的默认曲线偏"快出慢收", 而且时长比 Web 常见值短一点 */
  --dur-fast: 120ms;
  --dur:      200ms;
  --dur-slow: 320ms;
  --ease:     cubic-bezier(.4, 0, .2, 1);
  --ease-out: cubic-bezier(.16, 1, .3, 1);

  --z-sticky: 20;  --z-modal: 40;  --z-gate: 50;  --z-toast: 60;

  --bar-h: 52px;
}

/* ============================================================================
   浅色: 同一套变量换值。中性阶反过来, 主色压深保证对比度。

   ★ **靠 <html data-theme="light"> 生效, 不靠 @media**。

     原来这一块是 @media (prefers-color-scheme: light), 也就是**只有系统
     本来就是浅色时才看得到** —— 而人想"看一眼浅色长什么样"是做不到的,
     除非去改整个系统的外观。

     现在顶栏有个三档开关(跟随系统 / 浅色 / 深色), theme.js 在 <head> 里
     把"跟随系统"解析成具体的 data-theme。放 <head> 而不是 app.js 里是为了
     **不闪**: app.js 在 body 末尾, 那时候深色已经画出来了。
     (不能用内联 <script>: CSP 里没有 'unsafe-inline', 见 main.py。)
   ============================================================================ */
:root[data-theme="light"] {
    color-scheme: light;

    /* Apple 浅色: 内容区是纯白, 页面底是 systemGroupedBackground 那种极浅灰
       —— 和深色反过来(深色是"卡比底亮", 浅色是"卡比底白") */
    --c-bg:        #f2f2f7;
    --c-bg-sunken: #e5e5ea;
    --c-surface:   #ffffff;
    --c-surface-2: #f2f2f7;
    --c-surface-3: #e5e5ea;
    --c-scrim:     rgba(0, 0, 0, .28);

    /* 发丝线同样用半透明黑, 不用固定灰值 */
    --c-border:   rgba(60, 60, 67, .16);
    --c-border-2: rgba(60, 60, 67, .26);
    --c-border-3: rgba(60, 60, 67, .40);

    --c-text:     #000000;
    --c-text-2:   rgba(60, 60, 67, .78);
    --c-text-3:   rgba(60, 60, 67, .62);
    --c-text-4:   rgba(60, 60, 67, .48);
    --c-text-dim: rgba(60, 60, 67, .30);

    /* systemBlue / Green / Orange / Red 的浅色变体 —— 比深色那套压深,
       白底上才够 4.5:1 */
    --c-accent:        #007aff;
    --c-accent-hover:  #0062cc;
    --c-accent-text:   #0064d2;
    --c-accent-bg:     rgba(0, 122, 255, .10);
    --c-accent-border: rgba(0, 122, 255, .32);
    --c-accent-ring:   rgba(0, 122, 255, .30);

    --c-ok:        #248a3d;
    --c-ok-text:   #1c7430;
    --c-ok-bg:     rgba(36, 138, 61, .10);
    --c-ok-border: rgba(36, 138, 61, .26);

    --c-warn:        #c04c00;
    --c-warn-text:   #a63f00;
    --c-warn-bg:     rgba(192, 76, 0, .10);
    --c-warn-border: rgba(192, 76, 0, .26);

    --c-danger:        #d70015;
    --c-danger-text:   #c00012;
    --c-danger-bg:     rgba(215, 0, 21, .09);
    --c-danger-border: rgba(215, 0, 21, .26);

    --c-neutral-bg:     rgba(60, 60, 67, .10);
    --c-neutral-border: rgba(60, 60, 67, .20);

    --mat-bar:   rgba(242, 242, 247, .72);
    --mat-panel: rgba(255, 255, 255, .88);

    /* 浅色下阴影才是真的分层 */
    --shadow-xs: 0 1px 2px rgba(16, 17, 19, .05);
    --shadow-sm: 0 1px 3px rgba(16, 17, 19, .08), 0 1px 2px rgba(16, 17, 19, .04);
    --shadow-md: 0 8px 20px -6px rgba(16, 17, 19, .12);
    --shadow-lg: 0 24px 56px -16px rgba(16, 17, 19, .18);
    --shadow-edge: inset 0 1px 0 rgba(255, 255, 255, .8);
}


/* ============================================================================
   基础
   ============================================================================ */

* { box-sizing: border-box; }

/* ★ 必须压过任何 ID 选择器写的 display —— 浏览器自带的 [hidden]{display:none}
   是 UA 样式, 优先级低于 #gate{display:grid}。这个坑上一版踩过: 输完 token
   之后 gate 关不掉。 */
[hidden] { display: none !important; }

html { height: 100%; }

body {
  margin: 0;
  min-height: 100%;
  background: var(--c-bg);
  color: var(--c-text);
  font: var(--fw-normal) var(--fs-13) / var(--lh-snug) var(--font-sans);
  -webkit-font-smoothing: antialiased;
  /* 全站数字等宽对齐 —— 局数/经验/时间 3 秒一刷, 不能左右晃 */
  font-variant-numeric: tabular-nums;
  -webkit-tap-highlight-color: transparent;
}

#sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

h1, h2, h3, p, dl, dd, dt, ul, li { margin: 0; padding: 0; }
ul, li { list-style: none; }

code {
  font-family: var(--font-mono);
  font-size: .92em;
  padding: 1px 5px;
  color: var(--c-accent-text);
  background: var(--c-accent-bg);
  border-radius: var(--r-xs);
}

b { font-weight: var(--fw-medium); }

.ic {
  width: 14px; height: 14px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}

/* 滚动条: 深色下默认那条白灰滚动条非常突兀 */
* { scrollbar-width: thin; scrollbar-color: var(--c-border-3) transparent; }
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
  background: var(--c-border-2);
  border: 3px solid transparent;
  background-clip: content-box;
  border-radius: var(--r-full);
}
*::-webkit-scrollbar-thumb:hover { background: var(--c-border-3); background-clip: content-box; }

::selection { background: var(--c-accent-bg); color: var(--c-text); }


/* ============================================================================
   通用控件
   ============================================================================ */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  height: 30px;                 /* Apple 的小号控件是 28~32, 取中 */
  padding: 0 12px;
  font: var(--fw-medium) var(--fs-13) / 1 var(--font-sans);
  color: var(--c-text-2);
  background: var(--c-surface-2);
  border: 0.5px solid var(--c-border-2);
  border-radius: var(--r-md);
  /* 按下时轻微内收 —— Apple 控件的触感反馈, 比换背景色更像"按到了" */
  transition: background var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
  cursor: pointer;
  white-space: nowrap;
  transition: background-color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}
.btn:hover { background: var(--c-surface-3); border-color: var(--c-border-3); color: var(--c-text); }
.btn:active { transform: translateY(.5px); }
.btn:focus-visible { outline: none; box-shadow: var(--ring); }
.btn:disabled { opacity: .45; cursor: not-allowed; }

.btn--xs    { height: 24px; padding: 0 9px; font-size: var(--fs-11); }
.btn--block { width: 100%; height: 34px; font-size: var(--fs-13); }
.btn--icon  { padding: 0; width: 28px; }

.btn--ghost { background: none; border-color: transparent; color: var(--c-text-3); }
.btn--ghost:hover { background: var(--c-surface-3); border-color: transparent; }

.btn--primary {
  color: var(--c-text-on-accent);
  background: var(--c-accent);
  border-color: var(--c-accent);
}
.btn--primary:hover { background: var(--c-accent-hover); border-color: var(--c-accent-hover); color: #fff; }

/* 危险: 停止/开始/切模式。和安全按钮**长得不一样** —— 红字 + 红边 + 三角
   前缀。手快的时候确认框也是照点的, 差异必须在按下去之前就看见。 */
.btn--danger {
  color: var(--c-danger-text);
  background: var(--c-danger-bg);
  border-color: var(--c-danger-border);
}
.btn--danger::before {
  content: "";
  width: 0; height: 0; flex: none;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-bottom: 7px solid currentColor;
}
.btn--danger:hover { color: #fff; background: var(--c-danger); border-color: var(--c-danger); }

/* 删除: 不可逆。平时藏成幽灵按钮, 悬停才露红 —— 手不会顺路点到那个。
   要不要再加一道"打一遍名字"的闸看爆炸半径, 不是看这个类:
   删设备 / 清空一台的连拍要打, 删单组连拍不要(见 app.js 文件头第 5 条)。 */
.btn--destroy { background: none; border-color: transparent; color: var(--c-text-4); }
.btn--destroy:hover {
  color: var(--c-danger-text);
  background: var(--c-danger-bg);
  border-color: var(--c-danger-border);
}

/* 输入框 / 下拉 */
.btn:active:not(:disabled) { transform: scale(.97); }

input[type="text"], input[type="search"], input[type="password"],
input[type="number"], select, textarea {
  font: var(--fw-normal) var(--fs-13) / 1.4 var(--font-sans);
  color: var(--c-text);
  background: var(--c-surface-2);
  border: 0.5px solid var(--c-border-2);
  border-radius: var(--r-md);
  padding: 0 10px;
  height: 30px;
  min-width: 0;
  transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
textarea { height: auto; padding: 7px 9px; resize: vertical; }
input::placeholder, textarea::placeholder { color: var(--c-text-dim); }
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--c-accent-border);
  box-shadow: var(--ring);
}
input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; }

select {
  appearance: none;
  padding-right: 24px;
  /* 内联的小箭头, 零外链 */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3 4.5 6 7.5 9 4.5' fill='none' stroke='%239aa0a8' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 7px center;
  background-size: 12px;
  cursor: pointer;
}

.field { display: flex; flex-direction: column; gap: 6px; text-align: left; }
.field__label {
  font-size: var(--fs-11);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide);
  color: var(--c-text-4);
}
.field input { height: 34px; }


/* ============================================================================
   Token 门
   ============================================================================ */

#gate {
  position: fixed;
  inset: 0;
  z-index: var(--z-gate);
  display: grid;
  place-items: center;
  padding: var(--sp-4);
  background: var(--c-bg);
  overflow: auto;
}
/* 一层极淡的主色辉光。整个面板唯一一处装饰性渐变 —— 这一屏没有数据要读,
   可以稍微好看一点; 进去之后就再没有了。 */
.gate-glow {
  position: absolute;
  top: -10%; left: 50%;
  width: min(620px, 120vw);
  aspect-ratio: 1;
  transform: translateX(-50%);
  background: radial-gradient(circle, var(--c-accent-bg), transparent 62%);
  pointer-events: none;
}
.gate-card {
  position: relative;
  width: 100%;
  max-width: 372px;
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  padding: var(--sp-6);
  text-align: center;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-lg), var(--shadow-edge);
  animation: gate-in var(--dur-slow) var(--ease-out) both;
}
@keyframes gate-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

.gate-mark {
  width: 40px; height: 40px;
  margin: 0 auto;
  display: grid; place-items: center;
  color: var(--c-accent-text);
  background: var(--c-accent-bg);
  border: 1px solid var(--c-accent-border);
  border-radius: var(--r-lg);
}
.gate-mark svg { width: 22px; height: 22px; }

.gate-title {
  font-size: var(--fs-20);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--ls-tight);
}
.gate-sub { font-size: var(--fs-12); color: var(--c-text-3); margin-top: -8px; }

.gate-err {
  display: flex; align-items: center; gap: 7px;
  padding: 8px 10px;
  font-size: var(--fs-12);
  text-align: left;
  color: var(--c-danger-text);
  background: var(--c-danger-bg);
  border: 1px solid var(--c-danger-border);
  border-radius: var(--r-sm);
}
.gate-err::before {
  content: ""; flex: none;
  width: 0; height: 0;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-bottom: 9px solid currentColor;
}

.gate-note {
  font-size: var(--fs-11);
  line-height: var(--lh-normal);
  color: var(--c-text-4);
  text-align: left;
  padding-top: var(--sp-3);
  border-top: 1px solid var(--c-border);
}


/* ============================================================================
   顶栏
   ============================================================================ */

#bar {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  height: var(--bar-h);
  padding: 0 var(--sp-4);
  /* ★ vibrancy 只给顶栏这类**不滚动**的 chrome —— 见文件头第 2 条。
     内容从它下面划过时会透出来一点, 那正是 Apple 工具栏的观感。
     不支持 backdrop-filter 的浏览器落到不透明底色, 不会变透明。 */
  background: var(--c-bg);
  border-bottom: 0.5px solid var(--c-border);
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  #bar {
    background: var(--mat-bar);
    -webkit-backdrop-filter: var(--blur);
    backdrop-filter: var(--blur);
  }
}

.bar__brand { display: flex; align-items: center; gap: 7px; flex: none; }
.bar__mark { width: 20px; height: 20px; color: var(--c-accent-text); }
.bar__mark svg { width: 100%; height: 100%; }
.bar__name {
  font-size: var(--fs-13);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--ls-tight);
}

.bar__nav { display: flex; gap: 2px; min-width: 0; overflow-x: auto; scrollbar-width: none; }
.bar__nav::-webkit-scrollbar { display: none; }

.tab {
  height: 28px;
  padding: 0 11px;
  font: var(--fw-medium) var(--fs-12) / 1 var(--font-sans);
  color: var(--c-text-3);
  background: none;
  border: none;
  border-radius: var(--r-md);
  cursor: pointer;
  white-space: nowrap;
  transition: color var(--dur-fast) var(--ease), background-color var(--dur-fast) var(--ease);
}
.tab:hover { color: var(--c-text); background: var(--c-surface-2); }
.tab.is-on { color: var(--c-text); background: var(--c-surface-3); }
.tab:focus-visible { outline: none; box-shadow: var(--ring); }
/* 有待确认设备时给"设备"页一个小红点 —— 不在那一页也知道有事 */
.tab[data-badge]::after {
  content: "";
  display: inline-block;
  width: 5px; height: 5px;
  margin-left: 5px;
  vertical-align: 1px;
  border-radius: var(--r-full);
  background: var(--c-warn);
}

.bar__right { display: flex; align-items: center; gap: var(--sp-2); margin-left: auto; min-width: 0; }

.devpick { display: flex; align-items: center; gap: 6px; min-width: 0; }
.devpick__label {
  font-size: var(--fs-11);
  letter-spacing: var(--ls-wide);
  color: var(--c-text-4);
  flex: none;
}
#devpick { max-width: 190px; }

/* 连通指示: 这是**面板到后端**的连通性, 和设备的连通性是两回事 */
.conn {
  display: inline-flex; align-items: center; gap: 6px;
  flex: none;
  padding: 0 8px;
  height: 24px;
  font-size: var(--fs-11);
  color: var(--c-text-3);
  background: var(--c-surface-2);
  border: 1px solid var(--c-border);
  border-radius: var(--r-full);
}
.conn__dot { width: 6px; height: 6px; border-radius: var(--r-full); background: currentColor; flex: none; }
.conn[data-st="ok"]   { color: var(--c-ok-text); }
.conn[data-st="wait"] { color: var(--c-text-4); }
.conn[data-st="err"]  { color: var(--c-danger-text); background: var(--c-danger-bg); border-color: var(--c-danger-border); }


/* ============================================================================
   页面骨架
   ============================================================================ */

/* width:100% 不能省: 网格项带 margin:auto 时不再拉伸, 而是按内容收缩 ——
   宽表格(设备/卡密)的最小宽度会把整列撑出屏幕, 窄屏上整页横着滚, 而表格
   外面那层 .dtwrap 的横向滚动根本轮不到。 */
#app { padding: var(--sp-4); width: 100%; min-width: 0; max-width: 1560px; margin: 0 auto; }

.view { display: none; }
.view.is-on { display: block; animation: view-in var(--dur) var(--ease-out) both; }
@keyframes view-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }

.page-head {
  display: flex;
  align-items: flex-end;
  gap: var(--sp-3);
  flex-wrap: wrap;
  margin-bottom: var(--sp-4);
}
.page-title {
  font-size: var(--fs-16);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--ls-tight);
}
.page-sub { margin-top: 3px; font-size: var(--fs-12); color: var(--c-text-3); }
.page-acts { display: flex; gap: 6px; margin-left: auto; }

.foot-note {
  margin-top: var(--sp-4);
  font-size: var(--fs-11);
  line-height: var(--lh-normal);
  color: var(--c-text-4);
}

.rev-tag {
  display: inline-block;
  margin-left: var(--sp-2);
  padding: 1px 6px;
  font-family: var(--font-mono);
  font-size: var(--fs-11);
  color: var(--c-text-3);
  background: var(--c-surface-3);
  border-radius: var(--r-xs);
}

.hint {
  font-size: var(--fs-11);
  line-height: var(--lh-normal);
  color: var(--c-text-4);
}


/* ============================================================================
   设备卡片
   ============================================================================ */

.dev-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
  gap: var(--sp-3);
  align-items: start;
}

.dev-card {
  /* 每张卡自带一组"状态色"变量, 下面所有元素只读这三个, 不各写各的 */
  --st: var(--c-neutral);
  --st-bg: var(--c-neutral-bg);
  --st-bd: var(--c-neutral-border);

  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding: var(--sp-4);
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-xs), var(--shadow-edge);
  transition: border-color var(--dur) var(--ease),
              background-color var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease);
  animation: dev-card-in var(--dur-slow) var(--ease-out) both;
}
/* 进入: 只动 opacity + transform, 合成器就能做完, 稳 60fps */
@keyframes dev-card-in {
  from { opacity: 0; transform: translateY(6px) scale(.995); }
  to   { opacity: 1; transform: none; }
}
/* 轮询里就地换掉的卡: 它不是"新出现的", 只是数字变了 —— 不播入场动画,
   否则满屏卡片每 5 秒集体闪一次。见 app.js renderDevices 的说明。 */
.dev-card.is-static { animation: none; }

.dev-card:nth-child(2) { animation-delay: 40ms; }
.dev-card:nth-child(3) { animation-delay: 80ms; }
.dev-card:nth-child(4) { animation-delay: 120ms; }
.dev-card:nth-child(n + 5) { animation-delay: 160ms; }

.dev-card:hover {
  background: var(--c-surface-2);
  border-color: var(--c-border-2);
  box-shadow: var(--shadow-sm), var(--shadow-edge);
}

/* --- 在线 --- */
.dev-card[data-status="online"] {
  --st: var(--c-ok); --st-bg: var(--c-ok-bg); --st-bd: var(--c-ok-border);
}

/* --- 失联: 关键的一屏。
   不整卡压暗 —— 恰恰是这张卡上的字最需要看清(失联多久、最后停在哪、最后
   一条告警)。做法是把卡"沉下去"(背景比在线卡更深 + 去掉内高光和阴影),
   顶部一根红发丝线, 数字褪成次级色。形状 + 明度 + 位置三重信号。 --- */
.dev-card[data-status="offline"] {
  --st: var(--c-danger); --st-bg: var(--c-danger-bg); --st-bd: var(--c-danger-border);
  background: var(--c-bg-sunken);
  box-shadow: none;
}
.dev-card[data-status="offline"]::before {
  content: "";
  position: absolute;
  inset: -1px -1px auto -1px;
  height: 2px;
  background: var(--c-danger);
  opacity: .85;
  border-radius: var(--r-lg) var(--r-lg) 0 0;
}
.dev-card[data-status="offline"] .metric__v { color: var(--c-text-3); }
.dev-card[data-status="offline"]:hover { background: var(--c-surface); }

/* ★ 失联/停用之后卡上那颗游戏状态是"最后一次看到的", 是陈旧数据 —— 必须
   褪成中性, 否则一张卡上两处红, 分不清是"设备失联"还是"游戏挂了"。
   这正是踩过的那个坑的结构性解法。 */
.dev-card[data-status="offline"] .dev-state,
.dev-card[data-status="blocked"] .dev-state {
  color: var(--c-text-3) !important;
  background: var(--c-surface-3) !important;
  border-color: var(--c-border) !important;
}

/* --- 待确认: 顶到最前 + 琥珀边。它只能报到, 卡上要少一半东西 --- */
.dev-card[data-status="pending"] {
  --st: var(--c-warn); --st-bg: var(--c-warn-bg); --st-bd: var(--c-warn-border);
  order: -1;
  border-color: var(--c-warn-border);
  padding-top: 0;
}
.dev-card[data-status="pending"]:hover { border-color: var(--c-warn); }

/* --- 已停用: 全灰 + 沉下去, 且**不带**失联那根红线 ——
   "被我关掉的"和"半夜自己掉的"在余光里就得是两回事 --- */
.dev-card[data-status="blocked"] {
  --st: var(--c-text-4); --st-bg: var(--c-neutral-bg); --st-bd: var(--c-neutral-border);
  order: 1;
  background: var(--c-bg-sunken);
  box-shadow: none;
}
.dev-card[data-status="blocked"] .dev-card__name,
.dev-card[data-status="blocked"] .metric__v { color: var(--c-text-3); }

/* 待确认卡顶部那条 */
.dev-card__ribbon {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
  margin: 0 calc(var(--sp-4) * -1) var(--sp-1);
  padding: var(--sp-2) var(--sp-4);
  background: var(--c-warn-bg);
  border-bottom: 1px solid var(--c-warn-border);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
}
.dev-card__ribbon-t {
  flex: 1;
  min-width: 140px;
  font-size: var(--fs-12);
  font-weight: var(--fw-medium);
  color: var(--c-warn-text);
}

/* ---------- 头部 ---------- */

.dev-card__head { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }

.dev-card__name {
  margin: 0;
  min-width: 0;
  font-size: var(--fs-16);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--ls-tight);
  line-height: var(--lh-tight);
  color: var(--c-text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dev-card__note {
  margin-left: var(--sp-2);
  font-size: var(--fs-12);
  font-weight: var(--fw-normal);
  color: var(--c-text-4);
}
.dev-card__ver {
  margin-left: auto;
  flex: none;
  padding: 2px 6px;
  font-family: var(--font-mono);
  font-size: var(--fs-11);
  color: var(--c-text-4);
  background: var(--c-surface-3);
  border-radius: var(--r-xs);
}

/* --- 连通性: 形状 + 文字, 色盲也读得出。
   ★ "在线/失联"这组词**只给连通性用**, 绝不拿来指游戏状态。 --- */
.dev-status {
  display: inline-flex; align-items: center; gap: 7px;
  flex: none;
  font-size: var(--fs-12);
  font-weight: var(--fw-medium);
  color: var(--st);
  white-space: nowrap;
}
.dev-status__mark {
  width: 8px; height: 8px;
  flex: none;
  border-radius: var(--r-full);
  background: var(--st);
}
/* 在线 = 实心圆 + 一圈慢慢扩散的晕。
   晕做在伪元素上用 transform/opacity 跑 —— 动 box-shadow 会一直触发重绘,
   这个圈是常驻动画, 差别在电池上看得出来。 */
[data-status="online"] .dev-status__mark { position: relative; }
[data-status="online"] .dev-status__mark::after {
  content: "";
  position: absolute;
  inset: -3px;
  border-radius: inherit;
  background: var(--st);
  animation: dev-pulse 2.6s var(--ease) infinite;
  will-change: transform, opacity;
}
@keyframes dev-pulse {
  0%, 100% { transform: scale(1);   opacity: .26; }
  55%      { transform: scale(1.7); opacity: 0; }
}
/* 失联 = 空心圈 */
[data-status="offline"] .dev-status__mark {
  background: none;
  border: 2px solid var(--st);
  width: 9px; height: 9px;
}
/* 待确认 = 菱形 */
[data-status="pending"] .dev-status__mark { border-radius: 1px; transform: rotate(45deg); }
/* 已停用 = 横杠 */
[data-status="blocked"] .dev-status__mark { width: 10px; height: 3px; border-radius: 1px; }

/* ---------- 游戏状态行 ---------- */

.dev-line { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; min-height: 22px; }
.dev-line__d { width: 3px; height: 3px; border-radius: var(--r-full); background: var(--c-text-4); flex: none; }
.dev-line__t { font-size: var(--fs-12); color: var(--c-text-3); }
.dev-line__t b { font-family: var(--font-mono); font-weight: var(--fw-medium); color: var(--c-text-2); }

/* 游戏状态 badge: 默认中性; 只有"该管一下"的状态才上色。
   ★ state=offline 的中文是"游戏未启动", 不是"离线" —— 见 app.js 的 STATE_CN。 */
.dev-state {
  display: inline-flex; align-items: center; gap: 6px;
  height: 22px;
  padding: 0 8px;
  flex: none;
  font-size: var(--fs-12);
  font-weight: var(--fw-medium);
  color: var(--c-text-2);
  background: var(--c-surface-3);
  border: 1px solid var(--c-border);
  border-radius: var(--r-sm);
  transition: color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}
.dev-state::before {
  content: "";
  width: 5px; height: 5px;
  border-radius: 1.5px;
  background: currentColor;
  opacity: .75;
  flex: none;
}
/* 正在挣经验的三个状态: 主色 */
.dev-state[data-state="plane"],
.dev-state[data-state="skydive"],
.dev-state[data-state="ground"] {
  color: var(--c-accent-text);
  background: var(--c-accent-bg);
  border-color: var(--c-accent-border);
}
/* 需要人看一眼 */
.dev-state[data-state="second_pwd"],
.dev-state[data-state="popup"],
.dev-state[data-state="unknown"] {
  color: var(--c-warn-text);
  background: var(--c-warn-bg);
  border-color: var(--c-warn-border);
}
/* 没在跑 */
.dev-state[data-state="offline"],
.dev-state[data-state="stopped"] {
  color: var(--c-danger-text);
  background: var(--c-danger-bg);
  border-color: var(--c-danger-border);
}

/* 状态刚变: JS 加 .is-fresh, 闪一圈就自己散掉。
   动的是一圈光晕, 不碰 badge 自己的配色 —— 所以告警色/危险色的 badge 闪
   起来也不会变成蓝的。 */
.dev-state.is-fresh { animation: dev-flash 1.2s var(--ease-out) 1; }
@keyframes dev-flash {
  0%   { box-shadow: 0 0 0 0 var(--c-accent-border); }
  35%  { box-shadow: 0 0 0 4px var(--c-accent-bg); }
  100% { box-shadow: 0 0 0 6px transparent; }
}

/* ---------- 指标 ---------- */

.dev-metrics {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  margin: 0;
  padding: var(--sp-1) 0 0;
}
.metric {
  /* DOM 里 dt(标签) 在前 dd(数值) 在后 —— 这是 <dl> 的合法顺序; 视觉上数值
     在上, 靠 column-reverse 翻过来, 不牺牲语义 */
  display: flex;
  flex-direction: column-reverse;
  justify-content: flex-end;
  min-width: 0;
  padding: 0 var(--sp-3);
}
.metric:first-child { padding-left: 0; }
.metric:last-child  { padding-right: 0; }
.metric + .metric { border-left: 1px solid var(--c-border); }

.metric__v {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--fs-20);
  font-weight: var(--fw-medium);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-num);
  font-variant-numeric: tabular-nums;
  color: var(--c-text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.metric__v small { font-size: var(--fs-12); font-weight: var(--fw-normal); color: var(--c-text-3); margin-left: 1px; }
.metric__k {
  margin-top: 3px;
  font-size: var(--fs-11);
  letter-spacing: var(--ls-wide);
  color: var(--c-text-4);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* ---------- 告警 / 元信息 ---------- */

.dev-warn {
  /* ★ **不能 nowrap**。这是 grid 里的一项, 而 flex/grid 项的 min-width
     默认是 auto —— nowrap 让它的 min-content 宽度等于整句告警的宽度,
     于是这条轨道把整个页面顶宽, 375px 的手机上要横向拖才点得到
     "确认/删除"。而 recent_warn 服务端允许 500 字, 最坏能撑到几千 px。

     text-overflow: ellipsis 在这里一次都不会触发 —— 盒子是在**变长**
     而不是在被压缩。所以改成两行截断: 告警本来就要看得懂, 单行省略号
     经常正好把关键的那半句切掉。完整内容在 title 里。 */
  min-width: 0;
  margin: 0;
  padding: 7px 9px;
  font-size: var(--fs-12);
  line-height: var(--lh-snug);
  color: var(--c-warn-text);
  background: var(--c-warn-bg);
  border: 1px solid var(--c-warn-border);
  border-radius: var(--r-sm);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;
}
.dev-warn::before {
  content: ""; flex: none;
  width: 0; height: 0;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-bottom: 9px solid currentColor;   /* 三角 = 告警, 不识色也认得出 */
}

.dev-meta {
  display: flex; flex-wrap: wrap;
  gap: 3px var(--sp-3);
  font-family: var(--font-mono);
  font-size: var(--fs-11);
  color: var(--c-text-4);
}
.dev-meta b { font-weight: var(--fw-medium); color: var(--c-text-3); }

/* ---------- 操作条 ----------
   ★ 分成两个容器(安全 / 危险)而不是一排按钮: 一排按钮换行时会把"停止挂机"
     甩到"截图"和"ping"中间, 手快的时候确认框也是照点的。分组之后危险那几个
     永远抱团在右边。 */
.dev-acts {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin: 0 calc(var(--sp-4) * -1) calc(var(--sp-4) * -1);
  padding: var(--sp-3) var(--sp-4);
  border-top: 1px solid var(--c-border);
  border-radius: 0 0 var(--r-lg) var(--r-lg);
}
.acts-group { display: flex; flex-wrap: wrap; gap: 6px; min-width: 0; }
.acts-group--danger { margin-left: auto; }


/* ============================================================================
   日志
   ============================================================================ */

.toolbar {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  padding: var(--sp-2);
  margin-bottom: var(--sp-2);
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-xs), var(--shadow-edge);
}
.toolbar__gap { flex: 1 1 auto; }

.search { position: relative; display: flex; align-items: center; flex: 1 1 200px; min-width: 160px; }
.search__ic { position: absolute; left: 8px; color: var(--c-text-4); pointer-events: none; }
.search input { width: 100%; padding-left: 27px; }

/* 顶栏那个外观三档 —— 比设备卡上的更小一号 */
.seg--theme { flex: none; }
.seg--theme .seg__b { padding: 0 9px; height: 26px; line-height: 26px; }

/* 磁盘水位 —— 设备页顶上那条 */
.disk {
  display: flex; flex-direction: column; gap: 6px;
  padding: var(--sp-3) var(--sp-4); margin-bottom: var(--sp-4);
  background: var(--c-surface); border: 0.5px solid var(--c-border);
  border-radius: var(--r-lg); box-shadow: var(--shadow-edge);
}
.disk.is-low { border-color: var(--c-danger-border); background: var(--c-danger-bg); }
.disk__main { display: flex; align-items: center; gap: var(--sp-3); }
.disk__k { font-size: var(--fs-12); color: var(--c-text-3); flex: none; }
.disk__v { font-size: var(--fs-12); color: var(--c-text-2); flex: none; }
.disk__low { font-size: var(--fs-11); color: var(--c-danger-text); flex: none; }
.disk__bar {
  flex: 1 1 auto; height: 6px; min-width: 60px;
  background: var(--c-surface-3); border-radius: var(--r-full); overflow: hidden;
}
.disk__bar > i { display: block; height: 100%; background: var(--c-accent); }
.disk.is-low .disk__bar > i { background: var(--c-danger); }
.disk__row {
  display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap;
  font-size: var(--fs-11); color: var(--c-text-3);
}
.disk__row b { color: var(--c-text-2); font-weight: var(--fw-medium); }
.disk__retain { color: var(--c-text-4); }

/* "同一个指纹又来注册过" —— 指纹是可读值不是凭据, 所以这件事要显眼 */
.dev-meta__flag { color: var(--c-warn-text); font-weight: var(--fw-medium); }

/* 连拍页标题行里那个 run 下拉 */
.sel--sm select { height: 24px; font-size: var(--fs-11); padding: 0 6px; }

/* 设备卡上的结算截图档位 —— 三格分段控件 */
.dev-shot {
  /* ★ 要能换行。控件按 Apple 尺度放大之后(28->30, 12->13px), 窄卡片上
     「结算截图 + 三格 + 那句说明」一行放不下 —— 而不许换行的结果是
     分段控件里的字被竖着挤成一列, 三个档位全看不清。 */
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: 10px 0 0; padding-top: 10px;
  border-top: 0.5px solid var(--c-border);
}
/* 标签 + 控件一组, 整组换行 —— 见 shotModeRow 里那段 */
.dev-shot__grp { display: flex; align-items: center; gap: 8px; flex: none; }
.dev-shot__k { font-size: var(--fs-12); color: var(--c-text-3); flex: none; }
.dev-shot__n { font-size: 11px; color: var(--c-text-3); font-variant-numeric: tabular-nums; }
/* 设备拒了这个键 —— 三个格子一个都不亮, 免得看起来像"当前是这一档" */
.dev-shot__dead {
  /* 换行之后它独占一行, 就不用再省略号截断了 —— 那句话本来就要读全 */
  flex: 1 1 100%;
  font-size: var(--fs-11); color: var(--c-warn-text); min-width: 0;
}
.seg.is-dead { opacity: .55; }
.seg {
  display: inline-flex; flex: none;
  border: 0.5px solid var(--c-border-2);
  /* Apple 的分段控件是**胶囊内嵌**: 外壳圆角大、选中那格自己也是圆角 */
  border-radius: var(--r-md); overflow: hidden;
  background: var(--c-surface-2);
}
.seg__b {
  appearance: none; border: 0; background: none; cursor: pointer;
  padding: 4px 11px; font: inherit; font-size: var(--fs-12); line-height: 18px;
  /* 挤到了也不许把字竖着排 —— 宁可整个控件换到下一行 */
  white-space: nowrap;
  color: var(--c-text-3); border-left: 0.5px solid var(--c-border-2);
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.seg__b:first-child { border-left: 0; }
.seg__b:hover:not(:disabled) { color: var(--c-text); background: var(--c-surface-3); }
.seg__b:focus-visible { outline: none; box-shadow: var(--ring); z-index: 1; }
/* 当前档位 disabled(点了也没意义), 所以这条要压过下面那条通用 disabled */
.seg__b.is-on { color: var(--c-accent-text); background: var(--c-accent-bg); cursor: default; opacity: 1; }
.seg__b:disabled:not(.is-on) { opacity: .5; cursor: default; }

/* 调参页: 设备拒收的那一项 —— 值在库里、rev 也涨了, 但那台机器不认 */
.tune-dead { display: block; color: var(--c-warn-text); margin-bottom: 3px; }
.tune-row[data-dead="1"] .tune-input { opacity: .55; }

.chips { display: flex; gap: 3px; flex-wrap: wrap; }
.chip {
  height: 24px;
  padding: 0 8px;
  font: var(--fw-medium) var(--fs-11) / 1 var(--font-mono);
  letter-spacing: .01em;
  color: var(--c-text-4);
  background: var(--c-surface-2);
  border: 1px solid var(--c-border);
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: all var(--dur-fast) var(--ease);
}
.chip:hover { color: var(--c-text-2); border-color: var(--c-border-2); }
.chip:focus-visible { outline: none; box-shadow: var(--ring); }
/* 关掉的级别是"划掉"而不只是变灰 —— 一眼看出是被我关的, 不是没有数据 */
.chip:not(.is-on) { text-decoration: line-through; opacity: .6; }
.chip.is-on { color: var(--c-text); background: var(--c-surface-3); border-color: var(--c-border-2); }
.chip--warn.is-on { color: var(--c-warn-text); background: var(--c-warn-bg); border-color: var(--c-warn-border); }
.chip--err.is-on  { color: var(--c-danger-text); background: var(--c-danger-bg); border-color: var(--c-danger-border); }

.sel { display: flex; align-items: center; gap: 5px; }
.sel__label { font-size: var(--fs-11); letter-spacing: var(--ls-wide); color: var(--c-text-4); }
.sel select { max-width: 190px; }

/* 实时按钮: 亮着的时候有一颗跳动的点 —— "它真的在拉" 这件事要看得见,
   否则暂停和"后端挂了"长得一模一样 */
.btn--live.is-on { color: var(--c-ok-text); background: var(--c-ok-bg); border-color: var(--c-ok-border); }
.live-dot {
  width: 6px; height: 6px; flex: none;
  border-radius: var(--r-full);
  background: currentColor;
}
.btn--live.is-on .live-dot { animation: live-blink 1.6s var(--ease) infinite; }
@keyframes live-blink { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }

.log-scroll {
  height: calc(100vh - var(--bar-h) - 150px);
  min-height: 300px;
  overflow: auto;
  overscroll-behavior: contain;
  background: var(--c-bg-sunken);
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
}
.log-older { padding: var(--sp-2); text-align: center; border-bottom: 1px solid var(--c-border); }

.log-body { padding: var(--sp-1) 0; }

/* 一行日志。grid 而不是 table: 表格在 3000 行时布局要重算整表, grid 每行
   独立。列宽固定, 消息列自己换行。 */
.log-row {
  display: grid;
  grid-template-columns: 68px 52px 96px minmax(0, 1fr);
  gap: var(--sp-2);
  padding: 2px var(--sp-3) 2px calc(var(--sp-3) - 2px);
  border-left: 2px solid transparent;
  font-family: var(--font-mono);
  font-size: var(--fs-12);
  line-height: 1.5;
}
.log-row:hover { background: var(--c-surface); }
.log-row__t { color: var(--c-text-4); white-space: nowrap; }
/* overflow:hidden 是第二道闸: 万一将来冒出个更长的级别名, 它会被切掉而不是
   压到正文上面去(设备端比后端新的时候真的会出现没见过的级别) */
.log-row__l {
  font-weight: var(--fw-medium);
  color: var(--c-text-4);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.log-row__n { color: var(--c-text-4); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.log-row__m {
  color: var(--c-text-2);
  white-space: pre-wrap;
  overflow-wrap: anywhere;   /* 日志里常有一长串没有空格的路径 */
}

.log-row[data-lvl="WARNING"] { border-left-color: var(--c-warn); background: var(--c-warn-bg); }
.log-row[data-lvl="WARNING"] .log-row__l { color: var(--c-warn-text); }
.log-row[data-lvl="WARNING"] .log-row__m { color: var(--c-warn-text); }

.log-row[data-lvl="ERROR"], .log-row[data-lvl="CRITICAL"] {
  border-left-color: var(--c-danger);
  background: var(--c-danger-bg);
}
.log-row[data-lvl="ERROR"] .log-row__l, .log-row[data-lvl="CRITICAL"] .log-row__l,
.log-row[data-lvl="ERROR"] .log-row__m, .log-row[data-lvl="CRITICAL"] .log-row__m { color: var(--c-danger-text); }
.log-row[data-lvl="CRITICAL"] { box-shadow: inset 0 0 0 1px var(--c-danger-border); }

.log-row[data-lvl="DEBUG"] .log-row__m { color: var(--c-text-3); }
.log-row[data-lvl="TAIL"] { border-left-color: var(--c-border-3); }

/* 换 run 的地方插一条分隔 —— "哪一晚"是这个后端的核心索引 */
.log-sep {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  font-size: var(--fs-11);
  letter-spacing: var(--ls-wide);
  color: var(--c-text-4);
}
.log-sep::before, .log-sep::after { content: ""; height: 1px; background: var(--c-border); flex: 1; }

.statusline {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  margin-top: var(--sp-2);
  padding: 0 var(--sp-1);
  font-size: var(--fs-11);
  color: var(--c-text-4);
}
.statusline b { font-family: var(--font-mono); color: var(--c-text-3); }
.statusline .warnish { color: var(--c-warn-text); }


/* ============================================================================
   面板 / 分栏 (连拍 · 命令 · 调参共用)
   ============================================================================ */

.split { display: grid; grid-template-columns: 300px minmax(0, 1fr); gap: var(--sp-3); align-items: start; }
.split--wide { grid-template-columns: 300px minmax(0, 1fr); }

.panel {
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-xs), var(--shadow-edge);
  overflow: hidden;
}
.panel + .panel { margin-top: var(--sp-3); }

/* 标题行右端的删除键。#film-why 是一句长短不定的"为什么拍这组", 不锁
   flex 的话按钮会被它挤扁、位置一屏一个样。margin 不用加 —— .panel__head
   本身就有 gap。 */
.panel__head .btn--destroy { flex: none; margin-left: auto; }

.panel__head {
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--c-border);
}
.panel__title {
  display: flex; align-items: center; gap: 7px;
  font-size: var(--fs-13);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--ls-tight);
  color: var(--c-text);
  min-width: 0;
}
.panel__title .ic { color: var(--c-text-3); }
.panel__sub { font-size: var(--fs-11); color: var(--c-text-4); margin-left: auto; }
.panel__body { padding: var(--sp-4); display: flex; flex-direction: column; gap: var(--sp-3); }

.group-label {
  display: flex; align-items: center; gap: 5px;
  font-size: var(--fs-11);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-wide);
  color: var(--c-text-4);
}
.group-label--danger { color: var(--c-danger-text); }
.btn-row { display: flex; flex-wrap: wrap; gap: 6px; }


/* ============================================================================
   连拍 / 胶片
   ============================================================================ */

.shot-list { max-height: calc(100vh - var(--bar-h) - 130px); overflow: auto; }

.shot-item {
  display: block;
  width: 100%;
  padding: var(--sp-3) var(--sp-4);
  text-align: left;
  background: none;
  border: none;
  border-bottom: 1px solid var(--c-border);
  border-left: 2px solid transparent;
  cursor: pointer;
  color: inherit;
  transition: background-color var(--dur-fast) var(--ease);
}
.shot-item:hover { background: var(--c-surface-2); }
.shot-item.is-on { background: var(--c-surface-3); border-left-color: var(--c-accent); }
.shot-item:focus-visible { outline: none; box-shadow: var(--ring); }

.shot-item__top { display: flex; align-items: center; gap: var(--sp-2); }
.shot-item__tag {
  font-family: var(--font-mono);
  font-size: var(--fs-12);
  font-weight: var(--fw-medium);
  color: var(--c-text);
  min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.shot-item__n {
  margin-left: auto; flex: none;
  padding: 1px 5px;
  font-family: var(--font-mono);
  font-size: var(--fs-11);
  color: var(--c-text-3);
  background: var(--c-surface-3);
  border-radius: var(--r-xs);
}
.shot-item__why {
  margin-top: 3px;
  font-size: var(--fs-12);
  color: var(--c-text-2);
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
  overflow-wrap: anywhere;
}
.shot-item__meta {
  margin-top: 4px;
  display: flex; gap: var(--sp-2); flex-wrap: wrap;
  font-family: var(--font-mono);
  font-size: var(--fs-11);
  color: var(--c-text-4);
}

.panel--stage { display: flex; flex-direction: column; }

.film-stage {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-3);
  background: var(--c-bg-sunken);
}
.film-canvas {
  position: relative;
  flex: 1;
  min-width: 0;
  aspect-ratio: 16 / 9;
  display: grid;
  place-items: center;
  background: #000;
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
  overflow: hidden;
}
#film-img {
  max-width: 100%;
  max-height: 100%;
  display: block;
  /* 图是 1920x1080 缩下来的截图, 放大时不要抹平 —— 要看的正是那些小按钮 */
  image-rendering: -webkit-optimize-contrast;
}
#film-img[hidden] { display: none !important; }

.film-load { position: absolute; inset: 0; display: grid; place-items: center; background: rgba(0,0,0,.4); }
.spin {
  width: 20px; height: 20px;
  border: 2px solid var(--c-border-3);
  border-top-color: var(--c-accent-text);
  border-radius: var(--r-full);
  animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.film-nav {
  flex: none;
  width: 32px; height: 64px;
  font-size: 22px;
  line-height: 1;
  color: var(--c-text-3);
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
  cursor: pointer;
  transition: all var(--dur-fast) var(--ease);
}
.film-nav:hover:not(:disabled) { color: var(--c-text); background: var(--c-surface-3); border-color: var(--c-border-3); }
.film-nav:disabled { opacity: .3; cursor: not-allowed; }
.film-nav:focus-visible { outline: none; box-shadow: var(--ring); }

/* 图上那条说明 —— "点继续后第几秒"就在这里, 是这一页存在的理由 */
.film-cap {
  position: absolute;
  left: calc(32px + var(--sp-3) + var(--sp-2));
  right: calc(32px + var(--sp-3) + var(--sp-2));
  bottom: calc(var(--sp-3) + 8px);
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  padding: 6px 10px;
  font-size: var(--fs-12);
  color: var(--c-text);
  background: rgba(8, 9, 10, .82);
  border: 1px solid var(--c-border-2);
  border-radius: var(--r-sm);
  pointer-events: none;
}
.film-cap b {
  font-family: var(--font-mono);
  font-size: var(--fs-14);
  font-weight: var(--fw-semibold);
  color: var(--c-accent-text);
  letter-spacing: var(--ls-num);
}
.film-cap__idx { margin-left: auto; font-family: var(--font-mono); font-size: var(--fs-11); color: var(--c-text-4); }

.film-thumbs {
  display: flex;
  gap: 5px;
  padding: var(--sp-3);
  overflow-x: auto;
  border-top: 1px solid var(--c-border);
}
.thumb {
  position: relative;
  flex: none;
  width: 86px;
  aspect-ratio: 16 / 9;
  padding: 0;
  background: var(--c-bg-sunken);
  border: 1px solid var(--c-border);
  border-radius: var(--r-xs);
  cursor: pointer;
  overflow: hidden;
  transition: border-color var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.thumb:hover { border-color: var(--c-border-3); }
.thumb.is-on { border-color: var(--c-accent); box-shadow: 0 0 0 1px var(--c-accent); }
.thumb:focus-visible { outline: none; box-shadow: var(--ring); }
.thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.thumb__t {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 1px 3px;
  font-family: var(--font-mono);
  font-size: 9px;
  color: #fff;
  background: rgba(0, 0, 0, .68);
}


/* ============================================================================
   命令
   ============================================================================ */

.cmd-list { max-height: calc(100vh - var(--bar-h) - 130px); overflow: auto; }

.cmd-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 4px var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--c-border);
}
.cmd-row:hover { background: var(--c-surface-2); }

.cmd-row__top { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; min-width: 0; }
.cmd-op {
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  font-weight: var(--fw-medium);
  color: var(--c-text);
}
/* 危险 op 在审计表里也要一眼认出来 */
.cmd-op[data-danger="1"] { color: var(--c-danger-text); }
.cmd-op[data-danger="1"]::before {
  content: "";
  display: inline-block;
  width: 0; height: 0;
  margin-right: 5px;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-bottom: 7px solid currentColor;
}
.cmd-args {
  font-family: var(--font-mono);
  font-size: var(--fs-11);
  color: var(--c-text-4);
  overflow-wrap: anywhere;
}

/* ★ 这里**不能**写 grid-row: 1 / span 2。带确定行号的网格项会在自动布局
   之前先被放置, 于是状态 badge 抢到了第 1 列, 正文被挤到第 2 列并显示成
   右对齐 —— 看起来像"排版坏了"。正文本来就是一个整块 div, 两列足够。 */
.cmd-row__side { display: flex; align-items: flex-start; gap: 6px; }

.cmd-meta {
  display: flex; gap: var(--sp-1) var(--sp-3); flex-wrap: wrap;
  font-family: var(--font-mono);
  font-size: var(--fs-11);
  color: var(--c-text-4);
}
.cmd-meta b { color: var(--c-text-3); font-weight: var(--fw-medium); }

.cmd-result {
  margin-top: 4px;
  padding: 5px 8px;
  font-family: var(--font-mono);
  font-size: var(--fs-11);
  line-height: var(--lh-snug);
  color: var(--c-text-3);
  background: var(--c-bg-sunken);
  border: 1px solid var(--c-border);
  border-radius: var(--r-xs);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-height: 120px;
  overflow: auto;
}
.cmd-result[data-bad="1"] { color: var(--c-danger-text); border-color: var(--c-danger-border); background: var(--c-danger-bg); }

/* 状态 badge: 形状也不一样 —— 完成是实心点, 排队是空心圈, 失败是三角 */
.badge {
  display: inline-flex; align-items: center; gap: 5px;
  height: 20px;
  padding: 0 7px;
  flex: none;
  font-size: var(--fs-11);
  font-weight: var(--fw-medium);
  color: var(--c-text-3);
  background: var(--c-surface-3);
  border: 1px solid var(--c-border);
  border-radius: var(--r-sm);
  white-space: nowrap;
}
.badge::before { content: ""; width: 6px; height: 6px; border-radius: var(--r-full); background: currentColor; flex: none; }
.badge[data-st="done"] { color: var(--c-ok-text); background: var(--c-ok-bg); border-color: var(--c-ok-border); }
.badge[data-st="pending"], .badge[data-st="dispatched"] { color: var(--c-accent-text); background: var(--c-accent-bg); border-color: var(--c-accent-border); }
.badge[data-st="pending"]::before { background: none; box-shadow: inset 0 0 0 2px currentColor; }
.badge[data-st="dispatched"]::before { animation: live-blink 1.2s var(--ease) infinite; }
.badge[data-st="failed"], .badge[data-st="timeout"] {
  color: var(--c-danger-text); background: var(--c-danger-bg); border-color: var(--c-danger-border);
}
.badge[data-st="failed"]::before, .badge[data-st="timeout"]::before {
  width: 0; height: 0; border-radius: 0; background: none;
  border-left: 4px solid transparent; border-right: 4px solid transparent;
  border-bottom: 7px solid currentColor;
}
.badge[data-st="expired"]::before, .badge[data-st="canceled"]::before { width: 8px; height: 2px; border-radius: 1px; }


/* ============================================================================
   调参
   ============================================================================ */

.tune-list { display: flex; flex-direction: column; }

.tune-row {
  display: grid;
  grid-template-columns: minmax(150px, 1.1fr) 108px minmax(0, 1.4fr) auto;
  gap: var(--sp-2) var(--sp-3);
  align-items: center;
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--c-border);
}
.tune-row:hover { background: var(--c-surface-2); }
/* 改过的那几行要看得出来: 一根主色竖条 —— "哪些不是默认值"是这一页最重要的信息 */
.tune-row[data-over="1"] { box-shadow: inset 2px 0 0 var(--c-accent); }
.tune-row.is-dirty { background: var(--c-accent-bg); box-shadow: inset 2px 0 0 var(--c-accent-text); }

.tune-key {
  font-family: var(--font-mono);
  font-size: var(--fs-12);
  font-weight: var(--fw-medium);
  color: var(--c-text);
  overflow-wrap: anywhere;
}
.tune-key small {
  display: block;
  margin-top: 2px;
  font-family: var(--font-sans);
  font-size: var(--fs-11);
  font-weight: var(--fw-normal);
  color: var(--c-text-4);
  letter-spacing: var(--ls-wide);
}
.tune-input { width: 100%; font-family: var(--font-mono); text-align: right; }
.tune-input[data-bad="1"] { border-color: var(--c-danger-border); color: var(--c-danger-text); }

.tune-why {
  font-size: var(--fs-12);
  line-height: var(--lh-snug);
  color: var(--c-text-3);
  overflow-wrap: anywhere;
}
.tune-range {
  display: block;
  margin-top: 2px;
  font-family: var(--font-mono);
  font-size: var(--fs-11);
  color: var(--c-text-4);
}
.tune-row__act { display: flex; gap: 4px; justify-content: flex-end; }

.hist-list { max-height: 340px; overflow: auto; }
.hist-row {
  display: flex; align-items: baseline; gap: var(--sp-2); flex-wrap: wrap;
  padding: 7px var(--sp-4);
  border-bottom: 1px solid var(--c-border);
  font-size: var(--fs-12);
}
.hist-row__k { font-family: var(--font-mono); font-weight: var(--fw-medium); color: var(--c-text); }
.hist-row__v { font-family: var(--font-mono); color: var(--c-text-3); }
.hist-row__v b { color: var(--c-accent-text); }
.hist-row__why { color: var(--c-text-2); overflow-wrap: anywhere; }
.hist-row__meta { margin-left: auto; font-family: var(--font-mono); font-size: var(--fs-11); color: var(--c-text-4); }


/* ============================================================================
   空 / 错 / 骨架 —— 三个都要设计过, "暂无数据"是没设计过的写法
   ============================================================================ */

.pane-state, .dev-empty {
  grid-column: 1 / -1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-10) var(--sp-4);
  text-align: center;
}
.pane-state__icon, .dev-empty__icon {
  width: 40px; height: 40px;
  display: grid; place-items: center;
  margin-bottom: var(--sp-1);
  color: var(--c-text-4);
  background: var(--c-surface-3);
  border-radius: var(--r-md);
}
.pane-state__icon .ic, .dev-empty__icon .ic { width: 20px; height: 20px; }
.pane-state__t, .dev-empty__t { font-size: var(--fs-14); font-weight: var(--fw-semibold); color: var(--c-text); }
.pane-state__d, .dev-empty__d {
  max-width: 48ch;
  font-size: var(--fs-12);
  line-height: var(--lh-normal);
  color: var(--c-text-3);
}
.pane-state .btn, .dev-empty .btn { margin-top: var(--sp-2); }

.dev-empty {
  background: var(--c-surface);
  border: 1px dashed var(--c-border-2);
  border-radius: var(--r-lg);
}

/* 错误态: 同一个壳换个色, 不是另做一套 */
.pane-state--err .pane-state__icon { color: var(--c-danger-text); background: var(--c-danger-bg); }
.pane-state--err .pane-state__t { color: var(--c-danger-text); }

.dev-error {
  grid-column: 1 / -1;
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-4);
  font-size: var(--fs-12);
  color: var(--c-danger-text);
  background: var(--c-danger-bg);
  border: 1px solid var(--c-danger-border);
  border-radius: var(--r-lg);
}
.dev-error .btn { margin-left: auto; }

/* 骨架: 呼吸, 不用渐变扫光 —— 扫光是一条常驻的大面积重绘 */
.dev-card.is-skeleton { pointer-events: none; }
.sk {
  display: block;
  height: 10px;
  border-radius: var(--r-xs);
  background: var(--c-surface-3);
  animation: sk 1.4s var(--ease) infinite;
}
.sk--t { height: 16px; width: 42%; }
.sk--n { height: 26px; }
@keyframes sk { 50% { opacity: .42; } }


/* ============================================================================
   弹层 / 提示条
   ============================================================================ */

.modal {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: grid;
  place-items: center;
  padding: var(--sp-4);
}
.modal__scrim { position: absolute; inset: 0; background: var(--c-scrim); animation: fade var(--dur) var(--ease) both; }
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }

.modal__box {
  position: relative;
  width: 100%;
  max-width: 420px;
  max-height: calc(100vh - var(--sp-8));
  overflow: auto;
  padding: var(--sp-5);
  background: var(--c-surface);
  border: 0.5px solid var(--c-border-2);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-lg), var(--shadow-edge);
  animation: modal-in var(--dur) var(--ease-out) both;
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .modal__box {
    background: var(--mat-panel);
    -webkit-backdrop-filter: var(--blur);
    backdrop-filter: var(--blur);
  }
}
/* Apple 的弹层是"从中心稍微放大着淡入", 不是从下往上推 */
@keyframes modal-in { from { opacity: 0; transform: scale(.96); } to { opacity: 1; transform: none; } }

.modal__title { font-size: var(--fs-16); font-weight: var(--fw-semibold); letter-spacing: var(--ls-tight); }
.modal__body {
  margin-top: var(--sp-3);
  display: flex; flex-direction: column; gap: var(--sp-3);
  font-size: var(--fs-13);
  line-height: var(--lh-normal);
  color: var(--c-text-2);
}
.modal__body code { overflow-wrap: anywhere; }
.modal__acts { display: flex; justify-content: flex-end; gap: 6px; margin-top: var(--sp-5); }
.modal__acts .btn { height: 30px; padding: 0 13px; }

/* 不可逆操作的说明块 */
.modal-warn {
  display: flex; gap: 8px;
  padding: 9px 11px;
  font-size: var(--fs-12);
  line-height: var(--lh-snug);
  color: var(--c-danger-text);
  background: var(--c-danger-bg);
  border: 1px solid var(--c-danger-border);
  border-radius: var(--r-sm);
}
.modal-warn::before {
  content: ""; flex: none;
  width: 0; height: 0; margin-top: 3px;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-bottom: 9px solid currentColor;
}

.toasts {
  position: fixed;
  right: var(--sp-4);
  bottom: var(--sp-4);
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-width: min(380px, calc(100vw - var(--sp-8)));
  pointer-events: none;
}
.toast {
  display: flex; align-items: flex-start; gap: 8px;
  padding: 9px 12px;
  font-size: var(--fs-12);
  line-height: var(--lh-snug);
  color: var(--c-text);
  background: var(--c-surface-2);
  border: 1px solid var(--c-border-2);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-md), var(--shadow-edge);
  animation: toast-in var(--dur) var(--ease-out) both;
  overflow-wrap: anywhere;
  pointer-events: auto;
}
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.toast.is-out { animation: toast-out var(--dur) var(--ease) both; }
@keyframes toast-out { to { opacity: 0; transform: translateY(4px); } }
.toast::before { content: ""; flex: none; width: 3px; align-self: stretch; border-radius: 2px; background: var(--c-text-4); }
.toast[data-kind="ok"]::before   { background: var(--c-ok); }
.toast[data-kind="err"]::before  { background: var(--c-danger); }
.toast[data-kind="warn"]::before { background: var(--c-warn); }


/* ============================================================================
   响应式 —— 他会在手机上看"机器人还在跑吗", 一屏就要给出答案
   ============================================================================ */

@media (max-width: 900px) {
  .split, .split--wide { grid-template-columns: 1fr; }
  .shot-list { max-height: 240px; }
  .cmd-list { max-height: none; }
}

@media (max-width: 700px) {
  :root { --bar-h: 48px; }

  #bar { padding: 0 var(--sp-2); gap: var(--sp-2); flex-wrap: wrap; height: auto; padding-bottom: var(--sp-2); }
  .bar__brand { order: 0; }
  .bar__right { order: 1; }
  .bar__nav { order: 2; width: 100%; }
  .tab { flex: 1; }
  .bar__name { display: none; }
  .devpick__label { display: none; }
  #devpick { max-width: 120px; }

  #app { padding: var(--sp-2); }

  .dev-grid { grid-template-columns: 1fr; gap: var(--sp-2); }
  .dev-card { padding: var(--sp-3); gap: var(--sp-3); }
  .dev-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: var(--sp-3); }
  .metric:nth-child(3) { padding-left: 0; border-left: none; }
  .metric:nth-child(2) { padding-right: 0; }
  .dev-acts {
    margin: 0 calc(var(--sp-3) * -1) calc(var(--sp-3) * -1);
    padding: var(--sp-2) var(--sp-3);
  }
  .dev-card__ribbon { margin: 0 calc(var(--sp-3) * -1) var(--sp-1); padding: var(--sp-2) var(--sp-3); }
  .dev-meta { gap: 2px var(--sp-2); }

  /* 手指点得中。删除单独占一行, 不跟别的挤在一起 */
  .btn { height: 32px; padding: 0 12px; }
  .acts-group { flex: 1 1 100%; }
  .acts-group--danger { margin-left: 0; }
  .acts-group .btn { flex: 1 1 auto; }
  .btn--destroy { flex-basis: 100%; }
  /* 上面那条"删除单独占一行"是给设备卡的动作区写的(手指点得中)。标题行里
     只有这一个删除键, 撑满整行反而把标题挤到上一行 —— 退回自动宽度。 */
  .panel__head .btn--destroy { flex-basis: auto; }

  .log-scroll { height: calc(100vh - 240px); }
  .log-row { grid-template-columns: 58px 44px minmax(0, 1fr); }
  .log-row__n { display: none; }   /* logger 名在手机上不值一列 */

  .tune-row { grid-template-columns: minmax(0, 1fr) 96px; }
  .tune-why { grid-column: 1 / -1; }
  .tune-row__act { grid-column: 2; justify-content: flex-end; }

  .film-nav { width: 26px; }
  .film-cap { left: var(--sp-3); right: var(--sp-3); bottom: var(--sp-3); }

  .toasts { left: var(--sp-2); right: var(--sp-2); bottom: var(--sp-2); max-width: none; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
  .btn:active { transform: none; }
}


/* ============================================================================
   Apple 化的收尾 —— 放在最后, 覆盖前面按老尺度写的那些值
   ============================================================================ */

/* 发丝线: 高分屏上画成物理 0.5px, 普通屏退回 1px。
   Apple 的分隔线就是这个厚度, 1px 在 2x 屏上看起来偏重。 */
@media (min-resolution: 2dppx) {
  .card, .panel, .dev-card, .modal__box, .toolbar,
  .log-row, .shot-item, .tune-row, .cmd-row {
    border-width: 0.5px;
  }
}

/* SF 的字重在中文上会偏细, 所以标题类统一提一档。
   (苹方没有 SF 那么多字重, medium 在它上面就是"看得出是标题") */
h1, h2, h3, .panel__title, .modal__title, .dev-card__name {
  font-weight: var(--fw-semibold);
  letter-spacing: var(--ls-tight);
}

/* Apple 的滚动条在不滚时是隐形的 —— 面板一直挂着, 常驻滚动条很吵 */
* { scrollbar-width: thin; scrollbar-color: var(--c-border-3) transparent; }
*::-webkit-scrollbar { width: 8px; height: 8px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
  background: var(--c-border-2);
  border-radius: var(--r-full);
  border: 2px solid transparent;
  background-clip: content-box;
}
*::-webkit-scrollbar-thumb:hover { background: var(--c-border-3); background-clip: content-box; }

/* ★ 减弱动效: Apple 系统设置里那个开关, 尊重它。
   而且这面板有 3 秒一刷的告警闪烁, 对前庭敏感的人是实打实的问题。 */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}

/* ---------- 卡密 ---------- */

/* 卡密是这一页的**主体**, 不像"改动记录"是附属 —— 340px 的封顶会让
   卖到二三十张时永远在一个小窗里翻 */
#lic-list { max-height: none; }
.lic-row__acts { display: flex; gap: var(--sp-2); }

/* 作废的那几行压暗但不藏起来 —— 记录是留着给纠纷用的 */
.hist-row.is-dead { opacity: .5; }
.hist-row.is-dead .hist-row__k { text-decoration: line-through; }

/* 绑满了的标红。这是"想再卖一台得先解绑"的唯一提示 */
.hist-row__v b.is-warn { color: var(--c-warn-text); }

/* ★ 生成结果的那个框。等宽 + 可全选, 因为它的唯一用途是**抄走**:
   明文只出现这一次, 抄不利索就等于卡废了。 */
.lic-codes {
  width: 100%;
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  line-height: 1.7;
  letter-spacing: .02em;
  resize: vertical;
  background: var(--c-surface-3);
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
  padding: 10px 12px;
  color: var(--c-text);
}


/* ════════════════════════════════════════════════════════════════
   侧边栏 + 壳
   ════════════════════════════════════════════════════════════════

   顶栏那排横着的标签在六项时就已经挤了, 而这份面板往下还要加代理、
   流水、审计。竖排能装, 而且能**分组** —— "运营 / 分销 / 诊断"这几组
   本身就是信息, 横排给不了。

   ★ 侧栏是 position: sticky 不是 fixed: fixed 会脱离文档流, 主区就得
     自己留 padding-left, 而那个数字和侧栏宽度是两处要同步的东西 ——
     迟早对不上。grid + sticky 只有一处宽度。 */
#shell {
  display: grid;
  grid-template-columns: var(--side-w) minmax(0, 1fr);
  align-items: start;
}

#side {
  position: sticky;
  top: var(--bar-h);
  height: calc(100vh - var(--bar-h));
  display: flex;
  flex-direction: column;
  padding: var(--sp-3) 0 var(--sp-2);
  border-right: 0.5px solid var(--c-border);
  background: var(--c-bg-sunken);
  overflow-y: auto;
  scrollbar-width: thin;
}

/* 顶栏里已经有一次品牌了, 侧栏这个只在顶栏放不下时(窄屏)才露出来 */
.side__brand { display: none; }

.side__group {
  margin: var(--sp-3) 0 3px;
  padding: 0 var(--sp-3);
  font-size: var(--fs-10);
  font-weight: var(--fw-semibold);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--c-text-4);
}
.side__group:first-child { margin-top: 0; }
/* 按软件画出来的那几组(app.js renderSideProjects)包在一个 div 里 ——
   contents 让它们照样是 nav 这个 flex 列的直接成员(间距、窄屏规则都跟着走),
   而且组标题不会被上面那条 :first-child 吃掉上边距 */
#side-projects { display: contents; }
#side-projects > .side__group:first-child { margin-top: var(--sp-3); }

.side__nav { display: flex; flex-direction: column; gap: 1px; padding: 0 var(--sp-2); }

/* ★ 复用 .tab 这个类名不是偷懒 —— app.js 的 go() 和 init() 都是按
   `.tab[data-view]` 找的。换个名字就要同步改两处 JS, 而那正是最容易
   漏的一处(顶栏改侧栏时 badge 那段就差点漏了)。 */
#side .tab {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  height: 30px;
  padding: 0 var(--sp-2);
  border-radius: var(--r-sm);
  text-align: left;
  font-size: var(--fs-12);
}
#side .tab > span { flex: 1 1 auto; min-width: 0; }
#side .tab .ic { width: 15px; height: 15px; flex: none; opacity: .85; }
#side .tab.is-on .ic { opacity: 1; }

/* 待确认设备的红点。顶栏那版是 ::after 绝对定位, 侧栏这里改成一个
   真节点 —— 竖排有横向空间, 直接把数字写出来比一个点有用。 */
.side__n {
  flex: none;
  min-width: 17px;
  height: 17px;
  padding: 0 5px;
  border-radius: 9px;
  font: var(--fw-semibold) var(--fs-10) / 17px var(--font-mono);
  text-align: center;
  color: var(--c-warn-text);
  background: var(--c-warn-bg);
}
.side__n:empty { display: none; }

.side__foot {
  margin-top: auto;
  padding: var(--sp-2) var(--sp-3) 0;
  border-top: 0.5px solid var(--c-border);
  font: var(--fs-10) / 1.5 var(--font-mono);
  color: var(--c-text-4);
  white-space: pre-line;
}

/* 窄屏: 侧栏塌成一条图标栏 */
@media (max-width: 860px) {
  #shell { grid-template-columns: 52px minmax(0, 1fr); }
  #side .tab > span, .side__group, .side__foot { display: none; }
  #side .tab { justify-content: center; padding: 0; }
  .side__n { position: absolute; margin-left: 20px; margin-top: -14px; }
}


/* ════════════════════════════════════════════════════════════════
   概览
   ════════════════════════════════════════════════════════════════ */

/* ★ 用显式的 4 列, 不用 auto-fit。
   auto-fit + minmax(190px) 在 660px 宽的主区里只排得下 3 个, 第 4 个
   独占一行并被拉成整宽 —— 四个同级的数字长得不一样大, 人会以为那个
   最大的更重要。四个大数就该是四等份。 */
.kpis {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--sp-3);
  margin-bottom: var(--sp-4);
}
@media (max-width: 1080px) { .kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 520px)  { .kpis { grid-template-columns: minmax(0, 1fr); } }

.kpi {
  padding: var(--sp-3) var(--sp-4);
  background: var(--c-surface);
  border: 0.5px solid var(--c-border);
  border-radius: var(--r-lg);
}
.kpi__l { font-size: var(--fs-11); color: var(--c-text-3); margin-bottom: 6px; }
.kpi__v {
  font: var(--fw-semibold) 30px / 1.05 var(--font-mono);
  letter-spacing: -.02em;
  color: var(--c-text);
}
.kpi__s {
  margin-top: 7px;
  display: flex;
  flex-wrap: wrap;
  gap: 3px 12px;
  font-size: var(--fs-11);
  color: var(--c-text-3);
}
.kpi__s b { font-weight: var(--fw-medium); color: var(--c-text-2); }

/* 状态小圆点 —— 在线绿 / 离线灰 / 待确认黄 / 停用红 */
.dot { display: inline-flex; align-items: center; gap: 5px; }
.dot::before { content: ""; width: 6px; height: 6px; border-radius: 50%; flex: none; background: currentColor; }
.dot--ok   { color: var(--c-ok-text); }
.dot--warn { color: var(--c-warn-text); }
.dot--bad  { color: var(--c-danger-text); }
.dot--off  { color: var(--c-text-4); }

.ov-row {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
  gap: var(--sp-3);
}
@media (max-width: 900px) { .ov-row { grid-template-columns: minmax(0, 1fr); } }

/* ---- 要你处理 ---- */
.todo { display: flex; flex-direction: column; }
.todo__i {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 11px var(--sp-4);
  border-bottom: 0.5px solid var(--c-border);
  font-size: var(--fs-12);
  color: var(--c-text-2);
}
.todo__i:last-child { border-bottom: 0; }
.todo__bar { width: 4px; align-self: stretch; min-height: 22px; border-radius: 2px; flex: none; }
.todo__i[data-kind="warn"] .todo__bar { background: var(--c-warn-text); }
.todo__i[data-kind="bad"]  .todo__bar { background: var(--c-danger-text); }
.todo__n { font: var(--fw-semibold) var(--fs-14) var(--font-mono); color: var(--c-text); }
.todo__go { margin-left: auto; flex: none; }
.todo__ok {
  padding: 26px var(--sp-4);
  text-align: center;
  font-size: var(--fs-12);
  color: var(--c-text-3);
}

/* ---- 负载条 ---- */
.gauges { padding: var(--sp-3) var(--sp-4); display: flex; flex-direction: column; gap: 11px; }
.g {
  display: grid;
  /* 值那一列要放得下 "178 GB/228 GB" —— 66px 会折成两行, 而一条负载
     占两行之后整块的节奏就乱了。条子那列是弹性的, 让它去挤。 */
  grid-template-columns: 60px minmax(40px, 1fr) 104px;
  align-items: center;
  gap: 10px;
  font-size: var(--fs-12);
}
.g__n { color: var(--c-text-3); }
.g__bar { height: 6px; border-radius: 3px; background: var(--c-surface-2); overflow: hidden; }
.g__fill { height: 100%; border-radius: 3px; background: var(--c-accent); transition: width var(--dur) ease; }
.g__fill[data-lvl="warn"] { background: var(--c-warn-text); }
.g__fill[data-lvl="bad"]  { background: var(--c-danger-text); }
.g__v {
  text-align: right;
  font: var(--fs-11) / 1.3 var(--font-mono);
  color: var(--c-text-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* 取不到的项: 条是空的, 值显示 "—"。**不画成 0** —— 0 看起来像"很闲" */
.g__bar[data-none="1"] { opacity: .35; }


/* ════════════════════════════════════════════════════════════════
   设备表格
   ════════════════════════════════════════════════════════════════

   卡片换成表格是因为"列表显示数据全": 卡片一屏放得下六张, 而一千台
   的时候人要的是扫一眼比对, 那是表格的活。

   卡片没有被删掉 —— 点一行就在下面展开原来那张卡(devCard 原封不动)。
   表格用来**看**, 卡片用来**动手**。 */

.dtbar {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
  margin-bottom: var(--sp-3);
}
.search--sm { flex: 0 1 240px; min-width: 150px; }

.dtwrap {
  background: var(--c-surface);
  border: 0.5px solid var(--c-border);
  border-radius: var(--r-lg);
  overflow: auto;
  max-height: calc(100vh - 260px);
}

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

table.dt th {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: 9px 12px;
  text-align: left;
  white-space: nowrap;
  font: var(--fw-medium) var(--fs-10) / 1 var(--font-mono);
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--c-text-4);
  background: var(--c-surface);
  border-bottom: 0.5px solid var(--c-border-2);
}
/* 分组之间加一条竖发丝 —— 二十列平铺时人分不清哪几列是一伙的 */
table.dt th[data-first="1"], table.dt td[data-first="1"] {
  border-left: 0.5px solid var(--c-border);
}

table.dt td {
  padding: 8px 12px;
  white-space: nowrap;
  color: var(--c-text-2);
  border-bottom: 0.5px solid var(--c-border);
}
table.dt tbody tr:last-child > td { border-bottom: 0; }
table.dt tbody tr.dt__row { cursor: pointer; }
table.dt tbody tr.dt__row:hover > td { background: var(--c-surface-2); }
table.dt tbody tr.dt__row.is-open > td { background: var(--c-surface-2); }

.dt__name { color: var(--c-text); font-weight: var(--fw-medium); }
.dt__note { color: var(--c-text-4); font-size: var(--fs-11); }
.dt__m { font-family: var(--font-mono); }
.dt__r { text-align: right; font-family: var(--font-mono); }
.dt__dim { color: var(--c-text-4); }

/* 展开的详情行 —— 里面就是原来那张卡 */
.dt__detail > td { padding: 0; background: var(--c-bg-sunken); }
.dt__detail .dev-card { border-radius: 0; border-left: 0; border-right: 0; margin: 0; }

/* 展开箭头那一列。宽度写在这儿而不是 style 属性上 —— 面板的 CSP
   是 style-src 'self', 属性会被拦掉(见 app.js 的 gauge 那段)。 */
table.dt th.dt__caretcol { width: 22px; }

.dt__caret { display: inline-block; width: 10px; color: var(--c-text-4); transition: transform var(--dur); }
tr.is-open .dt__caret { transform: rotate(90deg); }

.dt__empty { padding: 40px var(--sp-4); text-align: center; color: var(--c-text-3); font-size: var(--fs-12); }


/* ══════════════════════════════════════════════════════════════
   门上的两种进法
   ══════════════════════════════════════════════════════════════ */

.gate-tabs {
  display: flex;
  gap: 2px;
  padding: 2px;
  margin-bottom: var(--sp-4);
  background: var(--c-surface-2);
  border-radius: var(--r-md);
}

.gate-tab {
  flex: 1;
  height: 30px;
  font: var(--fw-medium) var(--fs-12) / 1 var(--font-sans);
  color: var(--c-text-3);
  background: transparent;
  border: 0;
  border-radius: calc(var(--r-md) - 2px);
  cursor: pointer;
  transition: all var(--dur-fast) var(--ease);
}
.gate-tab:hover { color: var(--c-text-2); }
.gate-tab.is-on {
  color: var(--c-text);
  background: var(--c-surface);
  box-shadow: 0 1px 2px rgba(0, 0, 0, .25);
}
.gate-tab:focus-visible { outline: none; box-shadow: var(--ring); }


/* ══════════════════════════════════════════════════════════════
   顶栏: 我是谁

   多级代理下这一条是必需的 —— 同一个面板, 不同的人看到的数字完全不同,
   而"我现在以谁的身份在看"必须一眼可见。
   ══════════════════════════════════════════════════════════════ */

.who {
  display: inline-flex;
  flex-direction: column;
  gap: 1px;
  padding: 0 var(--sp-2) 0 var(--sp-3);
  line-height: 1.15;
  border-left: 0.5px solid var(--c-border);
}
.who__name { font: var(--fw-medium) var(--fs-12) / 1.15 var(--font-sans); color: var(--c-text); }
.who__org { font: var(--fs-10) / 1.15 var(--font-mono); color: var(--c-text-4); }


/* ══════════════════════════════════════════════════════════════
   代理树
   ══════════════════════════════════════════════════════════════ */

/* 缩进条。**宽度由 JS 用 CSSOM 设**, 不是 style="" 属性 ——
   面板的 CSP 是 style-src 'self', 属性会被静默丢掉。 */
.ag-ind { display: inline-block; height: 1px; vertical-align: middle; }

.ag-name { color: var(--c-text); font-weight: var(--fw-medium); }

.ag-acts { display: flex; gap: 6px; justify-content: flex-end; }

table.dt td.num, table.dt th.num {
  text-align: right;
  font-family: var(--font-mono);
}

/* 冻结的整行压暗 —— 一眼看出这条线是停着的, 而不用逐行读状态列 */
table.dt tbody tr.is-frozen > td { opacity: .55; }

.tag {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 5px;
  font: var(--fw-medium) var(--fs-10) / 1.4 var(--font-mono);
  border-radius: var(--r-sm);
}
.tag--me {
  color: var(--c-accent-text);
  background: var(--c-accent-bg);
  border: 0.5px solid var(--c-accent-border);
}

.pill {
  display: inline-block;
  padding: 2px 7px;
  font: var(--fw-medium) var(--fs-11) / 1.4 var(--font-sans);
  border-radius: 999px;
}
.pill--ok  { color: var(--c-ok-text);     background: var(--c-ok-bg);     border: 0.5px solid var(--c-ok-border); }
.pill--bad { color: var(--c-danger-text); background: var(--c-danger-bg); border: 0.5px solid var(--c-danger-border); }


/* ══════════════════════════════════════════════════════════════
   流水
   ══════════════════════════════════════════════════════════════ */

.led-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--sp-3);
  margin-bottom: var(--sp-4);
}

.card--num { padding: var(--sp-4); }
.card__k {
  margin: 0 0 6px;
  font: var(--fw-medium) var(--fs-10) / 1 var(--font-mono);
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--c-text-4);
}
.card__v {
  margin: 0;
  font: var(--fw-semibold) var(--fs-20) / 1.1 var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--c-text);
}

/* ★ 对账对不上要显眼。它意味着某处改了余额却没记流水(或者反过来) ——
   那种错今天不报警, 半年后才在对账时显形, 而那时已经查不出是哪一笔。 */
#led-ok-card.is-bad {
  border-color: var(--c-danger-border);
  background: var(--c-danger-bg);
}
#led-ok-card.is-bad .card__v { color: var(--c-danger-text); }

/* ★ 选择器要写成 `table.dt td.led-up` 而不是 `.led-up`。
   `table.dt td { color: var(--c-text-2) }` 的特异性是 (0,1,2), 光一个类
   是 (0,1,0) —— 压不过它, 于是正负数一个颜色。
   实机上就是这么发现的: 屏幕上 +500 和 -30 都是灰的, 而 CSS 里明明写着
   绿色, 控制台也没有任何异常。 */
table.dt td.led-up   { color: var(--c-ok-text); }
table.dt td.led-down { color: var(--c-text-2); }
table.dt td.led-note { white-space: normal; color: var(--c-text-4); max-width: 280px; }

.lic-cost {
  font: var(--fs-12) / 1.5 var(--font-sans);
  color: var(--c-text-3);
}


/* ══════════════════════════════════════════════════════════════
   多项目(按软件分)之后加的: 卡密表 / simple 设备表 / 概览按软件
   ══════════════════════════════════════════════════════════════ */

/* 分段按钮里的计数 —— 点了"未激活"之后别的按钮上的数也还在 */
.seg__n { margin-left: 3px; font-family: var(--font-mono); font-size: var(--fs-11); color: var(--c-text-4); }
.seg__n:empty { display: none; }
.seg__b.is-on .seg__n { color: inherit; opacity: .8; }

.search--lic { flex: 1 1 280px; max-width: 420px; min-width: 180px; }

.lic-bulk { display: flex; align-items: center; gap: var(--sp-2); }
.lic-bulk[hidden] { display: none; }
.lic-bulk__n { font-size: var(--fs-12); color: var(--c-accent-text); }

table.dt th.dt__chk, table.dt td.dt__chk { width: 30px; padding-right: 0; }
table.dt th.dt__acts, table.dt td.dt__acts { text-align: right; }
td.dt__acts .btn + .btn, td.dt__acts span .btn + .btn { margin-left: 4px; }

.lic-row.is-dead > td { opacity: .55; }
.lic-row.is-dead .lic-hint { text-decoration: line-through; }
.lic-hint { color: var(--c-text); font-weight: var(--fw-medium); letter-spacing: .03em; }
/* 备注列是唯一允许折行的一列。min-width 不能少: 表格被压窄时浏览器会先
   挤这一列, 挤到一个字一行 */
.lic-note { white-space: normal !important; min-width: 140px; max-width: 280px;
            overflow-wrap: anywhere; color: var(--c-text-2); }
.lic-bind .is-warn { color: var(--c-warn-text); }
.lic-devs { display: flex; flex-wrap: wrap; gap: 2px 10px; margin-top: 3px; font-size: var(--fs-11); }

.lic-more { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
            margin-top: var(--sp-2); font-size: var(--fs-12); }

.lic-detail { padding: var(--sp-3) var(--sp-4); color: var(--c-text-3); font-size: var(--fs-12); }
table.lic-hist { border-collapse: collapse; width: 100%; }
table.lic-hist th { text-align: left; font-weight: var(--fw-medium); color: var(--c-text-4);
                    font-size: var(--fs-11); padding: 4px 10px 6px 0; }
table.lic-hist td { padding: 5px 10px 5px 0; border-top: 0.5px solid var(--c-border);
                    color: var(--c-text-2); vertical-align: top; }

.lic-codes__acts { display: flex; gap: var(--sp-2); margin-top: var(--sp-2); }

table.dt--pdev tbody tr:hover > td { background: var(--c-surface-2); }
table.dt--lic td, table.dt--pdev td { vertical-align: top; }

/* 概览: 按软件 */
.ov-proj { display: flex; flex-direction: column; }
.ov-proj__row {
  appearance: none; border: 0; background: none; font: inherit; color: inherit; cursor: pointer;
  display: grid; grid-template-columns: minmax(90px, 1fr) minmax(110px, 1fr) minmax(160px, 2fr);
  gap: var(--sp-3); align-items: center; text-align: left;
  padding: 9px var(--sp-2); border-radius: var(--r-md);
  font-size: var(--fs-12); color: var(--c-text-2);
}
.ov-proj__row + .ov-proj__row { border-top: 0.5px solid var(--c-border); }
.ov-proj__row:hover { background: var(--c-surface-2); }
.ov-proj__row:focus-visible { outline: none; box-shadow: var(--ring); }
.ov-proj__name { color: var(--c-text); font-weight: var(--fw-medium); }
.ov-proj__cell b { color: var(--c-text); font-family: var(--font-mono); }
.ov-kpis-gap { margin-top: var(--sp-3); }

@media (max-width: 640px) {
  .ov-proj__row { grid-template-columns: 1fr; gap: 2px; }
  .search--lic { max-width: none; }
}

/* 绑定历史是嵌在 table.dt 里的一张小表 —— 外面那几条 table.dt th/td 规则
   (吸顶、底色、大写字距)会漏进来, 这里摆回普通样式 */
table.dt table.lic-hist th {
  position: static; background: none; border-bottom: 0;
  text-transform: none; letter-spacing: 0;
  font: var(--fw-medium) var(--fs-11) / 1.4 var(--font-sans);
  padding: 4px 12px 6px 0;
}
table.dt table.lic-hist td { padding: 6px 12px 6px 0; border-bottom: 0; white-space: nowrap; }


/* 账号详情里的小节标题(设备 / 充值记录 / 云端配置) */
.acc-sec { margin: 10px 0 4px; font-size: var(--fs-11); font-weight: var(--fw-semibold);
           color: var(--c-text-3); letter-spacing: .04em; }
.acc-sec:first-child { margin-top: 0; }
.lic-bind b { color: var(--c-text); }
/* 看云端配置的那个框: 宽一点, 等宽小字 */
.cfg-view { font-size: var(--fs-12); line-height: 1.5; min-height: 280px; }
.acc-note { color: var(--c-warn-text); margin-top: 4px; }

.modal--wide .modal__box { width: min(1200px, calc(100vw - 32px)); max-width: none; }
.modal--wide .modal__body { overflow-x: auto; }

.ceh-image { display: block; max-width: 100%; height: auto; object-fit: contain; }
#ceh-results { margin-top: 16px; }
