/* agent-team —— 视觉规范
 *
 * 这个页面只有一个任务：让机主一眼看出**哪个 cc 需要他管**。
 * 所以布景一律安静，唯一张扬的东西是「时间环」——每个 cc 的环填充度 =
 * 已持续 / 卡住阈值，编码的正是这块看板唯一重要的那个量：离"卡住"还有多远。
 * 它不是装饰：环满了就该去看那个 cc。
 *
 * 配色用 Apple 系统语义色的原值（机主在 Mac + iPhone 上看，渲染出来就是原生观感）。
 * 字体走 -apple-system：在他的设备上是真 SF，在 box 的浏览器里优雅回退。
 */

/* ⚠️⚠️ 全局兜底：`hidden` 属性必须永远赢。
 *
 * UA 样式表里的 `[hidden] { display: none }` 优先级**低于任何作者样式**，
 * 所以只要给元素写了 `display: block/flex/grid`，JS 里的 `el.hidden = true`
 * 就完全失效 —— 元素照常显示，**没有任何报错**。
 *
 * 这个坑在本项目连踩三次：抄送勾选框 → 回到底部箭头 → 长消息「展开全文」。
 * 前两次都是逐个补 `.xxx[hidden]` 规则，但那要求每加一个新组件都记得补一条，
 * 第三次就是这么漏的（连专门写的回归测试都只钉了前两个硬编码选择器）。
 * 一条全局 !important 从机制上根除，不再依赖记性。
 *
 * 它也很难被测出来：`el.hidden` 读的是 DOM 属性，设对了就返回 true，
 * 看上去一切正常 —— 必须查**计算样式**。
 */
[hidden] { display: none !important; }

:root {
  color-scheme: dark;

  /* 底与面：不是纯黑，是分层的半透明材质 */
  /* ⚠️⚠️ `--ink` 是**底色的黑**，不是文字色！深色下 #000、浅色下等于 --canvas。
     主文字色是 `--label`（深色 #f5f5f7 / 浅色 #1d1d1f）。
     2026-08-07 我在监控页和附件里 4 处拿 --ink 当文字色，深色下算成纯黑 ——
     机主看到的是"数据被挖掉了"：值确实渲染了，只是黑字压在黑底上。
     名字骗人：ink 听起来像墨水=文字，这里指的是纸底下那层黑。 */
  --ink:        #000000;
  --canvas:     #0b0b0d;
  --surface-1:  rgba(255, 255, 255, 0.045);
  --surface-2:  rgba(255, 255, 255, 0.075);
  /* ⚠️ 上面那几个 surface 都是**半透明**的（分层材质，故意的）。
     浮在图表上方的提示框不能用它们 —— 曲线会从字底下穿过去（机主 2026-08-07 提）。
     这两个是配套的**不透明**值：--pop 是浮层底，--card-solid 是卡片底色压平之后
     的等效实色（用于把画在图内的轴标签从曲线上"抠"出来）。 */
  /* 机主气泡的蓝。⚠️ **保持和 --blue 一致，是机主 2026-08-07 明确选的**：
     我先把它调深到 #0a6ede（白字对比度 3.65 → 4.89，够 WCAG AA），
     他看完说"颜色恢复一下，就用之前的蓝色"。**他的选择优先，别再自作主张调深。**
     ⚠️ 代价如实记在这儿：白字压在 #0a84ff 上是 3.65，低于 AA 的 4.5。
        但那次真正让他看不清的三样 —— 链接、截断提示、行内 code —— 是**灰字**问题，
        已经单独修掉了（见文件末尾"机主气泡里的对比度"那一节），不依赖底色变深。
     ⚠️ 留着这个令牌而不是写回 var(--blue)：气泡和按钮/链接/流光是**两个方向相反的用途**
        （前者是白字压蓝，后者是蓝字压深底），将来要动其中一个不该牵连另一个。 */
  --bubble:     #0a84ff;
  --pop:        #1e1e20;
  --card-solid: #1a1a1c;
  --hairline:   rgba(255, 255, 255, 0.085);
  /* ⚠️ `--sep` 被引用了 3 处（.pr 卡片边框 / .unsent-item / 「无 check」那个环的
     描边）却**从来没定义过** —— 2026-08-05 做 PR 看板时才暴露：无 check 的 PR
     环整个看不见，而那恰是最该被看见的一档（它长得像没问题，其实是 CI 没触发）。
     取值按 529 行那条注释里写的原意：比 hairline 略重一点的分隔色。 */
  /* 字体流光扫一圈的时长。**两处调用共用它**（cc 行标题、时间线节点悬浮），
     所以"全平台调速"就是改这一个数。
     2026-08-06 机主要求提速 20%：2.4s ÷ 1.2 = 2s。
     ⚠️ 提速要改**时长**不是改关键帧 —— row-sweep 的位移量和渐变的
     background-size 是绑死的（端点按光带半宽算），动关键帧会让光带断掉。 */
  --sweep-dur:  2s;
  /* 流光的渐变本体。⚠️ **必须只有这一份** —— 它此前在两处（列表卡片、时间线悬停）
     逐字抄了两遍，改一处另一处就会漂。旁边那条注释已经写了"要一致就复用变量"，
     但当时复用的只是颜色变量，整段渐变还是抄的。
     ⚠️ 里面的 `currentColor` 是在**使用它的那个元素**上解析的（自定义属性是文本替换，
        替换完才按该元素计算）—— 所以三行各自保留自己的层级色这件事不受影响。 */
  --sweep-image: linear-gradient(100deg,
    currentColor                                            0 37.5%,
    color-mix(in srgb, var(--sweep-mid) 35%, currentColor)  41%,
    color-mix(in srgb, var(--sweep-mid) 78%, currentColor)  44.5%,
    var(--sweep-mid)                                        47%,
    var(--sweep-core)                                       50%,
    var(--sweep-mid)                                        53%,
    color-mix(in srgb, var(--sweep-mid) 78%, currentColor)  55.5%,
    color-mix(in srgb, var(--sweep-mid) 35%, currentColor)  59%,
    currentColor                                            62.5% 100%);
  --sep:        rgba(255, 255, 255, 0.13);
  /* 同样是漏定义的一个（2026-08-05 一起补）。三处引用：时间线圆点靠它在竖线上
     "打洞"、.unsent-item 的底、以及 .pr 卡片的底 —— 未定义时卡片背景是**透明**的，
     PR 看板的连线会直接从卡片里穿过去。取 canvas 同色：打洞要的就是"和身后一样"。 */
  --bg-2:       var(--canvas);
  --ring-track: rgba(255, 255, 255, 0.19);

  /* 流光的核心色。⚠️ 不能直接用 --blue：深色下基色是近白(#f5f5f7)，
     而 --blue(#0a84ff) 比它**暗**，扫过去读起来是"变暗"而不是"发亮"。
     深色下要更亮更艳的青，浅色下基色是近黑、用实蓝对比就够。 */
  --sweep-core: #8ce0ff;
  --sweep-mid:  #4fc3f7;
  /* 热核。⚠️ 光之所以读起来是"光"，靠的是**亮度**不是色相 ——
     这条在做行级流光时验证过：核心用饱和色只会得到"一块颜色在移动"。
     深色下直接给纯白，色相退到两侧当光晕。 */
  --sweep-hot:  #ffffff;

  --hairline-2: rgba(255, 255, 255, 0.14);

  /* 文字（Apple label 层级） */
  --label:      #f5f5f7;
  --label-2:    #98989d;
  --label-3:    #636366;

  /* 语义色（Apple dark 系统色原值） */
  --blue:   #0a84ff;
  --red:    #ff453a;
  --orange: #ff9f0a;
  --yellow: #ffd60a;
  --green:  #30d158;
  --cyan:   #64d2ff;
  --grey:   #48484a;

  --radius:    14px;
  --radius-sm: 10px;
  --radius-lg: 20px;

  --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display",
          "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
  --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;

  --ease: cubic-bezier(0.32, 0.72, 0, 1);   /* Apple 常用的减速曲线 */
}

@media (prefers-color-scheme: light) {
  :root {
    color-scheme: light;
    --ink:        #f2f2f7;               /* systemGroupedBackground */
    --canvas:     #f2f2f7;
    --surface-1:  #ffffff;
    --surface-2:  rgba(0, 0, 0, 0.035);
    --bubble:     #007aff;      /* 浅色同理跟随 --blue */
    --pop:        #ffffff;
    --card-solid: #f7f7f9;
    --hairline:   rgba(0, 0, 0, 0.09);
    --sep:        rgba(0, 0, 0, 0.13);
    --bg-2:       var(--canvas);
    --ring-track: rgba(0, 0, 0, 0.15);
    --sweep-core: #0a84ff;
    --sweep-mid:  #3aa0ff;
    /* ⚠️ 浅色下热核不能用白 —— 白底上加白等于什么都没发生。只能靠高饱和色相。 */
    --sweep-hot:  #0a6cff;
    --hairline-2: rgba(0, 0, 0, 0.16);
    --label:      #1d1d1f;
    --label-2:    #6e6e73;
    --label-3:    #aeaeb2;
    --blue:   #007aff;
    --red:    #ff3b30;
    --orange: #ff9500;
    --yellow: #ffcc00;
    --green:  #34c759;
    --cyan:   #32ade6;
    --grey:   #c7c7cc;
  }
}

* { box-sizing: border-box; }

/* ━━━ 点击不要留下方框（机主 2026-08-16：「点击很多按钮时候，会有一个选中框，很丑」）━━━
 *
 * 是两件事，得分开治，只治一件会剩另一件：
 *
 * ① **`-webkit-tap-highlight-color`** —— 移动端浏览器的默认行为：手指点任何可交互
 *    元素都会盖一层半透明方块（实测默认值 `rgba(51,181,229,0.4)`，安卓上是一块蓝）。
 *    ⚠️ 它**跟 CSS 里写没写 outline 无关**，是 UA 自己画的，所以在样式表里
 *      grep `outline` 永远找不到它 —— 我先前就是这么找了一圈没找到。
 *    ⇒ 关掉。⚠️ 关掉之后**必须有别的按下反馈**（本文件里按钮普遍有 `:active`
 *      的 `transform: scale(.94)` 和底色变化），否则会变成"点了没反应"，
 *      那比丑更糟。
 *
 * ② **点击后残留的焦点圈** —— 这仓库已经统一用 `:focus-visible`（只在键盘导航时
 *    出现），实测点击后 `document.activeElement.matches(':focus-visible')` 是 false，
 *    所以这一半本来就是对的。**保留 `:focus-visible` 的样式** ——
 *    键盘用户靠它才知道自己在哪，为了好看把它一起关掉是可访问性倒退。
 */
* { -webkit-tap-highlight-color: transparent; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--ink);
  color: var(--label);
  font: 400 14px/1.5 var(--font);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* 时长、计数一律等宽数字：每秒跳动的数字不能让整行跟着抖 */
.tnum, .agent .meta, .tl-ts, .tl-dur, .now-box, footer, .count b, .msg .when {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

::selection { background: color-mix(in srgb, var(--blue) 35%, transparent); }

:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
  border-radius: 6px;
}

/* ⚠️⚠️ 上面那条对 <select> / <input> 会在**鼠标点击后**也生效 ——
   浏览器对能接键盘输入的表单控件强制标记 `:focus-visible`，不管焦点是怎么来的。
   机主 2026-08-16：「点那个消息优先级的按钮，还是有选中框」，就是这个。

   ⚠️ 我上一轮判过「点击后 `:focus-visible` 是 false，这半边本来就是对的」——
      **那是在 <button> 上量的**。换成 <select> 结论直接反过来（实测 true）。
      ⇒ 同一条规则在两种元素上给出相反的值，**只测一种等于没测**。

   做法：鼠标点出来的焦点不画环，一碰键盘（app.js 的 `data-nav`）立刻恢复。
   ⚠️ 特意列元素而不是写 `*`：要把特指度顶到 (0,3,1)，否则
      `.msg.foldable .echo-bar:focus-visible` 这类 (0,3,0) 的规则会盖过它。 */
html[data-nav="pointer"] :is(a, button, select, input, textarea, [tabindex]):focus-visible {
  outline: none;
}

/* ————————————————————— 骨架 ————————————————————— */

#app { display: flex; flex-direction: column; height: 100dvh; background: var(--canvas); }

header {
  display: flex; align-items: center; gap: 16px;
  padding: 0 18px; height: 52px; flex: 0 0 auto;
  background: color-mix(in srgb, var(--canvas) 72%, transparent);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border-bottom: 1px solid var(--hairline);
  position: relative; z-index: 10;
}
header h1 {
  margin: 0; font-size: 15px; font-weight: 600;
  letter-spacing: -0.015em; white-space: nowrap;
}
header .spacer { flex: 1; }

.counts { display: flex; gap: 14px; align-items: center; }
.count i { font-style: normal; }
.count {
  font-size: 12px; color: var(--label-2); white-space: nowrap;
  letter-spacing: -0.005em;
}
.count b { color: var(--label); font-weight: 590; }

/* 三栏宽度（机主 2026-08-14：「要允许调整宽度」+「左边那一竖栏默认窄 20%」）。
   ⚠️ 左栏 300 → **240px**（正好窄 20%）。行压成一行之后本来也不需要那么宽了。
   ⚠️ 用变量而不是写死：拖拽把手改的就是这两个变量，一处定义、CSS 与 JS 共用同一个名字。 */
:root { --col-l: 240px; --col-r: 320px; }

main { flex: 1; display: grid;
       grid-template-columns: var(--col-l) 1fr var(--col-r); min-height: 0;
       position: relative; }   /* 下拉刷新指示器 + 拖拽把手的定位上下文 */

/* 两根拖拽把手。⚠️ **absolute，不占 grid 列** —— 插进 grid 里会把三列变五列，
   而窄屏那条 `grid-template-columns: 1fr` 和所有按列写的规则都得跟着改。
   ⚠️ 视觉上只有 1px 的线（就是各栏本来的 border），但**热区 9px** ——
   1px 的拖拽目标在实际使用中几乎点不中。 */
.rsz {
  position: absolute; top: 0; bottom: 0; width: 9px; z-index: 6;
  cursor: col-resize; touch-action: none;
}
.rsz::after {                      /* 悬停/拖拽时才显形，平时让位给栏本身的分隔线 */
  content: ''; position: absolute; inset: 0 4px; background: var(--blue);
  opacity: 0; transition: opacity .12s var(--ease);
}
.rsz:hover::after, .rsz.dragging::after { opacity: .55; }
.rsz-l { left: calc(var(--col-l) - 4px); }
.rsz-r { right: calc(var(--col-r) - 4px); }
/* 拖拽期间禁选，否则整页文字会被一路刷蓝 */
body.resizing, body.resizing * { user-select: none; cursor: col-resize !important; }

.col { min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
.col--list  { border-right: 1px solid var(--hairline); padding: 8px; }
.col--main  { display: flex; flex-direction: column; position: relative; }
.col--right { border-left: 1px solid var(--hairline); padding: 16px; }

/* 细滚动条，不喧宾夺主 */
.col::-webkit-scrollbar, .stream::-webkit-scrollbar, dialog pre::-webkit-scrollbar { width: 8px; }
.col::-webkit-scrollbar-thumb, .stream::-webkit-scrollbar-thumb, dialog pre::-webkit-scrollbar-thumb {
  background: var(--hairline-2); border-radius: 4px;
  border: 2px solid transparent; background-clip: content-box;
}

/* ————————————————————— 左栏：cc 列表 ————————————————————— */

.agent {
  display: flex; gap: 12px; align-items: center;
  /* ⚠️ 11px → 7px（机主 2026-08-14「压缩每个 Tab 的大小」）：行里只剩一行文字了，
     原来的上下留白是按三行内容配的，不减的话反而显得空。 */
  padding: 7px 12px; border-radius: var(--radius);
  cursor: pointer; position: relative;
  transition: background 180ms var(--ease);
}
.agent + .agent::before {          /* 内嵌分隔线：从头像后开始，iOS 列表的做法 */
  content: ""; position: absolute; left: 50px; right: 12px; top: -1px;
  height: 1px; background: var(--hairline);
}
/* ⚠️⚠️ hover / active **不再换背景**（2026-08-19，机主报「悬浮在 Tab 上会变黄」时查出来的）。
   这两条是玻璃化之前留下的：它们用 `background` 简写，会把下面
   `.agent` 那层 `--glass-sheen` + `--glass-film` **整个盖掉换成纯色**
   ⇒ 卡片一悬浮，玻璃就没了，外观在那一刻突变。
   阻塞态尤其明显：`.agent.authwait:hover` 的黄从 16% 跳到 24%，
   而底下的玻璃同时被换成纯色 —— 看起来就像「悬浮才变黄」。
   ⇒ hover / active 的反馈交给 transform + box-shadow（见文件末尾玻璃那节），
     它们不碰 background，因此能叠在玻璃上而不是替换它。
   ⭐ 一般化：**用简写属性做状态样式，会连带清掉基础层里同属性的所有分层。**
     状态层要么只改自己那一层，要么就得把基础层完整重述一遍 —— 后者必然漂。 */
.agent:hover::before, .agent:hover + .agent::before { opacity: 0; }
.agent.active::before, .agent.active + .agent::before { opacity: 0; }

/* ★ 左侧的环：**只由任务状态驱动**，与"此刻在不在生成"无关。
 *   运行中 → 绿色转圈（这个状态本身就是"进行中"，用运动表达名副其实）
 *   其余   → 该状态颜色的完整实心环，静止
 *
 * ⚠️ 这里的"动"表示的是**任务阶段是运行中**，不是"CLI 此刻在吐字"。
 *   后者由标题上的流光表达，两者互不相干 —— 所以「阻塞 + 标题在扫」、
 *   「运行中 + 标题不扫」都是成立的组合。
 *
 * ⚠️ 早先环上还叠了一层"已持续/卡住阈值"的进度弧，现在去掉了：
 *   它让机主连着三次把环读成"缺了一块"，而"已持续多久"就写在旁边的文字里，
 *   是冗余信息。一个需要解释才能看懂、且数据已在别处的编码，不值得留。
 *
 * 颜色：黄=它明确说需要你（阻塞，正常状态不是故障）；橙=平台推测它可能出事
 *      （疑似卡住）；绿=运行中；蓝=等CI；灰=已完成。
 *      红色刻意不用于本列，留给真正的错误（投递失败那几种）。
 *
 * 转圈的结构参考 react-loader-spinner 的 CircularProgress：
 * 淡色轨道 + 中心实心点 + 一段 1/4 弧线性旋转。
 */
.ring {
  --tint: var(--grey);
  width: 26px; height: 26px; flex: 0 0 auto; overflow: visible;
}
/* ⚠️⚠️ **health → 颜色的映射只有这一份**，看板的环和首页的小圆点共用
   （2026-08-14 加首页时收的）。同一个 cc 在两个页面显示成不同颜色，
   比不显示更糟 —— 而"各写一份枚举"必然在加新 health 时漂掉一边。
   加档只改这五行；`.ring` 独有的 opacity 之类留在下面各自的规则里。 */
/* ⚠️ 首页那些 cc 小片用的**就是这个 `.ring`**（同一个 `ringSVG()` 画的），
   所以这份映射天然只有一处 —— 不需要再让第二个元素来"共享"它。
   2026-08-14 中途我确实加过一个 `.hm-cc-h` 小圆点并把这里改成
   `:is(.ring, .hm-cc-h)`，那是**让两个东西共享一份定义**；
   改成直接复用整个环之后，连共享都不必了。 */
.ring.blocked    { --tint: var(--yellow); }
.ring.stalled    { --tint: var(--orange); }
.ring.ok         { --tint: var(--green);  }
.ring.waiting_ci { --tint: var(--cyan);   }
.ring.done       { --tint: var(--grey); opacity: 0.55; }

.ring .track { fill: none; stroke: var(--ring-track); stroke-width: 2; }
.ring .fill  { fill: none; stroke: var(--tint); stroke-width: 2; }
.ring .dot   { fill: var(--tint); }
.ring .sweep { fill: none; stroke: var(--tint); stroke-width: 2; stroke-linecap: round; }

/* 默认：完整实心环，静止 */
.ring .dot, .ring .sweep { display: none; }
/* 运行中：换成转圈 */
.ring.ok .fill { display: none; }
.ring.ok .dot, .ring.ok .sweep { display: block; }
.ring.ok .sweep {
  transform-origin: 50% 50%;
  animation: ring-spin 1s linear infinite;
}
@keyframes ring-spin { to { transform: rotate(360deg); } }

/* 「此刻在生成」= 一道彩色的光扫过它的标题。
 * 与左边的环**互不相干**：环由任务阶段驱动，流光由"CLI 此刻在不在吐字"驱动。
 *
 * 三处按 Codex 的 shimmer.rs 对齐（读了它的源码，不是照感觉调的）：
 *   ① **窄光带**：它是固定 10 字符宽；这里取元素宽的 40%，不再几乎盖满整行。
 *   ② **升余弦衰减** 0.5(1+cos πd)：下面 9 个停靠点就是这条曲线上的采样，
 *      不是线性渐变——线性的两端会有可见的折角，读起来像"一段字变了色"。
 *   ③ **两次扫过之间留停顿**：它靠首尾各 10 字符的 padding 实现；这里让
 *      关键帧在 58% 处就走完，剩下 42% 停在画外。一直不停地扫会显得聒噪。
 *
 * 一处**有意不同**：Codex 是把字淡向背景的"暗波"（blend 的方向是 fg→bg），
 * 这里按机主要求做成亮的彩色光。
 *
 * ⚠️ background-clip:text + 透明文字**只加在 .gen 行上**。给所有标题都加的话，
 *    万一某个浏览器不支持 background-clip:text，就是整列标题全隐形 ——
 *    为一个装饰效果冒这种险不值得。 */
/* 「此刻在生成」= 一道彩色的光扫过整块内容（标题 / 状态行 / 步骤三行）。
 * 与左边的环互不相干：环由任务阶段驱动，流光由"CLI 此刻在不在吐字"驱动。
 *
 * 做法就是原来标题上那套：background-clip:text + 渐变，让**文字本身换色**。
 * 对比度是它好看的根本 —— 基色 currentColor → 核心 --blue，深色下是
 * "近白→蓝"、浅色下是"近黑→蓝"，两种底色上都是强烈的色相跳变。
 * 半透明叠加层做不到这一点（只能靠 alpha 加亮，宽了糊、窄了细），所以走这条路。
 *
 * ⚠️ 同步的关键是**让三行几何完全一致**：都是 .body 的块级子元素、
 *    等宽、左边缘对齐。background-position 的百分比相对各自元素宽度，
 *    宽度一样、起点一样，三条光带就必然重合。
 *    （之前 .name 是 inline-block 收缩到文字宽、叠加层又挂在更宽的 .agent 上，
 *      几何三者皆不同，于是出现"文字流光落后于行级流光"。）
 *
 * ⚠️ 基色用 currentColor，配合 -webkit-text-fill-color:transparent
 *    （它只让字透明、不改 color）—— 三行各自保留自己的层级色，不会被拍平。
 *    ⚠️ 不能改用 opacity 拉层次：opacity<1 会让子元素单独成层，
 *      父级的 background-clip:text 穿不过去，那一行会整个消失（实测踩过）。
 */
/* ⚠️⚠️ **流光只有这一份定义，三处共用**（2026-08-14 加首页时收的）：
     · 看板正在生成的行（.agent.gen 的三行）
     · 履历悬浮（.tl-node:hover）
     · 首页正在生成的 cc 小片（.hm-cc.gen）
   本文件在 .tl-node 那段早就写着：「要一致就复用变量，别照着渲染结果调色」
   —— 那次是 PR 环绕自配了一份颜色，浅色主题下光心变成白带把光劈成两半。
   加第三处时与其再抄一遍，不如把三处并成一条：**以后调光带、调时长、
   加第四处，都只动这里。** */
/* ⚠️ 2026-08-14 机主把行压成「状态动画 + 标题」一行、chip 去掉状态文字之后，
   `.meta` / `.step` / `.hm-cc-s` 在这两处**已经不存在了** ——
   死选择器留着不报错，但会让下一个人以为流光还铺三行。删掉。
   （`.agent.archived` 仍有 .meta/.step，但归档行本来就没有 .gen，见 archRowTag。） */
.agent.gen .name,
.hm-cc.gen .hm-cc-n {
  /* 带宽 = 元素宽的 50%（图像 37.5%–62.5%），中段大部分都是强色，
     不再只有中心一小撮 —— 亮的部分要够多才显眼。 */
  background-image: var(--sweep-image);
  background-size: 200% 100%;
  /* ⚠️ 必须允许平铺：走到两端时会有一截元素落在图像之外，那里没有背景图，
     而文字填充是透明的、完全靠背景上色 —— 那一截字会整个消失。
     渐变首尾都是 currentColor，平铺无缝。 */
  background-repeat: repeat;
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: row-sweep var(--sweep-dur) linear infinite;
}

/* ⚠️ 端点按光带半宽算，两者绑在一起：渐变跨图像 37.5%–62.5% = 元素宽的 50%，
   半宽 25%。size:200% 时带心位置 = 1 - P，所以
     完全出画（左）: (1-P)+0.25 <= 0 → P >= 125%
     完全出画（右）: (1-P)-0.25 >= 1 → P <= -25%
   再各留 4% 余量（渐变 100° 斜，斜角在垂直方向有偏移，取理论值会卡边界）。
   走完占 70%，剩下 30% 停在画外 —— 对应 Codex shimmer 首尾 padding 的间歇。 */
@keyframes row-sweep {
  0%   { background-position: 129% 0; }
  70%  { background-position: -29% 0; }
  100% { background-position: -29% 0; }
}

.agent .body { min-width: 0; flex: 1; }
.agent .name {
  font-size: 13.5px; font-weight: 590; letter-spacing: -0.01em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  /* ⚠️ 保持块级（曾经改成过 inline-block 让它收缩到文字宽度）。
     原因原本是"流光要三行同步、必须等宽同起点"——2026-08-14 压成一行之后
     那条约束自动满足了，但**块级仍然要保**：收缩到文字宽的话，
     `background-position` 的百分比就相对文字宽而不是行宽，短名字上光带会窜得飞快。 */
}
.agent .meta { font-size: 12px; color: var(--label-2); margin-top: 1px; }
.agent .step:empty { display: none; }
.agent .step {
  font-size: 12px; color: var(--label-3); margin-top: 1px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.section-title {
  padding: 18px 12px 6px; font-size: 11px; font-weight: 590;
  color: var(--label-3); text-transform: uppercase; letter-spacing: 0.06em;
}

/* ————————————————————— 中栏：对话 ————————————————————— */

.detail-head {
  padding: 14px 22px; flex: 0 0 auto;
  border-bottom: 1px solid var(--hairline);
  background: color-mix(in srgb, var(--canvas) 72%, transparent);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
}
/* 标题与那行灰色元信息并排，省下一整行高度（机主要求）。
   baseline 对齐而不是 center —— 两者字号差一倍，居中会让灰字看着往下坠。 */
.detail-head { display: flex; align-items: baseline; gap: 12px; }
/* ⚠️⚠️ 这里的 `text-overflow: ellipsis` **曾经是无效的、而且有害**（2026-08-09 修）：
   下面还有一条 `.detail-head .title { display: flex }`，把它变成了 flex 容器 ——
   而 ellipsis 对 flex 容器不生效（本文件在 `.pr-title` 那儿已经吃过一次）。
   于是长标题不是被省略，是被 `overflow: hidden` **连同末尾的按钮一起裁掉**，
   手机上标题一长「停止」和「更多操作」就凭空消失了（机主 2026-08-09 报的）。
   ⇒ 省略号必须落在**文本自己**（`.tname`）身上，容器只负责排版。 */
.detail-head .title {
  font-size: 17px; font-weight: 640; letter-spacing: -0.02em;
  flex: 0 1 auto; min-width: 0;
}
/* 只有名字会被压缩、被省略；按钮一律不参与压缩（见下面的 flex: 0 0 auto） */
.detail-head .title .tname {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* 标题后面那个「在哪台机器上」（机主 2026-08-24）。
   ⚠️ `flex: 0 0 auto` 是硬要求，理由同 `.brand`：参与压缩的东西会被
      `.title` 的 overflow 连带裁掉，长标题一来它就凭空消失。
      ⇒ 让位的永远是名字（`.tname` 有省略号），不是这一格。
   ⚠️ 用 mono + `--label-3`（和右边那行 `.sub` 同一套）：它是**元信息不是名字**，
      跟名字同字号同粗细会让人读成"名字的一部分"。 */
.detail-head .title .tmach {
  flex: 0 0 auto;
  font-family: var(--mono); font-size: 11.5px; font-weight: 500;
  letter-spacing: -0.01em; color: var(--label-3);
  cursor: default; user-select: none;
}
.detail-head .sub {
  flex: 0 1 auto; min-width: 0; margin-left: auto;   /* 推到右边 */
  font-size: 11.5px; color: var(--label-3);
  font-family: var(--mono); letter-spacing: -0.01em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-align: right;
}

/* ⚠️⚠️ `overscroll-behavior: contain` 不是可有可无的修饰（机主 2026-08-24 报的）。
   没有它时，对话记录滚到顶/底之后，**滚动会链到外层** —— 于是在对话记录区
   往下滚，**输入框会被一起拉上去**。他要的是：对话记录自己橡皮筋回弹，
   **输入框不动**。
   ⭐ `contain` 正好是这个语义：**保留元素自身的橡皮筋，只阻止链到外层**
   （不是 `none` —— 那会把回弹也一起去掉）。
   ⚠️ 同一份 CSS 的 `.col`（:272）早就有这一行了，是 `.stream` 漏了 ——
   ⭐ 又一次「同一份防护有多处，只加了当时看得见的那处」。 */
.stream { flex: 1; overflow-y: auto; padding: 20px 22px 8px;
  overscroll-behavior: contain; }

/* ⚠️ `width: fit-content` 是关键，不是可有可无的修饰。
   .msg 是块级元素，默认宽度 auto = 撑满可用空间，于是**每个气泡都恰好 72%**，
   跟内容多少无关 —— 一句「好」和一段汇报占一样宽。
   max-width 只是上限，管不了下限。 */
/* ⚠️ 88% 是机主 2026-08-14 定的（原来 72%）：「让每行能有更多的字」。
   72% 是聊天 UI 的常规值，用意是留出对侧空白好分辨方向 —— 但这个面板上
   方向已经由**对齐 + 底色**双重表达了（`.msg.down` 靠右且换底色），
   那 12%~28% 的空白买不到额外的可读性，只是把长汇报压成更多行。
   实测（1440×900，stream 820px）：72% 时每行 73~110 字 → 88% 后见下方提交信息。
   ⚠️ 别提到 94%：那是 `.msg:has(.acts)` 的值，两者要拉开，
      否则「有细节可看的消息更宽」这个既有层次就没了。 */
.msg { width: fit-content; max-width: 88%; margin-bottom: 14px; }
/* ⚠️ **折叠行成组时要更紧**（机主 2026-08-17 圈了尾部那块说"间隔太大"）。
   「会话内复述」「做了 N 件事」这类是**次要信息**：它们和真消息用同一个 14px
   间距，于是三行灰字占掉的竖向空间和一条真消息一样多。
   ⚠️ 只收**这几类**的间距，不动真消息之间的 14px —— 那个是让对话读得开的，
      一起收会把整页压成一片。 */
.msg.foldable.folded, .msg.nobody { margin-bottom: 2px; }
/* 紧挨着折叠行的**上一条真消息**，尾距也收掉一半 —— 否则"上一条的时间行"
   和下面那组之间仍然隔着 14px，看着还是断开的。

   ⚠️⚠️ **必须排除折叠行自己**（2026-08-23 实测量出来的 bug）：原来写的是
   `.msg:has(+ .msg.foldable.folded)`，而折叠行**自己也是 `.msg`** ⇒
   连续两个折叠行时，前一个被这条命中 ⇒ **折叠行之间反而变成 6px**，
   而折叠行→真消息只有 3px。机主原话「终端和工具调用之间的间距不一样大」。
   ⭐ 一条"管相邻关系"的规则，很容易把自己也管进去。 */
.msg:not(.folded):not(.nobody):has(+ .msg.folded),
.msg:not(.folded):not(.nobody):has(+ .msg.nobody) { margin-bottom: 6px; }
.msg .bubble {
  padding: 9px 14px; border-radius: 19px;
  white-space: pre-wrap; word-break: break-word;
  font-size: 14px; line-height: 1.42; letter-spacing: -0.005em;
}
/* 时间与投递状态并成一行（机主要求省空间）。
   ⚠️ 用 flex 而不是把两个 div 改成 inline：状态那边带按钮（重发/已读/看终端），
   inline 布局下按钮的垂直对齐和换行都不好控；flex + align-items:baseline
   才能让「机主 08-04 15:16」和「✓✓ 已提交」的文字基线对齐。
   ⚠️ flex-wrap: wrap —— 状态可能很长（"等待 cc 处理（已 3m）· 多行内容以文件
   方式投递"），窄屏放不下时要能折行，而不是把气泡撑宽。 */
.msg .foot {
  /* ⚠️⚠️ `width: 0; min-width: 100%` 是这里的关键，不是随手写的。
     父元素 .msg 用 fit-content 收缩到"最宽的那个子元素"，而这一行
     「机主 08-04 15:16 ✓✓ 已确认」比短消息的正文宽得多 ——
     实测「改好了」三个字的气泡被它撑到 160px。
     width:0 让它**不参与父元素的固有宽度计算**，min-width:100% 再让它
     铺满已经定下来的宽度。于是宽度只由气泡正文决定。
     ⚠️ 配套必须 nowrap：宽度被压到气泡那么窄之后，wrap 会让这一行折成
     两三行 —— 那就违背了"并成一行省空间"的初衷。宁可让它溢出。
     溢出方向由 justify-content 决定：up 向右、down 向左，都落在流内。 */
  width: 0; min-width: 100%;
  display: flex; align-items: baseline; gap: 8px;
  flex-wrap: nowrap; white-space: nowrap;
  margin-top: 4px; padding: 0 4px;
}
.msg.down .foot { justify-content: flex-end; }
.msg .when { font-size: 11px; color: var(--label-3); flex: 0 0 auto; }

.msg.up .bubble {
  background: var(--surface-1); border: 1px solid var(--hairline);
  border-bottom-left-radius: 6px;
}
.msg.down { margin-left: auto; }
.msg.down .bubble {
  background: var(--bubble); color: #fff; border: 1px solid transparent;
  border-bottom-right-radius: 6px;
}


/* 系统消息是居中的：收缩之后必须靠 margin-inline:auto 才能继续居中，
   光有 text-align:center 只会让文字在（已经收缩的）盒子里居中。 */
.msg.system { width: fit-content; max-width: 100%; text-align: center;
              margin: 20px auto; }
.msg.system .bubble {
  background: none; border: 0; padding: 0;
  color: var(--label-3); font-size: 11.5px;
}
.msg.system .when { display: none; }

/* 抄送：明显不是"对话"，虚线 + 收窄 */
.msg.cc .bubble {
  background: none; border: 1px dashed var(--hairline-2);
  color: var(--label-2); font-size: 12.5px; border-radius: var(--radius);
}
/* 待拍板：cc 在等人做决定，值得一条实边 */
.msg.ask .bubble { border: 1.5px solid var(--orange); }

.placeholder {
  color: var(--label-3); padding: 56px 22px; text-align: center;
  font-size: 13px; line-height: 1.7;
}
.placeholder code {
  font-family: var(--mono); font-size: 12px;
  background: var(--surface-2); padding: 2px 6px; border-radius: 5px;
}

/* 投递状态：五种失败绝不折叠成同一个黄色 */
.msg .st {
  font-size: 11.5px;
  display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
  color: var(--label-3);
}

.msg .st.warn { color: var(--orange); }
.msg .st.bad  { color: var(--red); }
.msg .st.ok   { color: var(--green); }
/* ⚠️ 只管**动作按钮**，别写成 `.msg .st button` —— 状态文字本身也是个 <button>
   （手机上兼作折叠开关），会被这条套成一颗灰药丸，看着像个可点的标签而不是状态。
   而且它 (0,2,1) 比 `.msg .st-txt` (0,2,0) 更特指，**是它赢，不是我写的那条**。 */
.msg .st-acts button {
  padding: 3px 10px; font-size: 11.5px; border-radius: 999px;
  background: var(--surface-2); border: 0; color: var(--label-2);
  /* ⚠️ 少了它，气泡窄的时候是**按钮里的字**换行（「标记已读」折成两行），
     于是 `st-acts` 明明写着 nowrap 却还是 34px 高 —— nowrap 管的是
     "孩子之间换不换行"，管不到"孩子自己内部换不换行"。 */
  white-space: nowrap;
}
.msg .st-acts button:hover { background: var(--hairline-2); color: var(--label); }
/* 三个动作按钮成组、组内不换行。见 app.js 里 st-acts 那段注释：
   拆行的原因是状态文字跟按钮同处一条 flex 行，不是按钮太宽。 */
.msg .st-acts { display: inline-flex; gap: 8px; flex-wrap: nowrap; align-items: center; }
/* 状态文字 + 那个 `i` 是一组，不许被换行拆开 —— 见 app.js 里同名注释。 */
.msg .st-txt { display: inline-flex; gap: 6px; flex-wrap: nowrap; align-items: center;
  background: none; border: 0; padding: 0; color: inherit; font: inherit; text-align: left; }
/* 箭头跟复述条 / 「做了 N 件事」用同一个（只画左边那条边，宽 4px）。
   ⚠️ 桌面不折叠，所以箭头也不该出现 —— 出现了就是在暗示一个不存在的交互。 */
.msg .st-txt .caret { display: none; }
/* ⚠️ 折叠开关**不套那条"命中区 ≥34px"的通则**：点歪的代价是多展开/收起一次，
   为零。套上去它就有 34px 高 —— 而这整件事就是为了把 59px 压下来。 */
.msg .st-txt { min-height: 0; padding: 2px 0; }

/* 往回翻记录时的新消息提示 */
.newpill {
  position: absolute; left: 50%; bottom: 108px; z-index: 6;
  transform: translateX(-50%);
  background: var(--blue); color: #fff; border: 0;
  border-radius: 999px; padding: 7px 16px;
  font-size: 12.5px; font-weight: 500;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.36);
  animation: pop 240ms var(--ease);
  /* ⚠️ 进场有 pop、退场却是 `hidden` 一帧消失 —— 同一个东西两条不同的路。
     退场沿原路缩回去。 */
  transition: opacity 160ms var(--ease), transform 160ms var(--ease),
              display 160ms allow-discrete;
  opacity: 1;
}
.newpill[hidden] { opacity: 0; transform: translateX(-50%) scale(0.94); }
@keyframes pop {
  from { transform: translateX(-50%) scale(0.94); opacity: 0 }
  to   { transform: translateX(-50%) scale(1);    opacity: 1 }
}

/* ————————————————————— 输入区 ————————————————————— */

.composer {
  /* ⚠️ 输入框单行高度和两个圆钮的对齐**全靠这几个变量算**，别写死数字。
     `--ta-h` = 行高 + 上下 padding + 上下边框，正好是输入框空着时的高度；
     圆钮抬起 (--ta-h - --btn)/2 就居中。移动端只改 `--ta-line` / `--btn` 两个值。 */
  --ta-line: 20px; --ta-pad: 10px; --btn: 30px;
  --ta-h: calc(var(--ta-line) + var(--ta-pad) * 2 + 2px);
  flex: 0 0 auto; padding: 10px 22px 16px;
  border-top: 1px solid var(--hairline);
  background: color-mix(in srgb, var(--canvas) 72%, transparent);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
}
.quick { display: flex; gap: 7px; margin-bottom: 9px; flex-wrap: wrap; }
.quick button {
  font-size: 12px; padding: 5px 12px; border-radius: 999px;
  background: var(--surface-1); border: 1px solid var(--hairline);
  color: var(--label-2);
}
.quick button:hover { background: var(--surface-2); color: var(--label); }
/* ⚠️⚠️ **`:hover` 在触摸屏上不存在** —— 关掉系统点击高亮之后，光有 hover
   等于没有按下反馈。⚠️ 而且这一条差点被我漏掉：用 playwright 的
   `mouse.down()` 去量时**会同时触发 hover**，底色确实变了 ⇒ 报"有反馈"。
   **模拟鼠标按下测不出"触摸屏上有没有反馈"** —— 判据得是"有没有 `:active` 规则"。 */
.quick button:active { background: var(--surface-2); transform: scale(0.97); }

/* ⚠️ 输入框的**单行高度必须是算得出来的**，下面按钮的垂直对齐要用它。
   原来靠 `margin-bottom: 5px` 硬凑 —— 那个 5 是按「桌面 30px 按钮 + 40px 输入框」
   配出来的魔数，移动端按钮变 36、输入框因为字号 16px 变 44，它没跟着变
   ⇒ 机主 2026-08-16：「附件和发送按钮有点偏高了」。
   ⇒ 现在行高显式定死，单行高度 = 行高 + 上下 padding + 上下边框，全部由变量算出来。 */
.composer textarea {
  width: 100%; resize: none; min-height: var(--ta-h); max-height: 180px;
  font: inherit; font-size: 14px; line-height: var(--ta-line); padding: var(--ta-pad) 14px;
  background: var(--surface-1); color: var(--label);
  border: 1px solid var(--hairline); border-radius: var(--radius-lg);
  transition: border-color 160ms var(--ease), background 160ms var(--ease);
}
.composer textarea::placeholder { color: var(--label-3); }
.composer textarea:focus {
  outline: none; border-color: var(--blue); background: var(--surface-2);
}

/* 输入框 + 抄送 + 发送同一行。align-items:flex-end —— textarea 会随内容长高，
   贴底对齐才不会让勾选框和圆钮跟着往上飘。 */
.composer-row { display: flex; align-items: flex-end; gap: 12px; }
.composer-row textarea { flex: 1 1 auto; min-width: 0; }
/* ⚠️ 2026-08-16 起「抄送」在 `.prio` 那一行，不在输入行了（见 index.html）。
   这条选择器跟着改 —— 留着 `.composer-row .chk` 不会报错，只会**静默不生效**。 */
.prio .chk {
  flex: 0 0 auto; font-size: 12.5px; color: var(--label-2);
  display: flex; align-items: center; gap: 6px; cursor: pointer;
  white-space: nowrap;
}
.prio .chk input { accent-color: var(--blue); }

/* 提示行：常态是空的，那一行整个塌掉；只有多行转文件/被拦截时才撑开。
   ⚠️ 别给它固定高度或 margin-top —— 空着时会白留一条缝，等于没省下这一行。 */
.composer .hint { display: block; font-size: 11.5px; color: var(--label-3); }
.composer .hint:not(:empty) { margin-top: 7px; }
.composer .hint.warn { color: var(--orange); }

.composer #send {
  flex: 0 0 auto;
  /* 贴底对齐 + 抬起「输入框单行高度和按钮高度之差的一半」= 单行时正好居中，
     多行时仍然贴着输入框底边。⚠️ 别写死数字：桌面和移动端两个值都不一样。 */
  margin-bottom: calc((var(--ta-h) - var(--btn)) / 2);
  width: var(--btn); height: var(--btn); padding: 0; border-radius: 50%;
  background: var(--blue); border: 0; color: #fff;
  font-size: 15px; line-height: 1;
  display: grid; place-items: center;
  transition: transform 140ms var(--ease), opacity 140ms var(--ease);
}
.composer #send:hover  { transform: scale(1.07); }
.composer #send:active { transform: scale(0.94); }
.composer #send:disabled { opacity: 0.35; transform: none; }

/* ————————————————————— 右栏：任务进度 ————————————————————— */

.tl-head {
  font-size: 11px; font-weight: 590; color: var(--label-3);
  text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 7px;
}
/* 「已持续」+「看终端快照」并排（机主 2026-08-07，省进度页顶部的高度）。
   左边随内容、右边占满剩余 —— 时长是短文本，按钮才该吃掉富余宽度。 */
.now-row { display: flex; align-items: stretch; gap: 7px; margin-bottom: 9px; }
.now-row .now-box { flex: 0 0 auto; margin-bottom: 0; }
.now-row #btn-term { flex: 1 1 auto; min-width: 0; }
@media (max-width: 860px) { .now-row #btn-term { min-height: 44px; } }

.now-box {
  margin-bottom: 9px; padding: 8px 11px; border-radius: var(--radius);
  background: var(--surface-1); border: 1px solid var(--hairline);
  font-size: 12px; color: var(--label-2);
}
.now-box b {
  display: block; margin-top: 1px;
  color: var(--label); font-size: 18px; font-weight: 590;
  letter-spacing: -0.03em;
}

/* ⚠️ 骨架线**不能用 `--sep`**：那是分隔线用的极淡色（浅色主题 rgba(0,0,0,.09)），
   放大到 8 倍看根本看不见 —— 计算值全对（left/width/color 都设了），
   而视觉上那条线压根不存在。用 label-3 调出来的中性灰，深浅主题各给一档。 */
:root { --tl-line: color-mix(in srgb, var(--label-3) 55%, transparent); }
@media (prefers-color-scheme: light) {
  :root { --tl-line: color-mix(in srgb, var(--label-3) 70%, transparent); }
}
:root[data-theme="light"] { --tl-line: color-mix(in srgb, var(--label-3) 70%, transparent); }
:root[data-theme="dark"]  { --tl-line: color-mix(in srgb, var(--label-3) 55%, transparent); }

/* 履历：一条竖线串起所有节点，时长标在节点之间的线段上。

   ⚠️ 描述放在圆点的**正右方**（垂直居中），不是右下方 —— 机主 2026-08-05 指定。
   含义是：这一步**可能发生在这个时间点之前，也可能之后**；放右下方会暗示
   "它属于下面那段区间"，而那正是我们不想断言的东西。

   ⚠️ 竖线必须**穿过节点**才叫"连起来"：所以线画在 .tl-node 的 ::before 上
   （整行高、绝对定位），圆点用与底色同色的描边在线上"打个洞"坐上去。
   只在 .tl-gap 上画线的话，线会在每个节点处断开，看着是一截一截的。 */
.tl-node {
  display: flex; align-items: center; gap: 10px;
  position: relative; padding: 1px 6px 1px 0; border-radius: 7px;
}
/* 穿过整行的竖线。左端对齐圆点圆心：圆点 margin-left 1px + 半径 4.5px = 5.5px */
.tl-node::before {
  content: ''; position: absolute; left: 4.75px; top: 0; bottom: 0;
  width: 1.5px; background: var(--tl-line); z-index: 0;
}
/* 首尾两端各收一半，免得线在列表上下缘凭空冒出来 */
.tl-node:first-of-type::before { top: 50%; }
.tl-node:last-of-type::before  { bottom: 50%; }
.tl-node .pip {
  position: relative; z-index: 1; flex: 0 0 auto;
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--label-3); margin-left: 1px;
  box-shadow: 0 0 0 3px var(--bg-2);      /* 在竖线上"打洞"，让圆点像坐在线上 */
  transition: background .12s, box-shadow .12s, transform .12s;
}
.tl-node .tl-body { min-width: 0; }
.tl-node .tl-ts { font-size: 11px; color: var(--label-3); font-family: var(--mono); }
.tl-node .tl-step { font-size: 13px; word-break: break-word; margin-top: 1px; letter-spacing: -0.005em; }

/* 悬浮：整行放大 + 文字走流光。
   ⚠️ 高亮的是**节点**不是区间 —— 区间不属于任何一步，高亮它会重新暗示归属。
   ⚠️ 流光**复用 .agent.gen 那一套**（同样的 --sweep-* 变量、同样的 row-sweep
      动画、同样的 background-clip:text）。昨天在 PR 环绕上吃过亏：自己另配一份
      颜色，浅色主题下光心变成一条白带、把光劈成两半。**要一致就复用变量，
      别照着渲染结果调色。** */
.tl-node { transform-origin: left center; transition: transform .14s ease, background .14s; }
.tl-node:hover {
  background: color-mix(in srgb, var(--blue) 8%, transparent);
  transform: scale(1.035);            /* 幅度要小：这一列很窄，放大过头会撞到右边界 */
}
.tl-node:hover .pip {
  background: var(--blue); transform: scale(1.3);
  box-shadow: 0 0 0 3px var(--bg-2), 0 0 0 5px color-mix(in srgb, var(--blue) 25%, transparent);
}
.tl-node:hover .tl-ts,
.tl-node:hover .tl-step {
  background-image: var(--sweep-image);
  background-size: 200% 100%;
  background-repeat: repeat;          /* 不平铺的话两端那截字会整个消失 */
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: row-sweep var(--sweep-dur) linear infinite;
}
.tl-node:hover .tl-step { color: var(--label); }    /* currentColor 决定流光的底色 */
@media (prefers-reduced-motion: reduce) {
  .tl-node:hover .tl-ts, .tl-node:hover .tl-step { animation: none; }
  .tl-node:hover { transform: none; }
}

/* 节点之间的线段。⚠️ 它自己也要画线：竖线在 .tl-node 上是逐行画的，
   而 .tl-gap 夹在两个 node 中间，不补这一段线就会断成一截一截。 */
.tl-gap { position: relative; padding: 2px 0 2px 24px; min-height: 8px; }
.tl-gap::before {
  content: ''; position: absolute; left: 4.75px; top: 0; bottom: 0;
  width: 1.5px; background: var(--tl-line);
}
.tl-gap.longest::before { background: var(--orange); }
.tl-gap > span { font-size: 11px; color: var(--label-3); font-family: var(--mono); }
.tl-gap.longest > span { color: var(--orange); font-weight: 600; }

/* ————————————————————— 通用控件 ————————————————————— */

button {
  font: inherit; cursor: pointer;
  border-radius: var(--radius-sm);
  border: 1px solid var(--hairline);
  background: var(--surface-1); color: var(--label);
  padding: 7px 13px; font-size: 12.5px;
  transition: background 160ms var(--ease), border-color 160ms var(--ease);
}
button:hover { background: var(--surface-2); }

/* ————————————————————— 页脚：平台自身状态 ————————————————————— */

/* ⚠️ 底栏 2026-08-20 由机主删掉（那一栏在手机上早就 display:none 了，
   理由见下方那条 media query：前两项顶栏已经说了，第三项列表自己数得出来）。
   里面混着的两条真告警搬去了 `.alertbar`，见 index.html 那段注释。 */

/* 告警条：平时不占位，坏了才出现。几何与动效复用 .stalebar，只换颜色 ——
   .stalebar 是「连接不稳定」（橙，会自愈），这条是「有东西真的断了」（红，要人管）。 */
/* ⚠️⚠️ **2026-08-25 从「顶部全宽红条」改成「右下角浮卡」**（机主：
   「把平台上所有的顶部红色 Banner 删掉，因为这个 Banner 会影响我看 Agent 的标题，
     做成其他形式的 Banner，而且要可关闭」）。
   ⭐ 原来是 `position:absolute; top:0; left:0; right:0` —— 它**盖在内容上面**，
     而正下方就是 Agent 标题行 ⇒ 一有告警就看不见自己在跟谁说话。
   ⇒ 挪到右下角、限宽、不占据顶部；`z-index` 保持高，但它只盖住一小块空白区。
   ⚠️ 颜色也从整块红底改成「深底 + 左侧红条」：整块红是**最高级别的视觉噪音**，
     而这里装的多半是"某个 Agent 送不进"这类**要看但不紧急**的信息。 */
.alertbar {
  transform: none; opacity: 1;
  transition: transform 200ms var(--ease), opacity 200ms var(--ease),
              display 200ms allow-discrete;
  position: fixed; right: 16px; bottom: 16px; left: auto; top: auto; z-index: 40;
  max-width: min(420px, calc(100vw - 32px));
  display: flex; align-items: flex-start; gap: 10px;
  padding: 10px 12px; text-align: left;
  font-size: 12px; line-height: 1.5; color: var(--label-1);
  background: var(--card-solid); border: 1px solid var(--hairline);
  border-left: 3px solid var(--red); border-radius: 8px;
  box-shadow: 0 6px 24px rgba(0,0,0,.32);
}
.alertbar[hidden] { transform: translateY(8px); opacity: 0; }
@starting-style { .alertbar:not([hidden]) { transform: translateY(8px); opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .alertbar, .alertbar[hidden] { transform: none; }
}
.alertbar .ab-text { flex: 1; min-width: 0; overflow-wrap: anywhere; }
/* 关闭按钮。⚠️ 要有足够的点击面积（不能只有那个 × 字形本身）。 */
.alertbar .ab-x {
  flex: none; width: 22px; height: 22px; margin: -2px -2px 0 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-radius: 6px; cursor: pointer;
  background: transparent; color: var(--label-3); font-size: 15px; line-height: 1;
}
.alertbar .ab-x:hover { background: var(--hairline); color: var(--label-1); }

.mode { display: inline-flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--label-2); }
.mode .pip { width: 7px; height: 7px; border-radius: 50%; background: var(--green); }
.mode.polling .pip { background: var(--green); }
.mode.offline .pip { background: var(--red); }
.mode.offline { color: var(--red); }

/* ————————————————————— 终端快照 ————————————————————— */

dialog {
  border: 1px solid var(--hairline-2); border-radius: var(--radius-lg);
  background: var(--canvas); color: var(--label);
  max-width: min(1000px, 92vw); width: 100%; padding: 0;
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.55);
}
dialog::backdrop { background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(4px); }

/* ————— 弹窗的入场 / 退场（2026-08-09）—————
   在此之前三个弹窗（归档确认 / 操作 / 终端快照）都是 showModal() **瞬间闪现**，
   连遮罩也是一帧变黑 —— 屏幕上凭空出现一块东西，看不出它从哪来。

   ⚠️ 纯 CSS，不碰 JS：`@starting-style` 给的是"元素刚进 DOM/刚显示时的初始值"，
      `allow-discrete` 让 `display` 和 `overlay`（top layer）这两个离散属性也能参与
      过渡 —— 没有它，退场那一帧元素已经 display:none 了，动画根本没机会播。
   ⚠️ 两者都是渐进增强：浏览器不支持就退回瞬间显示，不会坏。
   ⚠️ 幅度用 0.96 而不是更小：AUDIT 的判据是"现实里没有东西从无到有"，
      scale 太深会变成"弹出来"，那是另一种性格，跟这个看板不搭。 */
dialog {
  opacity: 1; transform: scale(1);
  transition: opacity 200ms var(--ease), transform 200ms var(--ease),
              overlay 200ms allow-discrete, display 200ms allow-discrete;
}
dialog:not([open]) { opacity: 0; transform: scale(0.96); }
@starting-style { dialog[open] { opacity: 0; transform: scale(0.96); } }

dialog::backdrop {
  opacity: 1;
  transition: opacity 200ms var(--ease),
              overlay 200ms allow-discrete, display 200ms allow-discrete;
}
dialog:not([open])::backdrop { opacity: 0; }
@starting-style { dialog[open]::backdrop { opacity: 0; } }

/* 减弱动效：**只去掉位移，保留淡入淡出**。全关掉的话弹窗又变回瞬闪，
   而"它出现了"这件事本身是需要被感知的。 */
@media (prefers-reduced-motion: reduce) {
  dialog, dialog:not([open]) { transform: none; }
  @starting-style { dialog[open] { transform: none; } }
}
dialog .dlg-head {
  padding: 13px 18px; border-bottom: 1px solid var(--hairline);
  display: flex; align-items: center; gap: 10px; font-size: 13.5px;
}
/* ⚠️⚠️ **头部的按钮不许被挤到换行**（机主 2026-08-17 报）。
   这一行是 flex，而 flex 项默认 `flex-shrink: 1` —— 中间那句
   「headless cc 没有终端（没有 TUI）……」很长，把右边的按钮一起压窄，
   于是「刷新」竖成了「刷/新」、「关闭」成了「关/闭」，头部从 34px 撑到 81px。
   ⚠️ 光加 `white-space: nowrap` 不够：不写 `flex: 0 0 auto` 的话按钮仍会被压窄，
      只是变成横向溢出/裁切。**两条要一起。** */
dialog .dlg-head button,
dialog .dlg-head a,
dialog .dlg-head .segmented { flex: 0 0 auto; white-space: nowrap; }
/* ⚠️ 该让路的是那句说明，不是按钮 —— 给它 `min-width: 0` 才收得住
   （flex 项的默认 `min-width: auto` 会让它坚持自己的内容宽度，
   于是被挤的永远是别人）。 */
dialog .dlg-head #term-hl-note { min-width: 0; }
dialog pre {
  margin: 0; padding: 18px; overflow: auto; max-height: 65vh;
  font-family: var(--mono); font-size: 11.5px; line-height: 1.5;
  color: var(--label-2);
}

/* ————————————————————— 登录 ————————————————————— */

.login-wrap { height: 100dvh; display: grid; place-items: center; background: var(--canvas); }
.login-card {
  background: var(--surface-1); border: 1px solid var(--hairline);
  border-radius: var(--radius-lg); padding: 34px 30px;
  width: min(340px, 88vw);
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
}
.login-card h1 { margin: 0 0 5px; font-size: 21px; font-weight: 640; letter-spacing: -0.025em; }
.login-card p { margin: 0 0 22px; color: var(--label-2); font-size: 13px; }
.login-card input {
  width: 100%; padding: 11px 14px; margin-bottom: 10px;
  font: inherit; font-size: 14px;
  background: var(--surface-2); color: var(--label);
  border: 1px solid var(--hairline); border-radius: var(--radius-sm);
  transition: border-color 160ms var(--ease);
}
.login-card input:focus { outline: none; border-color: var(--blue); }
.login-card button {
  width: 100%; padding: 11px; margin-top: 4px;
  background: var(--blue); border-color: var(--blue); color: #fff;
  font-size: 14px; font-weight: 500; border-radius: var(--radius-sm);
}
.login-card button:hover { background: color-mix(in srgb, var(--blue) 88%, #fff); }
.login-err { color: var(--red); font-size: 12.5px; min-height: 18px; margin-top: 10px; }

/* ————————————————————— 移动端 ————————————————————— */

.tabbar { display: none; }

@media (max-width: 860px) {
  main { grid-template-columns: 1fr; }
  .rsz { display: none; }          /* 单栏了，没有边界可拖 */

  .col { display: none; border: 0; }
  body[data-view="list"]     .col--list  { display: block; }
  body[data-view="chat"]     .col--main  { display: flex; }
  body[data-view="timeline"] .col--right { display: block; }

  /* ⚠️ 顶栏在 390px 上是**超载**的，不是"有点挤"：实测 counts 可视宽只剩 29px
     而内容有 207px，于是"6 阻塞"被切成半个字。h1 73 + 切换器 103 + 负载 45 +
     退出 48 + 间距 ≈ 已经吃满 390。所以这里做的是减法，不是缩字号。 */
  header {
    padding: 0 max(14px, env(safe-area-inset-left)) 0 max(14px, env(safe-area-inset-right));
    /* 48 → 42（机主 2026-08-16：「两个顶栏都扁一点点」）。
       ⚠️ 两条顶栏在手机上是**叠着的**（导航 48 + 对话页头 53 = 101px），
          content 还没开始就吃掉了八分之一屏。压的是两条**加起来**的那个数，
          单看任何一条都不算高 —— 这也是为什么它一直没被当成问题。 */
    height: calc(42px + env(safe-area-inset-top, 0px));
    padding-top: env(safe-area-inset-top, 0px);
    gap: 10px;
  }
  /* 应用名在手机上是纯冗余：tabbar 和飞书的标题栏都已经说明你在哪。 */
  header h1 { display: none; }
  /* ━━━ 顶栏 = 一级导航，和桌面**同一套五项**（机主 2026-08-16 要求统一）━━━
     ⚠️ 上一版为了腾地方，把 PR / 监控 / 功能从顶栏藏了、让底栏承担 ——
     结果两端的导航长得完全不一样：桌面「顶栏五项」，手机「顶栏两项 + 底栏六项，
     其中三项和顶栏概念重叠」。**同一个东西在两个端上叫不同名字、待不同位置，
     人得记两套。**
     ⇒ 改成：**顶栏在两端都是那五项**；底栏只留桌面用三栏并排表达、
       手机放不下才要切的那三个（列表 / 对话 / 进度）。
       —— 底栏不再是"另一套导航"，而是"桌面那三栏的手机版"。 */
  /* counts 在窄屏让位给导航：全局态势在列表页本来就逐个 cc 看得到，
     而导航是**每一页都要用**的。⚠️ 这不是"缩字"那种在受害者一侧想办法 ——
     是两个东西抢一条 48px 的横向空间，必须有一个退出去。 */
  .counts { display: none; }
  header .spacer { display: none; }
  header #logout { padding: 7px 12px; font-size: 12px; white-space: nowrap; flex: 0 0 auto; }

  .counts {
    flex: 1 1 auto; min-width: 0; gap: 12px;
    flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  /* 六项里只留**需要机主动手**的两项。运行中/等CI/已完成/已下线是背景态势，
     在手机上它们的作用只是把前两项挤出屏幕 —— 列表页本身就能看到每个 cc 的状态。 */
  .counts .count { display: none; }
  /* ⚠️ gap 不能省：inline-flex 会把 `<b>7</b>` 和「阻塞」之间的那个空白文本节点
     吃掉，显示成「7阻塞」（我改成 flex 才引入的，原来是 inline 没这问题）。 */
  .counts .count[data-k="blocked"],
  .counts .count[data-k="stalled"] { display: inline-flex; gap: 4px; }
  /* ⚠️ 这里一度改成窄屏显示「卡住」（省两个字）—— **回退了**：
     去掉顶栏那三个重复入口之后，counts 从 65px 变成 208px，
     「疑似卡住」完整放得下。
     ⇒ 记这一笔是因为那次改错了地方：**在被挤的那一侧想办法（缩字、少显示几项），
       而占地方的是别人**。省两个字治不了根，还丢掉了"疑似"这个限定
       —— 那个词是有意义的，判据是启发式不是确诊。 */
  /* CPU/RAM 平时是噪音，只在越过阈值（warm/hot）时才值得占顶栏的位置 */
  .load .ld:not(.warm):not(.hot) { display: none; }

  /* ⚠️ 对话页把**整条顶栏**去掉（机主要求），不只是里面的计数。
     手机上垂直空间是最稀缺的资源：顶栏 48px + 底部 tabbar，两头一夹，
     真正能看消息的高度所剩无几。而顶栏里那三样在对话页都不是必需品 ——
     「agent-team」标题（你知道自己在哪）、全局计数（列表页的信息）、
     「退出」（低频操作）。
     ⚠️ 只在 data-view="chat" 下隐藏：列表页 / 进度页照常有顶栏，
     退出和全局态势切一下就能拿到，不是删功能。
     ⚠️ 断线时的橙色横幅挂在 main 上、不在 header 里，所以这里去掉顶栏
     不会连带丢掉"连接不稳定"的提示 —— 那条最要紧的信号还在。 */
  /* ⚠️⚠️ **对话页不能再藏顶栏了**（2026-08-16）。
     原来藏它是为了省 48px 垂直空间，当时能藏是因为**底栏里有 PR/监控/功能兜底**。
     现在一级导航只剩顶栏这一处 —— 再藏就等于把人锁死在对话页里出不去。
     （下面那条 `.board-on` 的例外正是当时为了补这个洞加的，现在整条都不需要了。）
     ⇒ 省空间要另想办法（顶栏本身已经从 5 项+计数+登出瘦到只剩 5 项）。 */
  /* ⚠️ 但 PR 看板显示时**必须**把顶栏放出来：cc / PR 的切换按钮就在里面，
     藏了就等于把人锁在看板上出不去（手机实测：从某个 cc 的对话页切过去后，
     data-view 仍是 chat，顶栏被上面那条藏掉，点不到任何返回的入口）。
     靠更高的选择器特异性压过上一条，不用 !important。 */

  /* ⚠️ 底栏只留 cc 页内部的三栏（列表 / 对话 / 进度）——
     PR / 监控 / 功能回到顶栏了，两处都放就是让人记两套。 */
  .tabbar button[data-page] { display: none; }
  .counts::-webkit-scrollbar { display: none; }

  .mode { flex: 0 0 auto; }
  .mode #mode-text { display: none; }

  .agent { padding: 12px; }
  /* ⚠️ 跟着主值一起提（主值 2026-08-14 从 72% 提到 88%）。
     窄屏本来就该比宽屏更宽 —— 留 86% 会变成比主值还窄，关系颠倒。 */
  .msg { max-width: 92%; }
  .stream { padding: 16px 14px 8px; }
  /* 12 → 8：同上。⚠️ 只压**上下**内边距，左右不动 ——
     左右那 16px 是内容与屏幕边缘的距离，压了会顶到边上。 */
  .detail-head { padding: 8px 16px; }
  /* 393px 宽同时放标题和元信息会把两边都挤成省略号。
     元信息（session/pane/状态）在手机上价值低于标题，缩得更狠。 */
  /* ⚠️ 右侧信息**给标题让位**（机主 2026-08-16）。实测 390px：标题只剩 109px、
     被截成「E2B-San...」，而右边那串 165px 也照样被截 ——
     **两个都不完整，等于两边都白占**。
     ⇒ 判据：标题是**你在跟谁说话**（认人靠它），右边是态势（session 名重复了
       标题的信息、上下文和状态在别处也看得到）。让位的该是后者。
     ⇒ 上限压到 34%，并且**允许它被压到很小**（`flex: 0 1 auto` + min-width:0
       本来就允许，之前是 `max-width: 46%` 卡住了下限之外的空间）。 */
  /* ⚠️ 34% 装不下摘掉 session 之后剩的「60% · workflow 4 · 阻塞」（146 > 122），
     而标题这时已经不截了 —— **余量该给还差一点的那个**。40% 两边都完整。
     ⇒ 这个数不是拍的：摘内容（省掉 session 名）之后再调宽度，
       顺序反过来就会得出再宽也不够的错误结论。 */
  .detail-head .sub { font-size: 10.5px; max-width: 40%; }
  /* ⚠️ 窄屏摘掉两格：session 名 和「在跑什么」。
     **这个数是算出来的不是拍的**（390px 实测）：
       可用 358 ＝ 标题想要 140 + 右侧想要 146 + 两按钮 64 + gap 24 → **要 386，差 28**。
     ⇒ 继续在两边分百分比是分不出来的（40% 时标题又被截，两边都不完整）——
       必须真的**少放一样东西**。
     判据是「别处看不看得到」：session 名在标题已表明身份时可省；
     「在跑什么」在消息流和列表页都看得到；而**上下文% 和状态是驱动动作的**，留着。 */
  .detail-head .sub .sub-sess,
  .detail-head .sub .sub-bg { display: none; }
  /* ⚠️ Tag 的窄屏规则**不在这里**，在它自己那段旁边（本文件靠后）——
     它的主定义写在 2000 多行，同特异性下**后写的赢**，放这儿的 padding
     会被主定义静默覆盖（写这条时真踩了，被用例抓住）。 */
  .col--right { padding: 14px; }

  .composer { padding: 8px 12px 12px; }
  /* ≥16px：iOS Safari 对更小的输入框会在聚焦时放大整个页面，而且回不去 */
  /* 字号 16px 是为了 iOS 聚焦时不放大页面；行高和按钮尺寸跟着走，
     单行高度和按钮对齐由 `--ta-h` 自己算，不用再手配数字。 */
  .composer { --ta-line: 22px; --btn: 36px; }
  .composer textarea { font-size: 16px; }
  .composer-row .hint { display: none; }
  .newpill { bottom: 118px; }

  /* ⚠️ 这里原来是 `footer { display: none }` —— 那一栏 2026-08-20 已整个删掉。
     当时隐藏它的理由（前两项顶栏已经说了、第三项列表自己数得出来、
     垂直空间才是稀缺资源）正是后来删它的理由，只是从手机推广到了所有尺寸。 */

  .tabbar {
    display: flex; flex: 0 0 auto;
    background: color-mix(in srgb, var(--canvas) 80%, transparent);
    backdrop-filter: saturate(180%) blur(20px);
    -webkit-backdrop-filter: saturate(180%) blur(20px);
    border-top: 1px solid var(--hairline);
    padding-bottom: env(safe-area-inset-bottom, 0);
  }
  .tabbar button {
    flex: 1; border: 0; border-radius: 0; background: none;
    color: var(--label-3); padding: 13px 0 12px; font-size: 11.5px;
    letter-spacing: -0.005em;
    /* 44px 是 iOS 的最小可靠点击高度；原先 37px 的 tabbar 在走路时点不准 */
    min-height: 44px;
  }
  /* ⚠️ 选中态**不能只有颜色**。原来只改 `color` —— 色觉障碍者拿不到这个信息，
     而 11.5px 的蓝字在浅底上对比度本来就有限。
     参考 lersent001.github.io/orb 那个分段控件：它给选中段一层低透明度填充
     （实测 12%），未选段也有 5% —— 所以那一组在你看清"哪个是选中"之前，
     就已经读成一个整体。我们底栏三个按钮之间原来没有任何视觉联系。
     ⚠️ 用伪元素画内缩的块，**不改按钮盒子** —— 按钮必须保持 44px
     （上面那条注释：iOS 最小可靠点击高度，37px 的版本走路时点不准）。
     加 margin 去内缩会把命中区一起缩掉。 */
  .tabbar button { position: relative; z-index: 0; }
  .tabbar button.active { color: var(--blue); }
  .tabbar button.active::before {
    content: ""; position: absolute; z-index: -1;
    inset: 5px 22px; border-radius: 9px;
    background: color-mix(in srgb, var(--blue) 11%, transparent);
  }
  /* ⚠️⚠️ **上面那层填充撑不起这件事，算过才知道**（2026-08-17）：
     11% 蓝叠在 #F2F2F7 上是 1.14:1，而 WCAG 对非文字要求 **3:1**。
     加深也没用 —— 实测 **60% 才 2.17:1**：两个都是浅色，面积色之间的对比
     天然上不去。⚠️ 连 iOS 那个「灰凹槽 + 白块浮起」也只有 **1.15:1**，
     它靠的是**阴影这条边**，不是面积明度差。
     ⇒ 浅底上真正扛事的只有**墨**（描边 / 指示条 / 图标），不是**染**：
       同一个蓝拿来画 3px 指示条是 **3.60:1**，够。
     ⇒ 所以填充留着当锦上添花，**真正的判据信号是这条指示条**。
     ⚠️ 它画在按钮顶边（Material 的位置），不占高度、不动那 44px 命中区。 */
  .tabbar button.active::after {
    content: ""; position: absolute; z-index: 0;
    top: 0; left: 50%; transform: translateX(-50%);
    width: 26px; height: 2.5px; border-radius: 0 0 3px 3px;
    background: var(--blue);
  }
  /* ⚠️ 关掉系统的点击高亮之后，**没有 `:active` 的按钮会变成"点了没反应"** ——
     那比丑更糟。底栏这几个原来一点反馈都没有（全靠系统那层蓝方块），
     补一个轻的：底色一闪 + 微缩。
     ⚠️ 逐个查过：`.viewsw button` / `.iconbtn` / `.detail-head .stop` / `.agent`
       / `.fx-btn` / `.pr-chip` / `.opt-btn` / `#send` 本来就有；只有这里缺。
       **"关掉默认行为"这件事，代价落在没有自定义实现的那些地方** ——
       所以关之前要把它们数一遍。 */
  .tabbar button:active {
    background: var(--surface-2); transform: scale(0.97);
  }
  .tabbar button { transition: background 120ms var(--ease), transform 120ms var(--ease); }
  @media (prefers-reduced-motion: reduce) { .tabbar button { transition: none; } }
  .tabbar .badge {
    display: inline-block; min-width: 16px; margin-left: 5px; padding: 0 5px;
    background: var(--red); color: #fff; border-radius: 999px;
    font-size: 10px; line-height: 16px; font-weight: 590;
  }

  /* ————— 触控目标 —————
     摸底实测这些控件只有 23–30px 高。指尖的可靠命中区约 44px，桌面鼠标的 26px
     在手机上就是"点三次中一次"。这里只加高、不改视觉尺寸（图标还是原来那么大，
     靠 padding 把命中区撑开），所以布局观感不变。 */
  .pr-r, .pr-x { width: 34px; height: 34px; }
  .pr-r svg, .pr-x svg { width: 17px; height: 17px; }
  .pr-chip { min-height: 32px; padding: 4px 12px; font-size: 12px; }
  .pr-rerun { min-height: 34px; padding: 6px 12px; }
  /* ⚠️ 抬命中区**不能用 inline-flex**：`text-overflow: ellipsis` 对 flex 容器
     无效，标题会变成硬截断、连省略号都没有（机主 2026-08-06 那张手机截图里
     就是这样）。用 line-height 撑高度，保持它是普通的块级文本。 */
  .pr-title { min-height: 30px; line-height: 30px; }
  /* ⚠️ 折叠条从这条里摘出去了（2026-08-23）——「继续降低间距」。
     ⚠️ 但**真正生效的不是这条**，是下面 max-width:860px 里那条带 `.msg` 前缀的
        `min-height: 38px`。两处都要改，我第一次只改了这里、白量了一轮。 */
  .expand { min-height: 34px; }
  /* ⚠️⚠️ **展开态那条不吃这个 34px**（2026-08-23 量出来的）：
     34px 是给**折叠态**的触摸目标 —— 那时整行就是主要点击对象。
     而展开之后它只是「署名 · 时间」那一行，34px 会让它比原来的
     `.foot`（17px）**还高 21px** ⇒ 本来是省空间，结果反而多占。
     ⭐ 判据取自工具调用条：手机上它 **21px**，而机主一直在用 ——
       那就是可接受的下限，不必自己拍一个数。 */
  .msg.foldable:not(.folded) .echo-bar { min-height: 0; }
  /* 复选框：13px 的原生方块在手机上几乎点不中，连同它的文字一起做成一个可点区域 */
  .chk { min-height: 38px; display: inline-flex; align-items: center; gap: 7px; }
  .chk input[type="checkbox"] { width: 19px; height: 19px; }

  /* ————— 进度页 —————
     手机上一次只显示一栏，进入「进度」时屏幕上没有任何东西说明这是**谁**的进度。 */
  .tl-who { display: inline; }

  /* ————— 弹窗 —————
     终端快照在手机上应该占满屏：它的内容是 137 列的等宽文本，越窄越难读。 */
  dialog { max-width: 100vw; width: 100vw; height: 100dvh; max-height: 100dvh; border-radius: 0; border: 0; }
  /* ⚠️ 必须写 `dialog[open]`，不能写 `dialog`：UA 样式表用 `dialog:not([open])
     {display:none}` 来隐藏关着的弹窗，直接给 dialog 设 display:flex 会把它**常驻显示**。 */
  dialog[open] { display: flex; flex-direction: column; }
  dialog .dlg-head {
    padding: max(12px, env(safe-area-inset-top, 0px)) 14px 12px;
    flex-wrap: wrap; gap: 8px 10px; flex: 0 0 auto;
  }
  /* 元信息挪到第二行，按钮留在第一行右侧。原样 wrap 会把「关闭」挤到第三行去，
     头部吃掉 290px（844 的三分之一），而这弹窗的全部价值就是那片正文。 */
  dialog .dlg-head #term-meta { order: 3; flex: 1 0 100%; }
  dialog .dlg-head button { min-height: 34px; padding: 6px 12px; }
  /* 正文吃掉剩余高度：写死 calc(100dvh - 116px) 会在头部变成一行或三行时算错 */
  dialog pre { flex: 1 1 auto; min-height: 0; max-height: none; height: auto; font-size: 11px; }
  #term-turns { flex: 1 1 auto; min-height: 0; max-height: none; height: auto; }
  .modal .acts button { min-height: 42px; }
  .modal .opt { padding: 10px 0; }
  /* ⚠️ 上面那条把左右内边距归零（checkbox 项靠 gap 撑开，看不出来），
     但操作项是**整行按钮**：归零会让 hover 底色和边框直接贴到弹窗边缘。 */
  .modal .opt-btn { padding: 11px 10px; }

  /* 剩下这几个是摸底第二轮量出来的：顶栏切换器 36x26、退出 50x34、
     看终端快照 362x35、列表里的归档键 26x26、发送 30x30、快捷短语 23px 高。
     统一把命中区抬到 ≥38px（图标视觉尺寸不动）。 */
  /* ⚠️⚠️ **切换器这条 2026-08-16 从 38 降到 32**（机主：「Tab 就和整个栏一样高了」）。
     顶栏那时压到了 42px，而这条 `min-height: 38` 把按钮顶到 38 ⇒
     连同 2px 内边距正好铺满整条，看起来就是"Tab 和栏一样高"。
     ⚠️ 找了两轮才定位到这里：我先怀疑 header 的 `align-items`（它本来就是 center）、
       又怀疑 `.viewsw` 自己的 flex 拉伸（也不是）—— **撑高它的是一条写在
       另一节里、为触控命中区设的 `min-height`**。
       ⇒ 症状在最外层（"顶满顶栏"），根因可以在完全不相干的另一节；
         **看到"顶满"先去问浏览器谁在撑，别顺着视觉层级猜。**
     ⚠️ 降到 32 是**有代价的**：命中区从 38 变 32，低于那轮定的 ≥38。
       但这几个 tab 是**一排相邻的大目标**（每个约 56×32），误触的代价只是切错页、
       再点一下就回来 —— 和"点不中删除键"不是一个量级。
       ⇒ 不是所有命中区都值同样的钱；这里拿 6px 换顶栏不顶满，划算。 */
  .viewsw button { padding: 6px 13px; min-height: 32px; }
  header #logout { min-height: 38px; }
  #btn-term { min-height: 44px; font-size: 13px; }
  .loadmore { min-height: 38px; }

  /* ————— 字号 —————
     手机上 10–11px 的次要信息（时间戳、每步耗时）在户外基本读不出来。
     只抬这几类，正文不动 —— 正文本来就是 15/16px。 */
  .detail-head .sub { font-size: 11px; }
}

@media (max-width: 860px) and (orientation: landscape) {
  header { height: 42px; }
  .tabbar button { padding: 6px 0 5px; }
}

/* 尊重系统的减少动效设置 */
@media (prefers-reduced-motion: reduce) {
  /* 流光停掉后必须把标题还原成实色，否则渐变定格在那儿会像渲染坏了。
     ⚠️ 但不能还原成普通字色就完事：「生成中/等输入」那两行文字已经删了，
     流光是这个信息**唯一**的载体。所以这里换成静态的强调色 ——
     不动，但仍然看得出这一个跟别的不一样。 */
  /* 流光是"在不在生成"的唯一载体（那两行文字已删），所以减少动效下不能什么都不留：
     换成静态的强调色 —— 不动，但仍看得出这一行跟别的不一样。 */
  .agent.gen .name, .agent.gen .meta, .agent.gen .step {
    background: none; -webkit-text-fill-color: currentColor;
  }
  .agent.gen .name { color: var(--blue); }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

/* ————— 终端 / 对话 双来源查看器 ————— */
.segmented {
  display: inline-flex; padding: 2px; gap: 2px;
  background: var(--surface-2); border-radius: 9px;
}
.segmented button {
  border: 0; background: none; color: var(--label-2);
  padding: 4px 12px; font-size: 12.5px; border-radius: 7px;
}
.segmented button.active {
  background: var(--surface-1); color: var(--label);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.28);
}
#term-meta { color: var(--label-3); font-size: 11.5px; font-family: var(--mono); }
#term-turns { margin: 0; padding: 16px 18px; overflow: auto; max-height: 65vh; }
.turn { margin-bottom: 16px; }
.turn .role {
  font-size: 10.5px; font-weight: 590; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--label-3); margin-bottom: 4px;
}
.turn.assistant .role { color: var(--blue); }
.turn .txt {
  white-space: pre-wrap; word-break: break-word;
  font-size: 13px; line-height: 1.5; color: var(--label);
  border-left: 2px solid var(--hairline); padding-left: 12px;
}
.turn.assistant .txt { border-left-color: color-mix(in srgb, var(--blue) 45%, transparent); }
.turn .cut { color: var(--label-3); font-size: 11.5px; margin-top: 4px; }
#term-refresh:disabled { opacity: 0.45; cursor: default; }

/* 「不是通过平台发生的」那些消息：在别处说的话（主 cc 派活、机主直接在 tmux
   里敲、cc 自己的输出）。灰态 —— 保留左右分栏（谁说的仍看得出），但明显退后，
   一眼能分清"这句是不是通过 agent-team 说的"。 */
.msg.offside .bubble {
  background: none;
  border: 1px dashed var(--hairline-2);
  color: var(--label-2);
}
.msg.offside.down .bubble { background: none; color: var(--label-2); }
.msg.offside .when { color: var(--label-3); opacity: 0.8; }
.msg.offside { opacity: 0.82; }
.msg.offside:hover { opacity: 1; }

/* 未读气泡：iOS 风格的蓝色药丸。只在有未读时出现，正在看的那个 cc 会自动清零。 */
/* ⚠️ 进度就画在「在跑」栏那个 `Workflow` 标签自己身上（机主 2026-08-19：
   「不需要额外的 Tag」）。背景按完成度填充。
   ⚠️ 这里曾经在顶栏和列表行各有过一份，都被机主否了 —— **一个状态一个位置**。
   ⚠️ 文字**必须自己定成中性深色**：默认继承的是那圈蓝，蓝字配蓝底实测只有
      2.33:1（正文门槛 4.5）。这个错在代码里显得很自洽，因为两者都"来自 --blue"。
      改后实测：已填充段 9.89:1、未填充段 13.9:1，两段都过。
   ⚠️ 不设 height、不设纵向 padding —— 这一行的高度由别的东西决定。 */
.rt-row .rt-kind.wf-prog {
  color: var(--label);
  font-variant-numeric: tabular-nums;
  background: linear-gradient(90deg,
      color-mix(in srgb, var(--blue) 34%, transparent) 0 var(--p, 0%),
      var(--surface-2) var(--p, 0%) 100%);
  box-shadow: inset 0 0 0 1px var(--hairline);
}

.agent .unread {
  flex: 0 0 auto; align-self: center;
  min-width: 20px; padding: 0 6px;
  background: var(--blue); color: #fff;
  border-radius: 999px;
  font-size: 11.5px; font-weight: 590; line-height: 20px;
  text-align: center; letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
}
.agent .unread[hidden] { display: none; }

/* 「展开闲置会话」的折叠按钮 */
.fold {
  display: block; width: calc(100% - 8px); margin: 6px 4px;
  padding: 9px 12px; text-align: left;
  background: none; border: 1px dashed var(--hairline-2);
  color: var(--label-3); font-size: 12px; border-radius: var(--radius);
}
.fold:hover { color: var(--label-2); border-color: var(--label-3); background: var(--surface-1); }

/* 折叠条：一行摘要 + 展开按钮。⚠️ **折叠不是删除**，判错了最多多点一下。
   三种消息会折（机主 2026-08-23 定的阈值）：
     · 会话内复述（echo）—— 一律折，它连"在别处说的话"都不算，只是同一句的第二遍
     · 平台巡检（author=platform）—— 超过 **20 字符**就折
     · 灰态（在别处发生的：终端 / 原生 peer）—— 超过 **50 字符**就折
   ⚠️⚠️ 选择器 2026-08-23 从 `.msg.echo` 全局改成 `.msg.foldable`（20 处一起改）——
      只改一部分的话，没改到的那几条会**静默失效**：折叠条画出来了但气泡不隐藏、
      或者反过来。⇒ echo 消息现在同时带 `echo` 和 `foldable` 两个类。 */
.msg.foldable .echo-bar {
  display: flex; align-items: center; gap: 7px; width: 100%;
  padding: 1px 9px; border: 0; border-radius: 8px;
  background: none; cursor: pointer; text-align: left;
  font: inherit; font-size: 11.5px; color: var(--label-3);
  transition: background .15s ease, color .15s ease;
}
.msg.foldable .echo-bar:hover { background: var(--surface-2); color: var(--label-2); }
.msg.foldable .echo-bar:focus-visible { outline: 2px solid var(--blue); outline-offset: 1px; }
.msg.foldable .echo-bar .lbl { flex: 0 0 auto; }
.msg.foldable .echo-bar .pv {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  opacity: .62;
}
/* 展开态不再重复预览那半句 —— 正文就在下面 */
.msg.foldable:not(.folded) .echo-bar .pv { display: none; }

/* ⚠️⚠️ **展开之后那一行不许白占**（机主 2026-08-23 两轮反馈）。
   第一轮：「顶部是终端两个字独占一行……从顶部开始就是内容」——
   我把它绝对定位到 `.copy` 那一列，占位没了，但**制造了两个新问题**：
   收起键跑到右边（不在原地）、只有一行时和复制键**重合**。
   第二轮他说「参考那个工具调用那一块改一下」——

   ⭐ **工具调用条展开后也留在原位，而它不算浪费，因为它承载了署名+时间**
     （`.acts-ts`）—— 那一行本来就要占。而我的条展开后只剩"终端"两个字，
     **那才是浪费**。⇒ 解法不是把条藏起来，是**让它有内容**。

   ⇒ 展开态：条留在原位（收起键还在左上角、不碰 copy），
     里面显示「署名 · 时间」，同时把气泡下面那行 `.foot` 省掉。
   ⚠️ 只有 `.foot` 里**没有投递状态**时才这么搬 —— 巡检消息那一行还挂着
      ✓✓/恢复按钮，塞不进一个条（同 `__inline_ts` 对工具条的判据）。 */
.msg.foldable:not(.folded) .echo-bar .lbl { display: none; }
.msg.foldable:not(.folded) .echo-bar .fold-ts { display: inline; }
.msg.foldable .echo-bar .fold-ts {
  display: none; flex: 0 0 auto; margin-left: auto;
  font-size: 11px; color: var(--label-3);
  font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1;
}

.msg.foldable .echo-bar .caret {
  flex: 0 0 auto; width: 0; height: 0;
  border-left: 4px solid currentColor;
  border-top: 3.5px solid transparent; border-bottom: 3.5px solid transparent;
  transition: transform .18s ease;
}
.msg.foldable:not(.folded) .echo-bar .caret { transform: rotate(90deg); }

.msg.foldable.folded .bubble,
.msg.foldable.folded .when,
.msg.foldable.folded .st { display: none; }
/* ⚠️ 这一条比上面那条 `.msg.foldable.folded, .msg.nobody` **更靠后**，
   同特异性靠后者赢 —— 改间距要改这里，改上面那条会静默无效（踩过）。 */
.msg.foldable.folded { margin-bottom: 2px; opacity: .9; }

@media (prefers-reduced-motion: reduce) {
  .msg.foldable .echo-bar, .msg.foldable .echo-bar .caret { transition: none; }
}

/* 删除按钮：默认几乎不可见，hover 到那一行才浮出来 —— 这是个不可逆操作，
   不该在十几行里到处都是红色图标抢注意力，但也不能藏到找不着。 */
.agent .trash {
  flex: 0 0 auto; width: 26px; height: 26px; padding: 0;
  display: grid; place-items: center;
  border: 0; border-radius: 7px; background: none; cursor: pointer;
  color: var(--label-3); opacity: 0;
  transition: opacity .15s ease, background .15s ease, color .15s ease;
}
.agent:hover .trash, .agent .trash:focus-visible { opacity: 1; }
.agent .trash:hover { background: var(--red-soft, rgba(255,69,58,.14)); color: var(--red); }
.agent .trash:focus-visible { outline: 2px solid var(--red); outline-offset: 1px; }
/* 触屏没有 hover，永远浮不出来 → 直接常显 */
@media (hover: none) { .agent .trash { opacity: .55; } }

/* 置顶键：行尾那个位置（机主 2026-08-23 从归档换过来的）。
 *
 * ⚠️⚠️ **两个态现在都 hover 才浮现，而且收起时不占宽度**
 *    （机主 2026-08-24：「比较占位置……默认不显示这个按钮，这样一个 Agent tab
 *    里面能放更多字，指针悬停在上面的时候再显示，然后标题相应的给它让位置」）。
 *    这推翻了 08-23 那条「已置顶常显」—— 当时的理由是"它被顶住了"是个**状态**、
 *    藏起来等于没显示。机主取舍的是版面：置顶状态改由**排在最上面 +
 *    不参与闲置折叠**表达，那两条本来就一直在。
 *    ⇒ 别照着下面「结束归档」那条（状态要常显）把它改回来 ——
 *      那条说的是"有件事没收尾"，是**待办**；置顶只是**排序偏好**。
 * ⚠️ 用 `--blue` 而不是红色：置顶是中性动作，红色留给会删东西的那些。
 * ⚠️⚠️ **别写 `var(--accent)`** —— 这个仓库没有这个变量（第一版就是这么写的）。
 *    CSS 引用不存在的变量**不报错**，只是静默回落到继承色 ⇒
 *    已置顶那个图标一直是深灰的，和未置顶**根本分不出来**，
 *    而我以为它是蓝的。⭐ 又一次「引用了一个不存在的东西，且没有任何东西会说」。
 *    ⇒ 用真实存在的 `--blue`，并且**照样给 fallback**（换主题时不至于又静默回落）。
 */
.agent .pinbtn {
  /* ⚠️ 30px 而不是归档键那个 26px（机主 2026-08-23：「按钮稍微大一点」）——
     它现在是**每一行都在的常用动作**，而归档键当时是"偶尔用一次"。
     ⚠️ 行高是 `padding: 7px 12px` 自适应的，30px 不会把行撑高。

     ⚠️⚠️ **不占位（机主 2026-08-24：「这个置顶和取消置顶的按钮比较占位置」）。**
     此前是 `width: 30px` + `opacity: 0` —— **看不见，但 30px 一直占着**，
     加上 `.agent` 的 `gap: 12px` 共 42px，每一行的名字都少 42px 可写。
     ⭐ 「藏起来」有两种写法，差别正好在这里：`opacity: 0` 只是**不画**，
       盒子还在；要把地让出来必须让**宽度**归零。
     ⇒ 默认 `width: 0` + 负 margin 吃掉那道 gap，hover / 键盘聚焦时再撑开，
       名字（`.name` 带省略号）自动补上这 42px。
     ⚠️ **不能用 `display: none`**：那样它会**退出 tab 顺序**，键盘就再也够不到
       置顶了，而这件事在界面上看不出来（鼠标用户一切正常）。
       `width: 0` 仍然可聚焦，`:focus-visible` 那条会把它撑开。 */
  flex: 0 0 auto; width: 0; height: 30px; padding: 0;
  margin-left: -12px;                    /* 抵掉 .agent 的 gap，收起时真正 0 占位 */
  overflow: hidden;
  display: grid; place-items: center;
  border: 0; border-radius: 7px; background: none; cursor: pointer;
  color: var(--label-3); opacity: 0;
  transition: opacity .15s ease, width .15s ease, margin .15s ease,
              background .15s ease, color .15s ease;
}
/* 展开的三种情形：悬停这一行 · 键盘聚焦到它 · 上一次操作失败了（要让人看见）。
   ⚠️ `.failed` 必须在这儿一起撑开 —— 它下面那条只设了 `opacity: 1`，
      宽度还是 0 的话「失败」就是**画在一个零宽盒子里**，等于没提示。 */
.agent:hover .pinbtn, .agent .pinbtn:focus-visible, .agent .pinbtn.failed {
  width: 30px; margin-left: 0; opacity: 1;
}
/* hover 才变蓝 —— **蓝色是"可以点我"的召唤，不是"已经置顶了"的状态**。
   机主 2026-08-23：「颜色好像反了，没有置顶的 Agent 置顶按钮亮起」。 */
.agent .pinbtn:hover { background: rgba(10,132,255,.14); color: var(--blue, #0a84ff); }
.agent .pinbtn:focus-visible { outline: 2px solid var(--blue, #0a84ff); outline-offset: 1px; }
/* 已置顶：**常显但低调**。
   ⚠️ 常显本身就说明"这一行是置顶的"，不必再用颜色喊第二遍 ——
      上一版给它上了蓝，结果最显眼的反而是"已经做完的事"，
      而真正等着被点的（未置顶那些）却是灰的、还得 hover 才出现。
   ⚠️ 图标那时也换成了 `↓`（见 PIN_SVG）：点它做的是**取消**。 */
/* 已置顶：hover 出来之后**低调**，不喊第二遍。
   ⚠️ 上一版给它上了蓝，结果最显眼的反而是"已经做完的事"。
   ⚠️⚠️ **它此前是常显的**（`opacity: .75` 且宽度恒为 30px），理由写在上面那段：
      "它被顶住了"是个状态，藏起来等于没显示。机主 2026-08-24 推翻了这一条 ——
      他要的是每一行能多放字，`置顶和取消置顶的按钮`**两个都**默认不显示。
      ⇒ 于是"这一行被置顶了"现在只由**排在最上面 + 不参与闲置折叠**表达。
      别好心把常显改回来：那会把这 42px 又占回去，而那正是这次要省的。 */
.agent .pinbtn.on { color: var(--label-2, #6e6e73); }
.agent:hover .pinbtn.on { opacity: .75; }
.agent .pinbtn.on:hover { opacity: 1; color: var(--blue, #0a84ff); }
.agent .pinbtn.failed { color: var(--red); }
/* 触屏没有 hover，永远浮不出来 → 直接常显（宽度也要给，不然只是"看不见的 0 宽"）。 */
@media (hover: none) {
  .agent .pinbtn { width: 30px; margin-left: 0; opacity: .55; }
}

/* 「结束归档」：和归档键占同一个位置，但**常显不浮现** ——
   归档键是"随手可做的动作"，藏起来是对的；这个是**在提示机主有件事没收尾**，
   藏起来等于没做（同一天刚踩过：红边算对了却躺在折叠区里，屏幕上没有像素变化）。*/
.agent .settle {
  flex: 0 0 auto; padding: 3px 8px; white-space: nowrap;
  border: 1px solid var(--sep, rgba(0,0,0,.12)); border-radius: 7px;
  background: var(--bg-elev, rgba(255,255,255,.6)); cursor: pointer;
  font-size: 11px; line-height: 1.4; color: var(--label-2);
  transition: background .15s ease, color .15s ease, border-color .15s ease;
}
/* 按钮吃掉约 62px 宽 → 这一行的 meta 会折行，整行比别的高一截（实测 98 vs 76），
   一列卡片参差不齐比信息少一点更扎眼。⇒ 这一行的 meta 改成单行截断，
   重点（⚠️ 归档还没结束）在最前面，不会被截掉。 */
.agent.pending .meta { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.agent .settle:hover { border-color: var(--blue); color: var(--blue); }
.agent .settle:focus-visible { outline: 2px solid var(--blue); outline-offset: 1px; }
.agent .settle:disabled { opacity: .5; cursor: default; }
.agent .settle.failed { border-color: var(--red); color: var(--red); }

/* 已下删除令但还活着：整行退色 + 红色竖条，一眼看出"这行正在被处理" */
.agent.pending { opacity: .62; }
.agent.pending .meta-main { color: var(--red); }
.agent.pending .trash { display: none; }

/* 删除确认弹窗 */
.modal {
  border: 0; border-radius: 16px; padding: 0;
  background: var(--surface-1); color: var(--label);
  box-shadow: 0 18px 50px rgba(0,0,0,.32);
  max-width: 400px; width: calc(100vw - 40px);
  /* ⚠️ 弹窗**永远不能比屏幕高**：`<dialog>` 超出视口时底部那排按钮会被切掉，
     而那里正是「确认 / 返回 / 关闭」—— 人会以为弹窗坏了。
     实测操作弹窗在手机上是 372×915 = 整屏（机主 2026-08-17 报「太长」）。 */
  max-height: calc(100dvh - 40px);
  /* ⚠️ `display:flex` 会让 `<dialog>` 丢掉 UA 那条 `height: fit-content`，
     于是它被 `inset:0` 拉满到 max-height —— 实测确认屏里表单只有 354px，
     弹窗却是 875px，底下空一大片。显式写回来。 */
  height: fit-content;
}
/* ⚠️⚠️ **`display` 只能加在 `[open]` 上**（2026-08-17 我自己踩的）。
   写成 `.modal { display: flex }` 会盖掉 UA 那条 `dialog:not([open]) { display:none }`
   ⇒ **5 个关着的弹窗全部进入文档流**，把页面撑高 591px（实测 innerHeight 900、
   documentElement.scrollHeight 1491）⇒ 整个网页能上下滚、顶栏被滚走。
   机主在飞书 webview 里一眼看到的就是这个。
   ⚠️ 判据：给 `<dialog>` / `<details>` 这类**自带显隐语义**的元素设 `display`，
      永远要带上它的状态选择器 —— 无条件设等于把那条语义删了。 */
.modal[open] { display: flex; flex-direction: column; }
.modal form { min-height: 0; display: flex; flex-direction: column; }
/* 标题和底部按钮钉住，只有中间的列表滚 —— 长到要滚的时候，
   最不该滚走的恰恰是"这是在操作谁"和"怎么退出"。 */
#ops-list { overflow-y: auto; min-height: 0; }

/* 操作弹窗：桌面加宽 + 双栏。
   ⚠️ 400px 宽在 1440 的屏上只用了 28% 的宽、却占满 96% 的高（实测 400×866）——
      每项的说明被压成 3–4 行，是"窄"造出来的"长"。
   ⚠️ 只在够宽时才双栏：窄屏双栏会把每项挤成两三个字一行，比单栏更难读。 */
@media (min-width: 720px) {
  #ops-modal { max-width: 660px; }
  #ops-modal #ops-list {
    display: grid; grid-template-columns: 1fr 1fr; gap: 4px 10px; align-content: start;
  }
  /* ⚠️ 确认屏是**一项 + 一排按钮**，双栏会把它们并排摆成两半。
     它俩都是 `#ops-list` 的直接子元素，所以在这儿显式跨列。 */
  #ops-modal #ops-list > .opt-sure,
  #ops-modal #ops-list > .acts { grid-column: 1 / -1; }
  /* 双栏之后 `+ .opt-btn` 那条相邻边距会在换列处产生错位，改用 gap 统一管 */
  #ops-modal #ops-list > .opt-btn + .opt-btn { margin-top: 0; }
}
.modal::backdrop { background: rgba(0,0,0,.42); backdrop-filter: blur(2px); }
.modal form { padding: 22px 22px 18px; }
.modal h2 { font-size: 16.5px; font-weight: 640; letter-spacing: -.01em; margin: 0 0 6px; }
.modal h2 span { color: var(--blue); }
.modal .sub { font-size: 12px; color: var(--label-2); line-height: 1.65; margin: 0 0 16px; }

.modal .opt {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 9px 10px; border-radius: 9px; cursor: pointer;
  transition: background .12s ease;
}
.modal .opt:hover { background: var(--surface-2); }
.modal .opt input { margin-top: 2px; accent-color: var(--red); flex: 0 0 auto; }
.modal .opt span { display: flex; flex-direction: column; gap: 2px; }
.modal .opt b { font-size: 13px; font-weight: 560; }
.modal .opt i { font-style: normal; font-size: 11.5px; color: var(--label-3); }

/* 操作弹窗里的项：整行是一个按钮（不是 checkbox），点了就执行。
   ⚠️ 复用 `.opt` 的排版，但要把 <button> 的 UA 默认样式抹掉（边框/背景/居中/字体），
      否则同一个 `.opt` 在两个弹窗里长得不一样。 */
.modal .opt-btn {
  width: 100%; font: inherit; text-align: left;
  border: 1px solid transparent; background: none; color: inherit;
}
.modal .opt-btn + .opt-btn { margin-top: 2px; }
.modal .opt-btn:hover { background: var(--surface-2); border-color: var(--hairline-2); }
.modal .opt-btn i { line-height: 1.6; }
/* 危险项只把标题染红，不整行铺红 —— 弹窗里三项并列，整行红底会把视线全吸过去，
   而它并不比另外两项更该被先看到，只是更该被看清楚。 */
/* ⚠️ **只染标题那个 `b`，不能染说明里的**（2026-08-17 发现）。
   写成 `.danger b` 会连说明里 `**…**` 转出来的 `<b>` 一起染红 ——
   「强制交接」那项的摘要里有「**它已经坏了**」，于是整句飘红、和标题的红撞成一片，
   而这条规则自己的注释写的是「危险项**只把标题染红**」。
   ⚠️ 用直接子选择器钉住结构：标题是 `<span>` 的第一个 `<b>`，说明里的是嵌在 `<i>` 里的。 */
.modal .opt-btn.danger > span > b { color: var(--red); }
.modal .opt-btn.danger:hover { border-color: var(--red); }

/* 二次确认那一屏里被确认的那一项。⚠️ 它是**摊开给你看的东西，不是能点的东西** ——
   所以底色常亮（表示"就是这一项"）、指针不变手型、悬停也不再变化。
   长得像按钮却点不动，比长得不像更让人困惑。 */
.modal .opt-sure {
  cursor: default; background: var(--surface-2);
  border: 1px solid var(--hairline-2); padding: 11px 12px;
}
.modal .opt-sure:hover { background: var(--surface-2); }
.modal .opt-sure b { font-size: 13.5px; }
.modal .opt-sure i { line-height: 1.6; }
.modal .opt-sure.danger { border-color: var(--red); }
.modal .opt-sure.danger > span > b { color: var(--red); }

/* 操作弹窗在「列表」和「确认」两屏之间是 innerHTML 直接替换 —— 原来一帧就换掉了，
   看不出是同一个弹窗换了内容，还是弹了个新的。给换进来的内容一个短淡入。
   ⚠️ 140ms 短于弹窗自身 200ms 的入场：首次打开时两者叠在一起是"内容随面板落位"，
      不是两段动画打架。 */
@keyframes ops-swap { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
#ops-list > * { animation: ops-swap 140ms var(--ease) both; }
@media (prefers-reduced-motion: reduce) {
  #ops-list > * { animation-name: none; }   /* 位移去掉，内容照常显示 */
}

.modal .note { font-size: 11.5px; color: var(--label-2); margin: 12px 2px 0; min-height: 1em; line-height: 1.6; }
.modal .note.bad { color: var(--red); }

.modal .acts { display: flex; justify-content: flex-end; gap: 9px; margin-top: 16px; }
.modal .acts button {
  font: inherit; font-size: 13px; padding: 7px 16px;
  border-radius: 9px; border: 1px solid var(--hairline-2);
  background: var(--surface-2); color: var(--label); cursor: pointer;
  transition: background .12s ease, opacity .12s ease;
}
.modal .acts button:hover { background: var(--hairline-2); }
.modal .acts .danger { background: var(--red); border-color: var(--red); color: #fff; }
.modal .acts .danger:hover { filter: brightness(1.08); }
.modal .acts button:active:not(:disabled) { transform: scale(0.98); transition-duration: 90ms; }
.modal .opt-btn:active { transform: scale(0.995); transition-duration: 90ms; }
.modal .acts button:disabled { opacity: .5; cursor: default; }

@media (prefers-reduced-motion: reduce) {
  .agent .trash, .modal .opt, .modal .acts button { transition: none; }
}

/* 已归档：它不再是一个"在跑的 cc"，所以没有环、没有流光、整体退到背景里。
   但**可点** —— 历史消息一条没删，随时能翻回去看。 */
.agent.archived { opacity: .58; padding-left: 14px; }
.agent.archived:hover { opacity: .85; }
.agent.archived .body { padding-left: 0; }
.agent.archived .step {
  font-family: var(--mono); font-size: 10.5px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.agent.archived.restoring .meta-main { color: var(--blue); }

.agent .restore {
  flex: 0 0 auto; font: inherit; font-size: 11.5px;
  padding: 4px 11px; border-radius: 7px;
  border: 1px solid var(--hairline-2); background: var(--surface-2);
  color: var(--label-2); cursor: pointer;
  transition: background .12s ease, color .12s ease, border-color .12s ease;
}
.agent .restore:hover { background: var(--blue); border-color: var(--blue); color: #fff; }
.agent .restore:disabled { opacity: .55; cursor: default; }
.agent .restore.failed { border-color: var(--red); color: var(--red); }
/* 归档行信息密度高（时间+清理项+路径三行），任何一行换行都会把行高撑乱 */
.agent.archived .meta, .agent.archived .name {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* placeholder 绝不折行：textarea 的 placeholder 会自动换行，第二行超出可视高度后
   被裁成半截，观感比省略号差得多（手机 393px 实测踩到）。 */
.composer textarea::placeholder {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* 回到底部：往上翻记录时才出现。放右下角，与居中的「N 条新消息」药丸错开位置，
   但两者不会同时出现 —— 都是"跳回底部"，同时摆两个只让人多犹豫一次。 */
.tobottom {
  position: absolute; right: 24px; bottom: 108px; z-index: 6;
  width: 32px; height: 32px; padding: 0; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--surface-1); color: var(--label-2);
  border: 1px solid var(--hairline-2);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
  cursor: pointer;
  transition: background 140ms var(--ease), color 140ms var(--ease), transform 140ms var(--ease);
  animation: pop-plain 240ms var(--ease);
}
.tobottom:hover { background: var(--blue); color: #fff; border-color: var(--blue); }
.tobottom:active { transform: scale(0.96); }
.tobottom:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
@keyframes pop-plain { from { transform: scale(0.94); opacity: 0 } to { transform: scale(1); opacity: 1 } }

/* 输入框超过 max-height 后会冒出滚动条滑块 —— 机主要求去掉。
   ⚠️ 只是隐藏滑块本身，滚动能力保留（overflow 仍是 auto），
   否则长内容会被截断且没法往下看。 */
.composer textarea { scrollbar-width: none; }
.composer textarea::-webkit-scrollbar { width: 0; height: 0; }

@media (max-width: 860px) { .tobottom { right: 16px; bottom: 96px; } }
@media (prefers-reduced-motion: reduce) { .tobottom { animation: none; transition: none; } }

/* 历史懒加载的顶部提示条 */
.stream .loadmore, .stream .streamtop {
  display: block; width: fit-content; margin: 4px auto 16px;
  font-size: 11.5px; padding: 5px 14px; border-radius: 999px;
}
.stream .loadmore {
  background: var(--surface-1); border: 1px solid var(--hairline-2);
  color: var(--label-2); cursor: pointer;
  transition: background 140ms var(--ease), color 140ms var(--ease);
}
.stream .loadmore:hover:not(:disabled) { background: var(--surface-2); color: var(--label); }
.stream .loadmore:disabled { opacity: .6; cursor: default; }
.stream .streamtop { color: var(--label-3); border: 1px solid transparent; }

/* ————————————— 下拉刷新（仅移动端列表页）—————————————
   ⚠️ 指示器必须放在 #list **外面**：renderList 每轮都整份重建 innerHTML，
   放里面会被直接抹掉。所以挂在 main 上（main 已设 position: relative）。 */
.ptr {
  display: none;
  position: absolute; top: 0; left: 0; right: 0; height: 0;
  justify-content: center; align-items: flex-end;
  pointer-events: none; z-index: 4;
  overflow: hidden;
}
body[data-view="list"] .ptr { display: flex; }
@media (min-width: 861px) { .ptr { display: none !important; } }

.ptr-spin {
  width: 20px; height: 20px; margin-bottom: 9px;
  border-radius: 50%;
  border: 2px solid var(--hairline-2);
  border-top-color: var(--blue);
  opacity: 0;
  /* 下拉过程中由 JS 按拉动距离设 --p（0→1）驱动，松手后才自转 */
  transform: rotate(calc(var(--p, 0) * 300deg)) scale(calc(0.6 + var(--p, 0) * 0.4));
}
.ptr.armed .ptr-spin { border-top-color: var(--green); }
.ptr.busy  .ptr-spin { opacity: 1 !important; animation: ptr-spin 0.7s linear infinite; }
@keyframes ptr-spin { to { transform: rotate(360deg) scale(1); } }

/* 列表整体跟着手指走，做出橡皮筋手感 */
.col--list { will-change: transform; }
.col--list.ptr-drag { transition: none; }
.col--list.ptr-release { transition: transform 260ms var(--ease); }

@media (prefers-reduced-motion: reduce) {
  .col--list.ptr-release { transition: none; }
  .ptr.busy .ptr-spin { animation: none; opacity: 1 !important; }
}

/* ————————————— 长消息折叠 —————————————
   ⚠️ 与「回声折叠」是两套，视觉上必须一眼分得开：
     回声   —— 整条塌成一行灰条 + 三角（这是同一件事的第二遍，别占地方）
     长消息 —— 气泡照常在，只是限高 + 底部渐隐 + 「展开全文」（内容有用，只是长）
   共用一套会让人以为被折的都是重复内容。 */
/* ⚠️ 阈值只在这里定义一次，JS 读这个变量 —— 以前 CSS 写 260px、JS 写
   LONG_PX=260，两份魔数改一处漏一处就会"量的和显示的不是一回事"。 */
:root { --long-max: 520px; }

.msg.long .bubble {
  max-height: var(--long-max); overflow: hidden;
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 56px), transparent);
          mask-image: linear-gradient(to bottom, #000 calc(100% - 56px), transparent);
}
.msg.long.long-open .bubble {
  max-height: none;
  -webkit-mask-image: none; mask-image: none;
}

.msg .expand {
  display: block; margin: 5px 0 0; padding: 3px 10px;
  font: inherit; font-size: 11.5px; line-height: 1.5;
  background: none; border: 0; border-radius: 6px;
  color: var(--blue); cursor: pointer;
  transition: background 120ms var(--ease);
}
.msg .expand:hover { background: var(--surface-2); }
.msg .expand:focus-visible { outline: 2px solid var(--blue); outline-offset: 1px; }
.msg.down .expand { margin-left: auto; }        /* 机主那侧靠右，跟气泡对齐 */

/* 回声折叠优先：整条都塌了，长文按钮当然也不该露出来 */
.msg.foldable.folded .expand { display: none; }

@media (prefers-reduced-motion: reduce) { .msg .expand { transition: none; } }

/* ————————————— 断线降级 —————————————
   机主最初提「服务不可用」时的真实困扰是：分不清是平台挂了还是所有 cc 都停了。
   留白/报错两者长得一模一样；灰显 + 说明数据有多旧，才能一眼区分。 */
/* ⚠️ 入场（2026-08-09）：原来是凭空出现一条橙条。它压在内容上方，
   从顶边滑下来才说得清"这是系统级提示，不是内容的一部分"；退场沿原路回去。 */
.stalebar {
  transform: none; opacity: 1;
  transition: transform 200ms var(--ease), opacity 200ms var(--ease),
              display 200ms allow-discrete;
  position: absolute; top: 0; left: 0; right: 0; z-index: 8;
  padding: 6px 14px; text-align: center;
  font-size: 11.5px; color: #fff;
  background: var(--orange);
  box-shadow: 0 2px 12px rgba(0,0,0,.25);
}
.stalebar[hidden] { transform: translateY(-100%); opacity: 0; }
@starting-style { .stalebar:not([hidden]) { transform: translateY(-100%); opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .stalebar, .stalebar[hidden] { transform: none; }
}
.stalebar b { font-weight: 600; }

/* 整站打灰，但**不禁用交互** —— 机主仍然可以翻历史消息（那些是本地数据）。
   只是发出去的东西会失败，由投递状态机如实呈现。 */
body.stale .col--list,
body.stale .col--right,
body.stale .stream { filter: grayscale(0.85); opacity: .72; }
body.stale main { padding-top: 26px; }          /* 给横幅让位，别盖住第一行 */
@media (prefers-reduced-motion: reduce) { .stalebar { box-shadow: none; } }

/* ————————————— 气泡内的 Markdown —————————————
   ⚠️ 用 .md 关掉 pre-wrap：换行已经由 md-p / md-gap 这些块元素表达了，
   再叠 pre-wrap 会让每段之间多出一倍空白。 */
.msg .bubble.md { white-space: normal; }
.md .md-p { margin: 0; }
.md .md-gap { height: 0.62em; }
.md .md-h { font-weight: 640; margin: 2px 0 1px; letter-spacing: -0.01em; }
.md .md-h1 { font-size: 1.16em; }
.md .md-h2 { font-size: 1.09em; }
.md .md-h3, .md .md-h4 { font-size: 1.02em; }
.md .md-hr { border: 0; border-top: 1px solid var(--hairline-2); margin: 9px 0; }
.md .md-q {
  margin: 2px 0; padding: 1px 0 1px 10px;
  border-left: 2px solid var(--hairline-2); color: var(--label-2);
}
.md .md-l { margin: 2px 0; padding-left: 1.35em; }
.md .md-l li { margin: 1px 0; }
.md code {
  font-family: var(--mono); font-size: 0.87em;
  background: var(--surface-2); padding: 1px 5px; border-radius: 4px;
}
.md .md-pre {
  margin: 5px 0; padding: 9px 11px; border-radius: 9px;
  background: var(--surface-2); overflow-x: auto;
}
.md .md-pre code { background: none; padding: 0; font-size: 0.85em; line-height: 1.5; }

/* ————— 代码块复制按钮（机主 2026-08-20）—————
   ⚠️ 这一段**故意是全局的**，不像 `#dv .md*` 那样限定 —— 复制是**功能**不是排版，
   聊天消息和交付物查看器都要，而且两处该长得一样。
   （对照：排版必须限定，因为报告和聊天气泡要的排版根本不是一回事。） */
.md-prewrap { position: relative; }
.md-copy {
  position: absolute; top: 6px; right: 6px;
  padding: 3px 9px; border-radius: 6px; font-size: 11px; line-height: 1.5;
  border: 1px solid var(--hairline-2); background: var(--canvas);
  color: var(--label-2); cursor: pointer;
  /* ⚠️ 平时半隐：代码块本身才是内容，按钮不该一直抢注意力。
     ⚠️ 但**不能 `display:none`** —— 那样键盘用户 Tab 不到它。 */
  opacity: 0; transition: opacity 140ms var(--ease), background 140ms var(--ease);
  user-select: none;      /* ⚠️ 全选复制代码时别把"复制"两个字也带走 */
}
.md-prewrap:hover .md-copy, .md-copy:focus-visible { opacity: 1; }
.md-copy:hover { background: var(--surface-2); color: var(--label); }
.md-copy.done { opacity: 1; color: var(--green); border-color: var(--green); }
/* ⚠️ 触屏没有 hover —— 一直显示，否则在手机上**根本点不到**。 */
@media (hover: none), (pointer: coarse) {
  .md-copy { opacity: .72; }
}
.md a { color: var(--blue); text-decoration: none; }
.md a:hover { text-decoration: underline; }
.md .md-cut { color: var(--label-3); font-size: 0.9em; margin-top: 4px; }

/* 表格必须能横向滚动：窄屏或列多时，撑破气泡会把整个消息流的布局带歪 */
.md .md-tw {
  overflow-x: auto; margin: 6px 0;
  /* 让人一眼看出"这里能横着划" —— 手机上没有滚动条，不给提示就等于没有 */
  -webkit-overflow-scrolling: touch; overscroll-behavior-x: contain;
}
/* ⚠️⚠️ **`min-width: min(100%, max-content)` 在窄屏上等于没写**
   （机主 2026-08-16 截图）：窄屏时 `100%` 比 `max-content` 小，`min()` 取到 100%
   ⇒ 表格被压回容器宽度，最宽那一列只能一字一行竖着排
   （实测「alpha 的沙箱服务（standalone…）」排成了 20 多行，一行一个字），
   **而外面那个 `overflow-x: auto` 于是永远不会触发** —— 滚动容器形同虚设。
   ⇒ 改成 `width: max-content`：表格**按内容撑开**，撑不下就真的横向滚动。
   ⚠️ 这一族的判据：**给了滚动容器，就要让内容真的能超出它**；
     否则那个 `overflow` 只是一句没人执行的声明。 */
.md .md-t {
  border-collapse: collapse; font-size: 0.92em;
  width: max-content; max-width: none;
}
.md .md-t th, .md .md-t td {
  border: 1px solid var(--hairline-2); padding: 4px 9px; text-align: left; vertical-align: top;
  /* ⚠️ 单元格给一个下限和上限：没有下限时窄内容会被挤成一字一行；
     没有上限时一段长说明会把表格拉到几千像素宽，横向划不到头。 */
  min-width: 4.5em; max-width: 22em;
}
.md .md-t th { background: var(--surface-2); font-weight: 590; white-space: nowrap; }
/* 首列常是行标题（「alpha 的沙箱服务」这种），给它宽一点，别一上来就折。 */
.md .md-t td:first-child, .md .md-t th:first-child { min-width: 7em; }
/* infra 折叠按钮：紧贴主 cc 下方，视觉上比闲置/归档那两个更收敛
   —— 它是常驻分组的入口，不是"有一堆东西被藏起来了"的提示 */
.fold--infra { margin-top: 2px; margin-bottom: 6px; opacity: .8; }
.fold--infra:hover { opacity: 1; }

/* ————————————— 移动边框光 —————————————
 *
 * ⚠️ 上一版我做错了：一个 62px 的模糊光球沿周长飘，一半在行外、一半糊在内容上。
 *    那是"光球绕圈"，不是 border —— 机主给的两个参考里，**关键都在遮挡**：
 *
 *    ShimmerButton 的结构（magicui）：
 *      spark（旋转 conic 楔形，-z-30，blur 2px）
 *      → backdrop：`absolute inset-[--cut] -z-20 background: var(--bg)`
 *        一个内缩 --cut（0.05em ≈ 1px）、与容器同底色、层级压在 spark **之上**的块
 *      → 于是光只在最外那 1px 的环里露出来 = 边框在发光
 *    Aceternity moving-border 同理：容器 p-[1px] + 内层不透明块盖住中心。
 *
 * 我们的行里是真实内容，不能盖不透明块。等价做法是**遮罩**：
 * 用 padding + mask-composite:exclude 只保留最外一圈，中间挖空。
 * 这样内容完全不受影响，而光只在描边上跑。
 *
 * ⚠️ 光必须是这个元素的 background 才能被遮罩裁成环 —— 所以用旋转 conic 楔形
 *    （参考实现也是 conic），而不是上一版那种独立定位的光球。
 * ⚠️ 角度用 @property 注册成 <angle> 才能被动画平滑插值；
 *    不注册的话 CSS 变量按字符串处理，animation 会变成"跳变"而不是转动。
 * ⚠️ ::before 已被行间分隔线占用（.agent + .agent::before），只能用 ::after。
 */
/* ⚠️⚠️ **`inherits: true` 是刻意的**（机主 2026-08-20：「自动刷新会打断/重置环绕特效」）。
   原来动画挂在**每个 ::after 自己**身上 ⇒ 那个元素被 innerHTML 重建一次，
   动画就从 0° 重新开始。而 PR 看板每 5 秒整块重建 ⇒ **那圈环每 5 秒被打回原点**，
   转不满一圈。
   ⇒ 改成挂在 `:root` 上、变量向下继承：**元素重建时它继承到的是当时的角度**，
     视觉上完全连续。
   ⭐ 顺带解决第二件事：所有环从此**相位一致**（原来各转各的）。
   ⚠️ 代价：这个变量会出现在整棵树上。无害 —— 它只是个 angle，没人用就没有开销。 */
/* 环绕特效的两条动画**挂在 :root 上**，不挂在各个 ::after 上。
   理由见 `@property --mb-angle` 上面那段：挂在元素上时，那个元素每被重建一次
   动画就从头开始 —— PR 看板每 5 秒整块重建，于是那圈环永远转不满一圈。
   ⚠️ `--mb-dur` 也定义在这里：转速与「每 3 圈换一轮颜色」的关系靠它维系。 */
:root {
  --mb-dur: 3s;
  --mb-len: 105deg;
  animation: mb-spin var(--mb-dur) linear infinite,
             mb-hue calc(var(--mb-dur) * 3) ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  :root { animation: none; }
}

@property --mb-angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: true;
}

/* 色相偏移（机主 2026-08-20：「这个颜色轮换是每一圈轮换一次吗？
   能改成每两圈或者三圈轮换一次，这样包含更多颜色吗？」）。

   ⚠️ 原来一圈一圈完全一样 —— conic-gradient 是固定的，只有 `--mb-angle` 在转。
   ⇒ 再加一个**周期更长**的变量：色相偏移 18s 一个来回 = **每 3 圈换一轮颜色**。

   ⭐ 这正好解掉前几轮那个矛盾：**任一瞬间**的色相跨度仍然只有 125°（不吵），
     而**几圈累计**覆盖到 245°（够丰富）。

   ⚠️⚠️ **摆动幅度是算出来的，不是拍的**：CSS 的 `hue-rotate` 用 W3C 那个线性矩阵，
      **会改变亮度** —— 转到某些角度会让颜色在深色卡片上掉到 2.6:1（不达标）。
      逐 5° 扫过一遍，含 0° 的最长安全区间是 **-35° … +85°**；这里取 -30…+80 留余量。
      ⇒ 想再大幅度的话，代价是某几个瞬间深色主题下对比度不足 —— 见 test_home 那条。 */
@property --mb-hue {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: true;      /* 同 --mb-angle：挂在 :root 上跑，元素重建不打断 */
}

@supports (mask-composite: exclude) or (-webkit-mask-composite: xor) {
  .agent.gen::after,
  .bagent.gen::after,
  .hm-cc.gen::after,
  /* ⚠️⚠️ **`.pr.running::after` 2026-08-20 并进来**（机主：「全平台的环绕特效都
     同步成这样，能不能改成引用，这样改一次就同时改了」）。
     它此前是**完全独立的一份**，而且停在被淘汰的老规格上：
       · `--sweep-hot` 白热核 —— 正是被实测判为对白卡片 **1.00:1**（看不见）的那个
       · 三层 drop-shadow —— 规格里点名要避免的「厚重白边」
       · 3s / 82° 弧 / 五段旧配色 —— 一轮改进都没跟上
     ⇒ 并进同一条规则。**「改一次同时改」由同一条规则保证，比变量引用更硬** ——
       变量还能被某处 `background: xxx` 覆盖掉，同一条规则不会。
     ⚠️ 它的**几何**（inset -1.5px）与这里不同，在下面自己的块里覆盖，别搬进来。 */
  .pr.running::after {
    content: ''; position: absolute; inset: 0;
    border-radius: inherit; pointer-events: none; z-index: 3;
    /* 环的粗细。机主 2026-08-11：「稍微粗一点点」—— 1.6 → 2.2px。
       ⚠️ 它同时决定**能不能盖住底下那条静态边框**：环比边框细就会露出两条线
          （见下面 `.agent.gen::after` 的几何修正）。改细之前先看那段。 */
    padding: 2.2px;
    /* ⚠️⚠️ **色散描边**（2026-08-17 改）。原来是「一小段白热核 + 淡蓝光晕」，
       在**浅色主题下等于看不见** —— 实测白热核对白卡片 **1.00:1**、核心色 1.48、
       中段 2.00，而非文字元素的门槛是 3:1。
       根因是隐喻：**「发光」＝往底色上加亮度，而白色已经到顶了。**

       换成机主给的那份 iOS Liquid Glass 规格的路子：不靠发光，靠**锐利色带**
       （原话：「外围缓慢流动的**克制型**彩色光谱描边」「**避免过度发光、厚重白边**」
       「过渡保持足够**锐利**，不要变成柔和渐变」）。
       ⇒ 每一段自己对白底都在 **3.1–5.9:1**，相邻两段之间还有色相跳变。

       ⚠️ 色标成对写（46deg/47deg 相差 1°）是**故意的** —— 那 1° 就是"锐利"。
          写成平滑过渡会退回一团糊色，浅底上又看不清了。 */
    /* ⚠️ **只有一段弧有颜色，其余透明** —— 整圈铺满五个饱和色就成了机箱灯带，
       而规格的词是「**克制型**」「精致、轻盈」。整圈上色我试过，两种底上都清楚，
       但一屏三四个同时在跑时非常吵。
       ⇒ 保留原设计"一段光在绕"的结构，只把里面的**白热核换成锐利色带**。 */
    /* ⚠️⚠️ **2026-08-20 第三轮：颜色加回来，但跨度仍然收着。**
       机主：「现在**颜色太少了**，而且**速度比刚才慢了太多**。」
       ⇒ 我第二轮过校正了：3 色 / 36° 太素，9s 太慢。

       ⭐ **关键区分：颜色数量 ≠ 色相跨度。** 前一版把两件事一起砍了，
         而机主嫌的只有跨度（「反差太大」）。
         | | 色数 | 色相跨度 | 他的评价 |
         |---|---|---|---|
         | 一轮 | 4 | **324°**（几乎整圈色轮） | 反差太大 |
         | 二轮 | 3 | 36° | **颜色太少** |
         | 现在 | **6** | **125°** | —— |
       ⇒ 色数翻倍、跨度只回到原来的三分之一多一点。转速也回到 6s。

       实测（各色标 + 相邻插值，两个主题都算）：
       **白底最低 3.98 ｜ 深底最低 3.35**，全部过 3:1。

       —— 以下是第二轮定下、现在仍然成立的部分 ——
       机主：「渐变还是不好，也有可能是**颜色反差太大，变化太快**。」

       ## 「反差太大」有数字支撑
       上一版是 蓝→紫→红→橙，**色相跨度 324°** —— 几乎绕了整个色轮。
       现在是 蓝→蓝紫→紫，**跨度 36°**。

       ## 「变化太快」= 那 1° 的硬跳变，而它当初的理由已经不成立了
       上一版每两段之间只差 1°，注释写着「写成平滑过渡会糊成一团，浅底上看不清」。
       ⚠️ **那条推论是为「白热核」定的**（白色对白底 1.00:1，怎么过渡都看不见）。
          换成饱和色之后重算过：**平滑过渡的插值中点对白底 3.32–6.00，全部达标**
          （门槛 3:1）—— 比原方案里的橙色本身（**2.87，不达标**）还高。
       ⇒ 硬跳变不再是可见性的必要条件，只剩下"吵"。去掉。

       ## 现在这组的实测（含插值点，两个主题都算过）
       ```
       #2b6bff  白 4.52  深 3.77
       #5a5cf0  白 4.97  深 3.42
       #8b5cf6  白 4.23  深 4.02
       含插值最低：白 4.23 ｜ 深 3.42     ← 都过 3:1
       ```
       ⚠️ 试过跨度更小的青→蓝→靛（42°），**白底最低只有 2.54 —— 不达标**
          （青色太亮）。所以不是"越收敛越好"，有下界。

       ⚠️ 首尾淡出**必须用同色 alpha=0，不能用 `transparent`** ——
          后者等价 `rgba(0,0,0,0)`，sRGB 插值经过**透明黑**会浮出一圈脏灰边。 */
    /* ⚠️⚠️ **拖尾长度用 `--mb-len` 一个变量控制，色标按比例**（机主 2026-08-20：
       「让环绕的长度（也就是拖尾）变短」）。
       上一版是 8 个写死的角度 —— 要缩短就得手算 8 个数，**漏改一个不会报错**，
       只会让某一段颜色的宽度悄悄不成比例。
       ⇒ 同上一轮那个「色相周期 = 转速 × 3」：**用关系表达，不用一堆独立数字。**

       ⚠️ 缩短是有代价的，知道了再调：6 段颜色塞进更短的弧 ⇒ **色相变化率上升**。
          152° 时每段约 27°，105° 时约 18°。再短下去会重新逼近他早先嫌过的
          「变化太快」（那次是 1° 硬跳变 = 无限快）。 */
    background: conic-gradient(from var(--mb-angle),
      rgba(14, 139, 168, 0) 0deg,
      #0e8ba8               calc(var(--mb-len) * 0.118),
      #1e6fff               calc(var(--mb-len) * 0.276),
      #4c5ef0               calc(var(--mb-len) * 0.421),
      #7c4df5               calc(var(--mb-len) * 0.566),
      #a83bd8               calc(var(--mb-len) * 0.711),
      #d926a9               calc(var(--mb-len) * 0.842),
      rgba(217, 38, 169, 0) var(--mb-len),
      transparent           360deg);
    /* 只留最外一圈：整块 mask 减去 content-box，剩下的正好是 padding 那一环 */
    -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
            mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
    -webkit-mask-composite: xor;
            mask-composite: exclude;
    /* 辉光：drop-shadow 按 alpha 取形，遮罩之后剩下的正好是那一圈，
       所以光晕会**贴着描边**长出来，而不是糊成一个方块。
       （参考实现是给 spark 容器加 blur(2px)；但我们的环只有 1.6px，
         直接 blur 会把它抹平，drop-shadow 才能既保留细线又有光晕。） */
    /* ⚠️ **不再堆三层辉光**：那是"发光"那套的产物，浅底上只会糊出一圈白雾
       （规格里点名要避免的「厚重白边」）。留一层很淡的，只为让细线离开卡片边缘。 */
    /* ⚠️ `hue-rotate` 串在 drop-shadow 之后 —— 辉光也跟着一起转，
       否则一圈彩色描边配一圈固定蓝的光晕，转到紫粉那段会看着脏。 */
    filter: drop-shadow(0 0 3px rgba(10, 92, 255, 0.28)) hue-rotate(var(--mb-hue));
    /* plus-lighter = **加光**：描边和它的辉光叠加到底下的像素上，而不是盖住。
       深色底上这才是"发光"；不加的话只是"画了一条亮线"。 */
    mix-blend-mode: plus-lighter;
    /* 规格里的词是「**缓慢**流动」—— 3s 转一圈在一屏十几行里是躁的。 */
    /* ⚠️ 转速：6s → 9s（第二轮）→ **回到 6s**（第三轮，机主说「慢了太多」）。
       ⇒ 「变化太快」他指的是**色相跳变**，不是转速 —— 我第二轮把两件事一起调了，
         其中一件调错了方向。**一次只改一个变量**，否则分不清是哪个起的作用。 */
/* ⚠️ 动画本体已挪到 `:root`（见文件里 `:root { animation: mb-spin …`）——
       这里**不再声明 animation**。元素重建时从 `:root` 继承当时的角度，视觉连续。 */
  }

  /* ⚠️⚠️ **`.agent` 要外扩 1px，否则边框有重影**（机主 2026-08-11 报）。
   *
   * 三个用到这圈光的组件，边框画法**不一样**，几何不能一刀切：
   *   · `.bagent` / `.pr` —— 真 `border: 1px/1.5px solid`，画在**盒内** ⇒ inset:0 正好压住
   *   · `.agent`          —— `box-shadow: 0 0 0 1px`，画在**盒外** ⇒ inset:0 的环落在它里面，
   *                          于是屏幕上是**两圈同心线、相距 1px** = 机主看到的"重影"
   * ⇒ 只给 `.agent` 外扩：inset -1px 让环的外沿与 box-shadow 的外沿对齐，
   *   圆角同步 +1px（12 → 13），否则四个角会对不上、拐角处重新出现双线。
   *
   * ⚠️ 判据不是"看着差一点点挪一挪"，是**这条边框到底画在盒内还是盒外** ——
   *   box-shadow 的 spread 和 border 在盒模型里是两个位置，不看这个就会调不准。 */
  /* ⚠️⚠️ **2026-08-18 改回 inset:0**，理由和当初外扩时不是同一件事，别直接回退：
     机主发来手机截图说生成行"比 infra 宽"。逐像素量他那张图：
       未生成  边框带宽 3 device px、对比度 Δ22
       生成中  边框带宽 7 device px、对比度 Δ184
       外沿位置 23 vs 22 —— **几乎一样**。
     ⇒ 不是盒子更宽，是这圈环又厚(2.4×)又实(8×)，把形状整个读大了。
     ⇒ 把环收进卡片内沿，让两类行的**轮廓**一致。
     ⚠️ 收进来就会跟 `.agent` 自己那圈 `box-shadow 0 0 0 1px`（画在盒外）
        变成两圈同心线 = 2026-08-17 修过的"重影"。⇒ 所以下面把生成行的那圈细边
        **去掉**，让彩虹环成为它唯一的边。两个问题必须一起解，只改一个都会退化。 */
  /* ⚠️ 又挪回 -1px（同一天内第二次动它，两次理由不同，别当反复）：
     · 收进 inset:0 是为了别让它显得更宽 —— 但那是在它**底下还压着一圈
       `0 0 0 1px` 细边**的前提下；
     · 现在生成行自己那圈细边已经去掉了（见 .agent.gen），环挪回 -1px
       正好与普通行的细边**外沿重合** ⇒ 逐像素量出来两类行边界完全相同，
       同时也不会有当年那种"两圈同心线"（底下没有第二圈了）。
     ⇒ 判据是【painted 外沿位置】，不是【inset 写几】。实测：
       普通行 14/765，生成行 inset:0 时 16/763、inset:-1px 时 14/765。 */
  .agent.gen::after { inset: -1px; border-radius: 13px; }
  /* `.bagent` 是真 `border: 1px`（画在盒内）⇒ 外扩一个边框宽度，圆角不用动 */
  .bagent.gen::after { inset: -1px; }
  /* `.hm-cc`（首页那些 cc 小片）同理：真 border 1px、胶囊圆角靠 inherit 带过来 */
  .hm-cc.gen::after { inset: -1px; }

  /* 外扩一圈更糊、更淡的同款，做出描边周围的辉光 */
  .agent.gen { --mb-glow: 1; }

  @keyframes mb-hue {
    0%   { --mb-hue: -30deg; }
    50%  { --mb-hue:  80deg; }
    100% { --mb-hue: -30deg; }
  }

  @keyframes mb-spin {
    to { --mb-angle: 360deg; }
  }

  @media (prefers-reduced-motion: reduce) {
    /* ⚠️ 这里原来列着 `.agent.gen::after, .bagent.gen::after, .hm-cc.gen::after,
       .pr.running::after { animation: none }` —— **2026-08-20 起不需要了**：
       环绕的动画已经挪到 `:root`（见那里的注释），关它一处就够，
       而 `:root { animation: none }` 就在这个文件靠前的位置。
       ⚠️ 留着一条对着"已经没有动画的元素"关动画的规则，比删掉更糟：
         下一个人会以为动画还挂在那些元素上。 */
  }
}

/* ⚠️ 浅色主题：plus-lighter 在接近白的底上等于无操作（255 已到顶）。
   退回常规混合，靠高饱和色相与描边本身的对比度表达"光"。 */
@media (prefers-color-scheme: light) {
  .agent.gen::after, .bagent.gen::after, .hm-cc.gen::after { mix-blend-mode: normal; }
}
:root[data-theme="light"] .agent.gen::after,
:root[data-theme="light"] .bagent.gen::after,
:root[data-theme="light"] .hm-cc.gen::after { mix-blend-mode: normal; }
:root[data-theme="dark"]  .agent.gen::after,
:root[data-theme="dark"]  .bagent.gen::after,
:root[data-theme="dark"]  .hm-cc.gen::after { mix-blend-mode: plus-lighter; }

/* 打断生成：标题后面的红色方块。只在正在生成时出现。
   ⚠️ 机主反馈上一版"太粗"—— 原来是实心红块，19×19 一整块红挨着标题，
   分量比标题本身还重。改成**描边式**：细红框 + 小红方块，
   点击热区仍是 19×19（可点性不能因为变细而下降），只是墨量少了一大截。
   hover 时才填实 —— 悬停即将触发，此时加重反而是对的反馈。 */
/* gap 从 9 收到 6：两个按钮原本还各自带 margin-left:10，实际间距 19px，
   看着像两组东西而不是一排工具（机主 2026-08-09）。名字与按钮群之间
   靠 .stop 的 margin-left 单独补回 3px，保持"名字—工具"这层分组还在。 */
.detail-head .title { display: flex; align-items: center; gap: 6px; }
.detail-head .title .stop { margin-left: 3px; }

/* ————— 「它是什么」的 logo：Claude / Codex / …（机主 2026-08-21）—————
   ⚠️⚠️ **只有一个 logo，没有文字也没有胶囊**（机主第二版：「不要用文字，
      就一个 logo 就行」）。第一版是"图标 + 字"的胶囊标，他看过之后要更轻。
   ⇒ 文字没了，`title` / `aria-label` 里那句 hint 就成了**唯一**说得清
      「这是 Multica 上的、不是我们起的 Claude Code」的地方 —— 别当装饰删掉。
   ⚠️ `flex: 0 0 auto` 是硬要求：标题一长，参与压缩的东西会被 `.title` 的
      overflow 连带裁掉 —— 这个坑本文件上面那段注释里刚记过一次
      （长标题把「停止」和「更多操作」一起裁没了）。
   ⚠️ 摆在名字和按钮之间：它属于"这是谁"那一组，不属于工具组。
   ⚠️ 尺寸 15px 而不是原来的 11px：没有字扛着，logo 本身就是全部信息量。 */
.detail-head .title .brand {
  flex: 0 0 auto;
  display: inline-flex; align-items: center;
  /* ⚠️⚠️ **品牌色必须走变量，不能靠下面的 `.brand--claude` 直接设 color** ——
     那条只有 (0,1,0) 的特异性，会被这里的 (0,3,0) 压掉，表现是**图标一律灰色、
     一个品牌色都不出现**（第一版就是这样，无头浏览器截图当场看出来的）。
     变量没有这个问题：`--brand-c` 只在带族 class 的那个元素上被定义。 */
  color: var(--brand-c, var(--label-2));
  cursor: default; user-select: none;
}
.detail-head .title .brand svg { width: 15px; height: 15px; flex: 0 0 auto; }
/* 各家的色。⚠️ 只给认得出的那几族上色，`other` 保持中性 ——
   给陌生模型配一个像模像样的品牌色，等于在暗示我们认识它。 */
.brand--claude { --brand-c: #d97757; }
/* ⚠️ OpenAI / xAI 两家的标识本来就是黑白 —— 用 `--label-1` 跟着主题走，
   深色下白、浅色下黑，正是它们自己的用法。**别为了"有颜色好区分"给它配一个
   品牌上不存在的绿**（第一版配了 #10a37f，那是旧 ChatGPT 的色，不是 Codex 的）。
   区分靠的是图形，不是硬凑的色。 */
.brand--codex, .brand--gpt, .brand--grok { --brand-c: var(--label-1); }
.brand--gemini { --brand-c: #4285f4; }
/* ⚠️ 窄屏**不用再摘什么**了：本来就只有一个 logo。
   （上一版在这儿有一个 `@media` 摘文字，连同文字一起删了。） */
.detail-head .stop {
  flex: 0 0 auto; width: 19px; height: 19px; padding: 0;
  border: 1px solid color-mix(in srgb, var(--red) 62%, transparent);
  border-radius: 5px; cursor: pointer;
  background: none;
  display: grid; place-items: center;
  transition: background 120ms var(--ease), border-color 120ms var(--ease),
              transform 120ms var(--ease);
}
.detail-head .stop::before {
  content: ''; width: 6px; height: 6px; border-radius: 1px;
  background: var(--red);
  transition: background 120ms var(--ease);
}
.detail-head .stop:hover {
  background: var(--red);
  border-color: var(--red);
  transform: scale(1.06);
}
.detail-head .stop:hover::before { background: #fff; }
.detail-head .stop:active { transform: scale(0.94); }
.detail-head .stop:disabled { opacity: .4; cursor: default; transform: none; }
.detail-head .stop:disabled:hover { background: none; }
.detail-head .stop:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; }
.detail-head .stop.failed { border-color: var(--label-3); }
.detail-head .stop.failed::before { background: var(--label-3); }
@media (prefers-reduced-motion: reduce) { .detail-head .stop { transition: none; } }
/* 未在生成时淡化 —— 按钮仍可点（Esc 打在空闲 cc 上无害），
   但视觉上明确"现在没东西可停"。 */
.detail-head .stop.idle { border-color: var(--hairline-2); opacity: .55; }
.detail-head .stop.idle::before { background: var(--label-3); }
.detail-head .stop.idle:hover { opacity: 1; border-color: var(--red); background: none; }
.detail-head .stop.idle:hover::before { background: var(--red); }

/* 「已发出停止，等它停下」—— 第三种状态，不是 disabled 的灰，也不是能点的红。
   ⚠️ 它必须**看得出还在进行中**：机主报的 bug 正是"点完看不出发生了什么，
      于是又点一次"。所以给一个缓慢的呼吸，而不是静止的淡出 ——
      静止的淡出和 :disabled 长得一样，那正是原来那版的问题。
   ⚠️ 优先级：写在 .idle 之后，因为停止中的一瞬间 busy 常常已经翻成 false，
      两个类会同时出现，要让 .stopping 赢。 */
.detail-head .stop.stopping,
.detail-head .stop.stopping:disabled {
  opacity: 1;
  border-color: color-mix(in srgb, var(--yellow) 70%, transparent);
  animation: stop-pending 1.15s var(--ease) infinite;
  cursor: progress;
}
.detail-head .stop.stopping::before { background: var(--yellow); border-radius: 50%; }
@keyframes stop-pending {
  0%, 100% { opacity: .45; }
  50%      { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  /* 不能只是关掉动画就完事 —— 那样它退化成一个静止的淡按钮，
     跟 :disabled 又分不清了。改用实心底色来表达"进行中"。 */
  .detail-head .stop.stopping { animation: none; opacity: 1;
    background: color-mix(in srgb, var(--yellow) 22%, transparent); }
}

/* SSE 实时态：绿色常亮 + 轻微呼吸。
   ⚠️ 和 polling 用不同的颜色而不是只改文字 —— 窄屏下 #mode-text 是隐藏的
   （见上面的 @media），只剩那个圆点，颜色是唯一能传达状态的通道。 */
.mode.live .pip { background: var(--blue); animation: pip-breathe 2.4s ease-in-out infinite; }
.mode.live { color: var(--blue); }
@keyframes pip-breathe { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }
@media (prefers-reduced-motion: reduce) { .mode.live .pip { animation: none; } }

/* 未送达内容的入口。平时不占位（hidden），有货才出现在顶部。 */
.unsentbar { margin: 8px 12px; padding: 10px 12px; border-radius: 10px;
  background: color-mix(in srgb, var(--orange) 12%, var(--bg-2));
  border: 1px solid color-mix(in srgb, var(--orange) 35%, transparent); }
.unsent-head { font-size: 13px; font-weight: 600; color: var(--orange); margin-bottom: 6px; }
.unsent-item { padding: 6px 0; border-top: 1px solid var(--sep); }
.unsent-item:first-of-type { border-top: 0; }
.unsent-meta { font-size: 11.5px; color: var(--label-2); }
.unsent-prev { font-size: 12.5px; margin: 4px 0; white-space: pre-wrap;
  max-height: 3.6em; overflow: hidden; }
.unsent-act { display: flex; gap: 8px; }
.unsent-act button { font-size: 12px; padding: 3px 10px; }
.unsent-act .danger { color: var(--red); }

/* 卡在只有机主能解的状态（授权确认框 / 登录过期 / 额度用尽）→ 整行浅黄。

   ⚠️ 类名用 `authwait` 而不是 `blocked`：`blocked` 在这份样式里已经被
   **时间环**占用了，指的是 ccname 的「阻塞」状态（cc 自己声明"我需要人"）。
   这里说的是完全不同的一件事 —— cc **动不了**，而且它自己不会喊。
   同名不同义是最难查的那种 bug，宁可换个词。

   ⚠️ 用**背景色**而不是加个小徽章：这类 cc 在看板上原本长得和"安静地空闲着"
   一模一样（不 busy、状态也不动），只有整行变色才能让它从一屏里跳出来。 */
.agent.authwait { background: color-mix(in srgb, var(--yellow) 16%, transparent); }
/* ⚠️⚠️ **hover 不加深**（2026-08-24 删掉）：这里原来有
     `.agent.authwait:hover { … var(--yellow) 24% … }`
   —— 而 2026-08-19 机主报「悬浮在 Tab 上会变黄」时，只删了**通用**那两条
   （`.agent:hover` / `.agent.active`，见本文件 298 行那段注释），
   **这一条单独留着没动**。
   ⭐ 又一次「同一件事两处，只改了看得见的那一处」：通用那条一改，
     日常悬浮就正常了，于是没人再去看阻塞态 —— 而阻塞态恰恰是
     那段注释里点名"尤其明显"的一种（16% → 24% 同时玻璃被换成纯色）。
   ⇒ hover 的反馈统一交给 transform + box-shadow（文件末尾玻璃那节），
     它们不碰 background，因此能叠在玻璃上而不是替换它。 */
@media (prefers-color-scheme: dark) {
  .agent.authwait { background: color-mix(in srgb, var(--yellow) 13%, transparent); }
}
:root[data-theme="dark"] .agent.authwait {
  background: color-mix(in srgb, var(--yellow) 13%, transparent);
}
:root[data-theme="light"] .agent.authwait {
  background: color-mix(in srgb, var(--yellow) 16%, transparent);
}

/* ————————————————— PR 卡片（进度栏顶部）————————————————— */
.prs { display: flex; flex-direction: column; gap: 6px; margin: 0 0 9px; }

/* 边框语义（机主指定）：
     运行中 → 蓝色环绕特效（照抄 tab 那套 moving border）
     draft  → 灰   ·   全绿 → 绿   ·   CI 没过 → 红
   ⚠️ 边框是**结论**，圆环是**构成** —— 一个远看，一个近看，别互相重复。*/
.pr {
  position: relative; border: 1.5px solid var(--sep); border-radius: 12px;
  padding: 7px 10px; background: var(--bg-2);
  /* ⚠️ 环绕特效要**画到边框上**（见 `.pr.running::after`），而 `overflow:hidden`
     会把它裁回 padding box —— 那正是机主 2026-08-11 报的"边框有重影"的另一半。
     `overflow: clip` + `overflow-clip-margin` 能**只放行绘制、仍然裁内容**。
     ⚠️ 两条 `overflow` 都留着：老浏览器不认 `clip`，会退回上一条 `hidden`
     （退化的结果就是今天这个样子 —— 重影，但不会更糟）。
     ⚠️ 标题自己有 `overflow:hidden + ellipsis`，不靠这里裁，所以放宽是安全的
     （实测 4 张卡片，去掉裁剪后无任何子元素越界）。 */
  overflow: hidden;
  /* ⚠️ 余量给 **2px 就够**（环是 1.5px），不要贪多：`clip-margin` 放行的是**所有**
     绘制，包括本来被裁掉的内容。实测把标题灌成 40 倍长时，页脚那行会被撑出
     20px —— 那是既有的布局问题，今天靠 `hidden` 盖着；余量开多大，它就漏多少。 */
  overflow: clip; overflow-clip-margin: 2px;
}
.pr.success  { --edge: var(--green); border-color: var(--edge); }
.pr.failure  { --edge: var(--red); border-color: var(--edge); }
.pr.draft    { --edge: var(--label-3); border-color: var(--edge); }
/* 已合并 / 已关闭 → **灰态**（机主 2026-08-05 指定）。
   它们已经落定，不再需要注意力；整卡压暗让还在跑/挂了的那几张跳出来。
   ⚠️ 不是只把边框改灰：圆环上那一圈绿会照样抢眼。整卡降透明度 + 去饱和，
   让"这张已经不用管了"在余光里就成立。 */
.pr.merged,
.pr.closed   { --edge: var(--label-3); border-color: var(--edge); opacity: .62; filter: saturate(.35); }
.pr.merged:hover,
.pr.closed:hover { opacity: .9; filter: none; }   /* 想细看时还原 */
.pr.running  { --edge: color-mix(in srgb, var(--blue) 45%, transparent); border-color: var(--edge); }

.pr-top { display: flex; align-items: center; gap: 8px; }
/* 机主反馈 20px 看不清各段 → 放大到 30px。SVG 矢量，放大不糊。 */
/* ⚠️ 2026-08-16 起环是**可点的**（点开 check 明细面板）—— 要有指针反馈，
   否则那是个看起来不能点、其实能点的东西，没人会去点它。 */
.pr-ring { width: 30px; height: 30px; flex: 0 0 auto; cursor: pointer; }
.pr-ring:hover { opacity: .82; }
.pr-title {
  flex: 1; min-width: 0; font-size: 13px; font-weight: 600; color: var(--label);
  text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pr-title:hover { text-decoration: underline; }
/* 卡片右上角那两个按钮。⚠️ 用**内联 SVG 而不是字符**（原来是 ⟳ 和 ×）：
   字符是按基线排版的，它的**视觉中心不等于按钮盒子的中心**，所以旋转时
   图标会绕着一个偏心点甩 —— 机主反馈的"圆心位置不对"就是这个。
   SVG 的 viewBox 中心即盒子中心，转起来是正的。 */
.pr-r, .pr-x {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; padding: 0; border: 0; border-radius: 7px;
  background: transparent; color: var(--label-3); cursor: pointer;
  transition: color .12s, background .12s;
}
.pr-r svg, .pr-x svg { width: 15px; height: 15px; display: block; }
.pr-r:hover:not(:disabled) { color: var(--blue); background: color-mix(in srgb, var(--blue) 10%, transparent); }
.pr-x:hover { color: var(--red); background: color-mix(in srgb, var(--red) 10%, transparent); }
.pr-r:disabled { opacity: .5; cursor: default; }
.pr-meta { font-size: 11.5px; color: var(--label-2); margin: 2px 0 0 38px; }
.pr-meta .bad { color: var(--red); }
.pr-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  margin: 6px 0 0 38px; font-size: 11px; color: var(--label-3);
}
/* ⚠️ 这一行必须单行截断：右栏只有 ~285px，让它折行会把"27s 前"的"前"字
   单独甩到第二行，看着像坏了。信息本身不重要到值得占两行。 */
.pr-foot > span {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* ⚠️ 上面那条是给**状态文字**写的（单行截断），而 `.pr-acts` 也是 `.pr-foot`
   的直接 span 子元素，会一起吃到 `overflow:hidden` —— 结果是按钮被**裁掉**
   而不是换行，看着就是"挤在一起还溢出"。按钮组要的是不被裁、也不被压缩；
   该让位的是文字，它本来就有省略号。 */
.pr-foot > span.pr-acts { flex: 0 0 auto; overflow: visible; }
.pr-rerun {
  flex: 0 0 auto; font-size: 11.5px; padding: 3px 9px; border-radius: 7px;
  border: 1px solid var(--sep); background: transparent; color: var(--label);
  cursor: pointer;
}
.pr-rerun:hover:not(:disabled) { border-color: var(--red); color: var(--red); }
.pr-rerun:disabled { opacity: .5; cursor: default; }
.pr-err { margin: 6px 0 0 38px; font-size: 11px; color: var(--orange); }

/* CI 在跑 → 蓝色环绕。**与 .agent.gen 用同一套变量、同一套主题分支** ——
   机主反馈"一道光被劈成两半、中间是白色，和 tab 不一致"，根因就是我另写了
   一份、把光心硬编码成近白的 #eaf4ff。

   ⚠️ `--sweep-hot` 是**跟主题变的**：深色下是 `#ffffff`（配 plus-lighter 才是
   "光心"），**浅色下是蓝色 `#0a6cff`**。硬编码成白色的话，浅色主题没有
   plus-lighter 兜着，那段就是一条**实心白带**，把蓝色的光劈成两截。

   ⇒ 教训：想"和某个效果一致"，就**复用它的变量**，别照着它的渲染结果调色。
      照抄出来的常数会在另一个主题/另一种混合模式下失真。 */
@supports (mask-composite: exclude) or (-webkit-mask-composite: xor) {
  /* ⚠️ 这里**只剩几何**。视觉（渐变 / 辉光 / 动画 / mask）已并进上面那条共用规则 ——
     见那里 `.pr.running::after` 上方的注释。别再往这儿加视觉属性：
     加了就等于又分叉一份，而分叉不会报错，只会让两处慢慢长得不一样。 */
  .pr.running::after {
    /* ⚠️⚠️ **`inset: 0` 不是"贴着边框"**（机主 2026-08-11 报，实测确认）：
       绝对定位元素的包含块是祖先的 **padding box**，不是 border box ——
       `.pr` 有 1.5px 真边框，于是 `inset: 0` 的环落在边框**里面** 1px，
       屏幕上就是"彩色边框 + 一圈光"两条线。实测包含块偏移正好是 1px。
       ⇒ 外扩一个边框宽度，让环的外沿和边框外沿对齐。
       ⚠️ 圆角不用改：环的盒子现在正好是 border box，而 `border-radius`
          本来就是按 border box 算的，`inherit` 拿到的就是对的
          （`.agent` 那边要 +1px，是因为它的边框是画在**盒外**的 box-shadow）。 */
    inset: -1.5px;
  }
  @media (prefers-color-scheme: light) {
    .pr.running::after { mix-blend-mode: normal; }
  }
  :root[data-theme="light"] .pr.running::after { mix-blend-mode: normal; }
  :root[data-theme="dark"]  .pr.running::after { mix-blend-mode: plus-lighter; }
}
/* ⚠️ 这里原来有一条 `@media (prefers-reduced-motion: reduce) { .pr.running::after
   { animation: none } }` —— 已并进上面那条共用的（搜 `.pr.running::after { animation: none`）。 */

/* 顶栏的本机负载。阈值由机主定：>80% 黄，>90% 红。 */
.load { display: inline-flex; gap: 10px; font-size: 11.5px; color: var(--label-2);
  font-family: var(--mono); }
.load .ld { display: inline-flex; align-items: baseline; gap: 3px; }
.load .ld i { font-style: normal; font-size: 10px; color: var(--label-3); }
.load .ld.warm { color: var(--yellow); }
.load .ld.warm i { color: color-mix(in srgb, var(--yellow) 70%, transparent); }
.load .ld.hot { color: var(--red); font-weight: 600; }
.load .ld.hot i { color: color-mix(in srgb, var(--red) 70%, transparent); }
/* 窄屏顶栏很挤：只留数字，标签藏掉 */
@media (max-width: 860px) { .load .ld i { display: none; } }

/* 卡片上的手动刷新。⚠️ 后台每分钟自动刷一次，这个按钮是给"我现在就想确认"用的
   —— 机主曾因 ≤60 秒的滞后看到「GitHub 全绿、卡片显示红」，那不是 bug，
   但让人去猜"这个数字是不是旧的"是坏体验。 */
.pr-r {
  flex: 0 0 auto; border: 0; background: transparent; color: var(--label-3);
  font-size: 13px; line-height: 1; padding: 2px 3px; cursor: pointer;
}
.pr-r:hover:not(:disabled) { color: var(--blue); }
.pr-r:disabled { opacity: .5; cursor: default; }
/* ⚠️ 只转里面的 svg，不转按钮本身 —— 转按钮的话它的 hover 背景块也跟着转。
   transform-origin 显式写 50% 50%：别依赖默认值，svg 的默认原点在某些
   浏览器里跟着 viewBox 走。 */
.pr-r.spin svg { transform-origin: 50% 50%; animation: pr-spin .8s linear infinite; }
@keyframes pr-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .pr-r.spin svg { animation: none; } }

/* ————————————————————— 全平台 PR 看板 —————————————————————
   三栏：左「等你动手」/ 中 agent / 右「进行中·待合」，曲线把 PR 连到 agent。
   ⚠️ 横轴不是"被几个 agent 纳管"——见 app.js 里 renderBoard 的注释。 */

/* 顶栏的页面切换 */
.viewsw { display: flex; gap: 2px; padding: 2px; margin-left: 14px;
  background: var(--surface-1); border-radius: 9px;
  /* ⚠️⚠️ **必须 `align-self: center`**（机主 2026-08-16：「Tab 就和整个栏一样高了」）。
     `header` 是 flex 且没写 `align-items`（默认 `stretch`）⇒ 这个切换器会被
     **拉满整条顶栏**（实测占 100%：顶栏 42、它也 42）。
     ⚠️ 它自己从没设过高度，所以看代码完全看不出"它有多高" —— 高度来自**父容器**。
       这就是为什么把顶栏压扁之后，它跟着一起变矮、却始终顶满：**症状会跟着变，
       根因不在这儿。**
     ⇒ 让它按自己的内容收高，别参与拉伸。 */
  align-self: center;
  /* ⚠️⚠️ **真正撑高的是这一句**（找了两轮才定位对）：
     `.viewsw` 自己是 flex 容器且没写 `align-items` ⇒ 默认 `stretch`
     ⇒ **里面的按钮被拉满容器高度**（实测按钮内容只要 24px，却被拉到 38）。
     而容器的高度又来自 header —— 所以看起来像"容器顶满了顶栏"，
     实际是**按钮顶满了容器、容器再顶满顶栏**，两层拉伸叠在一起。
     ⚠️ 我先给容器加了 `align-self: center`（对的，但不够）—— 一点变化都没有，
       因为撑高它的不是父容器的对齐，是**它自己的子元素**。
       ⇒ 症状在最外层，根因可以在最内层；**看到"顶满"先问"谁在撑"**。 */
  align-items: center; }
.viewsw button {
  position: relative; border: 0; background: transparent; color: var(--label-2);
  font: inherit; font-size: 12px; font-weight: 510; padding: 4px 12px;
  border-radius: 7px; cursor: pointer; transition: background .18s, color .18s;
  /* ⚠️ 必须 nowrap：窄屏上按钮挤到放不下时，那颗红点会掉到第二行，
     把整个顶栏撑高一截（手机实测）。 */
  white-space: nowrap;
}
.viewsw button:hover { color: var(--label); }
/* ⚠️⚠️ **`:hover` 在触摸屏上不存在** —— 关掉系统点击高亮之后，顶栏这几个 tab
   就完全没有按下反馈了（实测按住不放，背景和 transform 一点变化都没有）。
   ⚠️ 我第一轮只补了底栏，是因为检测脚本读 `cssRules` **不会递归进 `@media`**
     ⇒ 顶栏那条 `:hover` 在媒体查询外、被读到了，看起来"有反馈"。
     **工具的盲区让我把有问题的那个判成了没问题的。**
     ⇒ 最后是真按下去量样式差异才看出来 —— **判据要用最终效果，别用规则表。** */
.viewsw button:active { background: var(--surface-2); transform: scale(0.96); }
@media (prefers-reduced-motion: reduce) { .viewsw button { transition: none; } }
.viewsw button.active { background: var(--surface-2); color: var(--label); }
/* 有几个 PR 在等你动手。红点的意义就是"不切过去也知道该不该切"。 */
.vsw-dot {
  margin-left: 5px; min-width: 15px; height: 15px; padding: 0 4px; border-radius: 8px;
  background: var(--red); color: #fff; font-size: 10px; font-weight: 620;
  display: inline-flex; align-items: center; justify-content: center;
}

.board { flex: 1; display: flex; flex-direction: column; min-height: 0; overflow: auto; }
.board-head {
  display: flex; align-items: center; gap: 12px; padding: 12px 20px 10px;
  font-size: 12px; color: var(--label-2);
  border-bottom: 1px solid var(--hairline); position: sticky; top: 0;
  background: var(--canvas); z-index: 3;
}
.board-head .spacer { flex: 1; }
.board-sum { font-variant-numeric: tabular-nums; }
.board-empty { padding: 40px 20px; text-align: center; color: var(--label-3); font-size: 13px; }

.board-grid {
  position: relative; display: grid; gap: 0 28px; align-items: start;
  grid-template-columns: minmax(0, 1fr) minmax(190px, 0.62fr) minmax(0, 1fr);
  padding: 16px 20px 40px;
}
/* ⚠️ 没有"等你动手"的 PR 时**整栏收掉**，不留一行空状态占着三分之一宽度。
   （这正是否掉"按纳管数量分左右"那版时给的理由：常年为空的分区白占最贵的资源。
   自己的方案在 CI 全绿的日子会犯同一个毛病，所以这里必须动态收。）

   ⚠️⚠️ **必须锁在宽屏**（机主 2026-08-06 报：手机 PR 页按钮挤在一起还溢出）。
   `.board-grid.no-act` 是 (0,2,0)，而窄屏那条 `.board-grid{grid-template-columns:1fr}`
   只有 (0,1,0) —— 特异性高的赢，跟写在哪个 media query 里无关。于是手机上只要
   当天没有"等你动手"的 PR，看板就变回两栏，卡片被挤到 **190px**，
   一个「重跑失败的 job」(96px) 加一个「合并」(49px) 就顶出了卡片。
   用 min-width 把它关在宽屏，比靠特异性去斗更不容易再错。 */
@media (min-width: 861px) {
  .board-grid.no-act { grid-template-columns: minmax(190px, 0.5fr) minmax(0, 1fr); }
  .board-grid.no-act #lane-act { display: none; }
}
/* 窄屏本来就一栏，"收掉左栏"只需要把它藏了 */
@media (max-width: 860px) {
  .board-grid.no-act #lane-act { display: none; }
}
/* 连线在最底层，且**绝不吃点击** —— 上面压着刷新/重跑按钮 */
/* ⚠️ 必须 inset:0，不能写 left:20px/top:16px 去"让开 padding" —— JS 里的坐标是
   相对 grid 的 getBoundingClientRect()（= border box）算的，再偏一次等于把 padding
   算两遍，线会整体右移 20px、连不到 agent 节点上。 */
.board-links { position: absolute; inset: 0; pointer-events: none; z-index: 0; overflow: visible; }
/* ⚠️ 线要看得见才有意义。第一版用 --hairline-2（0.14 透明度）画，截图上几乎
   看不出有线 —— 那就等于没做这个功能。这里用 label-3 打底，act 用红。 */
.board-links .lk { fill: none; stroke-width: 1.5; transition: stroke-opacity .18s; }
.board-links .lk-act  { stroke: color-mix(in srgb, var(--red) 70%, transparent); }
.board-links .lk-wait { stroke: color-mix(in srgb, var(--label-3) 85%, transparent); }
.board-links .lk-done { stroke: color-mix(in srgb, var(--label-3) 40%, transparent); }
/* 指到某个 agent 时把它的线挑出来、其余压暗。哪些线属于它由 JS 打 .on ——
   CSS 选择器没法拿 svg 上的值去比对 path 上的值，属性之间不能互相比较。 */
.board-links.dim .lk { stroke-opacity: .16; }
.board-links.dim .lk.on { stroke-opacity: 1; stroke-width: 2.4; }

.lane { display: flex; flex-direction: column; gap: 8px; position: relative; z-index: 1; min-width: 0; }
.lane-head {
  font-size: 10.5px; font-weight: 590; letter-spacing: 0.07em; text-transform: uppercase;
  color: var(--label-3); padding: 0 2px 2px;
}
.lane-head--act { color: var(--red); }
/* 时间线表头里的 cc 名。宽屏收起 —— 中栏标题已经写着谁了，再写一遍是冗余；
   窄屏（见 860px 那块）放出来，因为那时中栏根本不在屏幕上。 */
.tl-who { display: none; margin-left: 8px; color: var(--label-2); font-weight: 500;
  text-transform: none; letter-spacing: 0; }
/* 已合并/已关闭在右栏底部单开一段：它们既不"进行中"也不"待合"，
   混进上面那段会让表头那个数字失去意义。 */
.lane-head--done { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--hairline); }
.lane-head i { font-style: normal; opacity: .75; }
/* 详情栏「已合并 / 已关闭」那条折叠头。复用 .lane-head 的字号与色，只加交互态。 */
.pr-done-head { cursor: pointer; user-select: none; display: flex; align-items: center;
  gap: 5px; text-transform: none; letter-spacing: 0.02em; font-size: 11.5px; }
.pr-done-head:hover { color: var(--label-2); }
.pr-done-caret { display: inline-block; transition: transform .15s ease; font-size: 9px; }
.pr-done-head.is-open .pr-done-caret { transform: rotate(90deg); }
.lane-none { font-size: 12px; color: var(--label-3); padding: 10px 2px; }
.lane--mid { align-items: stretch; }

/* agent 节点。点它 = 回 cc 看板并打开它的对话。 */
.bagent {
  display: flex; flex-direction: column; gap: 2px; text-align: left;
  border: 1px solid var(--hairline); background: var(--surface-1);
  border-radius: var(--radius-sm); padding: 9px 11px; cursor: pointer;
  color: var(--label); font: inherit; transition: background .18s, border-color .18s;
}
.bagent:hover { background: var(--surface-2); border-color: var(--hairline-2); }
.bagent-name { font-size: 12.5px; font-weight: 540; }
.bagent-sub { font-size: 11px; color: var(--label-3); }
/* 运行中的 agent：**与 tab / 卡片同一套**流光 + 环绕（机主 2026-08-10）。
   ⚠️ 环绕是把 `.bagent.gen` 加进 `.agent.gen::after` 那份实现的选择器里，
      **没有抄第二遍** —— 那圈特效有 30 行（conic + mask + drop-shadow + 混合模式），
      复制一份意味着以后每次调色都要改两处，而这仓库已经因为"两份实现"栽过。
   ⚠️ 流光同理，用共享的 `--sweep-image`。 */
.bagent { position: relative; }
.bagent.gen .bagent-name {
  background-image: var(--sweep-image);
  background-size: 200% 100%; background-repeat: repeat;
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: row-sweep var(--sweep-dur) linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  /* 流光停掉后必须还原成实色，否则渐变定格在那儿像渲染坏了；
     换成强调色 —— 不动，但仍看得出这个 agent 跟别的不一样。 */
  .bagent.gen .bagent-name {
    background: none; -webkit-text-fill-color: currentColor; color: var(--blue);
  }
}
.bagent.arch { opacity: .6; }
/* 彻底消失的 cc（既不在 roster 也不在归档区）：点了没地方去，别假装能点 */
.bagent.gone { opacity: .45; cursor: default; }
.bagent.gone:hover { background: var(--surface-1); border-color: var(--hairline); }

/* 卡片上的 agent chip：宽屏有连线，不需要它 —— 只在窄屏出场 */
.pr-agents { display: none; gap: 6px; flex-wrap: wrap; margin: 6px 0 0 38px; }
.pr-chip {
  border: 1px solid var(--hairline); background: transparent; color: var(--label-2);
  font: inherit; font-size: 11px; padding: 2px 8px; border-radius: 20px; cursor: pointer;
}
.pr-chip:hover { color: var(--label); border-color: var(--hairline-2); }

@media (max-width: 860px) {
  /* 窄屏塞不下三栏连线图 —— 退化成一列按紧迫度排的卡片流，
     agent 归属改由卡片上的 chip 表达（连线在 JS 里也一并停画）。 */
  .board-grid { grid-template-columns: 1fr; gap: 0; padding: 12px 14px 32px; }
  .lane--mid { display: none; }
  .lane + .lane { margin-top: 16px; }
  .board-links { display: none; }
  .pr-agents { display: flex; }
  .viewsw { margin-left: 8px; }
  /* 概要与开关在 390px 上必然要换行，与其让它俩各折一半，不如整行摞起来 */
  .board-head { flex-wrap: wrap; gap: 6px 10px; padding: 10px 14px 8px; }
  .board-head .spacer { display: none; }
  .board-head .chk { white-space: nowrap; }
  .board-sum { flex: 1 1 100%; }
}


/* ————————————————————— 移动端触控目标（必须放在文件最后） —————————————————————
 *
 * ⚠️⚠️ **这一块的位置本身就是实现的一部分，别把它并回上面那个 860px 块。**
 * 那个块在第 710 行，而这些控件的基础定义在它**后面**：
 *   `.agent .trash` 989 · `.detail-head .stop` 1343 · `.composer #send`（用了更高
 *   特异性 id+class）。同特异性时后来者胜，更高特异性时无视顺序 ——
 * 第一版就写在上面那块里，改完量出来尺寸**一点没变**，而 CSS 不会为此报任何错。
 *
 * 尺寸取 38–40px：iOS HIG 说 44pt，但这些是密集列表里的次要动作，44 会把行撑散；
 * 38 已经能可靠命中，且**只加 padding、不放大图标**，所以观感不变。
 */
@media (max-width: 860px) {
  /* ⚠️⚠️ **负的纵向 margin 不能删**（机主 2026-08-18 第六次报「不一样宽」才查到）。
     他说「下面 4 个明显比上面 4 个更宽」。逐像素量他截图：8 行左右边界**完全相同**
     （23/1090），差的是**高度**：
       infra 行 168 device px   外面的行 208 device px
     infra cc 不可归档 ⇒ 没有这颗按钮；外面的行有，而它在触屏下被放大到 38px，
     比头像(26px)高 ⇒ 12×2+38=62 vs 12×2+26=50。
     **同宽同圆角时，矮的卡片圆角占比大 → 像药丸；高的更方 → 撑满宽度 → 看着更宽。**
     ⇒ 他说的"宽"其实是这个。别去调宽度，调高度。
     ⚠️ 用负 margin 而不是缩小按钮：**38px 的点击面积要保住**（那是触摸目标，
        缩了就难点）。负 margin 只让它在布局里按 26px 计，视觉和命中区都不变。 */
  .agent .trash { width: 38px; height: 38px; margin-block: -6px; }
  .agent .trash svg { width: 16px; height: 16px; }

  /* 尺寸走 `--btn`（见上），这里只调字号 —— 图标要跟着盒子一起大。
     机主 2026-08-16：发送键和 📎 必须成对，主次靠颜色不靠尺寸。 */
  .composer #send, .composer .icon-btn { font-size: 17px; }

  /* 「停止生成」原本只有 19x19 —— 它还是个**有副作用**的按钮（打断正在跑的 cc），
     点歪的代价比点不中更大 */
  /* 停止键：桌面 19px 是对的（机主 2026-08-06 明确说电脑端效果可以），
     手机只比桌面略大一点点，够点又不抢眼。
     ⚠️ 这里**故意不套那条"命中区 ≥38px"的通则**，两条理由：
       ① 它的视觉是「方框里一个小红点」，把盒子放大而红点不变，比例直接垮
          —— 我上一版放到 34px，机主一眼就说太大了；
       ② 它紧挨着标题，再往外扩十几像素的不可见命中区就会盖住标题右缘，
          而这是个**会打断正在跑的 cc** 的按钮，误触比点不中更贵。
     试过用透明 ::after 向外扩命中区（视觉不变），但实测 elementFromPoint
     在这个位置命中的仍是 .detail-head —— 技巧没生效，不留半套没验证的东西在这。
     红点同比放大到 7px，保持和桌面一样的比例。 */
  .detail-head .stop { width: 25px; height: 25px; border-radius: 6px; }
  .detail-head .stop::before { width: 8px; height: 8px; }   /* 25×0.32，与桌面同比 */

  .quick button { min-height: 38px; padding: 8px 13px; font-size: 12.5px; }
  /* ⚠️⚠️ **折叠条不吃这个 38px 命中区**（机主 2026-08-23「继续降低间距」）。
     量出来：手机上折叠条被撑到 38px，而**同一屏上的工具调用条只有 21px**
     —— 两者都是点击目标，凭什么一个比另一个高 17px。
     ⭐ 下限取自工具条：机主一直在用它，21px 就是可接受的。
     ⚠️ `.expand`（展开全文）保持 38px —— 它在**正文里**，不属于这一族
        "次要信息行"，一起改会动到真消息的排版。

     ⚠️⚠️ **这个属性在这份 CSS 里有两处**（另一处是 `.expand, .echo-bar
        { min-height: 34px }`，没有 `.msg` 前缀）。我第一次只改了那一处，
        量出来还是 38px —— **而我先怀疑的是别的地方**，是逐条问浏览器
        「谁在设这个值」才定位到这里。
        ⭐ 同一个属性多处设置，改一处不报错、也不生效。 */
  .msg .expand { min-height: 38px; }
  /* 投递状态那行的动作：看终端 / 重发 / 标记已读，原本 23px 高 */
  .st button { min-height: 34px; padding: 6px 11px; font-size: 12px; }

  /* 字号同理，也得放在最后、且特异性要跟得上基础定义：
     `.msg .when`(393) 与 `.tl-node .tl-ts`(579) 都是两级选择器，
     写成 `.when, .tl-ts` 会被它们压掉 —— 第一版就是这么写的，量出来一点没变。
     只抬时间戳与每步耗时这两类：户外光下 11px 的灰字基本读不出来。 */
  .msg .when { font-size: 11.5px; }
  .tl-node .tl-ts { font-size: 11.5px; }
  .lane-head { font-size: 11px; }
}

/* ═══════════════════ 左栏 cc 行 → 悬浮卡片（整块可回滚） ═══════════════════
 *
 * 2026-08-06 机主要求「做成悬浮卡片，先看别人怎么做，不好就回滚」。
 * **回滚 = 删掉从这行到文件末尾**，不需要动别处任何一行。
 *
 * 参考的是 Raycast / Linear / Vercel 那一路的做法，它们的共同点是
 * **用极细的环 + 顶部 1px 高光 + 很小的环境阴影**来做"浮起"，
 * 而不是大面积虚化投影 —— 后者在深色底上会糊成一团灰，且十几行叠在一起像结霜。
 * Raycast 的原方子（rgba(255,255,255,.05) inset 顶高光 + 1.19px 的极小投影 +
 * 1px 环）就是这个思路，这里按本项目的 token 重写了一遍。
 *
 * ⚠️ **本段原写着「刻意没给正在生成的行加额外抬升」，2026-08-17 机主拍板改了。**
 *   原理由是「已经有标题流光 + 环绕光边两层，第三层是同一件事说三遍」——
 *   保留它作为记录：那个判断在**三档层级**下成立，机主看过实际效果后要第四档。
 *   现在是：静止 → hover 微抬 → 选中 → 正在生成（见文件下方 `.agent.gen`）。
 */

/* 卡片之间要有缝，否则"卡片"读起来还是"行"。
 *
 * ⚠️⚠️ **不能用 `#list { display:flex; gap }`** —— 第一版就是这么写的，
 * 当天就被机主抓到：窄屏点开一个 cc 后，对话栏"从下方拉起、显示不全"。
 * 原因是窄屏靠 `.col{display:none}`(0,1,0) + `body[data-view=…] .col--list`(0,2,1)
 * 切换显隐，而 `#list` 是 **id 选择器 (1,0,0)**，把两条都压过了 ——
 * 于是切到对话页时列表**根本不隐藏**，占着 408px，把对话栏挤到 y=408 只剩 392px 高。
 *
 * 教训：**给一个由别处控制显隐的容器设 `display`，等于把它的显隐一起接管了。**
 * 这里只要间距，那就只设 margin —— 不碰 display 就不会跟显隐切换打架。 */
#list > * + * { margin-top: 6px; }

:root {
  --card-hi:      inset 0 1px 0 rgba(255, 255, 255, 0.05);
  --card-amb:     0 1px 2px rgba(0, 0, 0, 0.28);
  --card-amb-up:  0 3px 8px rgba(0, 0, 0, 0.34);
  --card-amb-sel: 0 3px 10px rgba(0, 0, 0, 0.40);
  /* 正在生成 = 比「选中」再高一档（机主 2026-08-17 拍板，见下面 .agent.gen）。
     ⚠️ 只有一两行会同时处于这一档，所以本节开头那条「大面积虚化投影在深色底上
     会糊成一团灰、十几行叠起来像结霜」不适用 —— 那说的是给**所有**卡片上大投影。 */
  --card-amb-gen: 0 7px 18px rgba(0, 0, 0, 0.68), 0 22px 46px -12px rgba(0, 0, 0, 0.64);
  /* 深色下卡片靠「比周围亮」浮起来。⚠️ 浅色不能这么做 —— 卡片本来就是纯白，
     白上加白等于什么都没发生（同本文件流光那条），浅色只能靠影子和缝隙。 */
  --card-bg-gen:  rgba(255, 255, 255, 0.115);

  /* ⚠️⚠️ `--glass-amb-gen` 的 **spread 必须是负的**（机主 2026-08-18 第四次报「宽窄不同」）。
     大模糊的投影会向【两侧】渗：`0 10px 22px` 实测让生成行的 painted 边缘比普通行
     外扩 3 CSS px（阈值 4 下 7/772 vs 14/765），肉眼就是"它更宽"。
     ⇒ 负 spread **大于半径的一半**（14/2=7 < 9）⇒ 横向渗出被完全抵消，竖向靠 y 偏移保住"浮起"。
     ⚠️ 代价如实记着：悬浮感比大投影弱一些。这是四次反馈换来的取舍 ——
       宽度一致 > 影子气派。想加回来之前先【量 painted 边缘】，别只看效果图。 */
  /* ——— 玻璃语汇（机主 2026-08-18：把液态玻璃按钮搬到左栏 tab）———
     ⚠️ 这里【没有】用那套 SVG 位移贴图折射。理由是实测出来的：左栏卡片背后是
       一整片平的底色，折射需要背后有东西可掰弯 —— 平面折射之后还是平面。
       上贴图只会给每一行加一个 filter + 每次重绘重算，换来零视觉差别。
     ⇒ 搬过来的是玻璃的【表面语汇】：半透的膜 + 上缘高光 + 细边 + 染色。
     ⚠️ 半透度不能照抄 demo（那边 .15）：demo 的背景又是网格又是字，膜薄才好看；
       左栏背后是平的，膜太薄行就跟底色糊在一起、读不出是一行。
       ⇒ 分隔改由【上缘高光 + 1px 环】承担，那也正是玻璃在 Apple UI 里的做法。 */
  --glass-film:     rgba(255, 255, 255, 0.085);
  --glass-amb-gen:  0 8px 14px -9px rgba(0, 20, 60, 0.85), 0 1px 2px rgba(0, 0, 0, 0.28);

  /* 见浅色那份说明：体积由背景渐变承担，这里只留极轻的左右两侧。 */
  --glass-bevel:
      inset 1px 0 0 rgba(255, 255, 255, 0.10),
      inset -1px 0 0 rgba(0, 0, 0, 0.22);

  --glass-sheen:
      linear-gradient(180deg,
      rgba(255, 255, 255, 0.10) 0%,
      rgba(255, 255, 255, 0.055) 22%,
      rgba(255, 255, 255, 0.02) 55%,
      rgba(0, 0, 0, 0.10) 100%);
  --glass-tint-gen:
      linear-gradient(180deg,
      rgba(80, 160, 255, 0.05) 0%,
      rgba(60, 140, 255, 0.10) 45%,
      rgba(40, 120, 250, 0.17) 100%);
}
@media (prefers-color-scheme: light) {
  :root {
    /* 浅色底上高光要几乎不可见（白上加白等于没发生），阴影反而要更实一点 */
    --card-hi:      inset 0 1px 0 rgba(255, 255, 255, 0.9);
    --card-amb:     0 1px 2px rgba(0, 0, 0, 0.07);
    --card-amb-up:  0 3px 8px rgba(0, 0, 0, 0.10);
    --card-amb-sel: 0 3px 10px rgba(0, 0, 0, 0.13);
    --card-amb-gen: 0 10px 22px rgba(0, 0, 0, 0.30), 0 28px 56px -14px rgba(0, 0, 0, 0.26);
    --card-bg-gen:  #ffffff;   /* 已是纯白，抬不动了 —— 浅色靠影子 */

  /* 见上方深色那份的说明。浅色下蓝要更实一点：底色本来就亮，淡蓝很容易被吃掉。 */
  /* ⚠️ 0.78 → 0.42：卡身原来合成后是 253（近纯白），**亮边没有余量** ——
     再加白等于什么都没发生（本文件多处记过这条，我 2026-08-18 又踩了一次：
     加了一圈白斜面，机主说「看着没有任何变化」，量出来卡内只有 252→254 两级差）。
     ⇒ 先把卡身压到 ~247，亮边到 255 才有 8 级可看；配合下缘暗边总落差 ~20 级。 */
  --glass-film:     rgba(255, 255, 255, 0.42);
  --glass-amb-gen:  0 8px 14px -9px rgba(0, 50, 120, 0.40), 0 1px 2px rgba(0, 0, 0, 0.07);

  /* ⭐ 方向性斜面（机主 2026-08-18 从两张 Liquid Glass 参考图里点出来的）：
     「通过深色和浅色的边框表现光线反射、折射，让按钮在浅色背景上也能有好的效果」。
     光从左上入射 ⇒ 上缘/左缘偏亮（反射），下缘/右缘偏暗（玻璃厚度的背面）。
     ⚠️ 平台原来只有 `--card-hi` 那一道**上缘白**，是单向的 —— 白底上加白
       等于什么都没发生，所以浅色下卡片一直立不起来。四条边一起给才有体积。
     ⚠️⚠️ **必须全部用 inset**：inset 阴影不参与 painted 尺寸。
       刚花六轮把 11 行宽高统一，任何新装饰都不许再动尺寸。
     ⚠️ 左右两条比上下弱一半：真实的斜面在侧面是掠射，不该和顶面一样亮。 */
  /* ⚠️⚠️ **不要用硬边线做斜面**（机主 2026-08-18：「边框和内部有割裂感」）。
     上一版是 `inset 0 2px 0 白` + `inset 0 -2px 0 黑` + 一条内侧亮线，
     实测底部从 241 **直接跳到 178**，中间还夹一条亮线 —— 那读起来是
     "在卡身外面贴了一个框"，不是玻璃自己的边。
     ⇒ 体积改由**背景那条连续渐变**独自承担（见 .agent 的 background），
       这里只留极轻的左右两侧，且**不设上下** —— 上下由渐变自己收边，
       任何一条横向硬线都会重新切出"框"。
     ⚠️ 判据：沿卡片纵向采样，相邻样本的跳变不应超过 ~4 级。硬线一定超。 */
  --glass-bevel:
      inset 1px 0 0 rgba(255, 255, 255, 0.75),
      inset -1px 0 0 rgba(0, 0, 0, 0.055);

  /* 玻璃的【结构层】—— 顶部入射、向下衰减。抽成 token 是为了让
     普通行和生成行**共用同一块玻璃**，生成行只在它下面多垫一层染色。
     ⚠️ 上一版 `.agent.gen` 直接 `background: var(--glass-film-gen)`，
        **把整层背景替换掉了** ⇒ 生成行丢掉这条柔光、换成一条 168° 斜向渐变，
        跟其它行不是同一种材质、方向也不一致。机主反馈「蓝色染色不太好看」，
        根子在这儿，不在蓝色本身。 */
  --glass-sheen:
      linear-gradient(180deg,
      rgba(255, 255, 255, 0.98) 0%,
      rgba(255, 255, 255, 0.80) 5%,
      rgba(255, 255, 255, 0.52) 14%,
      rgba(255, 255, 255, 0.28) 30%,
      rgba(255, 255, 255, 0.10) 52%,
      rgba(255, 255, 255, 0.02) 74%,
      rgba(0, 0, 0, 0.030) 100%);
  /* 染色层：**竖向**、上淡下浓 —— 光从上面进来，颜色在玻璃体内越往下积得越多。
     ⚠️ 饱和度要压：#007aff 原样上去会像一块蓝塑料。这里逐段往深蓝退、
       最浓处控制在 0.20 以内，让它读作【被染过的玻璃】而不是【蓝色控件】。 */
  --glass-tint-gen:
      linear-gradient(180deg,
      rgba(0, 122, 255, 0.035) 0%,
      rgba(0, 118, 245, 0.085) 38%,
      rgba(0, 112, 232, 0.150) 72%,
      rgba(0, 104, 215, 0.200) 100%);
  }
}
:root[data-theme="light"] {
  --card-hi:      inset 0 1px 0 rgba(255, 255, 255, 0.9);
  --card-amb:     0 1px 2px rgba(0, 0, 0, 0.07);
  --card-amb-up:  0 3px 8px rgba(0, 0, 0, 0.10);
  --card-amb-sel: 0 3px 10px rgba(0, 0, 0, 0.13);
  --card-amb-gen: 0 10px 22px rgba(0, 0, 0, 0.30), 0 28px 56px -14px rgba(0, 0, 0, 0.26);
  --card-bg-gen:  #ffffff;

  /* 见上方深色那份的说明。浅色下蓝要更实一点：底色本来就亮，淡蓝很容易被吃掉。 */
  /* ⚠️ 0.78 → 0.42：卡身原来合成后是 253（近纯白），**亮边没有余量** ——
     再加白等于什么都没发生（本文件多处记过这条，我 2026-08-18 又踩了一次：
     加了一圈白斜面，机主说「看着没有任何变化」，量出来卡内只有 252→254 两级差）。
     ⇒ 先把卡身压到 ~247，亮边到 255 才有 8 级可看；配合下缘暗边总落差 ~20 级。 */
  --glass-film:     rgba(255, 255, 255, 0.42);
  --glass-amb-gen:  0 8px 14px -9px rgba(0, 50, 120, 0.40), 0 1px 2px rgba(0, 0, 0, 0.07);

  /* ⭐ 方向性斜面（机主 2026-08-18 从两张 Liquid Glass 参考图里点出来的）：
     「通过深色和浅色的边框表现光线反射、折射，让按钮在浅色背景上也能有好的效果」。
     光从左上入射 ⇒ 上缘/左缘偏亮（反射），下缘/右缘偏暗（玻璃厚度的背面）。
     ⚠️ 平台原来只有 `--card-hi` 那一道**上缘白**，是单向的 —— 白底上加白
       等于什么都没发生，所以浅色下卡片一直立不起来。四条边一起给才有体积。
     ⚠️⚠️ **必须全部用 inset**：inset 阴影不参与 painted 尺寸。
       刚花六轮把 11 行宽高统一，任何新装饰都不许再动尺寸。
     ⚠️ 左右两条比上下弱一半：真实的斜面在侧面是掠射，不该和顶面一样亮。 */
  /* ⚠️ 再加重一档（机主 2026-08-18「差不多，再做的明显一点」）。
     两条硬边之外各补一条【柔化带】—— 真玻璃的斜面是有厚度的，
     一条 1px 硬线只像描边，硬线 + 向内衰减的柔光才像"切面"。
     下缘还多给一条**内侧亮线**：厚玻璃底部会把桌面的反射光兜回来，
     那条亮线正是"这块玻璃有厚度"最强的暗示（两张参考图里都有）。 */
  --glass-bevel:
      inset 0 2px 0 rgba(255, 255, 255, 1),
      inset 0 5px 5px -4px rgba(255, 255, 255, 0.95),
      inset 2px 0 0 rgba(255, 255, 255, 0.9),
      inset -2px 0 0 rgba(0, 0, 0, 0.13),
      inset 0 -2px 0 rgba(0, 0, 0, 0.22),
      inset 0 -5px 5px -4px rgba(0, 0, 0, 0.16),
      inset 0 -3.5px 0 rgba(255, 255, 255, 0.55);

  --glass-sheen:
      linear-gradient(180deg,
      rgba(255, 255, 255, 0.10) 0%,
      rgba(255, 255, 255, 0.055) 22%,
      rgba(255, 255, 255, 0.02) 55%,
      rgba(0, 0, 0, 0.10) 100%);
  --glass-tint-gen:
      linear-gradient(180deg,
      rgba(80, 160, 255, 0.05) 0%,
      rgba(60, 140, 255, 0.10) 45%,
      rgba(40, 120, 250, 0.17) 100%);
}
:root[data-theme="dark"] {
  --card-hi:      inset 0 1px 0 rgba(255, 255, 255, 0.05);
  --card-amb:     0 1px 2px rgba(0, 0, 0, 0.28);
  --card-amb-up:  0 3px 8px rgba(0, 0, 0, 0.34);
  --card-amb-sel: 0 3px 10px rgba(0, 0, 0, 0.40);
  /* 正在生成 = 比「选中」再高一档（机主 2026-08-17 拍板，见下面 .agent.gen）。
     ⚠️ 只有一两行会同时处于这一档，所以本节开头那条「大面积虚化投影在深色底上
     会糊成一团灰、十几行叠起来像结霜」不适用 —— 那说的是给**所有**卡片上大投影。 */
  --card-amb-gen: 0 7px 18px rgba(0, 0, 0, 0.68), 0 22px 46px -12px rgba(0, 0, 0, 0.64);
  /* 深色下卡片靠「比周围亮」浮起来。⚠️ 浅色不能这么做 —— 卡片本来就是纯白，
     白上加白等于什么都没发生（同本文件流光那条），浅色只能靠影子和缝隙。 */
  --card-bg-gen:  rgba(255, 255, 255, 0.115);

  /* ——— 玻璃语汇（机主 2026-08-18：把液态玻璃按钮搬到左栏 tab）———
     ⚠️ 这里【没有】用那套 SVG 位移贴图折射。理由是实测出来的：左栏卡片背后是
       一整片平的底色，折射需要背后有东西可掰弯 —— 平面折射之后还是平面。
       上贴图只会给每一行加一个 filter + 每次重绘重算，换来零视觉差别。
     ⇒ 搬过来的是玻璃的【表面语汇】：半透的膜 + 上缘高光 + 细边 + 染色。
     ⚠️ 半透度不能照抄 demo（那边 .15）：demo 的背景又是网格又是字，膜薄才好看；
       左栏背后是平的，膜太薄行就跟底色糊在一起、读不出是一行。
       ⇒ 分隔改由【上缘高光 + 1px 环】承担，那也正是玻璃在 Apple UI 里的做法。 */
  --glass-film:     rgba(255, 255, 255, 0.085);
  --glass-amb-gen:  0 8px 14px -9px rgba(0, 20, 60, 0.85), 0 1px 2px rgba(0, 0, 0, 0.28);

  /* 见浅色那份说明。深色下入射光弱得多，而暗边几乎看不见 ——
     ⇒ 亮的那两条压到浅色的 1/6，暗的两条基本取消，靠亮边单独立形。 */
  --glass-bevel:
      inset 0 1px 0 rgba(255, 255, 255, 0.16),
      inset 1px 0 0 rgba(255, 255, 255, 0.07),
      inset -1px 0 0 rgba(0, 0, 0, 0.20),
      inset 0 -1px 0 rgba(0, 0, 0, 0.30);

  --glass-sheen:
      linear-gradient(180deg,
      rgba(255, 255, 255, 0.10) 0%,
      rgba(255, 255, 255, 0.055) 22%,
      rgba(255, 255, 255, 0.02) 55%,
      rgba(0, 0, 0, 0.10) 100%);
  --glass-tint-gen:
      linear-gradient(180deg,
      rgba(80, 160, 255, 0.05) 0%,
      rgba(60, 140, 255, 0.10) 45%,
      rgba(40, 120, 250, 0.17) 100%);
}

.agent {
  /* 玻璃（机主 2026-08-18）。参数用他在 demo 上定的那三个：折射 20 / 模糊 2 / 饱和 150。
     ⚠️ 折射那一项这里【没有】搬过来，见 --glass-film 上方那段说明：
        背后是平的底色，位移贴图refract不到任何东西。 */
  /* ⚠️ 顶部宽幅高光（机主 2026-08-18 发来两张 Liquid Glass 参考图之后加的）。
     参考图里最能区分「玻璃」和「白卡片」的不是边框，是**占高度约 40% 的一道柔光**
     —— 光进入玻璃上表面之后在体内散开。平台原来只有 `--card-hi` 那 1px 内高光，
     那是"卡片"的语言不是"玻璃"的。
     ⚠️ 用 background 叠层实现，**不新增元素、不改任何几何** ——
        刚花了六轮才把 11 行的宽高统一，任何新装饰都不许再动尺寸。 */
  background:
      var(--glass-sheen),
    var(--glass-film);
  -webkit-backdrop-filter: blur(2px) saturate(150%);
  backdrop-filter: blur(2px) saturate(150%);
  border-radius: 12px;
  /* ⚠️ 用 --glass-bevel 取代单向的 --card-hi：四条边一起给才有体积，见其定义处。 */
  box-shadow: var(--glass-bevel), var(--card-amb), 0 0 0 1px var(--hairline);
  transition: box-shadow 180ms var(--ease), transform 180ms var(--ease),
              background 180ms var(--ease);
}
/* 卡片之间不再需要内嵌分隔线 —— 缝隙本身就是分隔 */
.agent + .agent::before { display: none; }

.agent:hover {
  transform: translateY(-1px);
  box-shadow: var(--glass-bevel), var(--card-amb-up), 0 0 0 1px var(--hairline-2);
}
/* 选中：环变蓝。用环而不是填充，是为了不跟 authwait 的黄底打架 */
.agent.active {
  transform: none;
  box-shadow: var(--glass-bevel), var(--card-amb-sel),
              0 0 0 1px color-mix(in srgb, var(--blue) 45%, transparent);
}
/* 已归档的不参与"浮起"：它已经不是一个在跑的 cc 了，压平比调透明度更说明问题 */
.agent.archived { box-shadow: 0 0 0 1px var(--hairline); background: transparent; }
.agent.archived:hover { transform: none; box-shadow: 0 0 0 1px var(--hairline-2); }

/* ══ 正在生成 → 再抬一档 ══════════════════════════════════════
 * 机主 2026-08-17 看过效果后拍板。**这条推翻了本节开头「刻意没做」那段**，
 * 那段已同步改掉 —— 别再按它去删这里。
 *
 * 原来的理由是「已经有标题流光 + 环绕光边两层表达了，第三层是同一件事说三遍」，
 * 它成立于**三档层级**的前提；现在层级是四档：
 *   静止 → hover 微抬 → 选中 → 正在生成。
 *
 * ⚠️ **本行 2026-08-18 订正**：原文写着「没有引入 backdrop-filter 那套玻璃 ——
 *   那会让这一行和其它卡片变成两种材质」。那个顾虑成立于「只有这一行是玻璃」的前提；
 *   机主当天决定**所有** tab 都上玻璃（未生成的用未染色的那版），前提没了。
 *   ⇒ 现在 .agent 本身就是玻璃，这一行只是在它基础上再抬一档 + 染蓝，仍然是同一种材质。
 * ⚠️ 位置必须在 `.agent:hover` 之后（同为 0,2,0，靠顺序赢）、
 *   `.agent:active` 之前（按下压平是那条给的，别夺走）。
 * ⚠️ 彩虹描边**不在这里** —— `.agent.gen::after` 早就有了，别再造第二个。 */
/* ⚠️⚠️ **不要把 scale 加回来**（机主 2026-08-18）。
   他先要「悬浮再明显一点」（于是有了 scale(1.026)），后来两次报「agent 宽窄不同」。
   这两件事是同一个属性的两面：**scale 就是在改宽度** ——
   实测桌面 1280 下正在生成的行 228.8px、其余 223px，差 5.8px。
   而 infra 组里通常只有一两行在生成，外面在生成的比例高，所以肉眼读成
   「infra 比其它窄」—— 变量其实是【在不在生成】，不是【在不在 infra】。
   ⇒ 宽度让位给一致性，悬浮改由【抬得更高 + 更重的影 + 蓝色染色】表达。
     染色是 08-18 才加的，当初决定用 scale 时还没有它 —— 现在信号够了。
   ⚠️ 移动端本来就没有 scale（见下方 max-width:860px 那条），所以这次改动
     等于把桌面拉齐到移动端的做法，两端终于是同一套。 */
.agent.gen {
  transform: translateY(-6px);
  /* 悬浮 + 染蓝（机主 2026-08-18）。蓝是【上浅下浓】的渐变不是平涂 ——
     平涂会立刻变回一片蓝色塑料，渐变才像有厚度的玻璃在积累颜色。
     ⚠️⚠️ **在同一块玻璃下面垫一层染色，不要替换整层背景。**
        上一版是 `background: var(--glass-film-gen)` —— 那会把 --glass-sheen
        一起顶掉，生成行于是变成另一种材质（斜向渐变、没有柔光结构），
        机主反馈「蓝色染色看着不太好看」，根子在这儿不在蓝色本身。
        叠放顺序：柔光（结构）→ 染色 → 底膜。
     ⚠️ **不给 `0 0 0 1px` 那圈细边**：彩虹环已经收进内沿当边用了（见 ::after 那段），
        再叠一圈就是两圈同心线，也就是 2026-08-17 修过的"重影"。 */
  background: var(--glass-sheen), var(--glass-tint-gen), var(--glass-film);
  box-shadow: var(--glass-bevel), var(--glass-amb-gen);
}
.agent.gen:hover {
  transform: translateY(-9px);
  box-shadow: var(--glass-bevel), var(--glass-amb-gen);
}
/* ⚠️⚠️ **窄屏不能放大**：≤860px 时卡片本来就贴边，`scale` 会把它顶出视口 ——
   实测 860px 处左右各超出 **3px**，边框和那圈彩虹会被切掉（而且不产生横向
   滚动条，所以不看数字发现不了）。窄屏改用「抬得更高」这一个线索。
   判据不是"看着差不多"，是量 `getBoundingClientRect()` 与 innerWidth 的差。 */
@media (max-width: 860px) {
  /* ⚠️ 这条原本是为了绕开「窄屏 scale 会把卡片顶出视口」；2026-08-18 桌面也去掉了
     scale（见上），两端做法已经一致。它现在只剩一个作用：**触屏没有真 hover**，
     所以 hover 态不要再抬一次 —— tap 之后那个假 hover 会让它一直浮在高处。 */
  .agent.gen        { transform: translateY(-6px); }
  .agent.gen:hover  { transform: translateY(-6px); }
}

/* ⚠️ **不给它加缝隙**（机主 2026-08-17：「不要改变上下的间距」）。
   上一版加过 `#list > .agent.gen { margin: 13px 0 }`，被否了。
   代价如实记着：没有缝，影子有一截会落在下一张卡片上，"浮起"少了一个线索
   —— 所以抬升/放大/提亮这三项要顶上去（见上面的取值）。
   ⚠️ 将来若有人想把缝加回来，必须写成 `#list > .agent.gen`：间距来自
   `#list > * + *`，那是 (1,0,0) 的 id 选择器，`.agent.gen` 只有 (0,2,0)，
   **id 赢** —— 实测用类选择器设 40px，计算值仍是 6px。 */
/* 选中 + 正在生成：影用生成那档，环仍然是选中的蓝 —— 两个维度各说各的 */
.agent.gen.active {
  box-shadow: var(--card-hi), var(--card-amb-gen),
              0 0 0 1px color-mix(in srgb, var(--blue) 45%, transparent);
}

/* ⚠️ 触屏上 tap 会触发一次**假 hover** 并且停在那里，直到点别处 ——
   卡片就那么一直浮着、时间线行一直放大着，看上去像选中了但其实没有。
   只关掉位移，底色/阴影的变化留着（那反而帮人看清点了哪一行）。 */
@media (hover: none), (pointer: coarse) {
  .agent:hover, .agent.gen:hover, .tl-node:hover { transform: none; }
  /* ⚠️⚠️ **底色和影子也必须还原**（机主 2026-08-18 第五次报「不一样宽」才查出来）。
     上一版这里只关了位移，注释写着"底色/阴影的变化留着，那反而帮人看清点了哪一行"。
     那句话现在作废，两个理由：
       ① `.agent:hover` 的 `--surface-1` 在浅色下是**不透明纯白**，而其余行是
          半透玻璃膜（合成后 252,252,254）。逐像素量机主截图：8 行里 7 行
          23/1090 中心色 252,252,254，**只有他最后点过的那一行** 22/1091、
          纯白 255,255,255 —— 更亮 ⇒ 看起来更宽（irradiation）。
          ⇒ 这就是他反复说"不一样宽"却每次指的行都不同的原因，
            也是我截图永远复现不出来的原因（无人 hover）。
       ② "点了哪一行"已经由 `.agent.active` 的蓝环表达了。粘住的假 hover
          是**第二个、而且会改变视觉尺寸**的指示器，不是帮忙。
     ⚠️ 用 `:not(.active)`：别把真正的选中态一起压掉。 */
  .agent:hover:not(.active) {
    background: var(--glass-film);
    box-shadow: var(--glass-bevel), var(--card-amb), 0 0 0 1px var(--hairline);
  }
  .agent.gen:hover:not(.active) {
    background: var(--glass-sheen), var(--glass-tint-gen), var(--glass-film);
    box-shadow: var(--glass-bevel), var(--glass-amb-gen);
  }
}

/* ⚠️ 按下要比松开**快**（2026-08-09）：按下是人主动做的动作，回应慢一拍就"黏"；
   松开是系统在收尾，稍慢反而顺。对称时长（按下松开都 140/180ms）是个可判定的缺陷。
   只覆盖时长，曲线沿用 --ease。 */
.composer #send:active,
.composer .icon-btn:active,
.detail-head .stop:active,
.tobottom:active,
.linkbtn:active,
.agent:active { transition-duration: 90ms; }

/* 点下去要有回弹感，别只有 hover */
.agent:active { transform: translateY(0); box-shadow: var(--card-hi), var(--card-amb), 0 0 0 1px var(--hairline-2); }

@media (prefers-reduced-motion: reduce) {
  .agent, .agent:hover, .agent:active,
  .agent.gen, .agent.gen:hover { transform: none; }
}

/* 窄屏：缝隙收一点，卡片贴边一点 —— 手机上垂直空间比"透气"更值钱 */
@media (max-width: 860px) {
  #list > * + * { margin-top: 5px; }
  .agent { border-radius: 11px; }
}

/* ————————————————————— 授权确认框 → 可点的选项 —————————————————————
   cc 停在权限框上时，输入框整块换成这个（机主 2026-08-06）。
   视觉上刻意**不做成消息气泡**：它不是一条消息，是一个等你处置的闸门。 */
/* ⚠️ 入场（2026-08-09）：这块出现意味着**输入框被换成了选项** —— 机主可能正在
   那个框里打字，一帧之间它就没了。这是全应用后果最重的一次界面替换，需要一座桥。
   从下方 6px 浮上来：它是从输入框那个位置长出来的，方向本身在说"我替换了它"。
   ⚠️ `hidden` 属性 = display:none，所以要 allow-discrete，否则退场那帧已经不显示了。 */
.authbox {
  opacity: 1; transform: none;
  transition: opacity 180ms var(--ease), transform 180ms var(--ease),
              display 180ms allow-discrete;
  flex: 0 0 auto; margin: 0 14px 14px; padding: 14px 16px 12px;
  border: 1px solid color-mix(in srgb, var(--yellow) 42%, transparent);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--yellow) 9%, var(--surface-1));
}
.authbox[hidden] { opacity: 0; transform: translateY(6px); }
@starting-style { .authbox:not([hidden]) { opacity: 0; transform: translateY(6px); } }
@media (prefers-reduced-motion: reduce) {
  .authbox, .authbox[hidden] { transform: none; }
}
.auth-head { display: flex; align-items: center; gap: 9px; margin-bottom: 9px; }
.auth-tag {
  font-size: 11px; font-weight: 620; letter-spacing: .02em; color: var(--yellow);
  border: 1px solid color-mix(in srgb, var(--yellow) 45%, transparent);
  border-radius: 6px; padding: 2px 7px;
}
.auth-kind { font-size: 12.5px; color: var(--label-2); font-family: var(--mono); }
/* 请求原文用等宽 + 可横向滚动：这是机主做决定的**全部依据**，不能截断也不能折行走样 */
.auth-body {
  margin: 0 0 9px; padding: 9px 11px; border-radius: var(--radius-sm);
  background: var(--bg-2); border: 1px solid var(--hairline);
  font-family: var(--mono); font-size: 12px; line-height: 1.55; color: var(--label);
  white-space: pre; overflow-x: auto; max-height: 30vh; overflow-y: auto;
}
.auth-q { font-size: 13px; color: var(--label); margin-bottom: 9px; }
.auth-opts { display: flex; flex-direction: column; gap: 6px; }
.auth-opt {
  display: flex; align-items: center; gap: 10px; text-align: left;
  border: 1px solid var(--hairline-2); background: var(--surface-1);
  border-radius: var(--radius-sm); padding: 9px 11px; cursor: pointer;
  color: var(--label); font: inherit; font-size: 13px;
  transition: background 160ms var(--ease), border-color 160ms var(--ease);
}
.auth-opt:hover:not(:disabled) { background: var(--surface-2); border-color: var(--label-3); }
.auth-opt:disabled { opacity: .55; cursor: default; }
.auth-opt.sending { opacity: .7; }
/* cc 屏幕上当前高亮的那一项。**只是告知，不是推荐** —— 平台绝不替机主预选 */
.auth-opt.cur { border-color: color-mix(in srgb, var(--blue) 45%, transparent); }
.auth-n {
  flex: 0 0 auto; width: 20px; height: 20px; border-radius: 6px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface-2); color: var(--label-2);
  font-family: var(--mono); font-size: 11.5px;
}
.auth-t { flex: 1 1 auto; min-width: 0; }
/* 自由输入项（`Type something.`）：点之前和别的选项长得一样，点开之后就地变成输入行 ——
   和 Claude Code TUI 里选中它的行为一致（机主 2026-08-24 要的「和 Cloud 那边保持一致」）。 */
.auth-opt-input .auth-text, .auth-opt-input .auth-send { display: none; }
.auth-opt-input.open .auth-t { display: none; }
.auth-opt-input.open .auth-text, .auth-opt-input.open .auth-send { display: block; }
.auth-text {
  flex: 1 1 auto; min-width: 0; border: 1px solid var(--hairline-2);
  background: var(--bg-2); color: var(--label); font: inherit; font-size: 13px;
  border-radius: 6px; padding: 5px 8px;
}
.auth-text:focus { outline: none; border-color: color-mix(in srgb, var(--blue) 55%, transparent); }
.auth-send {
  flex: 0 0 auto; border: 1px solid var(--hairline-2); background: var(--surface-2);
  color: var(--label); font: inherit; font-size: 12.5px; border-radius: 6px;
  padding: 5px 11px; cursor: pointer;
}
.auth-send:hover:not(:disabled) { border-color: var(--label-3); }
.auth-send:disabled, .auth-text:disabled { opacity: .55; cursor: default; }
.auth-foot { margin-top: 9px; font-size: 11.5px; color: var(--label-3); }
.auth-foot.bad { color: var(--red); }

@media (max-width: 860px) {
  .authbox { margin: 0 12px 12px; padding: 12px 13px 10px; }
  .auth-opt { min-height: 44px; }        /* 授权是要点准的动作，给足命中区 */
  .auth-body { font-size: 11.5px; max-height: 26vh; }
}

/* ————————————————————— PR 目标分支 + 合并 —————————————————————
   机主 2026-08-06：卡片显示目标分支，加一个能强制合并（跳过 CI 与 review）
   的按钮，点了要二次确认。 */
.pr-base {
  margin-left: 6px; font-family: var(--mono); font-size: 11px;
  color: var(--label-2); white-space: nowrap;
}
.pr-acts { display: inline-flex; gap: 6px; align-items: center; flex-wrap: nowrap; }
/* 合并键平时是安静的描边键 —— 它旁边就是「重跑」，两个都做成红的会互相抢注意力；
   真正的警示留给确认条，那里才有足够的地方把"合到哪、跳过什么"说清楚。 */
.pr-merge {
  border: 1px solid var(--hairline-2); background: transparent; color: var(--label-2);
  font: inherit; font-size: 11.5px; padding: 3px 10px; border-radius: 999px; cursor: pointer;
}
.pr-merge:hover { color: var(--red); border-color: color-mix(in srgb, var(--red) 55%, transparent); }

.pr-confirm {
  margin-top: 9px; padding: 9px 10px; border-radius: var(--radius-sm);
  border: 1px solid color-mix(in srgb, var(--red) 45%, transparent);
  background: color-mix(in srgb, var(--red) 8%, transparent);
}
.pr-confirm-q { font-size: 12px; color: var(--label); line-height: 1.6; }
.pr-confirm-warn { display: block; color: var(--red); font-size: 11.5px; margin-top: 2px; }
.pr-confirm-acts { display: flex; gap: 7px; justify-content: flex-end; margin-top: 8px; }
.pr-confirm-acts button {
  border: 1px solid var(--hairline-2); background: var(--surface-1); color: var(--label);
  font: inherit; font-size: 12px; padding: 5px 12px; border-radius: 8px; cursor: pointer;
}
.pr-confirm-acts button.danger {
  border-color: transparent; background: var(--red); color: #fff; font-weight: 560;
}
.pr-confirm-acts button:disabled { opacity: .55; cursor: default; }

@media (max-width: 860px) {
  .pr-merge { min-height: 34px; padding: 6px 12px; }
  .pr-confirm-acts button { min-height: 38px; }
  /* 窄屏放不下"状态文字 + 两个按钮"一行时，让按钮整组换到下一行 —— 裁掉不行，
     那是个能点的写操作按钮，看不全等于点不准。 */
  .pr-foot { flex-wrap: wrap; row-gap: 6px; margin-left: 0; }
  .pr-foot > span.pr-acts { margin-left: auto; }
  /* 38px 的左缩进是为了在宽屏上对齐圆环右侧；窄屏卡片本来就窄，纯浪费 */
  .pr-meta { margin-left: 0; }
  .pr-agents { margin-left: 0; }
}

/* ————————————————————— 一键复制整条消息 —————————————————————
   机主 2026-08-06。放在气泡右侧、和气泡顶端对齐。
   ⚠️ 桌面 hover 才浮现（平时不打扰阅读），**窄屏常驻** —— 手机没有 hover，
   藏起来等于没有这个功能。 */
.msg { position: relative; }
.msg .copy {
  /* ⚠️ 贴**底**不贴顶（机主 2026-08-06 定）：贴顶时它和气泡第一行齐平，视线一进来
     就先撞到一个按钮；贴底则落在时间戳那一行的高度上，读完才看见它 —— 而"读完想留下"
     正是要复制的时刻。 */
  /* top 由 JS 按气泡的真实下边缘算（见 app.js 里那段注释）——
     这里只给个兜底值，免得 JS 还没跑到时它飞到左上角。 */
  position: absolute; top: 0; width: 26px; height: 26px; padding: 0;
  display: grid; place-items: center;
  border: 1px solid var(--hairline); border-radius: 7px;
  background: var(--bg-2); color: var(--label-3); cursor: pointer;
  opacity: 0; transition: opacity 140ms var(--ease), color 140ms var(--ease),
                          border-color 140ms var(--ease);
}
/* 左下还是右下，看是谁发的：一律落在气泡的**外侧**下角 ——
   cc 的气泡靠左，按钮就在它右下；机主自己的靠右，按钮就在它左下。
   放外侧是为了不遮正文，放对应侧是为了跟着说话人走、不跨过对话中线。 */
.msg .copy { right: -32px; }
.msg.down .copy { right: auto; left: -32px; }
/* ⚠️ 机主那侧的类名是 `.down`（`.up` = cc 发的、`.down` = 机主发的）。
   第一版我按聊天软件的习惯写成了 `me`，而这个类在本项目里**根本不存在** ——
   规则一次都没生效，机主自己的气泡一直在吃默认的"右侧"，手机上就压在右上角。
   是量的时候「机主消息数 = 0」把它暴露出来的：**选择器错了才会一条都选不中**，
   这个 0 不是"没有数据"，是判据本身在报错。 */
.msg:hover .copy, .msg .copy:focus-visible { opacity: 1; }
.msg .copy:hover { color: var(--label); border-color: var(--hairline-2); }
.msg .copy.done { color: var(--green); border-color: var(--green); opacity: 1; }
.msg .copy.fail { color: var(--red); border-color: var(--red); opacity: 1; }
/* 折叠的「会话内复述」不挂复制键：它只是一行预览，正文在展开后的气泡里。
   展开后气泡出现，按钮照常对齐气泡底边。 */
.msg.foldable.folded .copy { display: none; }

@media (hover: none), (max-width: 860px) {
  /* ⚠️ 状态 tag 的三个按钮默认收起（机主 2026-08-17「那个 tag 太占地方了」）。
     实测：40px 的气泡下面挂 59px 的状态，而那个 cc 连着 7 条，就是 7 份。
     收起后 25px，点状态文字展开。**桌面不折** —— 那边宽度够，多一次点击是净损失。 */
  .msg .st-acts { display: none; }
  .msg .st.open .st-acts { display: inline-flex; }
  .msg .st-txt .caret { display: inline-block; }
  .msg .st.open .st-txt .caret { transform: rotate(90deg); }

  /* 触屏没有 hover：常驻显示，且淡一点别抢正文 */
  .msg .copy { opacity: .5; }
  .msg .copy.done, .msg .copy.fail { opacity: 1; }
}
@media (max-width: 860px) {
  /* ⚠️ 窄屏不能像宽屏那样浮到气泡外（两侧没有 32px 富余），但也**不能浮到气泡里**
     —— 第一版那么做，按钮直接压在正文第一行上（实测蓝气泡和"会话内复述"那行都被盖）。
     改成给 `.msg` 留一条 32px 的边槽：`.msg` 是 fit-content，留槽只会让气泡窄一点，
     不会把布局撑开，按钮落在槽里就不会盖任何字。 */
  .msg { padding-right: 32px; }
  .msg.down { padding-right: 0; padding-left: 32px; }
  .msg .copy { right: 0; width: 30px; height: 30px; }
  .msg.down .copy { right: auto; left: 0; }
}

/* ————————————————————— 发送优先级 / 中转站 —————————————————————
   机主 2026-08-06：默认低（等它停止生成再发），中/高必须写一句原因。 */
/* 优先级下拉框 + 快捷消息同一行（机主 2026-08-06，省垂直空间）。
   `.quick` 自己是 flex，塞进来当一个可伸缩的孩子。 */
/* 优先级 + 快捷消息**始终一行**，放不下就左右滚（机主 2026-08-07）。
   ⚠️ 三处缺一不可：
     · `flex-wrap: nowrap` 让它不换行
     · `overflow-x: auto` 给它滚动能力
     · 每个孩子 `flex: 0 0 auto` —— **少了这条，flex 会把按钮压扁而不是溢出**，
       于是永远滚不起来，文字反被挤成竖排。 */
/* ⚠️ 2026-08-16 改：**滚动的是 `.quick`，不再是整行**。
   原来整行滚，于是加进来的「抄送」会跟着滑出屏幕 —— 一个设一次的开关找不到了。
   现在优先级钉左、抄送钉右，中间的快捷短语自己滚。上面那三条不变式仍然成立，
   只是搬到了 `.quick` 身上。 */
.prio {
  display: flex; align-items: center; gap: 7px; margin-bottom: 8px;
  flex-wrap: nowrap;
}
.prio > * { flex: 0 0 auto; }
.prio > .quick {
  flex: 1 1 auto;
  min-width: 0;               /* ⚠️ 少了它 flex 项不肯缩到内容以下，永远滚不起来 */
  margin-bottom: 0; flex-wrap: nowrap;
  overflow-x: auto; overscroll-behavior-x: contain;
  scrollbar-width: none;      /* 滚动条会把这行撑高一截，藏掉 */
}
.prio > .quick::-webkit-scrollbar { display: none; }
/* 右边缘渐隐，告诉人"还能往右滑"。原来整行滚时它切在屏幕边上还看得出来，
   现在被「抄送」挡着，硬切看着像渲染坏了。
   ⚠️ 只渐隐容器最后 14px：内容没铺到那儿时那段本来就是空的 ⇒ **不滚就看不出效果**，
      不需要用 JS 判断能不能滚。 */
.prio > .quick {
  -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 14px), transparent);
          mask-image: linear-gradient(90deg, #000 calc(100% - 14px), transparent);
}
.prio > .quick > button { flex: 0 0 auto; white-space: nowrap; }
#prio-sel {
  flex: 0 0 auto; font: inherit; font-size: 12px; padding: 4px 8px;
  border: 1px solid var(--hairline); border-radius: 8px;
  background: var(--surface-1); color: var(--label-2); cursor: pointer;
  transition: color 140ms var(--ease), border-color 140ms var(--ease);
}
/* 中/高是会插队甚至打断别人的选择，给下拉框本身上色 —— 但不做成红色警告：
   它们是正当操作，只是要让人**意识到**自己选了什么。 */
#prio-sel.p-mid  { color: var(--yellow); border-color: color-mix(in srgb, var(--yellow) 55%, transparent); }
#prio-sel.p-high { color: var(--red);    border-color: color-mix(in srgb, var(--red) 55%, transparent); }

@media (max-width: 860px) {
  #prio-sel { min-height: 34px; font-size: 16px; }   /* ≥16px 免得 iOS 聚焦时放大页面 */
}

/* ————————————————————— infra cc 折叠成一个容器（机主 2026-08-07）—————————————————————
   ⚠️ 这一整块是「先存档、效果不好要回滚」的那一版 —— 回滚 = 删掉从这行到文件末尾，
   并把 app.js 里 `.infra-box` 那段还原成"按钮 + 平级的行"。

   原来那几个 infra cc 只是跟在灰按钮后面的兄弟节点，展开后和普通 cc 长得一样，
   看不出"这几个是一类"。现在让灰 tab 自己向下扩张，把它们框进去。 */
.infra-box {
  /* ⚠️⚠️ **虚线框 2026-08-20 删掉**（机主：「保留 cc 好像本来就有一个框，
     最开始时候做的，你看一下，把它删了」）。
     它是 08-07 加的，当时的语言是"这是一个可展开的坑位"。
     而 08-20 加了底色之后，框 + 底色成了**两层容器语言** —— 删掉框是对的。

     ⚠️ **但别让分界跟着一起消失**：折叠态原来全靠那圈虚线才看得出"这是一组"，
        删了框如果什么都不补，它就和「展开闲置 24h 以上的会话」那个普通折叠按钮
        长得一模一样了。⇒ 折叠态改用**和展开态同一个底色**，语言反而更统一。
     ⭐ 同「删掉含告警的东西时要把告警保住」：**删掉一个视觉语言时，
       要看它承载了什么，别把那件事一起删了。** */
  background: color-mix(in srgb, var(--label) 8%, transparent);
  border-radius: var(--radius);
  transition: background 180ms var(--ease);
  /* ⚠️ 竖线的定位基准（2026-08-20）。 */
  position: relative;
  /* ⚠️⚠️ **分界感靠间距 + 竖线，不靠边框和底色**（机主 2026-08-20：
     「保留 cc 和其它 cc 的分界感太弱，你想想怎么办」）。

     这块被调过四轮，前面两条路都走死了，别再试：
       · **边框** ⇒ border 占布局，子元素必然内缩 ⇒ 行比外面窄 2px，他看得出来
       · **底色** ⇒ 图底关系反转（灰片 on 白盒 vs 白片 on 灰底），
                    亮的物体看起来更大，怎么调都会被看出宽窄不同

     ⭐ 共同点：**它们都碰了卡片的几何或图底**。所以这次选三样都不碰的：
       ① 上下留大间距（gap 不影响宽度）
       ② 底色（⚠️ 用相对色 + 压着幅度，见 `.infra-box` 里那段）
       ③ 组标题本身加重
     ⚠️ 曾经还有一条左侧竖线，底色加深之后被机主删掉了 —— 两个东西说同一件事。
     ⚠️ 判据：**改完之后卡片的左右外沿必须和组外的行完全一致。** */
  margin: 12px 0;
}
/* ⚠️ 这里原来有一条 `.infra-box.open::before` 左侧竖线（2026-08-20 上午加的，
   当时底色还没加深、分界只能靠它）。机主同日让删掉 ——
   底色够了之后它就成了第二个说同一件事的东西。 */

/* ⚠️⚠️ 展开态**不能用 border**（机主 2026-08-18 第二次报「移动端 agent 宽窄不同」）。
   上一轮我把左右 padding 从 6px 去掉，以为"只剩边框那 2px、肉眼对不出来"——
   **那句判断是错的，他照样看得出来。** 实测：infra 行 372px，外面的行 374px。

   矛盾是结构性的，不是没调好：**有边框的容器，它的子元素不可能和容器外沿一样宽。**
   border 占布局 ⇒ 子元素必然内缩一个边框宽；要等宽就不能有左右边框。二选一。
   ⇒ 选等宽。分组改由「背景色 + 上下两条内嵌线」表达：展开态本来就有
     background:var(--surface-1)，跟侧栏底色有对比，边界一样看得见。
   ⚠️ 折叠态那圈**虚线保留** —— 它没有 .agent 子元素，不存在这个问题，
     而那圈虚线正是"这是一个可展开的坑位"的语言。 */
.infra-box.open {
  /* ⚠️⚠️ **2026-08-20：机主明确要「把保留 cc 那一块背景颜色加深」，
     覆盖下面 08-18 那条「展开态不要底色」。** 那条判断没错，但他知道代价、仍然要。

     ⚠️ 我用了三个办法把当年那个副作用压到最小，改之前先读懂再动：
       ① **用相对色 `color-mix(… var(--label) …)`，不用固定色** ——
          浅色主题下它变深，深色主题下 `--label` 是亮的、它自动变成"提亮"。
          写死一个深色的话，深色主题那边等于没有分界（本来就黑）。
       ② **幅度压到 4%** —— 当年的问题是 irradiation illusion（亮的物体看起来更大），
          它随**对比度**增强而放大。淡一点 ⇒ 分界看得见，而卡片与底的对比度几乎没变。
       ③ **左右不留内边距**（见下面那段）—— 保证卡片外沿仍与组外完全对齐。

     ⚠️⚠️ 如果他又报「保留 cc 那几行看着宽窄不同」，**第一个该怀疑的就是这里** ——
        当年那四轮的根因就是它。届时的选择是：调淡、或者退回 transparent。 */
  /* ⚠️ 底色**不在这里重复**：`.infra-box` 基础规则里已经有了，`.open` 继承它。
     （同一件事写两处的代价不是多写一遍，是改的时候会漏一处 ——
       就在几分钟前，两个测试各断言了一遍同一条不变式，我改了一个另一个当场红。）
     —— 以下是 08-18 那条的原文，留作那次的现场记录 ——
     ⚠️⚠️ **展开态不要底色**（机主 2026-08-18 第四次报「宽窄不同」之后定的）。
     他那张截图我逐像素量过 9 行：全部 23→1090，**几何上一模一样**。
     差别来自【图底关系反了】：
       盒子里  灰片 on 白盒        盒子外  白片 on 灰底
     亮的物体看起来更大（irradiation illusion），所以灰片总显得窄一圈。
     ⇒ 这类差别调不出来 —— 只要反转还在，量得再准他也会看出来。
       唯一的解法是让两者【完全一样】：盒子不铺白底，行还用外面那套玻璃。
     分组仍然读得出：顶上那颗按钮 + 一条下缘线。 */
  border: 0;                 /* ⚠️ 这条不动：border 占布局，会让行比外面窄 2px */
  /* ⚠️⚠️ **下缘线 2026-08-20 删掉**（机主：「我看还在，就是那个点击"展开保留 cc"
     会一起展开的框」）。
     我上一轮删的是**折叠态**那圈虚线 —— 删错了，他指的一直是展开之后
     把按钮和内容**一起包住**的那两条横线（容器下缘 + 按钮下边框）。
     两条横线 + 底色，读起来就是一个框。
     ⇒ **底色接管分界的职责**，横线全去掉。底色本身就能表达"这一块到哪儿为止"，
       不需要再画边。 */
  padding-bottom: 6px;
}
/* 折叠态：按钮自己就是整个盒子，别再画第二层框 */
.infra-box .fold--infra { border: 0; width: 100%; margin: 0; background: transparent; }
.infra-box.open .fold--infra {
  /* ⚠️ 这里原来有 `border-bottom: 1px solid` —— 和容器下缘那条一起构成了
     机主要删的那个"框"。见 `.infra-box.open` 里那段。 */
  border-radius: 0;
  margin-bottom: 6px; opacity: 1;
  /* 组标题加重：它是这一组唯一的"名字"，之前和普通折叠按钮长得一样。 */
  font-weight: 600; letter-spacing: .01em; color: var(--label-2);
}
/* ⚠️ **左右不留内边距**：留了的话 infra 行会比列表里的普通行窄
   —— 实测 `.infra-box` 边框 1px×2 + 这里 6px×2 = **一共窄 14px**，
   桌面和移动端都一样（机主 2026-08-17 在手机上看出来的：手机行宽 414px
   又贴着屏幕边，有一条很强的参照线，所以一眼就看出来；桌面列只有 223px
   宽、周围没有那条线，同样的 14px 就不显眼 —— 但它一直都在）。
   ⚠️ **本句 2026-08-18 作废**：原文写「去掉之后只剩边框那 2px，肉眼对不出来」——
   机主当天又报了一次，**他对得出来**。剩下那 2px 由展开态去掉左右 border 解决
   （见 .infra-box.open）。⇒ 别再用"肉眼对不出来"给残留误差结案，
   那是拿自己的感知替使用者的感知做判断。上下的 gap 保留。 */
.infra-kids { display: flex; flex-direction: column; gap: 5px; padding: 0; }
/* ⚠️ **不再给盒子里的卡片单独一套外观**（机主 2026-08-18）。
   原来是「压一档」：灰底 + 只留一圈细边、去掉投影，理由是"已经被框住了，
   再各自浮起就是两层容器打架"。那个理由在盒子有白底时成立；现在盒子没底色了，
   不存在两层容器，而**长得不一样正是他四次说"宽窄不同"的来源**（图底反转）。
   ⇒ 让它们和外面的行用同一套 `.agent`，一个字都不改。
   ⚠️ hover 仍然不抬（它在一个分组里，整组跟着动会很吵）。 */
.infra-kids .agent:hover { transform: none; }
/* ⚠️ 「正在生成」这一档在 infra 盒子里的样子**统一写在文件末尾**那处
   `.infra-kids .agent.gen`（2026-08-17 修 hover 下沉时建的）——
   同一个选择器只能有一处，写两处的话靠后的整块赢，前面那处形同虚设。
   我 2026-08-18 就在这里加过一份重复的，被末尾那处盖掉、且自带一个会把
   下沉 bug 放回去的 `hover{transform:none}`。⇒ 已删，改到末尾那处一起写。 */

/* ── 会话目标（`/goal`，机主 2026-08-18）───────────────────────
   ⚠️ 只在真的设了目标时才出现（见 app.js 的 `目标行`）—— 没设 / 已撤销都不占行。
   ⚠️ 用左侧色条而不是整块底色：它挂在 detail-head 里，整块底色会把标题那一行
      压下去、看着像另一个面板。 */
.col--right .goal {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  margin-top: 7px; padding: 6px 10px 6px 9px;
  border-radius: var(--radius-sm);
  border-left: 2.5px solid var(--blue);
  background: color-mix(in srgb, var(--blue) 8%, transparent);
  font-size: 12.5px; line-height: 1.45;
}
.col--right .goal.done { border-left-color: var(--green);
  background: color-mix(in srgb, var(--green) 8%, transparent); }
.col--right .goal-tag {
  flex: 0 0 auto; font-size: 11px; font-weight: 590; color: var(--blue);
  letter-spacing: .02em;
}
.col--right .goal.done .goal-tag { color: var(--green); }
.col--right .goal-txt { color: var(--label); min-width: 0; }
.col--right .goal-meta { color: var(--label-3); font-size: 11.5px;
  font-variant-numeric: tabular-nums; }

/* 切 cc 时的加载占位。用很淡的呼吸而不是转圈：它出现的时间通常不到 100ms，
   转圈会在这么短的时间里闪一下，比没有还吵。 */
.placeholder.loading { opacity: .55; animation: ph-breathe 1.4s ease-in-out infinite; }
@keyframes ph-breathe { 0%,100% { opacity: .35; } 50% { opacity: .7; } }
@media (prefers-reduced-motion: reduce) { .placeholder.loading { animation: none; } }

/* ⚠️⚠️ 上面那条与 `.agent.gen:hover` **特异性相同 (0,3,0)**，而它在文件里更靠后
   ⇒ **它赢**。于是正在生成的 infra 行一 hover 就从 -4px 掉回 0 —— 看起来是
   「往下沉」，而列表里普通的生成行是往上抬。机主 2026-08-17 在界面上抓到。
   实测：infra 生成行 静止 matrix(1.026,0,0,1.026,0,-4) → hover `none`；
        普通生成行 -4 → -6。

   ⚠️ **上面那条不能删**：它管的是「非生成的 infra 行不要浮」——盒子里的卡片
      再各自浮起就是两层容器打架，那个判断仍然成立。这里只把「生成中」单独接住。
   ⚠️ 影子也要一起给：`.infra-kids .agent` 的压平影 (0,2,0) 同样压过 `.agent.gen`
      (0,2,0)（它更靠后），不补这条的话会出现「抬起来了但没有影子」。 */
/* ⚠️ 染蓝也写在这里（机主 2026-08-18）：规则是按【在不在生成】分，
   不是按【在不在 infra 盒子里】分 —— 两处不一致的话，同一个状态两种样子。
   ⚠️ **没有 scale**：它就是在改宽度，见 `.agent.gen` 上方那段。 */
.infra-kids .agent.gen {
  background: var(--glass-sheen), var(--glass-tint-gen), var(--glass-film);
  box-shadow: var(--glass-bevel), var(--glass-amb-gen);   /* 细边同样不给，理由见 .agent.gen */
}
.infra-kids .agent.gen:hover {
  transform: translateY(-9px);
  box-shadow: var(--glass-bevel), var(--glass-amb-gen);
}
/* 触屏没有真 hover：tap 之后那个假 hover 会让它一直浮在高处 */
@media (max-width: 860px) {
  .infra-kids .agent.gen,
  .infra-kids .agent.gen:hover { transform: translateY(-6px); }
}

/* ————————————————————— 附件（机主 2026-08-07） —————————————————————
   落点 B：文件存平台自己的 data/attachments/，不写进 cc 的工作目录。
   ⚠️ 这一块**放在文件末尾是实现的一部分** —— 中间插入的规则会输给后面
   同等或更高特异性的定义（本仓库已经栽过两次）。 */

.composer .icon-btn {
  flex: 0 0 auto;
  margin-bottom: calc((var(--ta-h) - var(--btn)) / 2);   /* 和 #send 同一条基线 */
  width: var(--btn); height: var(--btn); padding: 0; border-radius: 50%;
  background: transparent; border: 0; color: var(--label);
  font-size: 15px; line-height: 1;
  display: grid; place-items: center;
  transition: transform 140ms var(--ease), background 140ms var(--ease);
}
.composer .icon-btn:hover  { background: var(--bg-2); transform: scale(1.07); }
.composer .icon-btn:active { transform: scale(0.94); }
  /* 尺寸走 `--btn`（见上），这里只调字号 —— 图标要跟着盒子一起大。
     机主 2026-08-16：发送键和 📎 必须成对，主次靠颜色不靠尺寸。 */
  .composer #send, .composer .icon-btn { font-size: 17px; }

/* 拖进来时给整个输入区一圈提示。⚠️ 用 inset 阴影不用 border：
   border 会改变盒子尺寸，让下面的 textarea 跳一下。 */
.composer.drop { box-shadow: inset 0 0 0 2px var(--blue); border-radius: 12px; }

.attach { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 2px 6px; }
.attach.busy { opacity: 0.55; }

.attach .chip {
  display: inline-flex; align-items: center; gap: 6px;
  max-width: 100%; padding: 3px 4px 3px 9px;
  background: var(--bg-2); border-radius: 999px;
  box-shadow: 0 0 0 1px var(--hairline);
  font-size: 12px; line-height: 18px;
}
/* ⚠️ 省略号要作用在这个 span 上，不能放在 .chip 上 ——
   flex 容器上的 text-overflow 不生效（本仓库踩过）。 */
.attach .chip-name {
  max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.attach .chip-size { color: var(--label); font-variant-numeric: tabular-nums; }
.attach .chip-x {
  flex: 0 0 auto; width: 18px; height: 18px; padding: 0; border: 0; border-radius: 50%;
  background: transparent; color: var(--label); font-size: 14px; line-height: 1;
  display: grid; place-items: center;
}
.attach .chip-x:hover { background: var(--hairline-2); color: var(--label); }

/* 手机：回形针和发送键一样要够大按（机主提过停止键太大，所以别再放大，
   只保证不小于 30px 的可点区域，跟 #send 保持一致）。 */
@media (max-width: 860px) {
  .attach .chip-name { max-width: 46vw; }
}

/* 拖文件经过时给整个聊天栏一圈提示（机主 2026-08-07：整页都能接，不只输入区）。
   ⚠️ 用 outline 不用 border/box-shadow 撑边：outline 不参与布局，
   不会让下面的消息流跳一下。 */
body.dropping .col--main {
  outline: 2px dashed var(--blue); outline-offset: -6px; border-radius: var(--radius);
}

/* 上传中 / 上传失败的附件（机主 2026-08-07：失败一定要看得见，且能重传）。
   ⚠️ 失败态不是"变个颜色"就够 —— 必须同时给出**原因**和**重试按钮**，
   否则机主只知道坏了、不知道为什么，也没法自己恢复。 */
/* 上传中的 chip **整个就是进度条**：从左往右填色（机主 2026-08-10）。
   ⚠️ 用 background-image 的硬停渐变，**不用 ::before 盖一层** ——
      chip 是 flex 容器、里面还有个 × 按钮，多叠一层就要处理 z-index 和
      pointer-events，而渐变一行搞定且绝不会挡住点击。
   ⚠️ 不再整体 opacity:0.6 —— 那是"这条还没好"的表达，现在填色已经在说这件事，
      再压透明度会让文字在填色上更难读。
   ⚠️ `--p` 由 JS 直接写在这个元素的 style 上（见 setChipPct），
      每次进度事件只改这一个变量，不重绘 DOM。 */
.attach .chip-up {
  background-image: linear-gradient(to right,
    color-mix(in srgb, var(--blue) 26%, transparent) 0 var(--p, 0%),
    transparent var(--p, 0%));
  /* 120ms 线性：进度事件本身是断续的，不补间会一格一格地跳 */
  transition: background-image 120ms linear;
}
.attach .chip-up .chip-size { color: var(--blue); font-weight: 560; }
@media (prefers-reduced-motion: reduce) { .attach .chip-up { transition: none; } }
.attach .chip-err { box-shadow: 0 0 0 1px var(--red); }
.attach .chip-err .chip-name { color: var(--red); }
.attach .chip-why { color: var(--red); font-size: 11px; }
.attach .chip-retry {
  flex: 0 0 auto; padding: 0 8px; height: 18px; border: 0; border-radius: 999px;
  background: var(--red); color: #fff; font-size: 11px; line-height: 18px;
}
.attach .chip-retry:hover { filter: brightness(1.1); }

/* ————————————————————— 监控页（机主 2026-08-07） —————————————————————
   按模块组织：一个集群一个 .mon-mod。现在只有「本集群」。 */

.monitor { flex: 1; min-height: 0; overflow: auto; padding: 16px 20px 28px; }
.mon-head { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
.mon-title { font-size: 15px; font-weight: 590; }
.mon-range { display: flex; gap: 2px; background: var(--surface-2); border-radius: 999px; padding: 2px; }
.mon-range button {
  border: 0; background: transparent; color: var(--label);
  font-size: 12px; padding: 4px 11px; border-radius: 999px;
}
.mon-range button.active { background: var(--surface-1); color: var(--label); box-shadow: 0 0 0 1px var(--hairline); }

/* 每个模块自己一个盒子（机主 2026-08-07：「需要分块，不要挤在一起，
   就像你当初把 Infra CC 和其他 CC 分块一样」）。
   ⚠️ 用的是和 .infra-box 同一套语言：实线细框 + surface-1 底 + 头部一条分隔线，
   里面的卡片压一档 —— 它们已经被框住了，再各自"浮起"就是两层容器打架。 */
.mon-mod {
  margin-bottom: 18px; padding: 0 0 12px;
  background: var(--surface-1);
  border: 1px solid var(--hairline); border-radius: var(--radius-lg);
}
.mon-mod-head {
  display: flex; align-items: baseline; gap: 10px;
  padding: 11px 14px; margin-bottom: 12px;
  border-bottom: 1px solid var(--hairline);
}
.mon-mod .mon-grid-cards { padding: 0 12px; }
/* 盒子里的卡片压一档：去掉自己的底色和外框，只留一条极淡的分隔 */
.mon-mod .mon-card {
  background: var(--surface-2);
  box-shadow: 0 0 0 1px var(--hairline);
}
.mon-mod-name { font-size: 13px; font-weight: 590; }
.mon-mod-sub { font-size: 11.5px; color: var(--label-2); }

/* 一行四列（机主 2026-08-07：「两行两列太占地方，改成一行四列，
   同时不要压缩太多信息」）。窄屏逐级降到两列 / 一列。

   ⚠️ 必须写 `minmax(0, 1fr)` 而不是 `1fr`：`1fr` 等价于 `minmax(auto, 1fr)`，
      auto 下限是内容宽度 —— 卡片里有不换行的读数和 SVG，列压不下去就会整体溢出。
   ⚠️ 这里从 auto-fit 改成固定列数：auto-fit 的列数由 minmax 下限反推，
      想要"恰好四列"就得反算一个下限，屏幕一变又不对。要几列就写几列。 */
.mon-grid-cards { display: grid; gap: 10px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 1279px) { .mon-grid-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.mon-card {
  background: var(--surface-1); border-radius: var(--radius);
  box-shadow: 0 0 0 1px var(--hairline); padding: 12px 14px 8px;
}
.mon-card-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; margin-bottom: 6px; }
/* ⚠️ 四列下卡片只有 ~330px，读数放不下时**换行**而不是被挤掉 ——
   机主的原话是"不要压缩太多信息"。 */
.mon-card-head .spacer { flex: 1 1 0; min-width: 0; }
.mon-card-title { font-size: 12.5px; font-weight: 590; }
.mon-legend { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; color: var(--label); }
.mon-legend i { width: 7px; height: 7px; border-radius: 2px; display: inline-block; }
.mon-legend b { color: var(--label); font-weight: 590; font-variant-numeric: tabular-nums; }

/* ⚠️ preserveAspectRatio="none" 会把线宽也横向拉伸 —— 用
   vector-effect 让线宽不随缩放变化，否则宽屏上线会变成一条粗带。 */
/* ⚠️ 高度 156→118：备注收进提示符后腾出的空间，机主要求图表更扁。
   viewBox 仍是 156 —— 它是拉伸的，两者不必相等；
   HTML 层的标签和标记点都按**百分比**定位，跟着一起变，不用改。 */
.mon-svg { width: 100%; height: 118px; display: block; overflow: visible; }
.mon-line { fill: none; stroke-width: 1.5; vector-effect: non-scaling-stroke;
            stroke-linejoin: round; stroke-linecap: round; }
.mon-grid { stroke: var(--hairline); stroke-width: 1; vector-effect: non-scaling-stroke; }
.mon-ytick { fill: var(--label-3); font-size: 9px; text-anchor: end; }
.mon-xtick { fill: var(--label-3); font-size: 9px; }
.mon-xend { text-anchor: end; }
.mon-note { font-size: 10.5px; color: var(--label-3); padding: 2px 0 4px; }
.mon-empty { color: var(--label-2); font-size: 12px; padding: 24px 0; text-align: center; }

@media (max-width: 860px) {
  .monitor { padding: 12px 12px 20px; }
  .mon-grid-cards { grid-template-columns: 1fr; }
  .mon-range button { padding: 4px 9px; }
}
.mon-ok  { font-size: 11px; color: var(--label-3); }
.mon-bad { font-size: 11px; color: var(--red); }

/* ⚠️ 模块头在窄屏会把标题挤断（实测「本集群」被拆成「本集 / 群」）——
   标题不许换行，让副标题自己折到下一行。 */
.mon-mod-head { flex-wrap: wrap; }
.mon-mod-name { flex: 0 0 auto; white-space: nowrap; }
@media (max-width: 860px) {
  .mon-mod-sub { flex: 1 1 100%; }
}

/* 悬停读数（机主 2026-08-07）。准星 + **标记点** + 一个跟着走的小浮层。
   ⚠️ 准星和标记点都是**绝对定位的 HTML 元素**，不画在 SVG 里 ——
   那个 SVG 是 preserveAspectRatio="none"，viewBox 被非等比拉伸，
   画在里面的 <circle> 会被压成细长椭圆，窄屏上根本认不出是个点。 */
.mon-plot { position: relative; }

.mon-cursor {
  position: absolute; width: 1px; margin-left: -0.5px;
  background: var(--label-2); opacity: .75; pointer-events: none; z-index: 1;
}
/* 标记点：小实心点 + 一圈同色光晕（机主 2026-08-07：「点太大了…加一个光圈或者光晕」）。
   ⚠️ 中间那层 surface-2 的细描边**别去掉** —— 它把点和它压着的曲线隔开，
      没有它的话点和线糊成一团，光晕反而让边界更模糊。
   ⚠️ 尺寸写在这一个地方：JS 里 margin 的负值靠 CSS 自己算（-50%），
      改大小时不用两头对齐。 */
.mon-marker {
  position: absolute; width: 6px; height: 6px;
  transform: translate(-50%, -50%);
  border-radius: 50%; background: var(--c);
  box-shadow:
    0 0 0 1.5px var(--card-solid),                                  /* 和曲线隔开：必须实色 */
    0 0 0 3px color-mix(in srgb, var(--c) 32%, transparent),        /* 光圈 */
    0 0 9px 3px color-mix(in srgb, var(--c) 45%, transparent);      /* 光晕 */
  pointer-events: none; z-index: 2;
}
.mon-tip {
  position: absolute; top: 4px; transform: translateX(8px);
  background: var(--pop); border-radius: var(--radius-sm);
  box-shadow: 0 0 0 1px var(--hairline), 0 6px 20px rgba(0,0,0,.35);
  padding: 6px 9px; font-size: 11px; line-height: 1.6;
  pointer-events: none;          /* ⚠️ 不加的话浮层会挡住自己的 pointermove，读数抖 */
  white-space: nowrap; z-index: 3;
}
.mon-tip.flip { transform: translateX(calc(-100% - 8px)); }
.mon-tip-t { color: var(--label-2); margin-bottom: 2px; font-variant-numeric: tabular-nums; }
.mon-tip-r { display: flex; align-items: center; gap: 5px; color: var(--label); }
.mon-tip-r i { width: 7px; height: 7px; border-radius: 2px; flex: 0 0 auto; }
.mon-tip-r b { color: var(--label); font-weight: 590; font-variant-numeric: tabular-nums;
               margin-left: auto; padding-left: 10px; }

/* ————— 监控页移动端（机主 2026-08-07） ————— */
@media (max-width: 860px) {
  /* 区间选择器跟着滚：整页 2000+px，选完档还要摸回顶部才能换，太别扭 */
  .mon-head {
    position: sticky; top: 0; z-index: 3; margin: -12px -12px 10px; padding: 10px 12px;
    background: var(--canvas); border-bottom: 1px solid var(--hairline);
  }
  .mon-mod-head { padding: 9px 11px; margin-bottom: 10px; }
  .mon-mod-sub { line-height: 1.5; }
  .mon-mod .mon-grid-cards { padding: 0 9px; }
  .mon-card { padding: 10px 11px 6px; }
  /* 9px 的刻度在手机上几乎读不出来 */
  .mon-ytick, .mon-xtick { font-size: 10px; }
  /* 浮层在窄屏容易被裁：字大一点、允许折行 */
  .mon-tip { font-size: 12px; max-width: 62vw; white-space: normal; }
}

/* 横轴刻度处的竖向辅助线。⚠️ 要比横向那几条更淡：它只是帮着对时间，
   画重了会跟数据曲线抢注意力。 */
.mon-vgrid { stroke: var(--hairline); stroke-width: 1; opacity: .5;
             vector-effect: non-scaling-stroke; }

/* 每个模块的采集开关（机主 2026-08-07：集群崩的时候别再往上加压）。 */
.mon-toggle {
  flex: 0 0 auto; margin-left: 10px; padding: 3px 11px;
  border: 0; border-radius: 999px; font-size: 11.5px;
  background: var(--surface-2); color: var(--label-2);
  box-shadow: 0 0 0 1px var(--hairline);
}
.mon-toggle:hover { color: var(--label); background: var(--hairline); }
.mon-toggle.on { background: var(--blue); color: #fff; box-shadow: none; }
.mon-toggle:disabled { opacity: .5; }
.mon-off { font-size: 11px; color: var(--orange); }
/* 暂停时整块压暗：一眼看出这块的数据是**停在过去**的，不是"负载很平" */
.mon-mod.paused .mon-grid-cards { opacity: .45; }

/* 坐标轴文字（机主 2026-08-07 改一行四列后）。
   ⚠️ 放在 HTML 层而不是 SVG 里：SVG 是非等比拉伸的，写在里面的 <text> 会被
   **横向压扁** —— 四列下卡片只有 336px，缩放 0.525 倍，"11:00" 挤成一团。
   位置用百分比给（x/W、y/H），与实际像素宽高无关，不需要 JS。 */
.mon-ylab, .mon-xlab {
  position: absolute; color: var(--label-3); font-size: 10px; line-height: 1;
  pointer-events: none; white-space: nowrap; font-variant-numeric: tabular-nums;
  /* ⚠️ 标签就画在图里，曲线会从它背后穿过（0.0% 那条尤其常撞）——
     用同底色的描边把字"抠"出来，比给它加个不透明底块干净。 */
  /* ⚠️ 原来用的是 --surface-2（半透明白）——在深色底上那是一层**微光**，
     根本挡不住穿过去的曲线。必须用不透明的卡片实色才真的把字抠出来。 */
  text-shadow: 0 0 3px var(--card-solid), 0 0 3px var(--card-solid),
               0 0 2px var(--card-solid), 0 0 1px var(--card-solid);
}
/* 压在网格线**上方**，不占横向空间 */
.mon-ylab { transform: translateY(-100%); padding-bottom: 2px; }
.mon-xlab { bottom: 1px; transform: translateX(-50%); }
.mon-xlab--start { transform: none; }
.mon-xlab--end   { transform: translateX(-100%); }

/* 标题右边的小提示符：备注收在这里，悬停 / 点按展开（机主 2026-08-07）。 */
.mon-hint {
  flex: 0 0 auto; position: relative;
  width: 14px; height: 14px; border-radius: 50%;
  background: var(--hairline); color: var(--label-2);
  font-size: 9.5px; font-style: italic; font-weight: 590;
  display: inline-grid; place-items: center;
  cursor: default;      /* ⚠️ 不用 help：机主明确说别变成问号 */
  align-self: center;
}
/* ⚠️ 悬停色**不要用 --surface-1**：它是 rgba(255,255,255,0.045)，
   拿来当文字色的话那个 i 会直接消失 —— 和 --ink 是同一类错：
   把「面」的颜色当「字」的颜色用。今天第二次了。 */
.mon-hint:hover, .mon-hint:focus { background: var(--hairline-2); color: var(--label); outline: none; }
/* ⚠️ 往**上**开（机主 2026-08-07 指定）。原来往下开会盖住图表本身 ——
   而人正是为了看懂这张图才去点它的。 */
.mon-hint-pop {
  position: absolute; bottom: calc(100% + 6px); left: -6px; z-index: 5;
  width: max-content; max-width: 260px;
  background: var(--pop); color: var(--label);
  border-radius: var(--radius-sm); padding: 7px 9px;
  box-shadow: 0 0 0 1px var(--hairline), 0 8px 24px rgba(0,0,0,.4);
  font-size: 11px; line-height: 1.55; font-style: normal; font-weight: 400;
  white-space: normal; text-align: left;
  opacity: 0; visibility: hidden; transform: translateY(3px);
  transition: opacity 120ms var(--ease), transform 120ms var(--ease), visibility 120ms;
}
.mon-hint:hover .mon-hint-pop, .mon-hint:focus .mon-hint-pop {
  opacity: 1; visibility: visible; transform: none;
}
/* 靠右两列的提示框往左开，别被模块边缘裁掉 */
.mon-grid-cards > .mon-card:nth-child(n+3) .mon-hint-pop { left: auto; right: -6px; }
@media (max-width: 1279px) {
  .mon-grid-cards > .mon-card:nth-child(even) .mon-hint-pop { left: auto; right: -6px; }
  .mon-grid-cards > .mon-card:nth-child(odd) .mon-hint-pop { left: -6px; right: auto; }
}
@media (max-width: 860px) {
  .mon-svg { height: 132px; }        /* 手机单列更宽，稍高一点才不至于太扁 */
  .mon-grid-cards > .mon-card .mon-hint-pop { left: -6px; right: auto; max-width: 70vw; }
}

/* 上下文交接（机主 2026-08-07）。放在详情头的副标题里，跟停止键做邻居。 */
/* 交接那两个按钮。⚠️ 机主 2026-08-07：「需要是一个按钮，而不是一个下划线，
   像超链接一样。」原来做成了带下划线的蓝字 —— 那是"链接"的视觉语言，
   而它做的是**一个不可逆的动作**，两者不该长得一样。
   现在对齐平台已有的小药丸按钮（和监控页那个启停键同一套）。 */
.linkbtn {
  padding: 3px 11px; border: 0; border-radius: 999px;
  font-size: 12px; line-height: 1.5;
  background: var(--surface-2); color: var(--label-2);
  box-shadow: 0 0 0 1px var(--hairline);
  transition: background 140ms var(--ease), color 140ms var(--ease);
}
.linkbtn:hover { background: var(--hairline); color: var(--label); }
.linkbtn:active { transform: scale(0.97); }
/* 交接进行中的「中止」是个纠正性动作，给它一点橙，和旁边"交接中"的提示同色系 */
#ho-cancel { color: var(--orange); box-shadow: 0 0 0 1px color-mix(in srgb, var(--orange) 40%, transparent); }
#ho-cancel:hover { background: color-mix(in srgb, var(--orange) 16%, transparent); color: var(--orange); }
/* 交接中给一个明确的在途标记 —— 这期间消息是被扣住的，必须让人看见 */
.ho-on { color: var(--orange); }

/* ————————————————————— 机主气泡里的对比度（机主 2026-08-07） —————————————————————
   气泡是**蓝底**，而 `.md` 那套样式是照"深色底 + 浅色字"设计的 ——
   放进蓝底就全都不成立了。实测（对比度，AA 要 ≥4.5）：

       正文白字 3.65 ·  链接 1.00（蓝压蓝，完全隐形） ·  截断提示 1.64 ·  行内 code 1.27

   ⚠️ 判据：**凡是靠"灰"来表达次要的，在蓝底上都要换成"半透明白"**。
   灰是相对深底调的，换个底就失效；半透明白跟着底走，永远成立。 */

.msg.down .bubble .md a { color: #fff; text-decoration: underline; text-underline-offset: 2px; }
.msg.down .bubble .md a:hover { opacity: .85; }
.msg.down .bubble .md-cut,
.msg.down .bubble .md blockquote { color: rgba(255, 255, 255, .78); }
.msg.down .bubble .md blockquote { border-left-color: rgba(255, 255, 255, .38); }
.msg.down .bubble .md code,
.msg.down .bubble .md pre {
  background: rgba(255, 255, 255, .18); color: #fff;
  border-color: rgba(255, 255, 255, .22);
}
.msg.down .bubble .md hr,
.msg.down .bubble .md .md-t th,
.msg.down .bubble .md .md-t td { border-color: rgba(255, 255, 255, .28); }
.msg.down .bubble .md .md-t th { background: rgba(255, 255, 255, .14); color: #fff; }

/* ⚠️⚠️ 选中态：全局 ::selection 是**蓝色半透明**，压在蓝气泡上等于隐形
   （机主原话："光标选中一段文字更是完全看不见"）。
   这里反过来：白底 + 蓝字 —— 在蓝气泡上是最高对比的一组。 */
.msg.down .bubble ::selection,
.msg.down .bubble::selection { background: #fff; color: var(--bubble); }

/* 交接按钮跟在停止键右边（机主 2026-08-07）。标题行是 baseline 对齐的，
   给它一点左边距别贴着那个圆钮。 */
.detail-head .title .ho-btn { flex: 0 0 auto; }
/* 「交接中…」这段文字**要比按钮先被挤掉**：它是状态、可以省，
   而按钮被挤掉就等于功能没了。 */
.detail-head .title .ho-on {
  font-size: 12px; flex: 0 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ⚠️ tmux 的 pane 编号长得像百分数（`%22` vs `22%`）——机主 2026-08-07 连着问了两次
   "标题右边那个 22% 是什么"。加上 `pane ` 前缀消歧，别让人把它当成某种占用率。 */
.detail-head .sub .pane { font-variant-numeric: tabular-nums; }

/* 上下文用量（机主 2026-08-07 用它换掉了原来那个 tmux pane 编号）。
   ⚠️ 阈值跟监控页同一套（70 黄 / 90 红）—— 这个数直接决定该不该点旁边那个
   「交接并清空」，两者放一起才有意义。 */
.detail-head .sub .ctx { font-variant-numeric: tabular-nums; }
/* ⚠️ 只有红这一档（机主 2026-08-07）。原来还有个 70% 的黄，删掉了 ——
   上下文用量常年在 60–90 正常游走，标黄会天天亮着，等于没有警告。 */
.detail-head .sub .ctx.hot { color: var(--red); font-weight: 590; }

/* 名字撞上归档记录（机主 2026-08-07 撞到的那次，是我自己造成的）。
   ⚠️ 用橙不用红：它**不是故障**，是"两件事重名了、需要你选一下怎么办"。 */
.detail-head .sub .conflict { color: var(--orange); }

/* ————————————————————— 集群告警弹窗（机主 2026-08-09）—————————————————————
   ⚠️ 这一版**只做出来、不激活**。`.al-off` 那条横幅是"配了也不会响"的唯一告知处，
      别当成装饰删掉 —— 一个配好了却不会响的告警，比没有配更危险。 */
.modal--wide { max-width: 640px; }
#alert-banner { margin: 0 0 14px; }
.al-off, .al-on {
  font-size: 11.5px; line-height: 1.7; padding: 9px 11px; border-radius: 9px;
}
.al-off { background: color-mix(in srgb, var(--orange) 12%, var(--surface-2));
          border: 1px solid color-mix(in srgb, var(--orange) 32%, transparent);
          color: var(--label-2); }
.al-off b { color: var(--orange); }
.al-on  { background: color-mix(in srgb, var(--red) 10%, var(--surface-2));
          border: 1px solid color-mix(in srgb, var(--red) 32%, transparent);
          color: var(--label-2); }
.al-on b { color: var(--red); }

.al-empty { font-size: 12px; color: var(--label-3); padding: 10px 2px; }
/* 一条规则一行。⚠️ 用 flex-wrap 而不是固定网格：中文标签 + 数字输入框在窄屏
   （手机上弹窗是全屏）必然换行，固定列宽会把最后那个收件人框挤出屏幕。 */
.al-rule {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  padding: 9px 10px; border-radius: 9px; background: var(--surface-2);
  border: 1px solid var(--hairline); font-size: 12.5px;
}
.al-rule + .al-rule { margin-top: 6px; }
.al-rule select, .al-rule input {
  font: inherit; font-size: 12.5px; padding: 4px 7px; border-radius: 7px;
  border: 1px solid var(--hairline-2); background: var(--surface-1); color: var(--label);
}
.al-rule input[type="number"] { width: 62px; }
.al-rule input[type="text"] { flex: 1 1 130px; min-width: 110px; }
.al-lbl, .al-unit { color: var(--label-3); font-size: 11.5px; }
.al-rule i {
  flex: 1 0 100%; font-style: normal; font-size: 11px; color: var(--label-3);
}
.al-rule i.al-hit { color: var(--orange); font-weight: 560; }
/* 「证据不足」= 这条规则此刻**没在守**，和「守着但没超」是两回事，不能长一样。
   ⚠️ 用红不用橙：橙是 `al-hit`（条件满足），两者混色会让人以为它在告警。 */
.al-rule i.al-thin { color: var(--red); }
.al-del {
  margin-left: auto; border: 0; background: none; color: var(--label-3);
  font-size: 16px; line-height: 1; padding: 2px 6px; border-radius: 6px; cursor: pointer;
}
.al-del:hover { background: var(--red-soft, rgba(255,69,58,.14)); color: var(--red); }
#alert-add { margin-top: 10px; }
.modal .acts .primary { background: var(--blue); border-color: var(--blue); color: #fff; }
.modal .acts .primary:hover { filter: brightness(1.08); }
/* 告警入口按钮：借用 .mon-toggle 的形状，但不是开关，别染成开关的强调色 */
.mon-toggle.mon-alert { color: var(--label-2); }
/* 「共享」档（机主 2026-08-25）：它是**可见性**不是采集开关 ——
   染成绿色，别和「停止/启动」那个蓝色开关混在一起。 */
.mon-toggle.mon-share.on { background: var(--green); color: #fff; box-shadow: none; }

/* ————— 进度栏的「交付物」（机主 2026-08-09）—————
   ⚠️ 钉在最上面是这个功能的**全部意义**：机主的原话是"被对话淹没之后我能直接
      在进度里找出来"。往下挪一格就等于回到了要翻的状态。 */
.deliv {
  margin: 7px 0 3px; padding: 7px 9px; border-radius: 10px;
  background: color-mix(in srgb, var(--blue) 7%, var(--bg-2));
  border: 1px solid color-mix(in srgb, var(--blue) 24%, transparent);
}
/* ⚠️ 默认显示 5 条、超出滚动（机主 2026-08-18）。
   ⚠️ **滚的是列表不是整块 `.deliv`** —— 标题「交付物」必须一直可见，
      否则滚下去之后不知道自己在看什么。
   ⚠️ 用 `max-height` 不用 `height`：少于 5 条时不该留一块空白。
   ⚠️ 高度**按实测的行高算**（--deliv-row 是量出来的，不是拍的）：
      行是单行省略的 `.deliv-item`，高度稳定；写死 px 的话字号一改就露半行。 */
.deliv-list {
  /* ⚠️ 23.1 是**量出来的**（实测 .deliv-row 高 23.09px），不是拍的。
     我第一版拍了个 22，结果只露出 4.76 行 —— 差的那 0.24 行肉眼就是"第五条被切了一刀"。 */
  --deliv-row: 23.1px;
  /* ⚠️ 5 行**再加 7px**：切在整行边界上会让人以为"就这 5 条"。
     露出第 6 行的一个边，是"还有更多"最省地方的提示 ——
     比加一行"共 N 条"省地方，也比指望滚动条可靠（macOS 的悬浮滚动条平时不显示）。 */
  max-height: calc(var(--deliv-row) * 5 + 7px);
  overflow-y: auto;
  overscroll-behavior: contain;     /* 滚到底不要把外层进度栏一起带走 */
  scrollbar-width: thin;
}
.deliv-list::-webkit-scrollbar { width: 6px; }
.deliv-list::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--blue) 30%, transparent); border-radius: 3px;
}
.deliv-list::-webkit-scrollbar-track { background: transparent; }

.deliv-head { font-size: 11.5px; font-weight: 600; color: var(--blue); margin-bottom: 4px; }
/* ⚠️ 正文现在是**标签**不是原始地址（机主 2026-08-13）：
   ① 去掉 `font-family: mono` —— 那是给机器串准备的，一句人话用等宽只会变宽变丑；
   ② 去掉 `word-break: break-all`，改成单行省略 —— 标签本就短，
      偶尔长的那条用省略号 + tooltip，比折成三行占掉整块进度栏好；
   ③ `min-width:0` 是省略号能生效的前提（flex 子项默认不肯收缩到内容以下）。 */
.deliv-item {
  display: flex; align-items: center; gap: 5px;
  width: 100%; text-align: left; margin-top: 1px;
  font-size: 11.5px; line-height: 1.4;
  color: var(--label); text-decoration: none;
  background: none; border: 0; padding: 3px 4px; border-radius: 6px; cursor: pointer;
}
.deliv-item > span {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* 图标不参与收缩 —— 它被挤没了的话，"这是链接还是文件"这个信息就丢了，
   而那恰恰是加它的理由。 */
/* 图标压到次要层级：它是**分类标记**不是装饰，抢眼了会盖过标签本身。 */
.deliv-ico { flex: 0 0 auto; color: var(--label-3); }
.deliv-item:hover .deliv-ico { color: var(--label-2); }
.deliv-item:hover { background: var(--surface-1); }
/* ⚠️ **正文不用蓝色**（机主 2026-08-13：「背景就是蓝色的，还用蓝色的字体，不好看」）。
   蓝字原本是在说"这条能点" —— 但那件事现在由**图标**承担了（链接一个、文件一个），
   颜色是冗余的第二套编码，而它落在蓝色底上正好是最难读的那种组合。
   ⇒ 去掉之后蓝色只剩两处：块的淡底 + 标题。**一块里一个强调色用一次就够。**
   可点这件事仍然看得出来：hover 有底色变化，链接 hover 还加下划线。 */
a.deliv-item:hover { text-decoration: underline; }

/* 详情头上的图标按钮（「更多操作」那个三横线）。机主 2026-08-09 把文字换成了图标。
   ⚠️ 用 grid 居中 SVG，别靠 line-height 凑 —— 那在不同字号下会各差一点。 */
.iconbtn {
  display: inline-grid; place-items: center; flex: 0 0 auto;
  /* ⚠️ 尺寸要跟旁边的停止按钮（19×19 / radius 5）同一量级 —— 它俩并排站在
     标题行里，差一圈就会显得这个更"重要"，而它其实只是个次要入口。 */
  width: 20px; height: 20px; padding: 0;
  border: 0; border-radius: 6px;
  background: var(--surface-2); color: var(--label-2);
  box-shadow: 0 0 0 1px var(--hairline);
  cursor: pointer;
  transition: background 140ms var(--ease), color 140ms var(--ease),
              transform 140ms var(--ease);
}
.iconbtn:hover { background: var(--hairline); color: var(--label); }
.iconbtn:active { transform: scale(0.94); transition-duration: 90ms; }
.iconbtn:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .iconbtn { transition: none; } }

/* ⚠️ 手机端的覆盖必须写在基础规则**之后** —— `.iconbtn` 的基础规则在文件末尾，
   同特异性下后写的赢，写进上面那个 @media 块里会被它压掉（实测就是这样：
   390px 视口下量到的仍是 20px）。本文件在附件那一节也吃过同样的亏。
   「更多操作」是操作弹窗的**唯一**入口，点不中就等于交接/压缩/Infra 三个功能
   在手机上全都够不着 —— 指尖可靠命中区约 44px，这里按同文件的先例撑到 32。 */
@media (max-width: 860px) {
  /* ⚠️ 32 → 28（机主 2026-08-16：「都改的小一点点」）。
     下限的判据不是好看，是**指尖能不能可靠点中** —— 再往下就该给
     `padding` 撑命中区、而不是继续缩视觉尺寸了。 */
  .iconbtn { width: 28px; height: 28px; }
  .iconbtn svg { width: 14px; height: 14px; }
  /* ⚠️⚠️ **停止按钮必须跟着一起放大**（机主 2026-08-16：「暂停和它右边的
     操作按钮大小不一致」）。上面那条只放大了 `.iconbtn`，而停止是另一个类
     ⇒ 手机上一个 19、一个 32，并排站着一大一小。
     ⚠️ 上面那段注释自己写着「尺寸要跟旁边的停止按钮同一量级……差一圈就会显得
       这个更重要」—— **道理写对了，改的时候只改了一半。**
       同一行里的兄弟元素，尺寸要一起调；分在两个类里最容易漏。 */
  .detail-head .stop { width: 28px; height: 28px; border-radius: 7px; }
  .detail-head .stop::before { width: 9px; height: 9px; border-radius: 2px; }
}
/* 取不到最后提交时间的那种：淡一档。⚠️ 不能藏起来 ——「不知道」本身是要说的，
   否则读的人会默认"没显示 = 刚更新过"。 */
.pr-foot .pr-noage { color: var(--label-3); font-style: italic; }

/* 交付物那个提示符复用监控页的 `.mon-hint`（见 app.js 里的注释）。
   这里只调**定位的锚点**：监控页的浮层锚在提示符上、往一侧展开，而右栏只有 ~300px
   宽还 `overflow:auto` —— 往右开会顶出栏外、往左开会退到栏外（两边都实测过）。
   ⇒ 改成锚在**整个交付物块**上、左右都撑满：宽度就等于块宽，必然放得下，
     也不用去猜该往哪边开。 */
/* ⚠️⚠️ **两处共用这一套**（2026-08-14 起）：交付物块，以及**聊天流里每条消息的
   投递状态行**。后者是机主当天报的「某些 Agent 的聊天页面可以左右滚动」——
   同一个病、同一个解，所以**一处定义两边共用**，不各写一份（这仓库为
   「同一件事两处各判一次」付过账，见 `ROW_FLAGS`）。

   ⚠️ 聊天流那处的机理有两层，缺一层都想不到：气泡的包含块本来是那个 **14px 的
   "i" 图标**（`.mon-hint` 是 relative），`width:max-content` 往右开 260px 直接顶出
   消息气泡；而它平时 `visibility:hidden` —— **hidden 仍占布局、absolute 仍计入
   祖先 scrollWidth**，于是一个看不见的东西把整条流撑宽了。
   实测：cc-archive 溢出 152px（4 个气泡）· nac-debug 32px（1 个）· 没有气泡的
   cc 正好 0px。⇒ 不是某些 agent 特殊，是那条消息带不带这个提示符。

   ⚠️ **别改成给 `#stream` 加 `overflow-x:hidden`**：滚动确实没了，但气泡会连着被
   裁掉，而它正是用来解释投递为什么失败的。 */
:is(.deliv, .msg .st) { position: relative; }
:is(.deliv, .msg .st) .mon-hint {
  position: static;          /* 让浮层的包含块变成外面那一块 */
}
.deliv .mon-hint {
  /* ⚠️ 8px 是照监控页 `.mon-card-head` 的 `gap: 4px 8px` 取的 —— 同一个组件在两处
     该有同样的呼吸感。这里 `.deliv-head` 不是 flex 容器，没有 gap 可用，
     所以显式给 margin（机主 2026-08-10：贴太近了）。
     ⚠️ 聊天流那边不要这条：`.msg .st` 本身是 flex 且有 `gap: 8px`，再加 margin 会双份。 */
  margin-left: 8px;
}
/* 宽度改由**块宽**决定：必然放得下，也不用去猜该往左还是往右开（两边都实测过会顶出去）。 */
:is(.deliv, .msg .st) .mon-hint-pop { left: 0; right: 0; width: auto; max-width: none; }

/* 悬停联动时被点名的那一端（机主 2026-08-10：指到 PR 也要能看出谁负责）。
   ⚠️ 用**外发光的环**而不是改背景色：卡片的背景/边框已经在表达 CI 状态
   （红=失败、绿=全绿、紫=已合并），再动那两样就会和状态打架 —— 一个被指到的
   失败 PR 不该看起来像变绿了。环是叠加的第二层信息，不覆盖第一层。 */
/* ⚠️ 高亮**加粗卡片自己那圈颜色**，不另叠一种色（机主 2026-08-10 指出：
   叠蓝色会和边框本来的绿/红/灰混在一起，两个颜色打架）。
   边框色按 CI 状态变，所以抽成 `--edge` 一处定义、两处用 —— 否则这里没法知道
   当前该用哪个色，只能硬写一个，而硬写就必然和某些状态冲突。
   ⚠️ 用 box-shadow 不用 border-width：改宽度会让卡片内容位移。 */
.board .pr.hi {
  box-shadow: 0 0 0 2px var(--edge, var(--hairline-2)), 0 6px 18px rgba(0, 0, 0, .18);
}
/* 中间栏的 agent 没有状态色，蓝色不会和谁打架 */
.board .bagent.hi { box-shadow: 0 0 0 2px var(--blue), 0 6px 18px rgba(0, 0, 0, .18); }
.board .bagent.hi { border-color: var(--blue); }
@media (prefers-reduced-motion: no-preference) {
  .board .pr, .board .bagent { transition: box-shadow 140ms var(--ease); }
}

/* ⚠️⚠️ **静态边加粗了，转动的那圈光也得跟着加粗**（机主 2026-08-13 报）。
 *
 * 上面那两条 `.hi` 用 `box-shadow: 0 0 0 2px` 把彩色边加粗 —— 而 box-shadow
 * 画在**盒外**。转动的光环（`::after`）几何是照着**没加粗**时算的，于是悬浮时
 * 静态边有 3px 多、光只有 2.2px 且缩在里面，看起来像"光比边细了一圈"。
 *
 * ⚠️ 判据仍然是那条：**这条边到底画在盒内还是盒外**，不是"看着差一点点挪一挪"。
 *   两种卡片的边框画法不同，几何不能一刀切：
 *     · `.bagent` —— `border: 1px`（盒内）+ hover 的 `0 0 0 2px`（盒外）
 *       ⇒ 彩色区间是 −2px … +1px，共 **3px**；环要 `inset:-2px; padding:3px`
 *     · `.pr`     —— `border: 1.5px`（盒内）+ 同样 2px（盒外）
 *       ⇒ 彩色区间是 −2px … +1.5px，共 **3.5px**；环要 `inset:-2px; padding:3.5px`
 * ⚠️ 圆角要跟着外扩量走：`inset:-2px` 的盒子比原来大 2px，圆角不 +2 的话
 *   四个角会对不上，拐角处重新出现双线（`.agent` 那边踩过一模一样的坑）。
 */
.board .pr.hi.running::after {
  inset: -2px; padding: 3.5px; border-radius: 14px;   /* .pr 圆角 12 + 2 */
}
.board .bagent.hi.gen::after {
  inset: -2px; padding: 3px;
  border-radius: calc(var(--radius-sm) + 2px);
}
/* 跟着 `.hi` 的 140ms 一起长，别一个渐变一个跳变 */
@media (prefers-reduced-motion: no-preference) {
  .board .pr.running::after,
  .board .bagent.gen::after {
    transition: inset 140ms var(--ease), padding 140ms var(--ease),
                border-radius 140ms var(--ease);
  }
}

/* 交付物每行右边一个 ×（机主 2026-08-10：不然越积越多）。
   ⚠️ 不做二次确认：交付物是**清单**不是历史 —— 删掉只是不再列出来，
      东西本身还在原处，重新 `agent-team deliver` 一次就回来。可逆的动作不该拦。 */
.deliv-row { display: flex; align-items: flex-start; gap: 4px; }
.deliv-row .deliv-item { flex: 1 1 auto; min-width: 0; }
.deliv-x {
  flex: 0 0 auto; width: 16px; height: 16px; margin-top: 3px; padding: 0;
  border: 0; border-radius: 50%; background: none; color: var(--label-3);
  font-size: 13px; line-height: 1; cursor: pointer; display: grid; place-items: center;
}
.deliv-x:hover { background: var(--red-soft, rgba(255,69,58,.14)); color: var(--red); }
.deliv-x:disabled { opacity: .4; cursor: default; }
/* 「还有 N 条更早的没显示」——**是一句话，不是一条交付物**：不给任何可点的东西。 */
.deliv-more {
  padding: 3px 2px; color: var(--label-3); font-size: 11px; line-height: 1.5;
}
/* 远端取不回来的那条：正文照常可点（值还在），只多一个警示标。
   ⚠️ **光标不许变成问号**（机主 2026-08-09）—— 那条规则是对整份 CSS 断言的
      （tests/test_cache_invariants.py），所以这里连默认光标都不改。
      说明靠 title 悬浮，不靠光标形状。 */
.deliv-warn {
  flex: 0 0 auto; margin-top: 3px; color: var(--orange, #ff9f0a); font-size: 12px;
  line-height: 1;
}
/* 订阅按钮：借 .pr-rerun 的形状，但**不用它的强调色** —— 重跑是写操作、订阅只是开关。 */
.pr-sub {
  font: inherit; font-size: 11px; padding: 3px 9px; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--hairline-2); background: var(--surface-2); color: var(--label-2);
}
.pr-sub:hover { background: var(--hairline); color: var(--label); }
.pr-sub.on { border-color: var(--blue); color: var(--blue); }
.pr-sub:disabled { opacity: .5; cursor: default; }
/* 看板中间栏：自定义名旁边的 session 名（唯一能拿去寻址的那个） */
.bagent-sid { font-style: normal; font-size: 10.5px; color: var(--label-3);
              margin-left: 6px; font-family: var(--mono); }

/* 改名弹窗（机主 2026-08-11）。 */
#rn-input {
  width: 100%; box-sizing: border-box; margin: 4px 0 2px;
  padding: 9px 11px; font: inherit; font-size: 14px;
  color: var(--label); background: var(--surface-1);
  border: 0; border-radius: 8px; box-shadow: 0 0 0 1px var(--hairline);
  transition: box-shadow 140ms var(--ease);
}
#rn-input:focus { outline: 0; box-shadow: 0 0 0 2px var(--blue); }
.modal .rn-sub { margin-top: 8px; }
.modal .rn-sub code {
  padding: 1px 5px; border-radius: 4px;
  background: var(--surface-1); box-shadow: 0 0 0 1px var(--hairline);
}
/* ⚠️ 铅笔比三横线小一号（20 → 18）：改名是**低频**操作，而它紧挨着
   「停止」——后者是需要一按就中的。两个同样大小的按钮并排，急着停生成时
   点错的代价远大于点错改名。 */
@media (max-width: 860px) {
  /* 手机端反过来：指尖没有"看准了再点"这回事，两个都得够得着。 */
}

/* 「此刻在跑什么」（后台 shell / monitor / workflow）。机主 2026-08-11。
   ⚠️ 用蓝而不是黄/红：它是**中性事实**不是告警 —— 一个 cc 挂着后台任务
   完全正常，标成警告色会让人学会忽略这一栏。 */
.detail-head .sub .running {
  color: var(--blue); font-variant-numeric: tabular-nums;
  cursor: default;
}

/* ————— 这一轮做了什么（机主 2026-08-11）—————
   ⚠️ 整块**贴着气泡下沿**、比正文暗一档：它是注脚不是内容。
   读对话是这个页面 90% 的用途，细节不能跟正文抢视觉重量。

   ⚠️⚠️ **排版有两个坑，都踩过一次**（机主 2026-08-11 报「居中而且太窄」）：
   ① `.msg` 是 `width: fit-content` —— 盒子宽度由**气泡里的文字**决定。
      在它里面写 `max-width: 78%` 等于"已经很窄的 78%"，于是又窄一层。
   ② 我给 `.msg.up .acts` 加了 `margin-left:auto`，把它推到那个窄盒子的右边，
      看起来就是**居中偏右**。cc 的消息本来就靠左，注脚没有理由往右跑。
   ⇒ 解法：**带 acts 的消息把 max-width 放宽**（靠 `:has`），同时让气泡
      自己 `fit-content` —— 否则盒子一宽，一句短话的气泡会跟着拉成一整条。 */
/* ⚠️ 1px 不是抠细节：这一条和上面那条折叠行是**同一类次要信息**，
   它们之间不需要"分隔感"，需要的是"读成一组"。3px 时看着像两件事。 */
.msg .acts { margin: 1px 0 0; width: 100%; }
/* 有细节要看的消息，盒子**固定**放宽到接近整栏；没有的保持原样（72%）。
   ⚠️ 这里必须写 `width` 而不是只放宽 `max-width`：`.msg` 是 `fit-content`，
      只抬上限的话盒子仍然缩到内容宽 —— 工具输出短的时候又变回一条窄条，
      而"有时宽有时窄"比一直窄更难看。 */
.msg:has(.acts) { width: 94%; max-width: 94%; }
/* ⚠️ 气泡不跟着变宽：`.bubble` 是块级元素，父盒子一宽它就填满，
   一句"好的"会被拉成 900px 的空条。 */
.msg .bubble { width: fit-content; max-width: 100%; }
/* ⚠️ 几何**逐项对齐 `.msg.foldable .echo-bar`**（机主 2026-08-11：「要和会话内复述对齐」）。
   两条注脚上下相邻，文字差 4px 就看得出来。差在三处，各贡献一点：
     padding-left 8 vs 9 · gap 6 vs 7 · 箭头 8px（四边 border）vs 4px（只有左边）
   ⇒ 全部改成跟它一样。**别只调 padding 去凑** —— 那样箭头又对不上，
     而且下次改另一个就又散了。同一种东西就该用同一套值。 */
.acts-bar {
  display: flex; align-items: center; gap: 7px; width: 100%;
  padding: 1px 9px; border: 0; border-radius: 8px; cursor: pointer;
  background: transparent; color: var(--label-3);
  font: inherit; font-size: 11.5px; text-align: left;
  transition: background 140ms var(--ease), color 140ms var(--ease);
}
.acts-bar:hover { background: var(--surface-2); color: var(--label-2); }
.acts-bar .lbl { flex: 0 0 auto; font-weight: 540; }
/* ⚠️ 摘要要能被压缩并省略；`min-width:0` 不能省，否则 flex 子项不肯缩 */
.acts-bar .pv { flex: 1 1 auto; min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; font-family: var(--mono); font-size: 11px; }
.acts-bar .acts-bad { flex: 0 0 auto; color: var(--red); font-weight: 560; }
/* 没正文那种把时间挂在条的右端 —— 省掉它原来独占的一整行。 */
.acts-bar .acts-ts { flex: 0 0 auto; margin-left: auto; color: var(--label-3);
  font-size: 11px; font-variant-numeric: tabular-nums; }
/* 与复述条同一个箭头：只画左边那条边（宽 4px），不是四边 border（宽 8px） */
.acts-bar .caret {
  flex: 0 0 auto; width: 0; height: 0;
  border-left: 4px solid currentColor;
  border-top: 3.5px solid transparent; border-bottom: 3.5px solid transparent;
  transition: transform 140ms var(--ease);
}
.acts.open .acts-bar .caret { transform: rotate(90deg); }
.acts-body { display: none; padding: 2px 0 0 10px; }
.acts.open .acts-body { display: block; }
.act {
  margin: 4px 0; padding: 6px 8px; border-radius: 8px;
  background: var(--surface-1); box-shadow: 0 0 0 1px var(--hairline);
}
/* 报错的那一条要一眼看见 —— 展开细节多半就是为了找它 */
.act.bad { box-shadow: 0 0 0 1px color-mix(in srgb, var(--red) 45%, transparent); }
.act-h { font-size: 11.5px; color: var(--label-2); display: flex; gap: 6px; align-items: baseline; }
.act-h b { color: var(--label); font-weight: 600; flex: 0 0 auto; }
.act-h code {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: var(--mono); font-size: 11px; color: var(--label-2);
}
.act-r {
  margin: 4px 0 0; padding: 5px 7px; border-radius: 6px;
  background: var(--bg-2); color: var(--label-2);
  font-family: var(--mono); font-size: 11px; line-height: 1.5;
  white-space: pre-wrap; word-break: break-word;
  /* ⚠️ 单条结果**必须限高**：一次 Read 可能是整个文件，不限高会把对话流撑成天书。
     服务端已经截到 1200 字，这里再限一次高度是为了"横向很长的一行"那种。 */
  max-height: 220px; overflow: auto;
}
.act-r.none { font-style: italic; color: var(--label-3); background: transparent; padding: 2px 0; }
@media (max-width: 860px) {
  .msg .acts { max-width: 100%; }
}

/* 「在跑」明细（机主 2026-08-12：全部信息同步上来）。
   ⚠️ 复用 .deliv 的外壳（同一栏、同一种卡片），只补内部行的排布 ——
      另造一套盒子意味着两处样式各自漂移，而那个坑这仓库踩过。 */
.rt-row { display: flex; align-items: center; gap: 7px; padding: 1px 0; font-size: 12px; }
.rt-kind { flex: 0 0 auto; padding: 1px 6px; border-radius: 4px; font-size: 11px;
           background: var(--chip-bg, rgba(127,127,127,.14)); }
.rt-shell   { color: #3b82f6; }
.rt-monitor { color: #a855f7; }
.rt-wf      { color: #f59e0b; }
.rt-age { flex: 0 0 auto; opacity: .65; font-variant-numeric: tabular-nums; }
.rt-cmd { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis;
          white-space: nowrap; opacity: .85; }
.rt-out { flex: 0 0 auto; border: 0; background: none; cursor: pointer;
          color: inherit; opacity: .6; font-size: 11px; padding: 1px 4px; }
.rt-out:hover { opacity: 1; text-decoration: underline; }
/* ⚠️ 输出必须能横向滚而不撑破整栏：一行 200 字符的日志会把进度栏顶宽，
      而那一栏是固定宽度的。 */
.rt-pre { margin: 2px 0 6px; padding: 6px 8px; max-height: 260px; overflow: auto;
          font-size: 11px; line-height: 1.45; white-space: pre; border-radius: 6px;
          background: var(--code-bg, rgba(127,127,127,.10)); }


/* ————————————————————— 右栏密度（机主 2026-08-12：「紧凑一点，能展示更多信息」）
 *
 * 做法是**逐项削掉留白**，不是整体缩放：字号基本没动（缩字号会先牺牲可读性，
 * 而这一栏的内容本来就密）。省下来的高度主要来自三处：
 *   · 时间线的节点/间隔 padding —— 12 个节点 + 11 段间隔，省一点要乘以 23
 *   · 「已持续」那块的 padding 与 22px 的大数字（窄栏里它不需要那么大）
 *   · 三个卡片（PR / 交付物 / 在跑）各自的外边距与内边距
 *
 * ⚠️⚠️ **触屏上必须把可点区域还回去。** 桌面靠指针，2px 的行距无所谓；
 *    手机上 `.deliv-item` / `.rt-out` 就是手指要点的目标，压到 2px 会点不中。
 *    ⇒ 密度是**桌面的优化**，不能连着把移动端的可用性一起优化掉。
 *    （860px 这个断点沿用本文件里已有的那个，别新造一个。）
 */
@media (max-width: 860px) {
  .deliv-item { padding: 7px 6px; line-height: 1.5; margin-top: 2px; }
  .rt-row { padding: 4px 0; }
  .rt-out { padding: 6px 8px; }
  .tl-node { padding: 4px 6px 4px 0; }
  .tl-gap { padding: 5px 0 5px 24px; }
}

/* ═══════════════════ 功能面板（机主 2026-08-12）═══════════════════
 *
 * 平台自己提供的能力：定时任务、内置巡检、事件订阅。
 *
 * ⚠️ 盒子语言沿用 `.mon-mod`（实线细框 + surface-1 底 + 头部分隔线），
 *    不另起一套 —— 两套并存会让人以为它们是不同性质的东西。
 * ⚠️ 这一页是**表单页**，和其余三页（看数据）不同：所以尺寸统一走下面这几个
 *    变量，别在各处手写 px —— 输入框和按钮高度差 2px，一行里就会明显不齐。
 */
.fx {
  flex: 1; min-height: 0; overflow: auto; padding: 20px 20px 40px;
  --fx-h: 32px;                 /* 所有输入控件和按钮的统一高度 */
  --fx-r: 9px;                  /* 统一圆角（比卡片小一档） */
  --fx-gap: 10px;
}
.fx-wrap { max-width: 860px; margin: 0 auto; }

.fx-top { margin: 0 2px 16px; }
.fx-title { font-size: 20px; font-weight: 620; margin: 0 0 3px; letter-spacing: -0.01em; }
.fx-lede { font-size: 12.5px; color: var(--label-3); margin: 0; line-height: 1.5; }

.fx-card {
  margin-bottom: 16px;
  background: var(--surface-1);
  border: 1px solid var(--hairline); border-radius: var(--radius-lg);
  overflow: hidden;                     /* 让子元素的分隔线贴住圆角 */
}
.fx-head {
  display: flex; align-items: center; gap: 12px;
  padding: 13px 16px; border-bottom: 1px solid var(--hairline);
}
.fx-headtext { flex: 1; min-width: 0; }
/* 卡标题旁的外部后台入口（机主 2026-08-25：「不然我每次都要找」）。
   ⚠️ 放在 h2 **内部**是故意的 —— 它要跟着标题走，标题换行时不会掉队。
   测试 `test_bot_module_merge` 按文本节点取标题，h2 里多一个 <a> 会多出一条
   文本节点、但精确匹配「飞书 Bot」那条仍恰好命中 1 次，所以不受影响。 */
.fx-h2link {
  margin-left: 8px; font-size: 11.5px; font-weight: 500;
  color: var(--label-3); text-decoration: none;
  vertical-align: middle; white-space: nowrap;
  opacity: .75; transition: opacity .12s, color .12s;
}
.fx-h2link:hover { opacity: 1; color: var(--accent, #3b82f6); text-decoration: underline; }
.fx-head h2 {
  font-size: 14px; font-weight: 590; margin: 0;
  display: flex; align-items: center; gap: 7px;
}
.fx-sub { display: block; font-size: 11.5px; color: var(--label-3); margin-top: 2px; line-height: 1.45; }
.fx-builtin {
  font-weight: 400; font-size: 10px; padding: 1.5px 7px; border-radius: 999px;
  background: color-mix(in srgb, var(--blue) 14%, transparent);
  color: var(--blue); border: 1px solid color-mix(in srgb, var(--blue) 26%, transparent);
}

/* ——— 按钮：一套三档（主 / 次 / 幽灵），高度与输入框对齐 ——— */
.fx-btn {
  height: var(--fx-h); padding: 0 14px; border-radius: var(--fx-r);
  border: 1px solid var(--hairline); background: var(--surface-2);
  color: var(--label); font-size: 12.5px; font-family: inherit;
  white-space: nowrap; flex-shrink: 0;
  transition: background 140ms cubic-bezier(0.23, 1, 0.32, 1),
              border-color 140ms cubic-bezier(0.23, 1, 0.32, 1);
}
.fx-btn:hover { background: var(--surface-1); border-color: var(--label-3); }
.fx-btn--primary {
  background: var(--blue); color: #fff; border-color: transparent; font-weight: 500;
}
.fx-btn--primary:hover { background: color-mix(in srgb, var(--blue) 86%, #fff); border-color: transparent; }
.fx-btn--ghost { background: transparent; }
.fx-btn:disabled { opacity: 0.45; }

/* ——— 表单：字段用栅格自动折行，按钮单独一行 ———
   ⚠️ 上一版把「时间 / 目标 / 内容 / 两个按钮」挤在同一行，窄一点就折得乱七八糟，
      而且每个框都没有标签 —— 靠 placeholder 说明自己，一填字说明就没了。 */
.fx-form {
  padding: 14px 16px 15px; border-bottom: 1px solid var(--hairline);
  background: var(--surface-2);
}
.fx-fields {
  display: grid; gap: var(--fx-gap) 12px;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}
.fx-field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.fx-field--wide { grid-column: 1 / -1; }
.fx-lab { font-size: 11px; color: var(--label-3); letter-spacing: 0.02em; }
.fx-inline { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }

.fx-form input, .fx-form select {
  height: var(--fx-h); box-sizing: border-box;
  border: 1px solid var(--hairline); background: var(--canvas);
  color: var(--label); font-size: 12.5px; font-family: inherit;
  padding: 0 10px; border-radius: var(--fx-r); width: 100%; min-width: 0;
  transition: border-color 140ms ease-out, box-shadow 140ms ease-out;
}
.fx-form select { padding-right: 26px; }
.fx-form input:focus, .fx-form select:focus {
  outline: none; border-color: var(--blue);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--blue) 18%, transparent);
}
.fx-form input.fx-num { width: 64px; flex: 0 0 64px; text-align: center; padding: 0 6px; }
.fx-form input[type="time"] { width: 118px; flex: 0 0 118px; }
.fx-inline select { width: auto; min-width: 88px; }

/* 多行输入（白名单的「额外规则」）。⚠️ **不能并进上面那条规则** ——
   那条写死了 `height: var(--fx-h)`，textarea 会被压成一行高，
   而 rows= 属性在有 height 的情况下一点用都没有（看着像 HTML 写错了）。 */
.fx-form textarea {
  box-sizing: border-box; border: 1px solid var(--hairline); background: var(--canvas);
  color: var(--label); font-size: 12.5px; font-family: inherit; line-height: 1.55;
  padding: 8px 10px; border-radius: var(--fx-r); width: 100%; min-width: 0;
  resize: vertical;
  transition: border-color 140ms ease-out, box-shadow 140ms ease-out;
}
.fx-form textarea:focus {
  outline: none; border-color: var(--blue);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--blue) 18%, transparent);
}

/* 表单里的提示条。⚠️ 这个类 2026-08-23 起就在 `fx-bot-note` / `fx-lb-note` 上用了，
   **但一直没有任何 CSS** —— 提示照样显示（div 默认就是块级），所以没人发现：
   它和"没有提示"的区别只是一行没有样式的黑字。`.bad` 同理，全局没定义过。 */
.fx-note {
  margin-top: 11px; font-size: 11.5px; line-height: 1.55; color: var(--label-2);
  white-space: pre-wrap; word-break: break-word;
}
.fx-note.bad { color: var(--red); }
.fx-note:empty { display: none; }

/* 搜人的候选行：⚠️ 每一条都要**同时**显示人名和 open_id ——
   只显示人名的话，两个同名的人在这张列表里长得一模一样，选错了没人看得出来。 */
.fx-cand {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 7px 0; border-bottom: 1px solid var(--sep);
}
.fx-cand:last-child { border-bottom: 0; }
.fx-cand b { font-weight: 590; color: var(--label); font-size: 12.5px; }
.fx-cand code { font-size: 11px; color: var(--label-3); }
.fx-cand .spacer { flex: 1; }

.fx-actions {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-top: 13px;
}
.fx-actions .spacer { flex: 1; }
.fx-hint { font-size: 11.5px; color: var(--label-3); line-height: 1.45; flex: 1 1 100%; order: -1; }
.fx-hint:empty { display: none; }
.fx-hint--ok { color: var(--green); }
.fx-hint--bad { color: var(--red); }

/* ——— 三选一 ——— */
.fx-seg {
  display: flex; gap: 2px; background: var(--canvas); border-radius: var(--fx-r);
  padding: 2px; border: 1px solid var(--hairline); height: var(--fx-h);
  box-sizing: border-box;
}
.fx-seg button {
  border: 0; background: transparent; color: var(--label-2);
  font-size: 12.5px; padding: 0 13px; border-radius: 7px; font-family: inherit;
  transition: background 140ms ease-out, color 140ms ease-out;
}
.fx-seg button.on { background: var(--surface-1); color: var(--label); box-shadow: 0 0 0 1px var(--hairline); }
.fx-when { display: inline-flex; gap: 8px; align-items: center; }

/* ——— 开关 ——— */
.fx-switch { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; flex-shrink: 0; }
.fx-switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.fx-switch-track {
  width: 38px; height: 22px; border-radius: 999px; background: var(--hairline);
  position: relative; transition: background 180ms cubic-bezier(0.23, 1, 0.32, 1);
}
.fx-switch-knob {
  position: absolute; top: 2px; left: 2px; width: 18px; height: 18px;
  border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
  transition: transform 180ms cubic-bezier(0.23, 1, 0.32, 1);
}
.fx-switch input:checked + .fx-switch-track { background: var(--green); }
.fx-switch input:checked + .fx-switch-track .fx-switch-knob { transform: translateX(16px); }
.fx-switch input:focus-visible + .fx-switch-track { box-shadow: 0 0 0 3px color-mix(in srgb, var(--blue) 25%, transparent); }
.fx-switch-lab { font-size: 12px; color: var(--label-2); }

/* ——— 巡检那三条规则：读成一句话，数字嵌在句子里 ———
   ⚠️ 比「标签 + 输入框」的表单好，因为这几个数**只有放回句子里才知道自己是什么意思**
      （「3 轮」和「20 分钟」是**与**的关系，拆成两个字段就看不出来了）。 */
.fx-rules { display: flex; flex-direction: column; gap: 9px; }
.fx-rule {
  display: flex; flex-wrap: wrap; align-items: center; gap: 7px;
  font-size: 12.5px; color: var(--label-2); line-height: var(--fx-h);
}

/* ——— 上行提醒：单独一组（机主 2026-08-14）———
   ⚠️ 视觉上必须**看得出是另一组** —— 它判的不是同一件事（上面管"看板描述过没过期"，
      它管"结果有没有告诉机主"）。挨着排会让人以为关掉上面那个总开关就全停了。 */
.fx-rules--sub {
  margin-top: 13px; padding-top: 12px;
  border-top: 1px dashed var(--sep);
}
/* ⚠️ 它当第一组时那条上分隔线是**悬空**的（2026-08-26 删掉上面两组之后）：
   分隔线的意思是"上面还有别的组"，而上面已经什么都没有了。
   ⭐ 用 `:first-of-type` 而不是去掉 `--sub` 类：将来若又加回一个主组，
     分隔线会自己回来，不用再想起改这里。 */
.fx-form > .fx-rules--sub:first-of-type {
  margin-top: 4px; padding-top: 0; border-top: 0;
}
.fx-rule--head { gap: 10px; }
.fx-rule--note {
  font-size: 11.5px; color: var(--label-3, var(--label-2));
  opacity: .85; line-height: 1.55;
}
.fx-switch--sm .fx-switch-lab { font-weight: 590; color: var(--label); }

/* ——— 列表 ——— */
.fx-row {
  display: flex; align-items: center; gap: 12px;
  padding: 11px 16px; border-bottom: 1px solid var(--sep);
  transition: background 140ms ease-out;
}
.fx-row:last-child { border-bottom: 0; }
.fx-row:hover { background: var(--surface-2); }
/* 暂停 / 静音的压暗但**不隐藏** —— 「它还在，只是现在不响」和「它没了」
   是两件事，看不见的那条会让人以为自己删过。 */
.fx-row--off { opacity: 0.5; }
.fx-main { flex: 1; min-width: 0; }
.fx-line1 { font-size: 13px; display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
.fx-line1 b { font-weight: 590; }
.fx-line2 {
  font-size: 12px; color: var(--label-2); margin-top: 3px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fx-meta { font-size: 11px; color: var(--label-3); margin-top: 3px; font-variant-numeric: tabular-nums; }
.fx-err { color: var(--orange); }
.fx-arrow { color: var(--label-3); }
.fx-to { color: var(--blue); }
.fx-any { color: var(--label-3); }
.fx-tag {
  font-size: 10px; padding: 1.5px 7px; border-radius: 999px;
  background: var(--surface-2); color: var(--label-3); border: 1px solid var(--hairline);
}
.fx-tag--auto { color: var(--label-2); }

/* 行内操作：平时淡、悬停才实。⚠️ **不是 hover 才出现** ——
   触屏上没有 hover，那样等于按钮不存在。 */
.fx-acts { display: flex; gap: 6px; flex-shrink: 0; opacity: 0.75; transition: opacity 140ms ease-out; }
.fx-row:hover .fx-acts { opacity: 1; }
.fx-acts button {
  height: 26px; padding: 0 10px; border-radius: 7px;
  border: 1px solid var(--hairline); background: var(--surface-2);
  color: var(--label-2); font-size: 11.5px; font-family: inherit;
  transition: color 140ms ease-out, border-color 140ms ease-out;
}
.fx-acts button:hover { color: var(--label); border-color: var(--label-3); }
.fx-acts .fx-danger:hover { color: var(--red); border-color: var(--red); }
.fx-acts button:disabled { opacity: 0.4; }
.fx-empty { padding: 22px 16px; font-size: 12.5px; color: var(--label-3); }

/* ——— 列表里的分组小标题（「我授出去的」/「别人授给我的」/「现有账户」）———
   ⚠️ 第一版拿 `.fx-notes` 当它用 —— 那个类是**卡片脚注**，自带 border-top 和
      背景块，用在列表中间会画出一条假的分隔线、看起来像卡片结束了又开始。
      **类名要按它是什么选，不是按它长得像什么。** */
.fx-grouplab {
  padding: 10px 16px 6px; font-size: 11px; color: var(--label-3);
  letter-spacing: 0.02em; background: var(--surface-2);
  border-bottom: 1px solid var(--hairline);
}
.fx-grouplab + .fx-row { border-top: 0; }

/* ——— 卡片脚注：事件清单 / 边界说明 ———
   它就是这个功能的说明书，人不会为了填一个输入框专门去翻文档。 */
.fx-notes {
  padding: 11px 16px 12px; border-top: 1px solid var(--hairline);
  background: color-mix(in srgb, var(--surface-2) 60%, transparent);
  font-size: 11.5px; color: var(--label-3); line-height: 1.6;
}
.fx-notes:empty { display: none; }
/* ⚠️ 只有**直接子元素**参与这个两栏布局。句子内部的 <em>/<code> 必须待在
   那个 <span> 里 —— 直接放进 flex 容器会各自变成 flex item，
   于是句子中间被塞进一个 gap（实测「任务 不会 一次补发」中间空一大块）。 */
.fx-notes div { display: flex; gap: 10px; align-items: baseline; }
.fx-notes > div > b {
  flex: 0 0 84px; color: var(--label-2); font-weight: 500; font-size: 11px;
}
.fx-notes > div > span { flex: 1; min-width: 0; }
.fx-notes em { font-style: normal; font-weight: 590; color: var(--label-2); }
.fx-notes code { font-size: 11px; color: var(--label-2); }

@media (max-width: 860px) {
  .fx { padding: 14px 12px 28px; }
  .fx-fields { grid-template-columns: 1fr; }
  .fx-row { flex-direction: column; align-items: stretch; gap: 8px; }
  .fx-acts { align-self: flex-end; opacity: 1; }
  /* 窄屏折成两行后必须给条目之间留白 —— 不留的话「标题/正文/标题/正文」
     会连成一片，看不出哪句配哪句。 */
  .fx-notes div { flex-direction: column; gap: 1px; }
  .fx-notes div + div { margin-top: 7px; }
  .fx-notes > div > b { flex: none; }
}

@media (prefers-reduced-motion: reduce) {
  .fx-btn, .fx-form input, .fx-form select, .fx-seg button,
  .fx-switch-track, .fx-switch-knob, .fx-row, .fx-acts { transition-duration: 1ms; }
}
.fx-rule b { font-weight: 590; color: var(--label); }
.fx-note-inline { font-size: 11px; color: var(--label-3); }
.fx-form input:disabled { opacity: 0.55; background: var(--surface-2); }

/* 巡检第三条里那个只读镜像（和第二条共用同一个轮数）。
   ⚠️ 做成 <b> 而不是 disabled 输入框：一个改不动的输入框只会让人试着去改它。 */
#fx-p-sturns2 {
  display: inline-block; min-width: 26px; text-align: center;
  padding: 0 6px; border-radius: 6px; background: var(--surface-1);
  border: 1px dashed var(--hairline); color: var(--label-2); font-weight: 500;
}

/* ═══════════ 功能面板：移动端（机主 2026-08-12）═══════════
 *
 * ⚠️ **写在文件最末**：同特异性下后写的赢。塞进上面那个 `@media (max-width: 860px)`
 *    里会被它后面的桌面规则压掉 —— 这个坑本文件 2760 行处已经记过一次。
 *
 * 实测（390×844，`is_mobile` + `has_touch`）：19 个控件不达标 ——
 * 输入框和按钮全是 32px、行内操作 26px。桌面上 26px 鼠标点得中，
 * 手机上就是"点三次中一次"。
 */
@media (max-width: 860px) {
  .fx {
    /* 44px 是 iOS 的最小可靠命中高度（tabbar 那条注释里同一个数） */
    --fx-h: 44px;
    padding: 12px 12px calc(20px + env(safe-area-inset-bottom, 0px));
  }

  /* 页面标题在手机上是纯冗余：tabbar 已经写着「功能」，每张卡也各有标题和说明。
     省下的 ~54px 直接变成能看见的内容 —— 和顶栏 h1 / footer 同一个判据。 */
  .fx-top { display: none; }

  /* ⚠️ ≥16px：iOS Safari 对更小的输入框会在聚焦时**放大整个页面，而且回不去**。
     这是这次移动端适配里最要紧的一条（`.composer textarea` 早就这么做了）。 */
  .fx-form input, .fx-form select { font-size: 16px; padding: 0 12px; }
  .fx-form input.fx-num { width: 78px; flex: 0 0 78px; }
  .fx-form input[type="time"] { width: 100%; flex: 1 1 140px; }

  /* 轨道比 --fx-h 高 2px：里面的按钮要减掉 padding 和边框，
     44 的轨道只剩 38 —— 而 44 说的是**手指点的那块**，不是外框。 */
  .fx-seg { height: calc(var(--fx-h) + 2px); flex: 1 1 100%; }
  .fx-seg button { flex: 1; font-size: 14px; }
  .fx-when { flex: 1 1 100%; }
  .fx-when select { flex: 0 0 auto; }

  .fx-btn { font-size: 14px; padding: 0 16px; }
  /* 两个按钮平分整行：单手拇指够得着的区域在下半屏，按钮铺开比靠右挤成一小块好点 */
  /* 按钮铺满整行：手机上「靠右挤成一小块」既难点，也浪费了唯一一行的宽度。
     隐藏那个 spacer 才铺得开 —— 它在桌面上负责把按钮推到右边。 */
  .fx-actions .spacer { display: none; }
  .fx-actions .fx-btn { flex: 1 1 0; }
  .fx-head .fx-btn--ghost { flex: 0 0 auto; }

  /* 行内操作：桌面 26px → 触屏 38px，且横向拉开。
     ⚠️ 只加高命中区，字号不变 —— 视觉密度不该为了触屏牺牲。 */
  .fx-acts { opacity: 1; gap: 8px; }
  .fx-acts button { height: 44px; padding: 0 16px; flex: 1 1 0; }

  /* 开关：轨道本身 22px 太小，靠 label 的纵向 padding 把命中区撑到 44px */
  .fx-switch { padding: 11px 0; }

  /* 规则那几句话在窄屏会折得很碎 —— 行高放开，别让上下两行黏在一起 */
  .fx-rule { line-height: 2.1; gap: 6px 8px; }
  /* 那句旁注在窄屏会卡在「分钟」后面折成孤零零一行 —— 给它整行，读起来才是一句话。
     ⚠️ 不删：它说的是"这条才是最常触发的"，正是调参时最需要知道的一句。 */
  .fx-note-inline { flex: 1 1 100%; line-height: 1.5; margin-top: -4px; }
  .fx-rules { gap: 6px; }

  .fx-head { padding: 12px 14px; }
  .fx-form { padding: 12px 14px 14px; }
  .fx-row { padding: 12px 14px; }
  .fx-notes { padding: 11px 14px 12px; }
}

/* ⚠️ 左栏是**主导航**，2026-08-13 之前它纯 click 驱动：键盘能 Tab 到行内的
   归档按钮，却选不中任何一个 cc。补了 tabindex 之后，焦点必须**看得见** ——
   一个能聚焦却没有焦点样式的元素，对键盘用户等于隐身。 */
.agent:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: -2px;
  border-radius: var(--radius);
}


/* 取不到确认框屏幕时的说明（2026-08-14）。
   ⚠️ 用**警示色而不是错误红**：它不是失败，是"这一刻看不到" ——
   红色会让人以为 cc 出事了，而其实只是取屏没排上队。 */
.auth-err {
  margin: 6px 0; padding: 9px 11px; border-radius: 10px; font-size: 12.5px;
  line-height: 1.5; color: var(--label-2);
  background: color-mix(in srgb, var(--yellow) 10%, var(--surface-1));
  border: 1px solid color-mix(in srgb, var(--yellow) 32%, transparent);
}


/* 归档卡住（机主 2026-08-14）。⚠️ 与普通「归档中」**必须长得不一样** ——
   两者都显示在主列表上，如果外观相同，那行字改了也没人会去读。
   用红边而不是变灰：这不是"已经没了"，是"该没而还在"，需要有人处理。 */
.agent.arch-stuck {
  box-shadow: var(--card-hi), var(--card-amb),
              0 0 0 1.5px color-mix(in srgb, var(--red) 60%, transparent);
}
.agent.arch-stuck .meta-main { color: var(--red); }

/* 会话换代的分隔线。⚠️ 它不是「一步」，是**两段历史之间的缝** ——
   所以没有 pip、不参与耗时连线，视觉上要能一眼看出"这里断过代"。 */
.tl-boundary {
  display: flex; align-items: center; gap: 10px;
  margin: 10px 0; color: var(--label-3); font-size: 11.5px;
}
.tl-boundary::before, .tl-boundary::after {
  content: ""; flex: 1 1 auto; height: 1px; background: var(--hairline);
}
.tl-boundary span { flex: 0 0 auto; white-space: nowrap; }

/* ————————————————————— 首页（机主 2026-08-14）—————————————————————
 *
 * 「账户 → 机器 → 每台机器上的 cc」三层，**包含关系**画出来：cc 在机器卡片**里面**。
 * ⚠️ 版面宽度对齐功能页的 `.fx-wrap`（860px）—— 同一个平台的几个页面不该各有各的宽度。
 */
.home { padding: 22px 20px 40px; overflow-y: auto; }
.home-wrap { max-width: 860px; margin: 0 auto; }
.home-top { margin: 0 2px 16px; }
.home-title { font-size: 20px; font-weight: 620; margin: 0 0 3px; letter-spacing: -0.01em; }
.home-lede { font-size: 12.5px; color: var(--label-3); margin: 0; line-height: 1.5; }

.hm-card {
  margin-bottom: 14px;
  background: var(--surface-1);
  border: 1px solid var(--hairline); border-radius: var(--radius-lg);
  padding: 14px 16px;
}

/* —— 账户卡 —— */
.hm-acct { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.hm-acct-l { display: flex; align-items: center; gap: 12px; min-width: 0; }
.hm-avatar {
  width: 38px; height: 38px; border-radius: 50%; flex: 0 0 auto;
  display: grid; place-items: center;
  background: var(--blue); color: #fff; font-weight: 620; font-size: 16px;
}
.hm-acct-name { font-size: 15px; font-weight: 590; }
.hm-acct-sub { font-size: 11.5px; color: var(--label-3); margin-top: 1px; }
.hm-acct-r { display: flex; gap: 22px; margin-left: auto; flex-wrap: wrap; }
.hm-kv { display: flex; flex-direction: column; gap: 1px; }
.hm-kv span { font-size: 11px; color: var(--label-3); }
.hm-kv b { font-size: 13px; font-weight: 560; }
.hm-kv i { font-size: 10.5px; color: var(--label-3); font-style: normal; }

/* —— 机器卡 —— */
.hm-mach.off { opacity: 0.62; }
.hm-mhead { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.hm-dot {
  width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto;
  background: var(--green); box-shadow: 0 0 0 3px color-mix(in srgb, var(--green) 22%, transparent);
}
.hm-dot.off { background: var(--grey); box-shadow: none; }
.hm-mname { font-size: 14px; font-weight: 590; }
.hm-mhost { font-size: 11.5px; color: var(--label-3); }
.hm-meta { font-size: 11px; color: var(--label-3); }
/* daemon 版本与平台对不上时标出来 —— 这一列存在的全部意义就是这一刻 */
.hm-meta.warn { color: var(--orange); }
.spacer { flex: 1 1 auto; }

/* —— 机器卡上就地选 Bot 下发凭证（机主 2026-08-26）——
   他的话：「点『配 bot』后会直接跳转到功能页面，那我怎么知道哪个 bot 对应哪台机器」
   ⇒ 面板就长在那台机器的卡里，"给谁配"这件事由位置本身回答，不靠人记。 */
.hm-botpick {
  margin-top: 11px; padding: 9px 11px; border-radius: 10px;
  background: var(--surface-2); border: 1px solid var(--hairline);
  display: flex; flex-direction: column; gap: 7px;
}
.hm-bp-hint { font-size: 12px; color: var(--label-2); }
.hm-bp-hint.dim { color: var(--label-3); font-size: 11px; }
.hm-bp-hint.bad { color: var(--red); }
/* ⚠️ 名字给一个下限宽度，让**状态标签在各行之间对齐** —— 不对齐时一眼扫不出
   哪台有哪台没有，而这个面板存在的全部意义就是让人一眼看出这件事。
   ⚠️ 用 `min-width` 不用固定宽：名字长了照样撑开（`flex-wrap` 兜住换行）。 */
.hm-bp-row { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
.hm-bp-name { font-size: 13px; color: var(--label); min-width: 9.5em; }
.hm-bp-off { font-size: 11px; color: var(--label-3); }
/* ⚠️ 三档各有各的颜色，「判不出」**不能和「没有」同色** ——
   那两者的处置不同：没有 ⇒ 下发；判不出 ⇒ 先看那台机器的 daemon 报没报 lark_bots。 */
.hm-bp-st { font-size: 11px; padding: 2px 7px; border-radius: 999px; background: var(--surface-1); }
.hm-bp-st.ok { color: var(--green); }
.hm-bp-st.warn { color: var(--orange); }
.hm-bp-st.unk { color: var(--label-3); }
.hm-bp-msg { font-size: 11px; color: var(--label-2); }
.hm-bp-msg.bad { color: var(--red); }

/* —— 机器里的 cc —— */
.hm-ccs { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 11px; }
.hm-cc {
  display: flex; align-items: center; gap: 7px;
  padding: 6px 11px 6px 9px; border-radius: 999px;
  background: var(--surface-2); border: 1px solid transparent; color: var(--label);
  font-size: 12px; cursor: pointer; max-width: 100%;
}
.hm-cc:hover { border-color: var(--hairline-2); background: var(--hairline); }
.hm-cc { position: relative; }        /* 环绕的 ::after 相对它定位 */
.hm-cc.off { opacity: 0.5; }
/* 正在生成：文字走流光 + **那道沿边框转的环绕光**。
   ⚠️⚠️ 环绕**不是自己写的** —— 用的就是 `.agent.gen::after` 那份 conic-gradient
   实现（`.bagent.gen` 早就是这么加进去的，这是第三个）。
   我第一版自造了一个 `box-shadow` 呼吸，机主一眼看出来："我看到的是一个蓝色边框
   在闪烁，而不是有环绕特效" —— **那确实是另一个东西**。
   ⇒ 同一个名字的效果，仓库里已经有实现时，加选择器，别照着感觉再做一个。
   ⚠️ `position: relative` 是那份实现的前提（::after 要相对它定位）。 */
.hm-cc.gen { position: relative; }
/* ⚠️ 减少动效由那份实现自己的 media 块统一关掉（见 `.agent.gen::after` 那段），
   这里只管流光那一半。 */
@media (prefers-reduced-motion: reduce) {
  .hm-cc.gen .hm-cc-n, .hm-cc.gen .hm-cc-s { animation: none; }
}
/* chip 里的环。⚠️ 只改尺寸 —— 颜色、转圈、各档语义全部继承 `.ring` 那一份。 */
.hm-cc .ring { width: 13px; height: 13px; }
.hm-cc-n { font-weight: 540; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* ⚠️ 状态文字 2026-08-14 去掉了（机主：「前面已经有状态的图标了」）——
   状态改由那个环表达，具体内容进 title。 */

.hm-empty { font-size: 12.5px; color: var(--label-3); padding: 2px 0; }
.hm-empty code { font-family: var(--mono); font-size: 11.5px;
  background: var(--surface-2); padding: 1px 5px; border-radius: 4px; }

@media (max-width: 700px) {
  .home { padding: 16px 14px 32px; }
  .hm-acct-r { margin-left: 0; gap: 16px; }
}
/* 首页机器卡上的「升级」按钮。⚠️ 刻意做得**不显眼** —— 它是唯一一个会换掉
   对方进程的按钮，不该长得像个随手可点的东西。 */
.hm-up {
  font-size: 11px; padding: 2px 9px; border-radius: 999px;
  background: transparent; border: 1px solid var(--hairline-2);
  color: var(--label-3); cursor: pointer;
}
.hm-up:hover:not(:disabled) { color: var(--label); border-color: var(--label-3); }
.hm-up:disabled { opacity: 0.55; cursor: default; }
/* 举起来等确认的那一下。⚠️ 颜色要**在按钮上**说清危险度 ——
   这是唯一一个会换掉对方进程的按钮，而它没有弹窗替它解释。 */
.hm-up.danger { color: var(--red); border-color: var(--red); }

/* ————— 飞书 Bot（机主 2026-08-14）————— */
.fx-item { display: flex; align-items: flex-start; gap: 12px; padding: 11px 14px;
  border-top: 1px solid var(--hairline); flex-wrap: wrap; }
.fx-item.off { opacity: .55; }
.fx-item-main { flex: 1; min-width: 0; }
.fx-item-title { font-size: 13.5px; font-weight: 560; display: flex; align-items: center;
  gap: 6px; flex-wrap: wrap; }
.fx-item-sub { font-size: 11.5px; color: var(--label-3); margin-top: 2px; word-break: break-all; }
.fx-item-acts { display: flex; gap: 6px; flex-wrap: wrap; }
.fx-tag { font-size: 10.5px; padding: 1px 6px; border-radius: 999px;
  background: var(--surface-2); color: var(--label-3); font-weight: 400; }
.fx-tag.warn { color: var(--orange); }
.fx-btn.danger { color: var(--red); }
/* 路由多选：一个 Bot 可以投给多个 Agent —— 那正是"Agent 层面的监听" */
.fx-routebox { flex: 1 0 100%; margin-top: 8px; padding: 10px 12px;
  background: var(--surface-2); border-radius: var(--radius-sm); }
.fx-routehint { font-size: 11.5px; color: var(--label-3); margin-bottom: 8px; }
.fx-checks { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 6px; }
.fx-check { display: flex; align-items: center; gap: 6px; font-size: 12.5px; cursor: pointer; }
.fx-check i { color: var(--label-3); font-style: normal; font-size: 11px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* ⚠️ 禁选的那一档（那台机器**真的**没做过 User 授权）。
   ⚠️ 只有这一档禁选 —— "查不动"是第三态，仍然可选（见 app/botchain.py 头注释）：
      把未知压成否定 ⇒ 假警告，而假警告的代价是让人不再看警告。 */
.fx-check.off { opacity: .5; cursor: not-allowed; }
.fx-check.off input { cursor: not-allowed; }
/* 投递对象那一行会同时挂状态徽章和「一键下发」按钮 ⇒ 允许换行，别把名字挤没。 */
.fx-check { flex-wrap: wrap; }
/* ━━━ 投递对象**按机器分组**（机主 2026-08-25 ②）━━━
   ⚠️ 组头自己带状态徽章 —— 不合格的机器是**折叠**不是删掉：
      删掉会让人以为"这台机器没有 Agent"，而且会把已选中的投递对象
      连同 DOM 一起带走（保存读的是 `input:checked`）。 */
.fx-mgrp { margin: 8px 0; padding: 8px 10px; border-radius: var(--radius-sm);
  background: var(--surface-1); border: 1px solid var(--hairline); }
.fx-mgrp.hot { border-color: var(--blue); }
.fx-mhead { display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  font-size: 12.5px; margin-bottom: 4px; }
.fx-msub { font-size: 11px; color: var(--label-3); margin: 2px 0 6px; }
.fx-mfold > summary { font-size: 11.5px; color: var(--label-3); cursor: pointer;
  padding: 4px 0; }
/* 「消息会被丢掉」那句 —— 它和「回落投给主 Agent」必须一眼分得开：
   前者是真的丢，后者照样送到。⭐ 两句合成一句"未配置"正是这次要修的病。 */
.fx-bad { color: var(--red); }
/* 绑定过程的原始输出：正则抓不到链接时，人还能自己从这里点 */
.fx-log { font-family: var(--mono); font-size: 11px; white-space: pre-wrap;
  max-height: 160px; overflow: auto; background: var(--surface-2);
  padding: 8px; border-radius: var(--radius-sm); margin: 6px 0 0; }
/* Bot 头像。⚠️ 取不到图时换成首字母块（见 app.js 的 onerror）—— 裂图比没有图更糟。 */
/* ━━━ 窄屏（≤700px）：功能页两处会散掉，2026-08-15 机主在手机上看出来的 ━━━ */
@media (max-width: 700px) {
  /* ① 飞书 Bot 卡：`.fx-item-main` 是 `flex:1; min-width:0`，三个按钮不换行
        ⇒ 主体被挤到 ~60px，**名字和 app_id 变成一个字一行**（实测「郑其昌的飞书
        CLI」竖排成 5 行）。⇒ 让按钮独占一行，主体就还得回宽度。 */
  .fx-item-acts { flex: 1 0 100%; }
  .fx-item-main { min-width: 140px; }

  /* 列表行同理：`.fx-row` 是 flex + `align-items:center`，主体三行、按钮两个 ⇒
     按钮被垂直居中到中间，左边空出一大块，看着像掉下去了。
     ⚠️ **和上面 `.fx-item-acts` 用同一个解法** —— 同一个页面里同类问题用两种修法，
        下一个人就得先搞清楚这里为什么和那里不一样。 */
  .fx-row { flex-wrap: wrap; }
  .fx-acts { flex: 1 0 100%; justify-content: flex-end; opacity: 1; }

  /* ② 巡检卡：`.fx-rule` 是 flex + wrap，**句子被数字输入框切成碎块**
        （「或它 没在生成 、产出 [5] 轮 且 已隔」然后 [10] 换到下一行）。
        ⇒ 窄屏改成正常文本流：句子照常折行，输入框以 inline 形式待在句子里。
        ⚠️ 只在窄屏改 —— 桌面端那套 flex 对齐是好的，别为了手机把两边都改了。 */
  .fx-rule { display: block; line-height: 2.1; }
  .fx-rule .fx-num,
  .fx-rule input, .fx-rule select { display: inline-block; vertical-align: middle; }
  .fx-rule--head, .fx-rule--note { display: flex; }
}

.fx-avatar { width: 34px; height: 34px; border-radius: 9px; flex: 0 0 auto;
  object-fit: cover; background: var(--surface-2); }
.fx-avatar--txt { display: grid; place-items: center; font-weight: 600;
  font-size: 15px; color: var(--label-2); }
/* 机器卡上的飞书授权面板 */
.hm-auth { flex: 1 0 100%; margin-top: 10px; padding: 12px; text-align: center;
  background: var(--surface-2); border-radius: var(--radius-sm); font-size: 12.5px; }
.hm-auth-hd { color: var(--label-2); margin-bottom: 8px; }
.hm-qr { width: 180px; height: 180px; image-rendering: pixelated;
  background: #fff; padding: 8px; border-radius: 8px; }
.hm-auth-url { margin: 8px 0; font-size: 11px; word-break: break-all; }
.hm-auth-url a { color: var(--blue); }

/* 机器卡上的 Claude 账户面板（存档 + 一键切号，机主 2026-08-23）。
   ⚠️ `.ok/.warn/.bad` 全局没有定义（现有的都带前缀，如 `.modal .note.bad`）——
      所以在这个作用域下自己定义，别引用不存在的东西：CSS 引用不存在的类/变量
      **不报错、静默回落**，颜色会悄悄变成继承色而看起来"只是不太对"。 */
.hm-cred-panel { flex: 1 0 100%; margin-top: 10px; padding: 10px 12px;
  background: var(--surface-2); border-radius: var(--radius-sm); font-size: 12.5px; }
.hm-cred-panel .ok { color: var(--green); }
.hm-cred-panel .warn { color: var(--orange); }
.hm-cred-panel .bad { color: var(--red); }
.hm-cred-hd { color: var(--label-2); padding-bottom: 8px; margin-bottom: 6px;
  border-bottom: 1px solid var(--hairline); display: flex; align-items: center;
  gap: 6px; flex-wrap: wrap; }
.hm-cred-run { color: var(--orange); font-size: 11.5px; }
.hm-cred-row { display: flex; align-items: center; gap: 8px; padding: 5px 0;
  border-bottom: 1px solid var(--hairline-2); }
.hm-cred-row:last-of-type { border-bottom: 0; }
/* 正在用的那一个要一眼看出来 —— 否则清单里全是名字，分不清哪个是当前的 */
.hm-cred-row.cur { background: color-mix(in srgb, var(--green) 9%, transparent);
  margin: 0 -12px; padding-left: 12px; padding-right: 12px; }
.hm-cred-sub, .hm-cred-exp { color: var(--label-2); font-size: 11.5px; }
.hm-cred-fp { color: var(--label-2); font-size: 10.5px; opacity: .65;
  font-family: var(--mono, ui-monospace, monospace); }
.hm-cred-no { color: var(--label-2); font-size: 11.5px; opacity: .8; }
.hm-cred-empty { color: var(--label-2); padding: 8px 0; }
.hm-cred-save { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--hairline); }
.hm-cred-in { flex: 0 1 180px; padding: 4px 8px; font-size: 12px;
  background: var(--surface-1); color: var(--label);
  border: 1px solid var(--hairline); border-radius: 6px; }
/* 结果条：替代 alert（飞书 webview 里原生弹窗可能不弹，且关掉就没了）。
   ⚠️ 切换成功那句「Agent 还在用旧号」要留在眼前，所以它是常驻的一块，不是 toast。 */
.hm-cred-msg { margin-top: 8px; padding: 7px 9px; border-radius: 6px;
  font-size: 12px; line-height: 1.6; background: var(--surface-1); }
.hm-cred-msg.ok { color: var(--green);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--green) 30%, transparent); }
.hm-cred-msg.warn { color: var(--orange);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--orange) 38%, transparent); }
.hm-cred-msg.bad { color: var(--red);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--red) 38%, transparent); }
/* ⚠️ 这句警告是这块屏最重要的一行（换文件≠换账号），别让它看起来像脚注 */
.hm-cred-warn { margin-top: 8px; color: var(--orange); font-size: 11.5px; line-height: 1.5; }
/* 卡片头上那颗「CC <账号>」标签：认得出是存档里哪个号时才点绿 */
.hm-cred.ok { color: var(--green); }

/* 新建 Agent 弹窗的表单行。⚠️ 复用 .modal 的既有排版，只加标签+控件的竖排 ——
   另造一套会让同一个弹窗系统里的两个弹窗看起来不像一家的。 */
.nw-l { display: flex; flex-direction: column; gap: 5px; margin: 12px 0;
        font-size: 12.5px; color: var(--label); }
.nw-l .sub { display: inline; margin-left: 6px; font-size: 11.5px; }
.nw-l select, .nw-l textarea, .nw-l input[type="text"] {
  font: inherit; padding: 7px 9px; border-radius: 7px;
  border: 1px solid var(--sep); background: var(--surface); color: var(--label);
}
.nw-l textarea { resize: vertical; min-height: 54px; }
.hm-up[disabled] { opacity: .45; cursor: not-allowed; }

/* 「正在跑」：详情页「在跑」那一节的第一行，headless 唯一的"此刻"信号。
   ⚠️ 比下面几行重一档（它回答的是"它此刻在干嘛"，其余是"背景里还有什么"），
      但**不要做成呼吸/闪烁** —— 那一栏本来就每几秒重绘一次，动效会一直重启，
      看着像卡住。用颜色区分就够。 */
.rt-now .rt-live { background: color-mix(in srgb, var(--green) 18%, transparent);
                   color: var(--green); font-weight: 590; }
.rt-now .rt-cmd { color: var(--label); }
.rt-more { font-size: 11px; color: var(--label-2); flex-shrink: 0; }
.rt-prog { font-size: 11.5px; color: var(--label-2); margin: -2px 0 6px 0;
           padding-left: 8px; border-left: 2px solid var(--sep); }

/* 跨账户授权卡的脚注：比一般脚注多一点行高和对比度 —— 它是**授权前必须读完**的，
   不是可看可不看的补充。 */
#fx-grant .fx-notes span, #fx-account .fx-notes span { color: var(--label-2); }

/* ⚠️⚠️ 这里原来还有一条 `#fx-grant select { ... }`，**是移动端排版坏掉的原因**：
      它是 **ID 选择器**，特异性高过 `.fx-form select`，于是把标准表单样式
      整条顶掉了 —— `width:100%` / `height:var(--fx-h)` / `border-radius` 全没了
      ⇒ 那两个下拉框不填满栅格、和页面上其它下拉框长得不一样，窄屏尤其明显。
   ⇒ **删掉，让它继承 `.fx-form select`。**
      给单张卡"微调"时别用 ID 选择器 —— 它赢得太容易，而你不会注意到自己赢了什么。
   （`.fx-rowmain` 也一起删了：那条是给第一版行结构写的，现在没人用了。） */


/* ——— PR 卡上的 CD 状态（部署 / 非 check 的 workflow）———
   ⚠️ 和上面那排 check 计数**分开一行**：它们来源不同（check_run vs workflow_run），
      并排会让人以为是同一套东西的两个数字。 */
.pr-cds { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.pr-cd {
  font-size: 11px; padding: 1px 8px; border-radius: 999px; text-decoration: none;
  border: 1px solid var(--hairline-2); color: var(--label-2); background: var(--surface-2);
  white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
}
.pr-cd.ok  { color: var(--green); border-color: color-mix(in srgb, var(--green) 40%, transparent); }
.pr-cd.bad { color: var(--red);   border-color: color-mix(in srgb, var(--red) 40%, transparent); }
/* 在飞行中：用和"运行中"一致的蓝，别用灰 —— 灰会被读成"没结论/不重要"，
   而它其实是"正在发生，等一下再看"。 */
.pr-cd.run { color: var(--blue);  border-color: color-mix(in srgb, var(--blue) 40%, transparent); }
/* 既没成功也没失败也不在飞（被取代 / 被顶掉）——保持默认灰，别用绿。
   绿会被读成"这步过了"。 */
.pr-cd.meh { color: var(--label-3); }
.pr-cd:hover { border-color: currentColor; }

/* ⚠️⚠️ CI 绿但 **CD 挂了** 的卡：整体观感必须不是"没事"。
   机主 2026-08-15 原话的要害是「它挂了 ci.done 一声不响，**而看板上还是绿的**」——
   只加一个小红标签不够，绿边框本身就在说"没问题"。
   ⚠️ 圆环**保持绿**：那是 check 的结论，check 确实全绿了。改它才是说谎。
      变的只有边框和那条左边线 —— "整体状态"和"CI 结论"是两个信号。 */
.pr.cd-bad { border-color: color-mix(in srgb, var(--red) 55%, transparent); }
.pr.cd-bad::before { background: var(--red); }

/* ——— 圆环的悬浮面板：逐个 check 的名字与状态（机主 2026-08-16）———
   ⚠️ 挂在 <body> 上，所以位置全靠 JS 算；这里只管长相。 */
.ck-pop {
  position: fixed; z-index: 90; max-width: min(420px, calc(100vw - 16px));
  max-height: 60vh; overflow-y: auto;
  /* ⚠️⚠️ **必须用 `--pop`，不能用 `--surface`**（机主 2026-08-16：「半透明，看不清」）。
     `--surface-1/2` 是 `rgba(255,255,255,0.045)` 这种**分层材质**，故意半透明 ——
     底下的卡片会从字后面透上来。仓库里早就为浮层备了一对不透明值
     （`--pop` / `--card-solid`），第 40 行那段注释写得清清楚楚：
     「浮在图表上方的提示框不能用它们 —— 曲线会从字底下穿过去」。
     ⇒ 我照样用了 `--surface`。**已经踩过并解决的坑**，
       判据是「这东西浮不浮在别的东西上」，浮就用 --pop。 */
  background: var(--pop); border: 1px solid var(--hairline);
  border-radius: 12px; padding: 8px 0 6px;
  box-shadow: 0 10px 34px rgba(0,0,0,.16), 0 2px 8px rgba(0,0,0,.08);
  font-size: 12px;
}
.ck-top {
  padding: 2px 12px 8px; font-weight: 600; color: var(--label);
  border-bottom: 1px solid var(--hairline); margin-bottom: 4px;
}
.ck-sum { display: block; font-weight: 400; color: var(--label-3); font-size: 11.5px; margin-top: 2px; }
.ck-h { padding: 7px 12px 3px; font-size: 11px; color: var(--label-3); letter-spacing: .02em; }
.ck-h b { color: var(--label-2); font-weight: 600; }
.ck {
  display: flex; align-items: center; gap: 7px; padding: 4px 12px;
  text-decoration: none; color: var(--label);
}
/* ⚠️ 条目的 hover 底用半透明是安全的 —— 它盖在**不透明的** --pop 上，不透。 */
.ck:hover { background: var(--surface-2); }
/* ⚠️ 名字要能挤扁并省略 —— NAC 的 check 名长到 "Build images for
   shared-cluster-test (push to docker.io)"，不省略会把面板撑成一条。 */
.ck-n { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ck-w { color: var(--label-3); font-size: 11px; flex: 0 0 auto; max-width: 40%;
        overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ck-dot { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto; background: var(--label-3); }
.ck-dot.bad { background: var(--red); }
.ck-dot.ok  { background: var(--green); }
.ck-dot.run { background: var(--blue); }
.ck-dot.meh { background: var(--label-3); }
.ck-more {
  display: block; width: calc(100% - 16px); margin: 6px 8px 2px; padding: 5px 8px;
  border: 1px dashed var(--hairline-2); border-radius: 8px; background: transparent;
  color: var(--label-2); font-size: 11.5px; font-family: inherit; cursor: pointer;
}
.ck-more:hover { border-color: var(--label-3); color: var(--label); }
.ck-empty { padding: 10px 12px; color: var(--label-3); }


/* ————————————— 交付物在线查看（机主 2026-08-20）—————————————
   「md 文件点击后直接展示，展示页面里面可以下载」。
   ⚠️ 几何复用 `#term` 那套（`dialog` + `.dlg-head`）—— 那个调好了，别另造。

   ⚠️⚠️ **下面每一条 `.md*` 都必须限定在 `#dv` 里。**
   `.md` / `.md-h` / `.md-p` / `.md-pre` / `.md-t` 这些类**平台早就在用**
   （聊天消息走同一个 `mdToHTML`，样式在 1714 行附近）。
   我第一版直接写 `.md .md-h { … }`，那是**全局覆盖聊天消息的排版** ——
   而且改的是字号行高这种"看着还行"的东西，**不会有任何报错**。
   ⇒ 一份报告要的排版（78ch 正文、大标题、宽表格）和聊天气泡里要的
     根本不是一回事，它们只是**碰巧共用一个渲染器**。共用渲染器 ≠ 共用样式。

   ⚠️ 表格的类名是 **`.md-t`** 不是 `.md-table`（见 `mdTable`）——
   我第一版写错了，那条 CSS 是死的、而表格照样宽得撑破弹窗。
   判据：**写完样式去 JS 里核对一遍类名**，别照着自己脑子里的名字写。 */
/* ⚠️⚠️ **必须写 `#dv[open]`，不能写 `#dv`**（机主 2026-08-20 报「消息页面又可以下滑」）。
   UA 样式表用 `dialog:not([open]) { display: none }` 隐藏关着的弹窗 ——
   直接给 dialog 设 `display: flex` 会把它**常驻显示**、撑高整个文档，
   于是页面多出一段可以下滑的空白，顶栏被推出视口。
   ⚠️ 而且 `#dv` 的特异性是 (1,0,0)，**比 1185 行那条正确的 `dialog[open]` 还高**
      —— 所以连已有的正确写法都盖不住它。
   ⭐ 这个坑第 1183 行的注释**一字不差地警告过**，我还是又犯了一次。
     ⇒ 光写注释挡不住，加了测试 `test_dialog_display.py` 钉住。 */
#dv[open] { display: flex; flex-direction: column; }
.dv-name {
  font-weight: 600; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; max-width: 46vw;
}
/* 下载按钮是 <a download>，不是 button —— 样式要自己给（flex 那条已在上面一并处理）。 */
#dv-dl {
  padding: 5px 12px; border-radius: 7px; font-size: 12.5px;
  border: 1px solid var(--hairline-2); color: var(--label);
  text-decoration: none; background: var(--surface-1);
}
#dv-dl:hover { background: var(--surface-2); }
.dv-body { flex: 1 1 auto; overflow: auto; padding: 20px 28px 40px; }
.dv-err { color: var(--red); font-size: 13px; line-height: 1.7; }
.dv-err a { color: var(--blue); }

/* ⚠️ 正文宽度收住：一份报告全宽铺开会到 1400px，一行 200 字没人读得下去。
   65 个字符左右是长期被验证的舒适区。 */
/* ⚠️ 宽度（机主 2026-08-20：「左右边距太大了，利用一下」）。
   第一版 78ch 是照英文正文的经验值定的 —— 而 `ch` 是 `0` 字符的宽度，
   **一个中文字约等于 2ch** ⇒ 78ch 只有 39 个中文字一行，比中文的舒适区窄一半。
   实测截图：弹窗内容区约 1320px，正文只占 915px，两侧各空 200px。
   ⇒ 放到 104ch（约 50 个中文字/行），并把 padding 收窄一点。
   ⚠️ 仍然**保留上限**，不铺满：全宽在 27 寸屏上一行能到 200 字，那是另一个极端。 */
#dv .md { max-width: 104ch; margin: 0 auto; font-size: 14px; line-height: 1.75; color: var(--label); }
#dv .md .md-h { font-weight: 650; line-height: 1.35; margin: 1.6em 0 .6em; }
#dv .md h1.md-h { font-size: 1.6em; } #dv .md h2.md-h { font-size: 1.32em; }
#dv .md h3.md-h { font-size: 1.13em; } #dv .md h4.md-h { font-size: 1em; }
#dv .md .md-h:first-child { margin-top: 0; }
#dv .md .md-p { margin: .7em 0; }
#dv .md .md-list { margin: .6em 0; padding-left: 1.5em; }
#dv .md .md-list li { margin: .25em 0; }
#dv .md .md-q {
  margin: .8em 0; padding: .1em 0 .1em 1em;
  border-left: 3px solid var(--hairline-2); color: var(--label-2);
}
#dv .md .md-hr { border: 0; border-top: 1px solid var(--hairline); margin: 1.8em 0; }
#dv .md code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .89em; padding: .12em .38em; border-radius: 4px;
  background: var(--surface-2);
}
#dv .md-pre {
  margin: .9em 0; padding: 12px 14px; border-radius: 9px;
  background: var(--surface-2); overflow-x: auto;
  font-size: 12.5px; line-height: 1.6;
}
#dv .md-pre code { background: none; padding: 0; font-size: inherit; }
/* ⚠️ 表格**必须能横向滚**，且滚的是它自己的容器 —— 平台自己产的报告几乎全是表格，
   有些五六列，全宽塞进 78ch 会把页面撑得横向滚动。 */
#dv .md-tw { overflow-x: auto; margin: 1em 0; }
#dv .md .md-t { border-collapse: collapse; font-size: 13px; min-width: 100%; }
#dv .md .md-t th, #dv .md .md-t td {
  border: 1px solid var(--hairline); padding: 6px 10px;
  text-align: left; vertical-align: top;
}
#dv .md .md-t th { background: var(--surface-2); font-weight: 600; white-space: nowrap; }
#dv .md a { color: var(--blue); }

@media (max-width: 760px) {
  .dv-body { padding: 14px 14px 32px; }
  .dv-name { max-width: 38vw; }
  #dv .md { font-size: 15px; }        /* 手机上正文要大一点 */
}

/* 功能面板里「机器上的 systemd 定时脚本」那一小节（机主 2026-08-20）。
   ⚠️ 视觉上要**明显弱于**平台自己的定时任务 —— 它是盲区清单，不是并列的另一种选择。 */
.fx-sub {
  margin: 14px 0 6px; font-size: 11.5px; color: var(--label-3);
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
}
.fx-sub .fx-hint { color: var(--label-3); opacity: .8; }
.fx-row--ro { opacity: .74; }
.fx-row--ro:hover { opacity: 1; }

/* PR 环的悬浮里那行变更量（机主 2026-08-20：「显示 PR 的变更量，例如 +225 -8」）。
   ⚠️ `tabular-nums`：这一行每轮刷新都可能变，非等宽数字会让它左右跳。 */
.ck-diff {
  display: inline-flex; align-items: baseline; gap: 6px;
  margin-left: 8px; font-size: 11.5px;
  font-variant-numeric: tabular-nums;
}
.ck-diff b { font-weight: 600; }
.ck-diff .add { color: var(--green); }
.ck-diff .del { color: var(--red); }
.ck-diff i { color: var(--label-3); font-style: normal; }

/* 「压缩中…」（机主 2026-08-20）。⚠️ 用中性色不用告警色：压缩是**正常**的，
   不需要人做任何事 —— 染成红/黄会让人以为出问题了。 */
.ctx.ctx-compact { color: var(--label-2); font-weight: 500; letter-spacing: -0.01em; }

/* 机器卡片上的 agent CLI 状态（daemon 探的；机主 2026-08-20）。
   ⚠️ 三态**必须视觉可分**：装了有凭证 / 装了缺凭证 / 没装。
      中间那态最容易被忽略，而它恰恰是"看起来就绪、实际一行活都干不了"的那种 —— 
      所以给它警示色，不要和"没装"共用一个灰。 */
.hm-cli { font-size: 11px; padding: 1px 6px; border-radius: 999px;
          border: 1px solid transparent; white-space: nowrap; }
.hm-cli.ok   { color: var(--ok, #2f855a);
               background: color-mix(in srgb, var(--ok, #2f855a) 10%, transparent); }
.hm-cli.warn { color: var(--warn, #b7791f);
               background: color-mix(in srgb, var(--warn, #b7791f) 14%, transparent);
               border-color: color-mix(in srgb, var(--warn, #b7791f) 32%, transparent); }
.hm-cli.off  { color: var(--muted, #8a8f98); opacity: .65; }
/* 「＋装」按钮：由 CLI 芯片变来，所以继承 .hm-cli 的形状 */
.hm-cli.act { cursor: pointer; border-color: color-mix(in srgb, currentColor 30%, transparent); }
.hm-cli.act:hover { opacity: 1; border-color: currentColor; }
.hm-cli.act.warn { color: var(--warn, #b7791f); opacity: 1;
                   border-color: color-mix(in srgb, var(--warn, #b7791f) 55%, transparent); }
.hm-cli.act[disabled] { cursor: default; opacity: .6; }

/* 账户额度标签（5 小时 / 7 天）。⚠️ 「旧数据」必须在视觉上就弱下去 ——
   人扫一眼状态栏时不会去读 tooltip，而一个过时的百分比看起来和实时值一模一样。 */
.hm-usage.warn { color: var(--orange); }
.hm-usage.bad { color: var(--red); }
.hm-usage.old { opacity: .5; font-style: italic; }

/* 扳手图标：跟着文字基线走，别把行高撑开（机主 2026-08-23 换掉「做了 N 件事」）。 */
.acts-bar .lbl .wr { vertical-align: -1.5px; margin-left: 2px; opacity: .85; }

/* ⚠️⚠️ **折叠态把工具调用一并折进去**（机主 2026-08-23：
   「如果工具调用前面一条是终端消息，就把这个工具调用合并到那个终端消息里面，
   也就是展开终端消息才会在终端消息展开框的底部看到可展开的工具调用」）。

   ⭐ 它们**本来就是同一条消息**（一轮里"说了什么"+"调了什么工具"），
   只是折叠态下条和工具条各占一行 ⇒ 视觉上成了
   `条A 工具A 条B 工具B` 交替，看起来像四条独立消息。
   ⇒ 折叠时只留一行（那条消息说了什么），工具调用跟着正文一起收进去。

   ⚠️ `:not(.nobody)` 不能省：**只调了工具、没说话**的那一轮，
      工具条就是它的全部内容 —— 一起藏掉的话那条消息什么都不剩。
      （那种消息现在也不再生成折叠条了，见 app.js 里 `可折` 那段。） */
.msg.foldable.folded:not(.nobody) .acts { display: none; }

/* ⚠️ **折叠条稍微大一点、颜色深一点**（机主 2026-08-23：工具调用折进去之后
   每条只剩一行，腾出了空间）。
   ⚠️ 只改**折叠态** —— 展开态那一行是「署名 · 时间」，属于次要信息，
      跟着变大会喧宾夺主（它在正文正上方）。
   ⚠️ `--label-3` → `--label-2`：**两个主题下 label-3 都是更不显眼的那档**
      （深色 #636366 比 #98989d 暗，浅色 #aeaeb2 比 #6e6e73 浅）——
      换成 label-2 在明暗两套下方向一致，不用分别调。 */
.msg.foldable.folded .echo-bar {
  padding: 3px 9px;
  font-size: 12.5px;
  color: var(--label-2);
}
/* 预览那半句也提上来一点 —— 它才是这一行真正要读的内容 */
.msg.foldable.folded .echo-bar .pv { opacity: .82; }

/* ————— 授权框的逃生口（机主 2026-08-24：「再加一个 ESC 按钮」）—————
   ⚠️ 摆在选项**外面**、样式压低：它不是"第 N 个选项"，选错的代价不一样。
   ⚠️ 但也**不能藏起来**（不用 hover 才出现那套）——它要救的正是
      "点什么都没反应"的场景，那时人已经在焦虑了，再让他找一个隐藏按钮是残忍的。
      ⭐ 同「结束归档」那条：**状态/出口要常显，普通动作才可以藏。** */
.auth-esc-row {
  display: flex; align-items: center; gap: 10px;
  margin-top: 8px; padding-top: 8px;
  border-top: 1px solid var(--hairline);
}
.auth-esc {
  flex: 0 0 auto; padding: 4px 10px;
  border: 1px solid var(--sep, rgba(0,0,0,.12)); border-radius: 7px;
  background: var(--bg-elev, rgba(255,255,255,.6)); cursor: pointer;
  font-size: 12px; line-height: 1.4; color: var(--label-2);
  transition: background .15s ease, color .15s ease, border-color .15s ease;
}
.auth-esc:hover:not(:disabled) { border-color: var(--label-3); color: var(--label-1); }
.auth-esc:focus-visible { outline: 2px solid var(--blue, #0a84ff); outline-offset: 1px; }
.auth-esc:disabled { opacity: .5; cursor: default; }
.auth-esc-note { font-size: 11.5px; color: var(--label-3); }

/* 新建 Agent 表单的即时查重提示（机主 2026-08-24）。
   ⚠️ 三档颜色是有分工的，别合并：
     ok   = 可以用 / 会自动生成 —— 中性，不该抢注意力
     warn = **撞了但仍然能创建**（后端会自动换名）—— 这是最要紧的一档，
            它说的是"会发生什么"，不是"不许"
     bad  = 名字不合法，点了必然失败 */
.nw-hint {
  min-height: 15px; margin: -6px 0 8px; padding: 0 2px;
  font-size: 11.5px; line-height: 1.35; color: var(--label-3);
}
.nw-hint--ok   { color: var(--label-3); }
.nw-hint--warn { color: var(--orange, #ff9f0a); }
.nw-hint--bad  { color: var(--red); }

/* ═══ 通知投递路由表（机主 2026-08-25：「投递到飞书还是 APP，还是都要」）═══
   ⚠️ 放在文件末尾是为了**少和别人撞**：这份 css 同时有别的 cc 在改。 */
.nrt-wrap { overflow-x: auto; }          /* ⚠️ 表格自己横滚，别让整页横滚 */
.nrt { width: 100%; border-collapse: collapse; font-size: 13px; min-width: 320px; }
.nrt th, .nrt td { padding: 9px 10px; border-bottom: 1px solid var(--hairline); text-align: left; }
.nrt thead th { font-size: 11px; letter-spacing: .06em; color: var(--label); opacity: .55;
                font-weight: 500; text-transform: uppercase; white-space: nowrap; }
.nrt tbody tr:last-child td { border-bottom: 0; }
.nrt .nrt-c { text-align: center; width: 92px; }
.nrt-k { display: block; font-size: 11px; opacity: .42; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.nrt-na { display: block; font-size: 10px; color: var(--orange); text-transform: none;
          letter-spacing: 0; opacity: .9; }

/* 开关。⚠️ 不可用的通道**照样能配**（配置是意愿不是执行），只是画成虚线边框 ——
   配了不生效却不说，正是这一版最该避免的那种错。 */
.nrt-sw { width: 40px; height: 23px; border-radius: 999px; border: 1px solid var(--sep);
          background: transparent; padding: 0; position: relative; cursor: pointer;
          transition: background .15s ease, border-color .15s ease; }
.nrt-sw .nrt-dot { position: absolute; top: 2px; left: 2px; width: 17px; height: 17px;
                   border-radius: 50%; background: var(--label); opacity: .45;
                   transition: transform .15s ease, opacity .15s ease; }
.nrt-sw.on { background: var(--green); border-color: var(--green); }
.nrt-sw.on .nrt-dot { transform: translateX(17px); opacity: 1; background: #fff; }
.nrt-sw.na { border-style: dashed; }
.nrt-sw.na.on { background: color-mix(in srgb, var(--green) 42%, transparent); }
.nrt-sw:disabled { opacity: .5; cursor: progress; }
.nrt-sw:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .nrt-sw, .nrt-sw .nrt-dot { transition: none; }
}
/* ━━━ PR 那一栏顶上的「这些 CI 通知发给谁、由谁发」（机主 2026-08-25）━━━
   ⚠️ 入口**故意长在这里**，不在设置页：他的判据是「信息/入口该放在【使用它的
      上下文】里，不是放在【检索得到的地方】」。
   ⚠️ 视觉上要**比 PR 卡片轻**——它是这一栏的注脚，不是又一张卡；
      做得跟卡片一样重会把注意力从"哪个 PR 红了"上抢走。 */
.pr-nt {
  position: relative; display: flex; align-items: center; gap: 8px;
  padding: 3px 4px 4px; font-size: 11.5px; line-height: 1.4; color: var(--label-3);
}
.pr-nt-t { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pr-nt-t b { color: var(--label-2); font-weight: 600; }
.pr-nt .bad, .pr-nt-msg.bad { color: var(--red); }
.pr-nt-b {
  flex: 0 0 auto; padding: 1px 7px; border-radius: 6px; font-size: 11px;
  border: 1px solid var(--sep, rgba(0,0,0,.12)); background: transparent;
  color: var(--label-2); cursor: pointer;
}
.pr-nt-b:hover { color: var(--label-1); border-color: var(--label-3); }
/* 面板挂在 `.pr-nt` 里而不是 body 上：这一栏没有 overflow 裁剪，
   而挂 body 要自己算锚点位置、还要跟着滚动 —— 那是给一个每月点一次的
   设置项付的持续成本。 */
.pr-nt-pop {
  position: absolute; z-index: 30; top: 100%; left: 0; right: 0;
  display: flex; flex-direction: column; gap: 6px;
  padding: 9px 10px; margin-top: 2px; border-radius: 9px;
  border: 1px solid var(--sep, rgba(0,0,0,.12));
  background: var(--bg-elev, #fff); box-shadow: 0 8px 24px rgba(0,0,0,.14);
}
.pr-nt-row { display: flex; align-items: center; gap: 6px; }
.pr-nt-row > label { flex: 0 0 62px; color: var(--label-3); }
.pr-nt-row input, .pr-nt-row select {
  flex: 1 1 0; min-width: 0; padding: 3px 6px; font-size: 11.5px;
  border-radius: 6px; border: 1px solid var(--sep, rgba(0,0,0,.12));
  background: var(--bg, #fff); color: var(--label-1);
}
.pr-nt-acts { flex-wrap: wrap; }
.pr-nt-acts .spacer { flex: 1 1 auto; }
.pr-nt-cands { display: flex; flex-direction: column; gap: 3px; max-height: 160px; overflow: auto; }
.pr-nt-cand {
  display: flex; gap: 6px; align-items: center; text-align: left;
  padding: 3px 6px; border-radius: 6px; cursor: pointer;
  border: 1px solid transparent; background: transparent; color: var(--label-2); font-size: 11.5px;
}
.pr-nt-cand:hover { border-color: var(--sep, rgba(0,0,0,.12)); color: var(--label-1); }
.pr-nt-msg { color: var(--label-3); font-size: 11px; }


/* 重启那张卡的结果区（功能页 `#fx-restart`）。
   ⚠️ id 用 `fx-restart` 不用 `fx-rules` —— 后者已经是**巡检卡里的一个 class**
      （`.fx-rules` / `.fx-rules--sub`），撞上不会报错但会让人读错。
   ⚠️ 结果画进卡里而**不用 alert**：alert 关掉就没了，而"跳过了谁、为什么"
      正是这块屏最需要留住的一句（同 `.fx-card` 里别处那条注释）。 */
.fx-out {
  margin-top: 10px; padding: 10px 12px; border-radius: 8px;
  background: var(--surface-1); border: 1px solid var(--line);
  font-size: 13px; line-height: 1.7; white-space: pre-wrap; word-break: break-word;
}
.fx-out.bad { border-color: var(--red); }
