/* 浅色 palette：默认层与显式 light 共用同一声明，权威唯一 */
:root,
:root[data-theme="light"]{
  color-scheme:light;
  /* Claude 风格基础 token */
  --bg:#f9f8f6;          /* 主背景 米白 */
  --bg-deep:#f0eeea;     /* 更深面（侧栏） */
  --panel:#ffffff;       /* 浮层/卡片 */
  --panel-2:#faf9f7;
  --input:#ffffff;
  --line:#e4e0da;
  --line-soft:#edeae5;
  --line-strong:#d4cfc6;
  --ink:#38352f;         /* 主文字 暖黑 */
  --ink-soft:#6d6a63;
  --ink-faint:#9c978e;
  --ink-strong:#221f1a;
  --accent:#d97757;      /* Claude 橙红 */
  /* 浏览器 chrome 颜色：与浅色主背景同色，理由同深色 */
  --theme-color:#f9f8f6;
  --accent-hover:#c96444;
  --accent-ink:#ffffff;
  --danger:#b3563f;
  /*
   * 警示/注意色（黄）。定义成主题令牌而不是各处写 #c98a2e 字面量：
   * 深色主题下同一个黄必须更亮才够对比，散落的字面量会让深色模式
   * 悄悄沿用浅底的黄（看不清），而没有任何地方会报错。
   * 已有引用（.msg-edit-warn / .to-wf-info.is-warn）保持字面量不动，
   * 新代码一律用本令牌。
   */
  --warn:#c98a2e;
  --radius:10px;
  /*
   * 内容列宽（唯一权威）。
   *
   * 消息区、会话配置条（系统提示词 / 设置）、输入框三处必须同宽同位——
   * 它们在同一条纵向轴上，宽度不一致时视觉上会显得"错位"。
   * 用户明确要求过"系统提示词控件对齐消息区域宽度就好"，
   * 在此之前三处各写了一个魔法数字（820 / 860 / 无限制），
   * 所以这里收敛成一个变量，改一处三处同时对齐。
   */
  --content-col:820px;
  /*
   * 编排区内容列宽上限（唯一权威）。
   *
   * 为什么与 --content-col 分开：那两个量**语义不同**，不是同一个数的两份写法。
   *   --content-col  是**阅读栏宽**：聊天页的消息流是一篇文章，
   *                  行太长会丢失阅读节奏，所以要有一个绝对上限（820px）。
   *   本变量          是**工作区栏宽**：编排页的内容是表单、输入框、
   *                  消息块与结果表（含多列表格），它该占多少宽取决于
   *                  **用户把版面留给了它多少**。
   *
   * ★ 默认 100%：不设绝对上限，让"版面"成为唯一权威（2026-10-03 修复）★
   *   改前这里是硬编码的 max-width:min(760px,100%)。760 是一个绝对上限，
   *   它对版面**无感知**：用户把左栏/设置区/右栏全收起来腾出的空间，
   *   内容一概不要（实测 1920 窗口下编排区可用 1553px，内容只占 760px，
   *   两侧共浪费约 790px）——于是"面板可以收起"这件事对主内容毫无价值，
   *   用户的原话正是"这样收缩就没有意义了"。
   *
   *   结构性判断：在编排页，**栏宽的上限该由版面自己决定**。
   *   版面的宽度是用户亲手调的（收面板、拖分隔条），那就是他对这份上限的
   *   表达；再叠一个他看不见、也改不动的绝对数字，等于给他的操作加一个
   *   不可见的否决权——这正是"第二份真相"的典型形态。
   *   需要收紧时改**这一处**即可（整个编排区的内容列都听它的），
   *   但默认值必须是"铺满"，否则"收起面板"这个动作就重新失去意义。
   */
  --orch-col-max:100%;
  /*
   * 消息区字体缩放系数（唯一权威）。
   *
   * 由 msg-font-init.js 写在内联样式上（首帧前生效、刷新不闪），
   * 这里声明的只是"没有 JS 时的兜底值"，两者不一致时以内联值为准。
   * 只影响 .msg 的正文（用户 / 助手两种基数字号），不改变按钮、头像、
   * 用量 chip 等界面元素的字号——那些是"控件"，不是"内容"。
   */
  --msg-font-scale:1;
  /*
   * 浮层层级令牌（唯一权威，别在各处写魔法数字）。
   * / 菜单必须高于输入区与普通卡片、低于模态弹窗——
   * 之前它的 z-index 是散落的 80，与别的浮层谁压谁只能靠猜。
   */
  --z-slash:80;
  /*
   * 弹窗是**两层**，两者是一组不可拆的令牌（分派逻辑见 modal-stack.js）：
   *
   *   --z-modal        普通弹窗（页面级；同一时刻只显示一个）
   *   --z-modal-child  从**已经打开的弹窗内部**再打开的弹层
   *                    （只读查看器 / 变量弹窗 / 工作流块弹窗 …）
   *
   * 不变量：后者严格大于前者、且小于 / 菜单（80）——因为 / 菜单是能在
   * 任意弹窗里召出的最上层浮层。
   *
   * 为什么必须登记成令牌，而不是让各弹层自己写数字：
   *   .modal 一律是 z-index:60，"谁压谁"就退化成 **DOM 顺序**。
   *   两个实测故障都源于此：
   *     · 展开在弹窗里的只读查看器沿用普通 .panel 的 31 → 落在弹窗背后，
   *       用户点完看不到任何东西（以为按钮坏了）；
   *     · 临时会话块弹窗里用 / 插变量 → 变量弹窗同为 60，而块弹窗挂在
   *       运行时新建的 #bkModalRoot（DOM 更靠后），于是变量弹窗被压在
   *       块弹窗**底下**，只在边缘露出一条，完全无法操作。
   *   两者的共同根因是"层级由 DOM 顺序决定"。所以层级在这里登记成令牌，
   *   并由 modal-stack.js 按**运行时事实**（打开这一刻是否已有弹窗在显示）
   *   自动分派——各弹窗的开与关只调用它，不自行声明层级。
   */
  --z-modal:60;
  --z-modal-child:65;   /* 从已打开弹窗内部再打开的弹层：高于弹窗、低于 / 菜单 */
  /*
   * 移动端底部抽屉。
   *
   * 压在页面内容与左侧工作流抽屉之上，但低于模态弹窗（60）——
   * 抽屉里点「＋ 快照」会打开命名弹窗，那个弹窗必须盖在抽屉上面。
   * 登记成令牌的理由同上面的 modal：谁压谁必须可读、不可能各写各的。
   */
  --z-sheet:50;
  /*
   * 左侧工作流抽屉（窄屏 ≤760px 时的 .orch-side）。
   *
   * ★ 为什么必须是一个**显式令牌**，而不是随手写 40 ★
   *   2026-10-06 用户报"编排页顶部按钮被遮住了、根本没法新建工作流"：
   *   抽屉里的 `.brand` 行（工作流标题 + ＋新建 + 收起）整行不见，
   *   列表直接从「编排列表」这行开始。
   *   根因是层级：抽屉写死 z-index:40，而顶栏为了让它自己的 ⋯ 面板
   *   浮在内容之上，后来加了 z-index:45（--z-head-overlay）。
   *   45 > 40 ⇒ 顶栏整块画在抽屉上面，把抽屉顶部那一行盖掉了。
   *   两处各自都是"局部看起来正确"的值，拼在一起才错——
   *   这正是层级必须集中在令牌里、且顺序有明确约束的原因。
   *
   * 取值 48 的约束（三条同时成立，不得只满足其中一条）：
   *   ① > --z-head-overlay(45)：抽屉是"盖住整页（含顶栏）"的一层，
   *      它连顶栏一起盖，才能保证抽屉自己的关闭按钮与标题永远可点
   *      （顶栏 ☰ 只负责"打开"抽屉，见 .orch-side-drawer-close 的说明）；
   *   ② < --z-sheet(50)：底部抽屉（版本历史/快照）要能盖在它上面；
   *   ③ < --z-modal(60)：弹窗永远在最上面。
   * 与遮罩的关系见 .scrim 的 --z-side-scrim(47)：遮罩紧贴抽屉之下、
   * 顶栏之上，于是抽屉打开时顶栏也被压暗、且点它不会误触。
   */
  --z-side-drawer:48;
  /*
   * 左侧抽屉的遮罩。紧贴抽屉之下、顶栏(45)之上：
   * 抽屉打开时整个页面（含顶栏）都退到底层，只剩抽屉可操作。
   * 与 --z-side-drawer 是一对，"谁遮谁、谁在上面"两个数写在一起。
   */
  --z-side-scrim:47;
  /*
   * 顶栏浮层（⋯ 面板 + 随它展开的页面导航）。
   *
   * 为什么必须是独立令牌而不是沿用 --z-sheet：
   *   它**不是**底部抽屉，跟着抽屉走会让"以后调抽屉层级"顺手改坏顶栏；
   *   而且它只需要压过页面内容，不需要压过侧栏抽屉(40)。
   * 取值 45：高于页面内容与编排区浮层，低于**左栏抽屉(48)与其遮罩(47)**、
 * 底部抽屉(50)与弹窗(60)。
 * ⚠ 45 与 48 之间只差 3 —— 它们描述的是两件不同的事：
 *   顶栏浮层要压过"页面内容"（编排区、消息列），
 *   左栏抽屉要压过"整个页面（含顶栏）"。
 *   所以这个数不能大于 --z-side-drawer，否则编排页窄屏打开
 *   工作流抽屉时，顶栏会把抽屉的标题行/新建按钮盖住（2026-10-06 线上事故）。
   */
  --z-head-overlay:45;
  /* 触控目标最小尺寸：Apple HIG 建议 44pt。移动端按钮统一引用它，不各写字面量。 */
  --tap:44px;
  /*
   * 图标按钮的**视觉边长**（用户 2026-10-01 反馈"图标还是很大"后新增）。
   *
   * ★ 为什么必须与 --tap 分开：可点面积 ≠ 视觉尺寸 ★
   *   上一版直接把 --tap 当按钮宽度用，于是每个图标按钮都是一个 44×44 的方块：
   *   手机上实测一排图标（🕒 / ↕ / ⋯）各自撑满，看起来像三个大砖头，
   *   竖向也白吃掉一整行的高度。用户的原话就是"这些图标还是很大"。
   *
   *   但 --tap=44 这条**不能动**：它是"手指点得中"的物理下限，
   *   缩到 36px 会让误触变多（而误触在这个页面上可能是删掉一条消息）。
   *   两者要的本来就是两件事，所以拆成两个令牌：
   *     · --tap        管**热区**（不可缩，决定点得中点不中）；
   *     · --tap-visual 管**看得见的那个方块**（可紧凑，决定界面轻重）。
   *   落地方式见下方：按钮本身做成 --tap-visual 大小，用伪元素把热区撑到 --tap。
   *   这样视觉变小、手指体验不变 —— 这是唯一能同时满足两边的做法。
   *
   * 取值 34px：与桌面端这排按钮的观感（.t-actions button.is-icon 为 24px 宽）
   * 同一档量级，又不至于小到认不出图标（图标字号 15px）。
   */
  --tap-visual:34px;
  --font:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;
  --mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
}
:root[data-theme="dark"]{
  color-scheme:dark;
  /* Claude 深色 palette：亮度堆叠，主区偏暖深灰，侧栏更深 */
  --bg:#232220;          /* 主区背景（Claude #1f1e1d 附近偏暖） */
  --bg-deep:#191817;     /* 侧栏更深的炭黑 */
  --panel:#2a2927;       /* 浮层/卡片 稍亮 */
  --panel-2:#1e1d1b;
  --input:#2e2d2a;
  --line:#34332f;
  --line-soft:#2c2b28;
  --line-strong:#3f3e3a;
  --ink:#efece6;         /* 暖白文字 */
  --ink-soft:#b8b4ab;
  --ink-faint:#87837a;
  --ink-strong:#faf8f4;
  --accent:#e0845f;      /* 提亮一点保证对比 */
  /* 浏览器 chrome 颜色：必须与主背景同色。
     之前用 accent 陶红，移动端 PWA 会把它铺到状态栏与 overscroll 区域，
     滚动到底部时整片陶红，非常刺眼。 */
  --theme-color:#232220;
  --accent-hover:#ea916d;
  --accent-ink:#1b130f;
  --danger:#e07a60;
  --warn:#e6b84a;          /* 深色下提亮的黄，保证在炭黑底上可读 */
}
*{box-sizing:border-box}
html,body{height:100%}
/* html 与 body 同铺主题背景，避免画布/overscroll 露出白底 */
html{background:var(--bg)}
body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.6 var(--font);-webkit-font-smoothing:antialiased}
button{font-family:inherit;cursor:pointer}
textarea{font-family:inherit}
button{-webkit-tap-highlight-color:transparent}

.shell{display:flex;height:100vh;height:100dvh;overflow:hidden}

/* 侧栏 */
/*
 * 宽度由 CSS 变量 --side-w 驱动，唯一权威是设置项（settings 表 side_width）。
 *
 * 为什么是变量而不是直接写死 / 也不在 JS 里直接改 style.width：
 *   · 写死宽度就没有拖拽；
 *   · JS 到处改内联宽度会散出多份宽度来源（内联 style、CSS 常量、设置值），
 *     三者一旦不一致就会出现"刷新后跳一下""拖动时对不上"。
 *   现在只有一条链：设置值 → 应用到 :root 的 --side-w → CSS 消费它。
 *   拖拽只负责把像素写回设置，不直接改别的任何地方。
 */
aside{
  width:var(--side-w,280px);flex:none;background:var(--bg-deep);border-right:1px solid var(--line);
  display:flex;flex-direction:column;min-height:0;position:relative;
}
/* 桌面端侧栏右缘的拖拽手柄：只负责捕获指针，样式保持"看不见但摸得到" */
.side-resizer{
  position:absolute;top:0;right:-3px;width:7px;height:100%;z-index:5;
  cursor:col-resize;background:transparent;border:0;padding:0;
  touch-action:none;
}
.side-resizer:hover,.side-resizer.is-dragging{
  background:color-mix(in srgb,var(--accent) 55%,transparent);
}
/*
 * 拖拽期间：屏蔽文本选中、让 iframe 不抢事件。
 * 类名对**全站四类边界共用**（is-layout-resizing）：
 * 同一时刻只可能拖一条，而"现在正在拖"对整页而言就是一件事。
 * 历史类名 is-side-dragging 保留兼容——它仍可能被老缓存里的脚本加上。
 */
body.is-layout-resizing,body.is-side-dragging{cursor:col-resize;user-select:none}
body.is-layout-resizing iframe,body.is-side-dragging iframe{pointer-events:none}
.brand{
  display:flex;align-items:center;justify-content:space-between;
  padding:16px 14px 10px;
}
.brand b{font-size:15px;letter-spacing:.02em;color:var(--ink)}
/*
 * 版本标识：紧跟标题的小圆点，悬停显示版本指纹。
 * 用 <i> 而不是块级元素，避免破坏 .brand 的 space-between 布局。
 * 颜色随状态变化：绿=资源指纹与页面声明一致（当前版本），
 * 橙=不一致（浏览器跑着旧资源，需要刷新）。
 */
.build-tag{
  width:6px;height:6px;border-radius:50%;flex:none;
  background:var(--line-strong);margin-left:6px;margin-right:auto;
  transition:background .2s ease;cursor:help;
}
.build-tag.ok{background:#4caf7d}
.build-tag.stale{background:var(--accent)}
.build-tag.unknown{background:var(--line-strong)}
.brand button{
  width:30px;height:30px;border:1px solid var(--line);border-radius:8px;background:var(--panel);
  font-size:16px;color:var(--ink);display:flex;align-items:center;justify-content:center;
}
.brand button:hover{border-color:var(--line-strong)}
.caption{
  font-size:11px;color:var(--ink-faint);letter-spacing:.12em;text-transform:uppercase;
  padding:6px 16px 8px;
}
#sessions{flex:1;overflow:auto;padding:0 10px 16px;-webkit-overflow-scrolling:touch}
.session-item{display:flex;align-items:stretch;border-radius:8px;margin-bottom:2px}
.session-item:hover{background:var(--panel)}
.session-title{
  /*
   * 标题行的定位基准：会话 id 靠右浮在标题末尾，且必须在标题**被截断**时
   * 依然完整可见（id 是拿来核对/复制的事实，不是装饰），
   * 所以标题与 id 分属两个子元素，各自处理溢出（见 .session-title-text）。
   */
  position:relative;flex:1;display:flex;align-items:center;gap:6px;
  text-align:left;border:0;background:transparent;padding:9px 10px;border-radius:8px;
  font-size:13px;color:var(--ink-soft);
}
/* 标题本身承担截断：flex:1 + min-width:0 是让 ellipsis 在 flex 容器里生效的前提 */
.session-title-text{
  flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
/*
 * 会话 id：等宽字体 + 更淡的颜色，与标题形成层级；flex:none 保证它永不被压缩，
 * 因此在任何标题长度下都完整可读。
 */
.session-id{
  flex:none;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:11px;color:var(--ink-faint);letter-spacing:.2px;opacity:.75;
  white-space:nowrap;
}
.session-title.active .session-id{color:var(--ink-soft);opacity:1}
.session-title.active{background:var(--panel);color:var(--ink);font-weight:600}
.session-del{
  border:0;background:transparent;color:var(--ink-faint);font-size:14px;line-height:1;padding:0 8px;display:none;
}
.session-item:hover .session-del{display:block}
.session-del:hover{color:var(--danger)}
.side-hint{padding:18px 18px;color:var(--ink-faint);font-size:12px}

/* 主区 */
/*
 * position:relative 是「停靠态设置面板」的定位基准（.modal.docked 用 absolute）。
 * 放在 main 上而不是 body 上：面板要停在**聊天区域**的左上角，
 * 而不是整个窗口的左上角（后者会盖住左侧会话栏）。
 */
main{flex:1;min-width:0;display:flex;flex-direction:column;min-height:0;position:relative}
/*
 * 聊天页顶栏。选择器用 .chat-head 而**不是**裸 header：
 * 编排页 .orch-head、日志页 .log-head 也是 <header>，裸标签选择器会把
 * 三个页面的顶栏绑在一起（基础高度/内边距/横滑行为互相污染）。
 * 顶栏的身份是"哪一个顶栏"，标签名不携带这个信息。
 */
.chat-head{
  height:56px;flex:none;display:flex;align-items:center;gap:10px;
  padding:0 20px;border-bottom:1px solid var(--line);background:var(--bg);
}
#menu{
  display:none;border:0;background:transparent;font-size:20px;color:var(--ink);padding:2px 6px;
  line-height:1;
}
/* 同一套动作图形：桌面图文并列，手机仅隐藏文字，不复制行为入口。 */
.chat-action-icon{width:18px;height:18px;flex:none;vertical-align:middle}
.chat-head .new-chat-btn,.chat-head #chatSettingsBtn{display:inline-flex;align-items:center;justify-content:center;gap:6px}
.chat-navigation{display:contents}
.chat-navigation>summary{display:none}
.chat-head .chat-navigation>.head-nav{display:flex}

.head-title small{display:block;font-size:10px;letter-spacing:.14em;color:var(--ink-faint)}
.head-title h1{font-size:16px;margin:2px 0 0;font-weight:650;color:var(--ink-strong)}
/*
 * ================= 主题切换按钮（对话 / 编排 / 日志三页同一颗） =================
 *
 * 2026-10-17 用户要求：编排页与日志页也要能直接切深浅色，
 * 否则每次都要绕回对话页。「三页各有一个主题按钮」是这件事的落地，
 * 但**样式只能有一份**：三页各写一套 px 值，它们的静宽就不可能一样，
 * 而三页顶栏右簇都是「靠右」的 —— 右簇左缘由总宽反推，
 * 宽度差几个像素，切页时主题按钮就会左右跳一下。
 *
 * 因此选择器按**类**而不是按 id：
 *   .theme-toggle = 外观与尺寸的唯一权威；
 *   #chatThemeBtn / #orchThemeBtn / #logThemeBtn = 三个页面各自的挂载点。
 *
 * 形态是"胶囊 + 文字"（☾ 切到深色 / ☀ 切到浅色），不是一颗裸图标：
 * 三页顶栏上其它控件都写着字（刷新 / 版本历史 / 显示右侧栏 / 对话·编排·日志），
 * 只放一个 ☾ 会让人分不清"这是深色图标"还是"点了会变深色"。
 * 文案由 /theme-button.js 按唯一权威投影，这里只管尺寸与配色。
 *
 * 高度用 --tap-visual：与全站图标按钮共用同一个高度常量，
 * 任何一处改高度都自动三页同步（窄屏另按 --tap 放大到可点，见各页断点段）。
 */
.theme-toggle{
  flex:none;display:inline-flex;align-items:center;justify-content:center;
  gap:4px;
  min-height:var(--tap-visual);
  font-size:12px;line-height:1;color:var(--ink-soft);
  white-space:nowrap;
  border:1px solid var(--line);border-radius:999px;background:var(--panel);
  cursor:pointer;padding:0 10px;
  transition:border-color 150ms ease,background 150ms ease,color 150ms ease;
}
.theme-toggle:hover{border-color:var(--line-strong);background:var(--panel-2);color:var(--ink)}
.theme-toggle:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
#model{
  font-size:12px;color:var(--ink-faint);border:1px solid var(--line);
  padding:4px 10px;border-radius:999px;background:var(--panel);cursor:pointer;
  /*
   * min-width 是必需的：按钮文字现在由服务端下发（见 index.html 的注释），
   * 在 /api/health 返回之前它是空的。没有 min-width 时按钮会先缩成一个小点
   * 再撑开，工具栏上其它按钮跟着左右横跳。留一个最小宽度让占位稳定。
   */
  min-width:64px;
}
#model:hover{border-color:var(--line-strong);background:var(--panel-2)}
#model:active{transform:scale(.97)}

.toolbar{
  flex:none;display:flex;gap:8px;align-items:center;
  padding:12px 20px;border-bottom:1px solid var(--line-soft);background:var(--bg);
}
.toolbar select{
  max-width:46%;font-size:13px;padding:7px 10px;border:1px solid var(--line);border-radius:8px;
  background:var(--input);color:var(--ink);outline:none;
}
.toolbar button{
  font-size:13px;padding:7px 12px;border:1px solid var(--line);border-radius:8px;background:var(--input);
  color:var(--ink);white-space:nowrap;
}
.toolbar button:hover{border-color:var(--line-strong);background:var(--panel-2)}
/* 状态文字本身只负责排版；它现在是浮层卡片的内容，不再是工具条的一行。
   max-width 让长错误不至于把卡片撑得比消息区还宽（换行由 .error 的 white-space 决定）。 */
#status{display:block;max-width:100%;font-size:12px;line-height:1.5;color:var(--ink-faint);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* 失败提示要能被看见：换行而不是被省略号截断（错误信息往往是"哪里失败了"这句话本身） */
#status[data-kind="error"]{color:var(--danger, #d9534f);white-space:normal;overflow:visible}
/* 成功/进度类提示走淡出：先降低不透明度，再由 JS 清空（清空后整卡片收起） */
#status[data-fading="true"]{opacity:0;transition:opacity .45s ease}

/* ---------- 聊天页顶部：会话级系统提示词（默认收起） ----------
   与编排页是同一件事的另一个宿主（.orch-chips/.orch-chip/.fsel 共用），
   差别有两点：
     1) 作用范围是整个会话（不是整个工作流）；
     2) **默认收起**——多数会话不选文件，不该为此长期付版面成本。
   所以这里不套 .sec-body 的卡片外壳，只用一条左侧色条标明这是「会话配置」
   而不是「对话内容」，避免在消息区上方又多出一层视觉噪音。

   收起/展开只切 .collapsed 这一个类，body 的显隐交给它，不在 JS 里各判一次
   （两处判断 = 两处可能不一致，展开动画与 aria-expanded 会打架）。 */
.sys-bar{
  flex:none;display:flex;flex-direction:column;gap:8px;
  /*
   * 整条仍是通栏（下边框要横贯，否则会变成一块悬空的卡片），
   * 但**内容**收进与消息区同宽的列里居中。
   * 用户说的"不要那么宽、对齐消息区域宽度"指的就是内容宽度——
   * 之前搜索框从屏幕最左拉到最右，而消息只占中间 820px，两者明显错位。
   */
  padding:10px 20px 12px;border-bottom:1px solid var(--line-soft);background:var(--bg);
}
.sys-bar-head,.sys-bar-body{
  width:100%;max-width:var(--content-col);margin-left:auto;margin-right:auto;
  box-sizing:border-box;
}
.sys-bar.collapsed .sys-bar-head{max-width:var(--content-col)}
/* 收起态：只留一行触发器，去掉下边框与纵向内边距，视觉上不构成"一条横幅"。
 * 横向内边距必须保留（padding 简写会连它一起清零）——清零后触发器会顶到视口左右边缘，
 * 与消息列的 22px 边距对不齐，"对齐消息区宽度"这个诉求就只对了一半。 */
.sys-bar.collapsed{padding-top:0;padding-bottom:0;gap:0;border-bottom:none;background:transparent}
.sys-bar.collapsed .sys-bar-body{display:none}

.sys-bar-toggle{
  display:flex;align-items:center;gap:8px;width:100%;
  /*
   * 左右内边距与消息列的左边距对齐（消息列在宽屏时距视口至少 22px）。
   * 收起态的触发器是整行点击目标，必须撑满列宽，否则点空白处没反应。
   */
  padding:7px 0;border:0;background:transparent;cursor:pointer;text-align:left;
  color:inherit;font:inherit;
}
.sys-bar-toggle:hover{background:var(--panel-2)}
.sys-bar-toggle:focus-visible{outline:2px solid var(--line-strong);outline-offset:-2px}
/* 展开箭头：纯 CSS 三角，不引图标字体 */
.sys-bar-caret{
  flex:none;width:0;height:0;border-left:4.5px solid currentColor;border-top:4px solid transparent;
  border-bottom:4px solid transparent;transition:transform .15s ease;opacity:.55;
}
.sys-bar:not(.collapsed) .sys-bar-caret{transform:rotate(90deg)}
/* 收起但已有选中文件时：整条要给个"这里有东西"的提示，否则用户会忘了自己设过 */
.sys-bar.collapsed.has-files .sys-bar-title{color:var(--ink-soft)}
.sys-bar.collapsed.has-files .sys-bar-caret{opacity:.9}

/* 移动端：折叠触发器就是整行的点击目标，必须达到触控下限（44px）。
   实测收起态只有 33px（7px 上下 padding + 内容行高），iPhone 上点不准。 */
@media (max-width:760px){
  .sys-bar-toggle{padding-top:4px;padding-bottom:4px;min-height:44px}
}

.sys-bar-head{display:flex;align-items:center;gap:8px;min-height:20px}
.sys-bar-body{display:flex;flex-direction:column;gap:8px;padding:0 0 2px}
.sys-bar-title{
  font-size:11.5px;font-weight:600;color:var(--ink-faint);
  letter-spacing:.1em;text-transform:uppercase;display:flex;align-items:center;gap:7px;
}
.sys-bar-title::before{content:'';width:3px;height:11px;border-radius:2px;background:var(--line-strong)}
.sys-bar-hint{
  font-size:12px;color:var(--ink-faint);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
/* 聊天页的文件搜索框比编排页小一号：它是常驻控件，不是主操作区 */
.sys-bar .fsel{margin-top:0}
.sys-bar .fsel-input{padding:6px 10px;font-size:12.5px}

/*
 * 搜索框 + 最近使用文件标签：同一行，各占一半。
 *
 * 为什么是"各占一半"而不是按字符数定宽：
 *   先前按 20ch 定宽是错的——ch 是数字 0 的宽度，而中文占两个字宽，
 *   于是 20ch 连占位文字都显示不全（实测被截成"搜索 OB 文件，回车选"），
 *   下拉候选里的文件名也全被截断。字符数这个单位在中文界面里根本不可用。
 *   "各占一半"是布局意图的直接表达，也不依赖字体度量：
 *   两边都用 flex:1 1 0 + min-width:0，于是无论窗口多宽都精确对半分。
 *   min-width:0 是必须的：flex 项默认 min-width:auto，
 *   长文件路径会把标签区撑宽、把搜索框挤扁（这正是分区失效的常见原因）。
 *
 * 整行**只允许一行**（用户要求 2026-09-17）：需要换行的标签直接不显示。
 *
 * 为什么不是"让它换行、行高变大"：
 *   这一行在系统提示词条的顶部，一旦换行整个条会突然变高，
 *   把下面的聊天消息整体往下推——每加一个文件界面就跳一次，非常烦躁。
 *   保持恒定的单行高度，界面才稳定。
 *
 * 溢出的处理是**隐藏**（overflow:hidden）而不是省略号：
 *   省略号在这里会误导——它让人以为"只有这一个被截断了"，
 *   实际是后面还有若干个标签完全没显示。整块裁掉更符合事实。
 *   flex-wrap:nowrap + 每个 chip 的 flex:none 保证它们排队而不是被压缩，
 *   于是超出部分自然被裁掉，且被裁的是**排在后面的**（最近使用顺序，
 *   越靠前越相关），信息损失最小。
 */
/*
 * 搜索框 + 最近使用标签所在的这一行。
 *
 * 这一行是**单行布局**：输入框与标签区各占一半，放不下的标签由行尾裁掉。
 *
 * ===== 为什么这里有 overflow:hidden，以及由此产生的硬约束 =====
 *
 * overflow:hidden 是"整行恒为一行"的实现手段（超出即裁掉）。但它在 CSS 里
 * 有一个常被忽略的副作用：**overflow:hidden 的元素是一个滚动容器**。
 * 于是这一行同时成了三样东西：flex 行、裁剪框、滚动容器。
 *
 * 【2026-09-18 线上故障】点开搜索框后输入框整个消失，只剩下列表挂在原处。
 * 实测根因（逐层证据，勿再猜）：
 *   · 下拉 .fsel-menu 绝对定位挂在 .sys-bar-pick 上，但**仍计入它的
 *     scrollHeight**（实测 scrollHeight=332 / clientHeight=28）；
 *   · 输入框获得焦点时浏览器把"聚焦元素滚入可视区"，因它是滚动容器，
 *     于是设了 scrollTop=69；
 *   · overflow:hidden 只是不显示滚动条，**并不阻止程序化滚动**，
 *     整行内容被上移 69px，输入框 top 135→66，正好被裁出可视区；
 *   · 而绝对定位的浮层按行内几何摆放，于是正好压在原输入框位置上。
 *
 * 所以：**浮层绝不能成为这一行滚动内容的一部分**。这就是下面
 * .sys-bar-pick .fsel-menu 用 position:fixed 的原因——它是唯一能同时
 * 逃出「裁剪」与「滚动」的手段（absolute 只能逃出裁剪，逃不出滚动）。
 *
 * 验收口径（唯一权威，见 e2e）：菜单打开前后，
 *   ① 输入框 getBoundingClientRect().top 逐像素不变；
 *   ② 本行 scrollTop 恒为 0。
 */
.sys-bar-pick{display:flex;align-items:flex-start;gap:10px;flex-wrap:nowrap;overflow:hidden}
.sys-bar-pick .fsel{flex:1 1 0;min-width:0}
/*
 * 标签区：不换行、不收缩，超出部分由外层 overflow:hidden 裁掉。
 * min-width:0 仍要保留，否则长文件名会把搜索框挤扁。
 */
.sys-recent{flex:1 1 0;min-width:0;display:flex;flex-wrap:nowrap;align-items:center;gap:6px;overflow:hidden}
.sys-recent-label{font-size:11px;color:var(--ink-faint);flex:none}
/*
 * 最近使用文件的标签：一眼能看出是"可点的快捷入口"（而不是只读文字），
 * 所以给描边 + hover 变强调色；文件名过长时省略号，不撑破行。
 */
.sys-recent-chip{
  /*
   * flex:none 是"整行只一行"的关键：标签保持自身宽度、不参与收缩，
   * 于是放不下的标签会溢出容器被裁掉（而不是被压成一条挤在一起）。
   * white-space:nowrap 保证标签自身不被拆成两行。
   * 单个标签仍允许内部文件名省略（max-width + 内部的 .src-name 省略号），
   * 这样即使只有一个超长文件名的标签，也不会把整行撑爆。
   */
  display:inline-flex;flex:none;align-items:center;gap:5px;white-space:nowrap;
  max-width:100%;min-width:0;
  border:1px solid var(--line);border-radius:999px;background:var(--panel-2);
  color:var(--ink-soft);font-size:11.5px;padding:3px 9px;line-height:1.5;
}
.sys-recent-chip:hover{border-color:var(--accent);color:var(--accent)}
.sys-recent-chip .src-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/*
 * 已加入本会话的标签：虚线边框 + 变淡，表示"这条已经在系统提示词里了"，
 * 所以左键点击不再有动作——但**它仍然是可交互的**（右击能从最近使用移出）。
 *
 * 因此不能用 cursor:default（那是在说"这里没东西可操作"），
 * 也不能用 disabled（那会让辅助技术与自动化工具整个跳过它）。
 * 悬停色仍保留，配合 title 里"右击可从最近使用移出"的提示，
 * 让用户能发现这条路径——一个只能靠猜才能发现的交互等于不存在。
 */
.sys-recent-chip.is-added{border-style:dashed;opacity:.55}
.sys-recent-empty{font-size:11px;color:var(--ink-faint)}

@media (max-width:760px){
  /*
   * 手机上"各占一半"会让两边都无法使用（搜索框只剩十几个字、标签只剩半截文件名），
   * 所以窄屏改为上下两行，各自占满整行。
   * 这不是破例，而是同一意图在窄屏下的正确表达：两个区域都要能用。
   */
  .sys-bar-pick{flex-direction:column;align-items:stretch;gap:8px}
  .sys-bar-pick .fsel,
  .sys-bar-pick .sys-recent{flex:1 1 auto;width:100%}
}
/*
 * ===== 聊天页的下拉浮层：position:fixed，彻底脱离裁剪与滚动 =====
 *
 * 为什么必须是 fixed（而不是 absolute）：
 *   absolute 的包含块是 .sys-bar-pick，因此它**仍属于该元素的滚动内容**，
 *   会把 scrollHeight 撑大（实测 28 → 332）；而 .sys-bar-pick 是 overflow:hidden
 *   的滚动容器，输入框一聚焦就触发"滚入可视区"，整行被 scrollTop:69 顶走
 *   ——输入框消失、浮层压在原位。fixed 以视口为包含块，不进任何祖先的
 *   滚动内容，两个症状同时消失。
 *
 * 代价与它的处理：fixed 元素不再自动跟随锚点，所以坐标必须**量出来**。
 * 量的实现是唯一一处——block-kit.js 的 positionMenu()：
 *   · 以锚点输入框的 getBoundingClientRect() 为唯一定位权威；
 *   · 打开时"先定位、后显示"，显示后再校准一次（隐藏时量不到真实高度）；
 *   · 内容高度变化、窗口 resize / 滚动时重算（否则位置会过期）。
 * 这是本设计的明确取舍："浮层与锚点解耦"优先于"少写几行定位代码"——
 * 前者是结构正确性（不这么写就会出现本页记录的故障），后者只是省事。
 *
 * 这里只给外观与上限；left/top/width 一律由 JS 写入——
 * 定位权威只有一个，CSS 不得再参与定位（两条规则同时定坐标时，
 * 胜出者取决于特异性，很难看出是谁在生效）。
 */
.sys-bar-pick .fsel-menu{
  position:fixed;
  z-index:60;
  max-height:min(300px,46vh);
}
.sys-bar .orch-chips{min-width:0}

/* 模板面板 */

.panel-title{font-size:12px;color:var(--ink-soft);margin-bottom:10px}
.panel textarea{
  width:100%;height:150px;border:1px solid var(--line);border-radius:8px;padding:10px;
  font:12.5px/1.7 var(--mono);resize:vertical;outline:none;background:var(--input);color:var(--ink);
}
.panel textarea:focus{border-color:var(--line-strong)}
.panel-actions{margin-top:10px;display:flex;gap:8px}
.panel-actions button{
  font-size:13px;padding:7px 14px;border:1px solid var(--line);border-radius:8px;background:var(--input);
  color:var(--ink);
}
.panel-actions #save{background:var(--ink);color:var(--bg);border-color:var(--ink)}
.panel-actions #save:hover{background:var(--ink-strong)}

/* 对话区 */
#chat{
  flex:1;overflow:auto;min-height:0;
  padding:26px max(22px,calc((100% - var(--content-col))/2)) 10px;
  -webkit-overflow-scrolling:touch;
}

/* ===== 消息导航区段开始（msg-nav:begin） ===== */
/* ============================================================================
 * 消息导航（聊天页右侧）—— 全站唯一实现，逻辑与几何见 public/msg-nav.js。
 *
 * 几何权威只在 msg-nav.js 的一组常量里（行高 / 横线列宽 / 横线长度与粗细 /
 * 容器右内边距 / 容器宽度）。本文件只负责**颜色与材质**，不定义任何尺寸——
 * 尺寸若在两处定义，三态对齐（横线不位移）这条不变量就守不住了。
 *
 * 三态与源码端一致：
 *   静默态 = 只有横线（容器背景透明、无边框无阴影）
 *   容器展开态 = 容器向左生长、浮现背景/边框/阴影，左侧摘要列淡入
 *   行悬停态 = 该行横线加长变色，并在左侧弹出更完整的详情气泡
 * ========================================================================== */

/*
 * 导航宿主：与 #chat 同级，absolute 出流。
 *
 * 为什么必须 absolute 且 pointer-events:none：
 *   · absolute 出流 → 导航的存在/展开不改变 #chat 与 .composer 的任何盒子，
 *     因此不会反过来影响"是否可滚/是否在底部"这些由几何推导的判据
 *     （与 .scroll-bottom-btn 出流是同一条理由）。
 *   · pointer-events:none → 宿主这一整片区域不吃指针事件，
 *     消息区的滚动、选中、点击在导航空白处照常工作；
 *     只有导航自身盒子（下面 .msg-nav-shell）才重新打开指针事件。
 */
.msg-nav-host{
  /*
   * 垂直边界由 JS 依 #chat 的真实几何写入（top/height），
   * 这里只声明"贴 main 右缘、零宽"。不写 top:0;bottom:0：
   * 那会让导航垂直居中于**整个 main**（含顶栏与输入框），
   * 而用户要的是"居中于消息区"。几何来自现场量取，不靠假设。
   */
  position:absolute;right:0;width:0;
  pointer-events:none;z-index:20;
}

/*
 * 壳层：right 锚定的定位盒，宽度由 JS 按展开态写入。
 *
 * right:12px 是**唯一**的横向锚点：展开时只改 width（向左生长），
 * 所以横线列（贴容器右内边距）的屏幕 x 坐标在三态之间恒定。
 * top:50% + JS 给的 translateY 让它垂直居中于消息区并上抬半个输入框高度，
 * 避免底部被输入框压住（与源码端同一算法）。
 */
.msg-nav-shell{
  /*
   * top:50% 是相对**宿主**（= 消息区那条带）的，不是相对整个 main。
   * 因此"垂直居中于消息区"是几何上必然成立的，不需要再减输入框高度。
   */
  position:absolute;right:12px;top:50%;
  pointer-events:none;
  transition:width .2s ease-out;
}

/*
 * 容器：可滚动的条目列。
 * 滚动条永不占位（scrollbar-width:none）——滚动条一旦占位，
 * 容器内容宽度就会随"是否需要滚动"变化，横线列随之左右抖动。
 * 这是保持三态零位移的必要条件，不是审美选择。
 */
.msg-nav-container{
  pointer-events:auto;
  display:flex;flex-direction:column;align-items:stretch;
  max-height:46vh;overflow-y:auto;overflow-x:hidden;
  padding-top:6px;padding-bottom:6px;
  border:1px solid transparent;border-radius:12px;
  background:transparent;
  transition:background-color .2s ease-out,border-color .2s ease-out,box-shadow .2s ease-out;
  scrollbar-width:none;overscroll-behavior:contain;
}
.msg-nav-container::-webkit-scrollbar{width:0;height:0}
.msg-nav-container.expanded{
  border-color:var(--line);
  background:color-mix(in srgb,var(--panel) 94%,transparent);
  box-shadow:0 10px 30px -12px rgba(0,0,0,.55);
  backdrop-filter:blur(4px);
}

/* 行：按钮语义，整行可点；布局由 JS 写内联几何（唯一尺寸来源） */
.msg-nav-row{
  display:flex;width:100%;min-width:0;flex:none;align-items:center;justify-content:flex-end;
  border:0;background:transparent;padding:0;cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}

/*
 * 左侧摘要列：静默态宽度 0 且透明，展开态定宽显示。
 * 宽度/透明度/右边距都由 JS 按展开态写内联样式——三态差异只在这里，
 * 而横线列完全不受影响（横线右侧对齐、位置固定）。
 */
.msg-nav-label{
  display:flex;align-items:center;gap:6px;min-width:0;overflow:hidden;white-space:nowrap;text-align:left;
  font-size:11px;line-height:1;text-overflow:ellipsis;
  color:var(--ink-faint);
  transition:flex-basis .2s ease-out,opacity .2s ease-out,color .2s ease-out,margin .2s ease-out;
}
.msg-nav-preview{min-width:0;overflow:hidden;text-overflow:ellipsis}
.msg-nav-number{
  display:inline-flex;flex:none;align-items:center;justify-content:center;
  box-sizing:border-box;min-width:18px;height:18px;padding:0 4px;
  border:1px solid var(--line);border-radius:5px;background:var(--input);
  color:var(--ink-soft);font-size:10px;font-weight:600;font-variant-numeric:tabular-nums;
}
.msg-nav-row.active .msg-nav-number{
  color:var(--accent);border-color:color-mix(in srgb,var(--accent) 45%,var(--line));
  background:color-mix(in srgb,var(--accent) 12%,var(--input));
}
.msg-nav-row.hovered .msg-nav-label{color:var(--ink)}
.msg-nav-row.active .msg-nav-label{color:var(--accent)}

/* 横线列：三态同尺寸同位置，只有颜色与长度随状态变化 */
.msg-nav-dash-col{display:flex;flex:none;align-items:center;justify-content:flex-end}
.msg-nav-dash{
  display:block;border-radius:999px;background:var(--line-strong);
  transition:background-color .15s ease-out,width .15s ease-out;
}
.msg-nav-row.hovered .msg-nav-dash{background:var(--ink-soft)}
.msg-nav-row.active .msg-nav-dash{background:var(--accent)}

/*
 * 详情气泡：行悬停时在导航左侧弹出。
 * 它挂在壳层上（不是容器内），所以不会被容器的 overflow 裁掉，
 * 也永远不吃指针事件（否则会和行的 mouseleave 互相打架，气泡闪烁）。
 */
.msg-nav-bubble{
  position:absolute;right:100%;margin-right:8px;
  pointer-events:none;
  border-radius:10px;border:1px solid var(--line);
  background:var(--panel);color:var(--ink-soft);
  padding:8px 12px;font-size:11px;line-height:1.55;
  box-shadow:0 8px 24px -10px rgba(0,0,0,.6);
  overflow:hidden;
  overflow-wrap:anywhere;word-break:break-word;
}
.msg-nav-bubble.hidden{display:none}

/*
 * 空会话时不渲染导航：由 JS 给宿主加全局 .hidden（见 style.css 的统一隐藏规则）。
 * 这里**不再**定义第二套隐藏样式——隐藏语义只允许一个权威。
 */

/* ============================================================================
 * 第二个宿主：编排页的**编排区**（左栏 .orch-col-edit）
 *
 * 与聊天页**共用上面同一套壳层/容器/行/横线/气泡样式**（这正是本模块只写一份
 * 的理由）。这里只覆盖"宿主盒子怎么摆"这一件事，几何尺寸一个都不重复。
 *
 * ★ 位置：导航属于编排区，不属于右侧的版本/快照面板 ★
 *   它导航的对象（消息序列）在编排区里，导航就必须长在编排区上——
 *   放到右栏会变成"看着一个地方、点着另一个地方"，是纯粹的位置错误。
 *
 * 为什么是**覆盖层**（absolute）而不是编排区里的一个普通子元素：
 *   编排区本身可滚动（overflow:auto）。导航若当普通子元素，会跟着消息
 *   一起滚走，那就不再是导航（只是一条会消失的标题）；而且它会抢占
 *   消息列的横向空间与纵向空间。做成覆盖层后：
 *     · 不随消息滚动（与聊天页那条导航同一位置关系）；
 *     · 出流，不改变编排区任何盒子的尺寸，因此不会干扰
 *       "能不能滚、滚到哪"这些由几何推导的判据。
 *
 * 为什么不会遮挡滚动条：编排区的滚动条贴在最右缘；导航的 right 偏移
 * （由 JS 写入壳层宽度决定，见下）在滚动条左侧，两者不重叠。
 * ========================================================================== */
.orch-nav-host{
  /*
   * 定位基准是 .orch-cols（宿主与两栏是兄弟，见 bianpai.html）。用 absolute
   * 把它压在编排区（左栏）上：左栏宽 = 总宽 − 右栏宽，而右栏宽是 minmax(340px,40%)
   * 这类不定值，所以这里**不自己算左栏宽度**，而是用 right 把宽度"顶"出来：
   * 右侧预留出右栏的宽度不可行（值不确定），因此改为整块铺满再靠右内边距
   * 让导航落在左栏右缘 —— 宽度计算交给下面 .orch-nav-shell 的 right。
   */
  position:absolute;inset:0;
  pointer-events:none;z-index:20;
}
.orch-nav-host .msg-nav-shell{
  /*
   * 壳层 right 锚定在编排区右缘内侧 12px 处；top:50% + translateY(-50%)
   * 就是"垂直居中于编排区"，与聊天页同一条算法（JS 的 positionMode='center'
   * 分支会写这个 transform，因此这里保留 center 语义）。
   *
   * ★ 12px 的来历（2026-10-03 更新，勿再按"留白"理解）★
   *   这个数是"别贴住区域右缘"的间距常量（与聊天页 NAV_REGION_EDGE_GAP 同值），
   *   曾经它恰好落在编排区右缘与消息列右缘之间的留白里 —— 因为那时消息列被
   *   一个对版面无感知的 max-width:760px 居中，收面板腾出的空间一概不要。
   *   那个绝对上限已删除（见 :root 的 --orch-col-max）：编排区的内容现在铺满
   *   版面宽，于是**导航的静默横线列会落在内容右缘之上**。
   *   这是**接受**的：导航是覆盖层，重叠本就是它的正常语义（聊天页同款，
   *   且编排页已经刻意不弹详情气泡来避免压正文，见下面的 .msg-nav-bubble）。
   *   更重要的是：**不允许**为了给导航让位而让内容按导航尺寸收窄 ——
   *   那就是把覆盖层的外观当成了主内容的布局输入（本文件明令禁止，
   *   见 .orch-col-edit > * 那一段的历史说明）。静默态的容器背景/边框都是
   *   透明的，可见的只有右端那几根细横线，视觉影响极小。
   */
  /*
   * ★ 横向锚点必须由**编排区自己的右缘**决定，不能写死百分比 ★
   *
   * 踩过的坑：写 right:calc(40% + 12px) —— 40% 看着像"右栏宽度"，
   * 实际那个 grid 是 minmax(340px,40%)，右栏宽度是**有下限的不定值**
   * （窗口一窄就锁在 340px，百分比不再成立）。实测偏差直接变成重叠：
   *   导航右缘 958 vs 编排区滚动条左缘 955 → 压住滚动条 3px；
   *   导航左缘 910 vs 消息气泡右缘 935 → 压住正文 25px。
   * 两个都是"拿一个近似值当几何权威"造成的，修法不是微调那个数字，
   * 而是让锚点只依赖一个**确定**的量：#msgNavHost 铺满 .orch-cols（见上），
   * 再由 JS 把编排区（.orch-col-edit）的右缘写进 right —— 与聊天页
   * 注入 getRegionEl 的几何同一条思路：CSS 不猜，几何来自现场量取。
   *
   * 初值给一个安全兜底（窄屏断点会覆盖它），真实值由 layoutHost 写入。
   */
  position:absolute;right:12px;top:50%;
  pointer-events:none;
}
/*
 * 窄屏（两栏叠成一列）：编排区占满整宽，导航贴右缘即可。
 * 断点与 .orch-cols 的响应式规则同源（760px），不另立一套。
 */
@media (max-width:760px){
  .orch-nav-host .msg-nav-shell{right:12px}
}
/*
 * 编排页容器仍用聊天页那套 max-height:46vh 与内部滚动：
 * 导航是覆盖层（高度不受编排区约束），条目多时必须自己滚，
 * 否则会顶出编排区上缘。
 */
/*
 * 详情气泡：编排页里导航贴编排区右缘，向左弹出的气泡会**盖住消息正文**
 * （聊天页有空白带可让，这里左侧就是消息列）。因此编排页不弹出气泡 ——
 * 行本身已经在展开态显示摘要，detail 仍挂在 aria-label 上供读屏用。
 * 宁可不出提示，也绝不盖住正文（与 msg-nav.js 里同一条原则）。
 */
/* 全文详情覆盖层：独立于消息布局，鼠标可进入并滚动阅读。 */
.msg-nav-full-detail .msg-nav-bubble{
  pointer-events:auto;overflow-y:auto;overscroll-behavior:contain;
  white-space:pre-wrap;font-size:12px;line-height:1.7;
  color:var(--ink);box-sizing:border-box;
}
.msg-nav-full-detail .msg-nav-shell::before{
  content:"";position:absolute;right:100%;top:0;width:8px;height:100%;
}
/* ===== 消息导航区段结束（msg-nav:end） ===== */
.msg{max-width:var(--content-col);margin:0 auto 18px;display:flex;align-items:flex-start}
.msg.user{justify-content:flex-end}
.msg.user .msg-main{flex:0 1 auto;max-width:85%}
.msg.assistant .msg-main{width:100%}
.user-bubble{padding:10px 14px;border:1px solid var(--line-strong);border-radius:18px 4px 18px 18px;background:var(--panel-2)}
.user-fold-viewport{min-width:0}
.user-bubble.is-folded .user-fold-viewport{max-height:var(--user-fold-height);overflow:hidden;mask-image:linear-gradient(to bottom,#000 calc(100% - 28px),transparent)}
.user-fold-toggle{display:flex;align-items:center;justify-content:center;width:36px;height:28px;margin:6px auto 0;border:1px solid var(--line);border-radius:999px;background:var(--panel);color:var(--ink-soft);cursor:pointer}
.user-fold-toggle[hidden]{display:none}
.user-fold-toggle svg{width:16px;height:16px}
.user-bubble:not(.is-folded) .user-fold-toggle svg{transform:rotate(180deg)}
.msg-main{flex:1;min-width:0}

/*
 * ---------- 正文元素（.md）的基础排印：**必须自带，不能依赖聊天页祖先** ----------
 *
 * 为什么把 white-space / overflow-wrap 从 `.msg .md` 提到 `.md` 本身：
 *   `.md` 是「一条消息的一段正文」这个元素的唯一载体，而它已经**不属于聊天页**——
 *   编排页的运行结果区、日志页的对话面板都通过 window.MessageElements 渲染出
 *   同样的 `.md`（那是本项目刻意共用的一份渲染实现）。
 *   只要基础排印挂在 `.msg` 上，任何**非聊天页**宿主拿到的就是一份"没有
 *   white-space:pre-wrap"的正文：换行全部塌掉，整段挤成一坨。
 *   实测事故：日志页对话面板里 AI 回答的 markdown 标题/列表/段落全被压成
 *   一行连续文字（图片证据：用户报"里面完全没有换行啊"）。
 *
 *   因此这里确立一条不变量（由 message_element_contract.test.js 断言）：
 *   **`.md` 的基础排印必须自足；`.msg` 只允许承载"聊天页特有"的差异**
 *   （角色字号、颜色、头像、操作栏……），不得让别的宿主因为缺少它而渲染错误。
 *
 * pre-wrap 而不是 normal：正文的权威是**纯文本字符串**，
 * 其中可能有富文本解析器不处理的换行（例如只有单个换行符的软换行，
 * 或富文本不可用而降级为纯文本时）。pre-wrap 保证"文本里的换行一定看得见"。
 */
.md{white-space:pre-wrap;overflow-wrap:anywhere}
/* 正文基数字号 × 缩放系数：两种正文各按自己的基数等比缩放，
 * 于是"用户 14 / 助手 15"这个层级差在任何缩放下都保持不变。
 * 用 calc 而不是改写成绝对像素，是为了让系数成为唯一变量，
 * 将来调基数只动这一个数字。 */
.msg.user .md{font-size:calc(14px * var(--msg-font-scale));color:var(--ink)}
.msg.assistant .md{font-size:calc(15px * var(--msg-font-scale));line-height:1.75}

/* 思考容器：默认折叠，标题行显示字数，点击展开 */
.reasoning{
  margin:0 0 10px;border:1px solid var(--line);border-radius:var(--radius);
  background:var(--panel-2);overflow:hidden;
}
.reasoning-head{
  width:100%;display:flex;align-items:center;gap:8px;border:0;background:transparent;
  padding:7px 11px;font-size:12px;color:var(--ink-soft);text-align:left;
}
.reasoning-head:hover{background:var(--panel)}
.reasoning-title{font-weight:600;color:var(--ink-soft)}
.reasoning-meta{font-size:11px;color:var(--ink-faint);margin-left:auto}
.reasoning-caret{
  flex:none;width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;
  border-top:5px solid var(--ink-faint);transition:transform .16s ease;
}
.reasoning.open .reasoning-caret{transform:rotate(180deg)}
.reasoning-body{
  display:none;padding:0 11px 10px;border-top:1px solid var(--line-soft);
  white-space:pre-wrap;overflow-wrap:anywhere;
  font-size:12.5px;line-height:1.7;color:var(--ink-soft);
  max-height:340px;overflow:auto;
}
.reasoning.open .reasoning-body{display:block;padding-top:8px}

/* 底部用量：非缓存 / 缓存读 / 输出（+ 思考） */
.msg-usage{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
.msg-usage.hidden{display:none}
.usage-chip{
  display:inline-flex;align-items:center;gap:4px;font-size:11px;line-height:1;
  padding:3px 8px;border-radius:999px;border:1px solid var(--line);
  background:var(--panel-2);color:var(--ink-soft);
}
.usage-chip i{font-style:normal;color:var(--ink-faint)}
.usage-chip b{font-weight:650;color:var(--ink);font-variant-numeric:tabular-nums}
.usage-chip.live{border-style:dashed}
.usage-chip.live b{color:var(--accent)}
/*
 * 计价器：金额是 chip 内的第二个 <b>，与 token 数同格。
 * 用等宽数字（tabular-nums）让各行金额上下对齐，方便竖着扫一眼比对；
 * 颜色用强调色而不是正文字色——它是"这笔钱"的提示，不是 token 统计的一部分。
 */
.usage-cost{color:var(--accent);font-variant-numeric:tabular-nums}
/*
 * 金额与 token 数之间加一个分隔点。
 * 用 :not(:first-child) 而不是 `i + .usage-cost`：后者只在金额**紧挨**
 * 标签时才命中，而正常格子里中间还夹着 token 数（<i>标签</i><b>token</b><b>金额</b>），
 * 于是分隔点只在"合计"那一格出现——同一排 chip 里两种样子，看起来像没对齐。
 */
.usage-chip .usage-cost:not(:first-child)::before{content:'·';margin:0 5px;color:var(--ink-faint)}
.md.typing::after{content:"▍";animation:blink 1s steps(2) infinite;color:var(--accent)}
@keyframes blink{50%{opacity:0}}
.md.error-text{color:var(--danger);white-space:pre-wrap}

/*
 * ---------- 消息正文的富文本（public/rich-text.js 的样式投影） ----------
 *
 * 职责边界：这里是富文本元素的**全部**视觉定义，rich-text.js 只产出
 * 结构（.rt-* 类名 + 正确的语义标签），不产出任何内联样式。
 * 于是"长什么样"只有一处权威：换主题、调排版都只动这里。
 *
 * 为什么全部用主题 token（--ink/--line/--panel-*）而不是写死颜色：
 * 浅色与深色两套 palette 共用同一份规则，暗色适配不需要第二份 CSS
 * ——那是"两套实现慢慢走偏"的经典起点。
 *
 * 字号刻意用 em（相对正文）而不是 px：正文支持 --msg-font-scale 缩放，
 * 富文本元素必须跟着一起缩放，否则用户放大字号后代码块与表格会显得错位。
 */

/* 段落与标题：间距全部用 em，跟随正文缩放 */
.rt-p{margin:0}
.rt-p+.rt-p{margin-top:.7em}
.msg-flow .rt-p+.rt-p{margin-top:.4em}
.rt-h{margin:1em 0 .45em;line-height:1.4;font-weight:650;color:var(--ink-strong)}
.rt-h:first-child{margin-top:0}
.rt-h1{font-size:1.5em}
.rt-h2{font-size:1.32em}
.rt-h3{font-size:1.16em}
.rt-h4,.rt-h5,.rt-h6{font-size:1em}

/* 列表：缩进用 padding 而不是 margin，避免外层 margin 折叠后层级看不出来 */
.rt-ul,.rt-ol{margin:.4em 0;padding-left:1.5em}
.rt-ul{list-style:disc}
.rt-ol{list-style:decimal}
.rt-ul li,.rt-ol li{margin:.18em 0}
.rt-ul li::marker,.rt-ol li::marker{color:var(--ink-faint)}

/* 引用：左侧竖线 + 弱化文字，与气泡内已有元素（.tv-msg）同一套视觉语言 */
.rt-quote{
  margin:.6em 0;padding:.1em 0 .1em .9em;
  border-left:3px solid var(--line-strong);
  color:var(--ink-soft);
}

/* 分隔线 */
.rt-hr{margin:1em 0;border:0;border-top:1px solid var(--line)}

/*
 * 行内代码：底色 + 等宽 + 轻微内边距。
 * 用 .rt-code 而不是裸 code：裸 code 会连代码块里的 <code> 一起命中，
 * 于是代码块内部又套一层底色（视觉上像两层框）。
 */
.rt-code{
  font:.88em/1.5 var(--mono);
  background:var(--panel-2);border:1px solid var(--line-soft);border-radius:5px;
  padding:.08em .34em;color:var(--ink);
  overflow-wrap:anywhere;
}

/* 代码块：整块可横向滚动，绝不撑破气泡宽度 */
.rt-pre{
  margin:.6em 0;padding:10px 12px;
  background:var(--panel-2);border:1px solid var(--line);border-radius:var(--radius);
  overflow-x:auto;-webkit-overflow-scrolling:touch;
}
.rt-pre code{
  font:.86em/1.6 var(--mono);color:var(--ink);
  white-space:pre;display:block;min-width:max-content;
}

/* 表格：外层 wrap 负责横向滚动，表格自身不溢出（窄屏下也不挤爆气泡） */
.rt-table-wrap{margin:.6em 0;overflow-x:auto;-webkit-overflow-scrolling:touch}
.rt-table{
  border-collapse:collapse;font-size:.92em;min-width:max-content;
  border:1px solid var(--line);
}
.rt-table th,.rt-table td{
  border:1px solid var(--line);padding:5px 9px;text-align:left;vertical-align:top;
}
.rt-table th{background:var(--panel-2);font-weight:650;color:var(--ink-strong)}
.rt-table tbody tr:nth-child(even){background:var(--panel-2)}

/* 链接：强调色 + 下划线，悬停加深 */
.rt-link{color:var(--accent);text-decoration:underline;text-underline-offset:2px;overflow-wrap:anywhere}
.rt-link:hover{color:var(--accent-hover)}
/* 粗体/斜体/删除线：语义标签自带样式，这里只补删除线的弱化色 */
.rt-del{color:var(--ink-faint)}

/* 输入区 */
.composer{
  flex:none;margin:0 auto 18px;
  /*
   * position:relative 只为一件事：让「滚动到底」按钮能挂在输入框**上沿**
   * （见下方 .scroll-bottom-btn 的 bottom:100%）。
   * 它是定位基准，不是布局成员——按钮因此不参与 .composer 的盒子计算。
   */
  position:relative;
  /*
   * 输入框比消息列略宽是刻意的：它是有边框的实体控件，同宽时会显得比内容窄。
   * 但仍然从同一个 token 派生（+40px），不写第二个魔法数字——
   * 否则以后改列宽就会漏掉这一处。
   */
  width:min(calc(var(--content-col) + 40px),calc(100% - 40px));
  border:1px solid var(--line);border-radius:14px;background:var(--panel);
  box-shadow:0 4px 18px rgba(0,0,0,.05);padding:12px 14px 10px;
}
/*
 * 「滚动到底」按钮：挂在输入框上沿的浮层。
 *
 * 结构要点（这是"能不能不添乱"的全部关键）：
 *   · position:absolute + bottom:calc(100% + 8px) —— **出流**，不占 .composer 的盒子，
 *     所以它显示/隐藏时输入框和消息区都纹丝不动（不变量：开关前后
 *     输入框 getBoundingClientRect().top 逐像素不变）；
 *   · 定位基准是 .composer 自身（见上面 position:relative），坐标由 CSS 从
 *     "输入框上沿"这一个已知量推出，**不需要 JS 量坐标**——
 *     因此窗口缩放、软键盘弹出、输入框长高，它都自动跟随，没有会过期的中间量。
 *   · 右对齐：与右侧「发送」同一竖列，是"这条栏里的操作"的直觉位置；
 *     居中会让人误以为它属于消息列。
 *
 * 状态只有一个：显示 / 不显示。由 app.js 的 syncScrollBottomButton() 依 #chat
 * 的真实几何**现场推导**，不持久化、不叠加"新消息到达"之类的第二判据
 *（两套判据必然分叉，会出现"有按钮却已到底"或"没按钮却上不去"）。
 */
.scroll-bottom-btn{
  position:absolute;bottom:calc(100% + 8px);right:0;z-index:30;
  display:inline-flex;align-items:center;justify-content:center;gap:5px;
  border:1px solid var(--line);border-radius:999px;background:var(--panel);
  color:var(--ink-soft);font-size:12px;line-height:1;padding:6px 12px;
  box-shadow:0 3px 12px rgba(0,0,0,.10);
  cursor:pointer;
}
.scroll-bottom-btn:hover{border-color:var(--line-strong);color:var(--ink)}
.scroll-bottom-btn::before{
  content:'';flex:none;width:0;height:0;
  border-left:4px solid transparent;border-right:4px solid transparent;
  border-top:5px solid currentColor;
}
/*
 * 隐藏用 hidden 属性（由 JS 写入），而不是把它移出 DOM。
 * 出流的浮层显示/隐藏都不影响任何盒子的几何，所以可以放心用属性切换；
 * 保留在 DOM 里也让"锚点位置"这件事在两种状态下完全一致，便于回归验证。
 */
.scroll-bottom-btn[hidden]{display:none}
@media (max-width:760px){
  /* 移动端手指更粗：给足点击面积（仍不占布局） */
  .scroll-bottom-btn{padding:9px 14px;font-size:12.5px}
}

.composer textarea{
  width:100%;min-height:64px;max-height:220px;border:0;outline:0;resize:none;
  font:14px/1.6 var(--font);color:var(--ink);background:transparent;
}
.composer-actions{display:flex;align-items:center;gap:10px;margin-top:6px}
#hint{font-size:11.5px;color:var(--ink-faint);flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/*
 * 输入框底部的上下文占用指示（小圆环）。
 *
 * 颜色用 currentColor 驱动（SVG 里的描边读的就是它），所以配色只有一个
 * 权威：下面的 [data-level] 换 color，圆环与文字一起变——不需要在 JS 里
 * 再写一遍颜色常量。这正是"样式归 CSS、逻辑归 JS"的边界。
 *
 * 尺寸固定 16px 且不参与 flex 伸缩：它是个状态指示，不该因为 hint 文本
 * 变长而被压扁或撑大。
 */
.ctx-meter{
  flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;
  width:20px;height:20px;border-radius:50%;
  color:var(--ink-faint);
  cursor:default;outline:none;
  /* 常态安静：它每天都在，不该喧宾夺主 */
  opacity:.75;
  transition:opacity .15s ease,color .15s ease;
}
.ctx-meter:hover,.ctx-meter:focus-visible{opacity:1}
.ctx-meter:focus-visible{box-shadow:0 0 0 2px var(--accent-soft,var(--line))}
/* 接近预算：提醒但不刺眼 */
.ctx-meter[data-level="warn"]{color:#c9973f;opacity:.95}
/* 真的会丢内容：明确示警（服务端告知发生裁剪时才会到这里） */
.ctx-meter[data-level="danger"]{color:#c0533f;opacity:1}

#send{
  border:0;border-radius:10px;background:var(--ink);color:var(--bg);font-size:13.5px;
  padding:8px 18px;
  /*
   * 图标与文字并存时用 inline-flex 对齐：
   * 基线对齐会让图标看起来偏下，居中才是视觉上"贴在一起"。
   */
  display:inline-flex;align-items:center;justify-content:center;gap:6px;
}
#send:hover{background:var(--ink-strong)}
#send:disabled{background:var(--ink-faint);cursor:not-allowed}

/*
 * 生成中：按钮变成「停止」。
 *
 * 用描边危险色而不是实心：停止是"打断正在进行的事"，
 * 需要一眼可辨但不必像删除那样刺眼——它是个高频且无害的操作。
 * 悬停才转实心，给一个明确的"按下去会发生什么"的确认感。
 */
#send.is-stop{
  background:transparent;color:var(--danger);
  box-shadow:inset 0 0 0 1px var(--danger);
  cursor:pointer;
}
#send.is-stop:hover{background:var(--danger);color:var(--bg);box-shadow:none}
/* 停止不可用（正在停止中）时保持轮廓，不要突然变成灰块 */
#send.is-stop:disabled{background:transparent;color:var(--danger);opacity:.5;cursor:progress}

#send .send-ico{width:13px;height:13px;flex:none;display:block}
/* 生成中图标轻微呼吸，提示"正在进行"；尊重减少动效偏好 */
@media (prefers-reduced-motion: no-preference){
  #send.is-stop .send-ico{animation:sendbreathe 1.4s ease-in-out infinite}
}
@keyframes sendbreathe{0%,100%{opacity:1}50%{opacity:.45}}

.hidden{display:none!important}

/* ======================================================================
 * 右栏（版本历史 / 快照）的首帧形态 —— 由 <html> 上的 data 属性决定（唯一权威）
 * ======================================================================
 *
 * ★ 为什么必须是 CSS，不能等 JS（2026-09-28 实测的真实抖动）★
 *
 *   面板的显隐曾是"HTML 写死 hidden + 等 JS 把工作流拉回来再展开"。
 *   实测刷新时序（每 25ms 采样 #versionPanel 与编排区宽度）：
 *     ① 解析前            : 无元素
 *     ② HTML 解析到        : hidden，编排区 40px（布局未稳）
 *     ③ 样式就位           : hidden，编排区 602px（面板不存在 → 全宽）
 *     ④ 布局稳定           : hidden，编排区 343px
 *     ⑤ JS 取回工作流后     : 才展开，编排区 343px
 *   用户看到的就是"版本历史从无到有地弹出来"，同时编排区宽度抖一下。
 *
 *   根因：显隐依赖了一次**网络请求**（取当前工作流）。而"这一栏该不该在、
 *   现在该显示哪一块"是**版面事实**，在首帧就必须正确 —— 与主题、字号、
 *   分栏宽度同类。引导脚本 public/orch-rightcol-init.js 在 <head> 里同步执行
 *   （先于样式表），所以用它的投影驱动 CSS，首帧就是用户上次留下的那一态。
 *
 *   与分栏宽度的 --orch-*-w 完全同构：都是"引导脚本投影 → CSS 消费"。
 *
 * ★ 用户 2026-10-01 的结构改动：两块各自折叠 → 一个容器 + 两个 tab ★
 *   改前 #versionPanel 与 #snapShotBody 各有一个 data-orch-*-open 决定自己
 *   折不折，于是两块同时占高度，两个列表各只有一百多像素。
 *   现在只有一个状态：**哪个 tab 被选中**（data-orch-right-tab），
 *   被选中的那个 pane 吃掉整个右栏高度。所以下面按 tab 选择器控制显隐，
 *   而不再按"某一块自己的折叠态"。
 *
 * 这一整段只在**宽屏**生效（min-width:1081px）：
 *   窄屏（≤1080px）右栏整列由 CSS 退出布局（.orch-col-run{display:none}），
 *   两块正文此刻在底部抽屉里 —— 若这些规则在那里也生效，
 *   它们会把抽屉的内容一起藏掉（藏的是同一份 DOM，见 syncSheetLayout）。
 *   1081 这个断点与 .orch-cols 的桌面网格、layout-resize.js 的 desktopQuery 同源。
 */
@media (min-width: 1081px){
  /*
   * 整栏收起：把这一列**原地压成 0 宽**（不占位）。
   *
   * ★ 用 width:0 + overflow:hidden，不用 display:none ★
   *   与设置区折叠同一个理由（见 html[data-orch-settings-collapsed="1"] .orch-cols 段）：
   *   这一栏在网格里占一条轨道，display:none 会让后面的兄弟节点
   *   顶上来认领轨道，列序一变就是一次静默错位。
   *
   * ★ 0 轨道而不是"把宽度变量清零"★
   *   网格模板用的是 var(--orch-snap-w)（用户可拖）。收起时把这条轨道
   *   压成 0，但**变量本身不动** —— 展开时原样回来，用户拖过的宽度
   *   不会因为收了一次就被打回默认值（那会让人以为"收起把我的设置弄丢了"）。
   *   与左栏折叠、设置区折叠是同一套做法。
   *
   * ★ 这里**只声明自己那两条轨道**（2026-10-03 结构性修复）★
   *   本规则曾把五条轨道**整条重写一遍**（且把设置区那条写死成
   *   var(--orch-set-w,320px)）。于是它与 `html[data-orch-settings-collapsed="1"] .orch-cols`
   *   成了同一个属性的两个完整声明，靠特异性决胜负：
   *   本规则 (0,2,1) 胜过 (0,2,0)，设置区折叠被整条丢弃 ——
   *   三个面板全收起时，编排区左边仍空着一条 320px 死轨道。
   *   现在每段只写自己那条：同时折叠时两个声明**互不相干**，都生效。
   */
  html[data-orch-right-open="0"] .orch-cols{
    --orch-split-resizer-track:0px;
    --orch-snap-track:0px;
  }
  html[data-orch-right-open="0"] .orch-col-run{
    /* 仍在网格流里，只是被压成 0 宽；不得用 display:none（会长出上面的问题） */
    min-width:0;padding:0;border:0;overflow:hidden;visibility:hidden;
  }
  html[data-orch-right-open="0"] .orch-split-resizer{
    /* 同设置区：留在原处，只是不可见、不可抓（不销毁，展开后立即可用） */
    visibility:hidden;pointer-events:none;
  }

  /*
   * 两个 pane 的显隐 —— **完全由选中的 tab 决定**（一个状态，一个出口）。
   *
   * 为什么不做成 JS 加 .hidden：
   *   那会成为"首帧靠 CSS、之后靠 JS"两套机制，必然在切换的那一帧出现
   *   空档（JS 还没跑到，两块都按默认显示 → 闪一下双份内容）。
   *   现在 CSS 是唯一下发点，JS 只改 data 属性。
   */
  html[data-orch-right-tab="ver"] #secSnapshots{display:none}
  html[data-orch-right-tab="snap"] #versionPanel{display:none}

  /*
   * ★ "当前没有打开的工作流" 不再动 pane 的显隐（2026-10-01 右栏 tab 化时纠正）★
   *
   *   旧写法是 #versionPanel.needs-workflow{display:none}。
   *   它在"版本历史只是一块可折面板、栏宽由固定网格轨道决定"的年代是安全的：
   *   藏掉它不改变任何几何，纠正发生在数据到位之后，用户看不见。
   *
   *   右栏 tab 化之后这条不再成立，而且**实测有可见的抖动**：
   *   tab 机制让 pane 默认就是可见的，"有没有工作流"成了唯一会隐藏它的东西。
   *   而 current 是异步取回的，于是每次刷新都必然经历
   *       第 0 帧   pane 可见、列表还是空的（一个 684px 的空盒子）
   *       第 1~3 帧 .needs-workflow 加上 → pane 塌成 0
   *       第 4 帧   数据到位 → 又弹回 842px
   *   这正是用户两次反馈过的"从无到有地弹一下"（见 5f32dd5）。
   *
   *   结构上的正确分法：把两个状态各自归位到**不会移动几何**的那个层面 ——
   *     · "用户要不要看这一栏 / 在看哪个 tab"（持久偏好）
   *         → data 属性，首帧由引导脚本投影，只由它决定 pane 的显隐。
   *           于是 pane 的几何全程恒定（这本来也是用户要的"高度很大"）。
   *     · "此刻有没有工作流"（易变业务态）
   *         → 只影响** pane 内部的正文**（列表自己的空态文案，见 renderVersionList），
   *           不再让整块 pane 出现/消失。
   *   两个层面各自唯一、互不越界：偏好管"在不在"，业务态管"里面是什么"。
   */
}

/*
 * 右栏整栏收起后，顶栏那个按钮必须**立刻**变成"可点开"的样子 ——
 * 它同时也是"右栏现在收着"的唯一指示（栏本身已经不在屏幕上了）。
 * 与 .orch-pref-toggle.is-on 同一套视觉语言（实心强调 = 当前处于"已收起"这个状态）。
 */
#orchRightToggle.is-off{color:var(--accent);border-color:color-mix(in srgb,var(--accent) 45%,var(--line))}

/*
 * 聊天页设置的「首帧打开态」。
 *
 * 弹窗在 HTML 里是 .modal.hidden（默认关），这个类由 chat-settings-init.js 在
 * <head> 中**首帧之前**打到 <html> 上——那时 body 还没解析，改不到弹窗本身，
 * 所以只能用祖先选择器把初始显示态定下来。
 *
 * 为什么必须用 !important：
 *   .hidden 是 !important 的，而本规则要覆盖它，同等优先级下后者才生效。
 *   这不是"打补丁"，而是两条规则的优先级必须明确：
 *   "首帧静态初值"（本规则）要能压过"HTML 里的默认隐藏"。
 *   一旦 app.js 启动，它会调 clearBootstrapMark() 摘掉这个类，
 *   之后弹窗的显示完全由 .hidden 与 setChatSettingsOpen() 决定，本规则不再参与。
 */
html.chat-settings-open #chatSettingsModal{display:flex!important}

/* 移动端遮罩层 */
/*
 * 这一条是**聊天页**的口径：那边侧栏是 z-index:30，遮罩 29 紧贴它之下。
 * 编排页的抽屉是另一个层级（--z-side-drawer=48），遮罩必须跟着抬，
 * 见下方 @media (max-width:760px) 里的 .orch-page .scrim 覆盖 ——
 * 两页各自的值都写在自己那一处，不在这里做一个"两页通用"的折中值
 * （折中值必然对其中一页是错的）。
 */
.scrim{
  position:fixed;inset:0;z-index:29;background:rgba(0,0,0,.32);
  opacity:1;transition:opacity .2s ease;
}
.scrim.hidden{display:none}

/* 登录页 */
.login-page{min-height:100dvh;display:grid;place-items:center;padding:24px;background:var(--bg)}
.login-card{width:min(100%,380px);padding:32px;border:1px solid var(--line);border-radius:16px;background:var(--panel);box-shadow:0 14px 45px rgba(0,0,0,.08)}
.login-mark{width:48px;height:48px;border-radius:13px;background:var(--accent);color:var(--accent-ink);display:grid;place-items:center;font-weight:700;font-size:25px;margin-bottom:22px}
.login-card h1{font-size:22px;margin:0 0 6px;color:var(--ink-strong)}
.login-card p{margin:0 0 24px;color:var(--ink-soft);font-size:13px}
.login-field{display:block;margin:14px 0 0;color:var(--ink-soft);font-size:12px}
.login-field input{display:block;width:100%;margin-top:6px;padding:11px 12px;border:1px solid var(--line);border-radius:9px;background:var(--input);color:var(--ink);font:14px var(--font);outline:none}
.login-field input:focus{border-color:var(--accent)}
.login-submit{width:100%;margin-top:22px;padding:11px;border:0;border-radius:9px;background:var(--ink);color:var(--bg);font:14px var(--font);cursor:pointer}
.login-submit:disabled{opacity:.6}
.login-error{min-height:20px;margin-top:12px;color:var(--danger);font-size:12px}
@media(prefers-color-scheme:dark){.login-card{box-shadow:0 14px 45px rgba(0,0,0,.3)}}

/* 移动端 */
@media (max-width:760px){
  body{
    /* 防止 iOS 输入框聚焦时整体放大，并避免软键盘撑高触发布局跳动 */
    -webkit-text-size-adjust:100%;text-size-adjust:100%;
  }
  aside{
    position:fixed;top:0;left:-300px;width:300px;height:100%;z-index:30;
    transition:left .22s ease;box-shadow:6px 0 24px rgba(0,0,0,.12);
  }
  aside.open{left:0}
  /*
   * 移动端侧栏是"抽屉"：宽度固定 300px、由 ☰ 控制开合。
   * 抽屉上做拖拽宽度既没有意义（它不是常驻布局），还会和左右滑动
   * 手势打架（用户想关抽屉，结果把手柄拖动了）。所以手柄在窄屏直接不显示。
   */
  .side-resizer{display:none}
  #menu{display:block}
  /* 移动端无 hover：删除按钮常驻显示，方便直接操作 */
  .session-del{display:block;padding:0 12px;min-width:40px;display:flex;align-items:center;justify-content:center}
  /* 单行紧凑栏。跨页入口共用 details 菜单，320px 也无需横滑找按钮。
     图形仅 18px，操作按钮宽44px、高36px；浮层出流，不参与顶栏高度计算。 */
  .chat-head{
    position:relative;height:40px;gap:4px;overflow:visible;
    padding-left:max(8px,env(safe-area-inset-left));
    padding-right:max(8px,env(safe-area-inset-right));
  }
  .chat-head>#menu,.chat-head>#newChat,.chat-head>#chatSettingsBtn,
  .chat-head>#chatThemeBtn,.chat-navigation>summary{
    display:inline-flex;align-items:center;justify-content:center;flex:0 0 44px;
    width:44px;height:36px;min-height:36px;margin:0;padding:0;
    border:0;border-radius:10px;background:transparent;color:var(--ink-soft);
    touch-action:manipulation;cursor:pointer;
  }
  .chat-head .chat-action-label,.chat-head .theme-toggle-label{display:none}
  .chat-head .theme-toggle-icon{display:flex;align-items:center;justify-content:center}
  .chat-head>#model{
    flex:0 1 120px;min-width:48px;max-width:120px;height:28px;min-height:28px;
    margin:0 auto 0 0;padding:0 10px;font-size:12px;
    white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  }
  .chat-head>#chatThemeBtn{margin-left:auto}
  .chat-navigation{display:block;flex:0 0 44px}
  .chat-navigation>summary{list-style:none}
  .chat-navigation>summary::-webkit-details-marker{display:none}
  .chat-head .chat-navigation>.head-nav{
    display:none;position:absolute;right:8px;top:calc(100% + 4px);z-index:45;
    min-width:160px;padding:6px;flex-direction:column;align-items:stretch;gap:2px;
    border:1px solid var(--line);border-radius:12px;background:var(--panel);
    box-shadow:0 8px 28px rgba(0,0,0,.18);
  }
  .chat-head .chat-navigation[open]>.head-nav{display:flex}
  .chat-head .head-nav .view-toggle{min-height:44px;justify-content:flex-start;padding:0 14px;font-size:14px}
  .chat-head button:active,.chat-navigation>summary:active{background:var(--panel-2)}
  .chat-navigation>summary:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
  /* 压缩顶部：目录下拉 + 两个按钮同一行，减少垂直占位，扩大聊天显示区 */
  .toolbar{padding:8px 12px;flex-wrap:nowrap;gap:6px}
  .toolbar select{flex:1 1 96px;max-width:none;order:0;min-width:96px;font-size:12px;padding:6px 8px}
  .toolbar button{flex:0 0 auto;min-height:34px;font-size:12px;padding:6px 9px}
  .toolbar #build{order:1}
  .toolbar #edit{order:2}
  /* 状态提示在移动端仍是出流浮层，只调锚点与字号：贴 header(40px) 下沿、
     左右各留 12px，避免在小屏上溢出；order/flex 随流内角色一并作废。 */
  .toolbar-status{top:calc(40px + 8px);left:12px;right:12px;max-width:none;width:auto;font-size:11px}
  #status{font-size:11px}
  #chat{padding:12px 14px 6px}
  .msg{gap:9px;margin-bottom:20px}
  .reasoning-body{max-height:260px;font-size:12px}
  .usage-chip{font-size:10.5px;padding:2px 7px}
  .composer{
    width:calc(100% - 16px);padding:8px 10px;
    margin-bottom:calc(6px + env(safe-area-inset-bottom));border-radius:16px;
  }
  html[data-chat-page] .composer .composer-input{min-height:32px;max-height:min(180px,28dvh)}
  .composer-actions{margin-top:2px;gap:8px}
  .composer #send{width:44px;height:44px;min-height:44px;padding:0;border-radius:12px;flex:none}
  .composer #send>span{display:none}
  .composer #send .send-ico{width:18px;height:18px}
  .composer textarea{min-height:56px;font-size:16px}
  .panel{margin:12px 12px 0;max-height:60vh;overflow:auto}
  /* iOS 刘海/底部安全区适配 */
  aside{padding-top:env(safe-area-inset-top)}
  #menu{margin-left:env(safe-area-inset-left)}
}

/* ================= 账号信息注入 ================= */
/*
 * 状态栏：窄屏下宁可换行也不截断成半句。
 * 白名单：这一行信息重要（构建结果/压缩说明），被 ellipsis 截掉会让人看不懂；
 * 允许最多两行，并在极窄时收缩，避免把工具条撑破。
 */
#accStatus{
  font-size:12px;color:var(--ink-faint);margin-left:auto;
  min-width:0;flex:0 1 auto;text-align:right;line-height:1.45;
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;
  line-clamp:2;overflow:hidden;overflow-wrap:anywhere;
}

/* 设置面板：文档 / 图片双列 */
.acc-panel-body{display:flex;gap:18px;align-items:flex-start}
.acc-col{flex:1 1 0;min-width:0}
.acc-col-head{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-faint);margin-bottom:8px}
.acc-list{max-height:320px;overflow:auto;border:1px solid var(--line);border-radius:8px;background:var(--input);padding:6px}
.acc-item{display:flex;align-items:center;gap:8px;padding:5px 6px;border-radius:6px;font-size:12.5px;color:var(--ink-soft);cursor:pointer}
.acc-item:hover{background:var(--panel-2)}
.acc-item input{accent-color:var(--accent);flex:none}
.acc-item .acc-unavailable{color:var(--ink-faint);opacity:.6}
.acc-item-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

/*
 * 预览面板单独放宽高度。
 *
 * 实测（1100px 视口）：通用 .panel 的 min(78vh,640px) 让内容区只有 517px，
 * 而文件列表需要 568px，最后一项被截断、必须滚动才能看全，
 * 同时面板下方还剩大量空白没利用上。
 *
 * 高度按「视口高 - 顶部偏移(约172px) - 底部留白」计算，而不是继续用 vh 比例：
 * 面板上方有工具栏，偏移是固定值；纯按 vh 算会让「面板高 + 偏移」超过视口，
 * 把底部操作栏推出屏幕外（实测 760px 视口下越界）。保留 900px 上限防止超大屏过高。
 */
#accPreview{max-height:min(calc(100vh - 240px),900px)}

/* 输入框上方标记条 */
.chips{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:10px}
.chip{
  display:inline-flex;align-items:center;gap:8px;max-width:100%;
  padding:5px 8px 5px 10px;border:1px solid var(--line-strong);border-radius:999px;
  background:var(--panel-2);font-size:12px;color:var(--ink);
}
.chip .chip-dot{width:7px;height:7px;border-radius:50%;background:var(--accent);flex:none}
.chip .chip-label{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px}
.chip .chip-meta{color:var(--ink-faint);white-space:nowrap}
.chip button{border:0;background:transparent;color:var(--ink-faint);font-size:12px;line-height:1;padding:2px 4px;border-radius:6px}
.chip button:hover{color:var(--ink);background:var(--panel)}
.chip .chip-x:hover{color:var(--danger)}

/* 消息气泡上的「附带账号」标识 */
.msg-accounts{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}
.msg-account-tag{
  font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--line);
  color:var(--ink-soft);background:var(--panel-2);
}

#accRefresh{
  font-size:13px;padding:7px 10px;border:1px solid var(--line);border-radius:8px;
  background:var(--input);color:var(--ink-soft);
}
#accRefresh:hover{border-color:var(--line-strong);color:var(--ink)}

/*
 * 中窄屏（761–1080px）：工具条按钮 + select 已经占满一行，
 * 状态栏只分到几十像素，两行 clamp 也会被截。
 * 这里让它独占一整行（同移动端做法），彻底避免「半句话」。
 */
@media (max-width:1080px){
  #accStatus{
    flex:1 1 100%;margin:6px 0 0;width:100%;
    text-align:left;-webkit-line-clamp:3;line-clamp:3;
  }
  #status{text-align:left}
}

@media (max-width:760px){
  #accStatus{
    flex:1 1 100%;margin:2px 0 0;width:100%;font-size:11px;
    text-align:left;-webkit-line-clamp:3;line-clamp:3;
  }
  /* 按钮文字不再换行，靠 select 收缩让位 */
  .toolbar button{white-space:nowrap}
  .acc-panel-body{flex-direction:column;gap:14px}
  .acc-list{max-height:180px}
  .chip .chip-label{max-width:110px}
}

/* ================= v2：条数上限 / 预览导航 / 图片预览 ================= */
.acc-row-limit{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:14px;
  padding:10px 12px;border:1px solid var(--line);border-radius:8px;background:var(--panel-2)}
.acc-row-limit label{font-size:12.5px;color:var(--ink-soft)}
.acc-row-limit input{width:110px;padding:6px 8px;border:1px solid var(--line);border-radius:7px;
  background:var(--input);color:var(--ink);font:13px var(--font);outline:none}
.acc-row-limit input:focus{border-color:var(--accent)}
.acc-row-hint{font-size:11.5px;color:var(--ink-faint)}

/* 预览：左导航 + 右内容
 *
 * 高度约定：整块由面板 flex 分配（flex:1 + min-height:0），左右两栏 height:100% 填满、内部滚动。
 *
 * 改造原因：左右两栏此前各自写死 max-height:56vh，与面板自身的高度上限
 * （min(78vh,640px)）独立计算、互不知情，两套高度来源容易对不上，
 * 栏内可用高度也无法随面板变化。改为完全由面板分配后，高度只有一处权威来源。
 * 注意：两栏高度不再单独限制，其可用高度由 #accPreview 决定。
 */
.acc-preview-body{display:flex;gap:14px;align-items:stretch;flex:1;min-height:0}
.acc-preview-nav{
  flex:0 0 210px;height:100%;overflow:auto;
  border:1px solid var(--line);border-radius:8px;background:var(--panel-2);padding:6px;
}
.acc-preview-nav-item{
  display:block;width:100%;text-align:left;border:0;background:transparent;
  padding:6px 8px;border-radius:6px;font-size:12px;color:var(--ink-soft);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer;
}
.acc-preview-nav-item:hover{background:var(--panel);color:var(--ink)}
.acc-preview-nav-item.active{background:var(--panel);color:var(--ink);font-weight:600}
.acc-preview-nav-group{font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;
  color:var(--ink-faint);padding:8px 8px 4px}
.acc-preview-nav-meta{font-size:10.5px;color:var(--ink-faint);padding:0 8px 2px}
.acc-preview-main{flex:1;min-width:0;min-height:0;display:flex;flex-direction:column}
.acc-preview-section{min-height:0;flex:1;display:flex;flex-direction:column}
.acc-preview{flex:1;min-height:0;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;
  font:12px/1.65 var(--mono);color:var(--ink);background:var(--input);
  border:1px solid var(--line);border-radius:8px;padding:12px;margin:0}

/* 图片画廊 */
.acc-preview-gal{display:flex;flex-direction:column;gap:14px;flex:1;min-height:0;overflow:auto;
  border:1px solid var(--line);border-radius:8px;background:var(--input);padding:12px}
.acc-preview-gal figure{margin:0}
.acc-preview-gal figcaption{font-size:11.5px;color:var(--ink-soft);margin-bottom:6px}
.acc-preview-gal img{max-width:100%;height:auto;border:1px solid var(--line-soft);border-radius:6px;display:block;background:#fff}

@media (max-width:760px){
  .acc-preview-body{flex-direction:column}
  /* 窄屏纵向堆叠：导航栏固定高度、内容区占剩余空间，同样由 flex 分配而非写死 vh */
  .acc-preview-nav{flex:0 0 auto;height:150px;width:100%}
  .acc-preview-main{flex:1;min-height:160px}
  .acc-row-limit{flex-direction:column;align-items:flex-start;gap:6px}
}

/* ================= 跨页导航（对话 / 编排 / 日志） =================
 *
 * 这三个入口在三个页面里都出现，语义是同一件事：切到另一个页面。
 * 曾经的问题有两层，都在这里一次性收口：
 *
 * 一、它们只写了 class="view-toggle" 却**从未定义过样式**，渲染成裸文字链接，
 *     夹在一排描边胶囊按钮中间，同一行里出现三套视觉语汇，像没写完。
 *     处理：与同排描边控件对齐尺寸与圆角体系（同高、同字号、同描边，形状取胶囊），
 *     但**不复用按钮类**——按钮带「可按下/选中」语义与 aria-pressed，
 *     导航链接点了就跳页，两者语义不同，混用会让
 *     「当前是否开启」与「当前在哪一页」两个概念纠缠不清。
 *     当前页用 aria-current="page" 标记，样式为常驻浅底：
 *     既不抢强调色（那是 active 开关的专属），又能一眼看出自己在哪。
 *
 * 二、位置在三个页面不统一（2026-09-19 用户指出）：编排页与日志页的导航落在
 *     顶栏最右端角落，聊天页却夹在一排控件中间，切页时手要重新找位置。
 *     处理：把「导航永远在顶栏最右端」定为**三页同一条约定**，并拆成两条可执行规则——
 *       · DOM：<nav class="head-nav"> 必须是顶栏的最后一个子元素（三页一致）；
 *       · 布局：顶栏右簇以 .head-right 起头，用自动外边距吃掉左侧空档，
 *         于是右簇整体靠右、导航贴着右边内边距。
 *     2026-10-17 补充：右簇的第一颗控件必须**三页同构**（都是主题切换按钮），
 *     因为「右簇靠右」意味着左缘位置由右簇总宽决定，三页右簇长度不同则
 *     主题按钮在切页时会左右跳。三页同构由 head_nav_position.contract.test.js
 *     的「主题切换按钮三页同构」那组契约钉住（位置 / 样式唯一 / 投影唯一）。
 *     为什么不用「给导航自己加 margin-left:auto」：那会把主题/模型/追踪留在左侧，
 *     导航孤零零飞到最右，中间空一大段，反而和编排页/日志页的观感不一致；
 *     那两页的右簇本来就是「工具 + 导航」连在一起的整体。
 *     为什么用自动外边距而不是空 div 当弹性空档：窄屏顶栏是横向滚动容器
 *     （见 @media max-width:760px），空 div 在溢出时会变成 0 宽，
 *     而自动外边距在无空档时自然归零，两种情况下行为都正确。
 *     这两条约定由 head_nav_position.contract.test.js 静态钉住。
 */
/* 三个导航项聚成一组：与相邻的全局控件在视觉上分开 */
.head-nav{display:flex;gap:6px;align-items:center}

/*
 * 顶栏右簇起点。放在右簇第一个元素上，把右簇整体顶到顶栏右端。
 * 编排页/日志页靠标题或状态条 flex:1 天然占满空档，不需要它；
 * 聊天页顶栏全是定宽控件，必须显式指定从哪里开始靠右。
 *
 * 三页右簇的第一个元素都是主题切换按钮（#chatThemeBtn / #orchThemeBtn /
 * #logThemeBtn），实现见下方「主题切换按钮」一节 —— 主题按钮的**唯一权威**
 * 是 /theme-init.js 的 window.PromptLabTheme，按钮本身只是它的投影。
 */
.head-right{margin-left:auto}

.view-toggle{
  display:inline-flex;align-items:center;gap:6px;
  font-size:12px;line-height:1;padding:5px 12px;border-radius:999px;
  border:1px solid var(--line);background:var(--panel);color:var(--ink-faint);
  white-space:nowrap;text-decoration:none;box-sizing:border-box;
  transition:background .15s ease,color .15s ease,border-color .15s ease;
}
.view-toggle:hover{border-color:var(--line-strong);color:var(--ink-soft);background:var(--panel-2)}
.view-toggle:active{transform:translateY(.5px)}
.view-toggle:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
.view-toggle[aria-current="page"]{
  background:var(--panel-2);border-color:var(--line-strong);color:var(--ink);font-weight:600;
}
.view-toggle[aria-current="page"]:hover{color:var(--ink)}

@media (max-width:760px){
  .view-toggle{font-size:11px;padding:4px 9px}
}

/* PWA overscroll：上下越界时露出的必须是主背景，而不是强调色 */
html{overscroll-behavior-y:none}
body{overscroll-behavior-y:none}

/* 通用弹窗 */
.modal{position:fixed;inset:0;z-index:var(--z-modal,60);display:flex;align-items:center;justify-content:center;padding:20px}
/*
 * 嵌套弹层：在"已经有弹窗正显示"的前提下打开的这一层。
 *
 * 这个类**只由 modal-stack.js 依据运行时事实上/下**，任何页面都不该手写——
 * 手写就等于把"该在哪一层"重新变回各处的自由判断（正是本类要消灭的东西）。
 * 非嵌套时它被移除，弹窗回到 --z-modal 这一层，行为与改动前逐字一致。
 */
.modal.is-nested{z-index:var(--z-modal-child,65)}
.modal::before{content:'';position:absolute;inset:0;background:rgba(0,0,0,.5)}
.modal-card{
  position:relative;width:min(1000px,100%);max-height:86vh;display:flex;flex-direction:column;
  background:var(--panel);border:1px solid var(--line);border-radius:14px;overflow:hidden;
  box-shadow:0 20px 60px rgba(0,0,0,.35);
}
.modal-head{
  display:flex;align-items:center;justify-content:space-between;gap:12px;
  padding:14px 18px;border-bottom:1px solid var(--line);font-size:14px;font-weight:600;
}
.modal-body{padding:16px 18px;overflow:auto;min-height:0}
.icon-btn{
  border:1px solid var(--line);background:var(--panel);color:var(--ink-soft);
  border-radius:8px;padding:4px 9px;font-size:13px;line-height:1.2;
}
.icon-btn:hover{border-color:var(--line-strong);color:var(--ink)}

@media (max-width:760px){
  .modal{padding:10px}
  .modal-card{max-height:92vh}
}

/* ================= 顶部新建按钮（原标题位置） ================= */
.new-chat-btn{
  display:inline-flex;align-items:center;gap:7px;
  padding:7px 14px;border-radius:10px;
  border:1px solid var(--line);background:var(--panel);color:var(--ink);
  font-size:13px;font-weight:600;white-space:nowrap;
}
.new-chat-btn:hover{border-color:var(--accent);color:var(--accent)}
.new-chat-plus{font-size:15px;line-height:1;color:var(--accent)}

/* ================= 面板：移动端可关闭（关键修复） =================
 * 问题：面板内容长时，底部「关闭」按钮会被滚出视口；移动端弹窗外点也没反应。
 * 方案：面板整体限高 + 内容区滚动 + 操作栏吸底常驻；并加遮罩支持点击关闭。
 */
.panel > .panel-body{display:flex;flex-direction:column;min-height:0}
/* 操作栏：普通流式排列即可；关闭入口已由顶部常驻的 × 提供 */
.panel-actions{flex:none;flex-wrap:wrap}
@media (max-width:760px){
  /* 窄屏浮层接近全屏，同样不占文档流 */
  .panel{width:96vw;max-height:92vh;padding:12px}
  .panel-actions button{flex:1 1 auto;min-width:74px}
  .panel-head{margin-bottom:8px}
}

/* 面板遮罩：移动端点击面板外可关闭 */
.panel-backdrop{
  position:fixed;inset:0;z-index:30;background:rgba(0,0,0,.28);
  display:none;
}
.panel-backdrop.show{display:block}

/*
 * 面板 = 浮层弹窗（不占文档流）。
 *
 * 原实现把面板当作普通块级元素（margin:14px 20px 0，无定位），
 * 它插在消息区与输入框之间：展开时撑开几百像素高度，把下方内容整体推下去，
 * 关闭时又弹回来——每点一次预览，消息区就抖一下。
 *
 * 改成 position:fixed 居中浮层后，面板脱离文档流，显示/隐藏不影响任何内容布局。
 * 尺寸与既有 trace 弹窗保持同一套观感；宽度放宽到 1200px，
 * 因为预览里常是长行表格（历史文章等），宽一点少折行。
 */
.panel{
  position:fixed;
  left:50%;top:50%;transform:translate(-50%,-50%);
  z-index:31;
  width:min(1200px,94vw);
  margin:0;
  max-height:min(86vh,900px);
  display:flex;flex-direction:column;
  /* 视觉样式 */
  border:1px solid var(--line);border-radius:var(--radius);background:var(--panel);
  padding:14px;box-shadow:0 2px 14px rgba(0,0,0,.04);
}
.panel.hidden{display:none}

/* 浮层内需要滚动的区域 */
.panel > .panel-body,
.panel > .acc-panel-body,
 .panel > .panel-body{
  overflow:auto;min-height:0;
}

/* 面板标题行：标题 + 常驻关闭按钮（不随内容滚动） */
.panel-head{
  display:flex;align-items:center;gap:10px;flex:none;
  margin-bottom:10px;padding-bottom:9px;border-bottom:1px solid var(--line-soft);
}
.panel-head .panel-title{margin:0;flex:1;min-width:0}
.panel-x{
  flex:none;width:28px;height:28px;line-height:1;padding:0;
  border:1px solid var(--line);border-radius:8px;background:var(--input);
  color:var(--ink-soft);font-size:15px;
}
.panel-x:hover{border-color:var(--line-strong);color:var(--ink)}

/* 已发送消息里的账号标识：可点击回看预览 */
.msg-account-tag.clickable{
  cursor:pointer;border-color:var(--line-strong);color:var(--ink);
}
.msg-account-tag.clickable:hover{border-color:var(--accent);color:var(--accent)}

/* ---------- 批量重跑（全部重跑）进度 ---------- */

.acc-batch-bar{
  height:6px;border-radius:999px;background:var(--line-soft);overflow:hidden;
}
.acc-batch-fill{
  height:100%;width:0;border-radius:999px;background:var(--accent);
  transition:width .25s ease;
}




/* 旧的输入框浮层（textarea + 文件选择器）已随块编辑器改造移除；
   现在聊天输入框用的是 .t-input + .fsel（与编排页同一套） */

/* 文件芯片（输入框上方的已选列表） */
.file-chip{
  display:inline-flex;align-items:center;gap:7px;max-width:100%;
  padding:5px 8px 5px 10px;border:1px solid var(--line-strong);border-radius:999px;
  background:var(--input);color:var(--ink);font-size:12px;
}
.file-chip-name{max-width:260px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.file-chip-x{
  border:0;background:transparent;color:var(--ink-faint);cursor:pointer;
  font-size:13px;line-height:1;padding:0 2px;
}
.file-chip-x:hover{color:var(--danger)}

/* 消息里的文件标签：可点击查看 */
.msg-files{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
.msg-file-tag{
  display:inline-flex;align-items:center;gap:6px;max-width:100%;
  padding:3px 9px;border:1px solid var(--line);border-radius:999px;
  background:var(--panel-2);color:var(--ink-soft);font-size:11.5px;
}
.msg-file-tag.clickable{cursor:pointer;border-color:var(--line-strong);color:var(--ink)}
.msg-file-tag.clickable:hover{background:var(--panel);color:var(--accent)}
.msg-file-tag .msg-file-meta{color:var(--ink-faint);font-size:10.5px}
#filePreview{max-height:min(calc(100vh - 240px),900px)}
.file-preview-meta{font-size:11.5px;color:var(--ink-faint);flex:none}

/* 芯片主体是按钮：清掉按钮默认样式，保持与芯片外观一致 */
button.file-chip-name{
  border:0;background:transparent;color:inherit;font:inherit;
  padding:0;max-width:260px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  cursor:pointer;
}
button.file-chip-name:hover{color:var(--accent);text-decoration:underline}
button.file-chip-name:disabled{cursor:default;text-decoration:none}

/* ---------- 消息操作按钮（复制 / 重试 / 分支 / 编辑） ---------- */
/*
 * 操作按钮排：复制 / 重试 / 分支 / 编辑。
 *
 * 它是**独立的一排**，不挂在 .msg-usage 里——后者是"用量 chip"，
 * 用户消息没有用量，两者语义不同（此前合并导致用户消息悬停看不到按钮）。
 *
 * ===== 显隐权威：三个条件取并集，只有这一处 =====
 *
 *   ① .msg:hover              —— 指针（鼠标）停在消息内：最常见路径，不必先点一下；
 *   ② .msg:focus-within       —— 消息获得键盘焦点（Tab 进入 / 键盘选中）；
 *   ③ .msg.is-actions-open    —— 点击或 Enter/空格**选中**这条消息（常驻）。
 *
 * 为什么三者都要（少任何一个都会造出一个"看不见"的死角）：
 *   · 只有 ③（此前状态）时，鼠标悬停不显示，必须先点一下——用户报告的实际体验；
 *   · 只有 ① 时，键盘用户和"点开就要它常驻（比如连着点几个按钮、
 *     或选中后把鼠标移开去做别的事）"的场景会看不到；
 *   · 只有 ①② 时，触屏没有 hover，点选后就再也不显示。
 *
 * 为什么用 opacity 而不是 visibility：visibility 是"显示/不显示"的开关，
 * 无法做淡入淡出的过渡；而且 visibility:hidden 的元素仍占位却不可命中，
 * 与"进/出消息区域时视觉上自然浮现"的期望不一致。opacity 保留同一套
 * 可访问性命中规则（pointer-events 显式跟进），过渡只改变观感。
 *
 * 命中规则必须跟显隐严格一致：不可见的按钮绝不能吃点击，否则用户会
 * 点到一排看不见的按钮（复制/删除都在这排里）。
 */
.msg-toolbar{
  display:flex;align-items:center;justify-content:flex-start;margin-top:4px;min-height:36px;
  opacity:0;pointer-events:none;
  transition:opacity .12s ease;
}
.msg.user .msg-toolbar{justify-content:flex-end}
.msg:hover .msg-toolbar,
.msg:focus-within .msg-toolbar,
.msg.is-actions-open .msg-toolbar{opacity:1;pointer-events:auto}
.msg-actions{display:inline-flex;gap:4px;flex-wrap:wrap}
.msg-action{
  display:inline-flex;align-items:center;justify-content:center;
  width:36px;height:36px;padding:0;border-radius:8px;
  border:0;background:transparent;color:var(--ink-soft);cursor:pointer;
}
.msg-action svg{width:17px;height:17px;flex:none}
.msg:focus-visible{outline:1px solid var(--line-strong);outline-offset:4px}
.msg-action:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
.msg-action:hover{border-color:var(--line-strong);background:var(--panel);color:var(--ink)}
.msg-action:disabled{opacity:.5;cursor:default}
.msg-action.done{border-color:var(--accent);color:var(--accent)}
/*
 * 破坏性操作（删除）：常态与其他按钮一致（它每天都在，不该一直刺眼），
 * 只有**悬停时**才转成危险色——那正是"按下去会发生什么"最需要被看见的一刻。
 * 用与停止按钮同一个 --danger，全站破坏性操作的观感因此保持一致。
 */
.msg-action.danger:hover{
  border-color:var(--danger);background:transparent;color:var(--danger);
}
/*
 * 实时时间戳（用户 2026-10-01 要求）。
 *
 * 两个元素，各自回答一件事：
 *   · .msg-stamp（按钮）    —— "这条消息现在带不带时间戳"，可点、亮灭表达状态；
 *   · .msg-stamp-badge（徽标）—— "带的是什么时间"，只读、由服务端 content 派生。
 *
 * ★ 亮态为什么用 accent 实心而不是只换边框色 ★
 *   用户原话是"点击后变亮"。这排按钮常态是透明的、悬停才显形，
 *   若亮态只比常态深一点点，在一排按钮里根本看不出"这条开着"——
 *   而那正是这个按钮唯一的表达。用强调色填充，状态一眼可辨。
 *   时钟 emoji 本身带颜色，所以亮态同时补一个深底色保证对比度。
 */
.msg-action.msg-stamp.is-on{
  border-color:var(--accent);
  color:var(--accent);
  background:color-mix(in srgb, var(--accent) 14%, transparent);
}
.msg-action.msg-stamp.is-on:hover{
  border-color:var(--accent);
  background:color-mix(in srgb, var(--accent) 22%, transparent);
  color:var(--accent);
}
/*
 * 时间戳徽标：只读的旁注，不是正文的一部分。
 *
 * 刻意做小、做淡（与 .msg-meta 同一档视觉重量）：它是一条元信息，
 * 不该与用户写的字争注意力，但必须一眼可辨"这是系统加的时间"。
 * 等宽字体 + 完整北京时间，读起来像一条时间记录而不是一句正文。
 */
.msg-stamp-badge{
  display:inline-block;
  font-size:11px;line-height:1.5;
  font-variant-numeric:tabular-nums;
  color:var(--ink-soft);
  border:1px solid var(--line);
  border-radius:6px;
  padding:1px 7px;
  margin-bottom:6px;
  background:var(--input);
  user-select:text;
}

/* 用量 chip 那排：只放 chip，靠左排布 */
.msg-usage{display:flex;align-items:center;flex-wrap:wrap;gap:6px}

/* =====================================================================
 * 消息编辑态
 *
 * 编辑框长在被编辑的那条气泡里（不是弹窗）：用户改的就是"这一条"，
 * 位置关系本身就是说明；弹窗还得额外回答"我在改哪一条"。
 * 样式沿用消息区既有 token，不另立配色。
 * ===================================================================== */
.msg-edit{display:flex;flex-direction:column;gap:6px}
.msg-edit-label{font-size:11px;color:var(--ink-soft)}
.msg-edit-input{
  width:100%;min-height:96px;max-height:52vh;resize:vertical;
  font:inherit;font-size:13px;line-height:1.6;
  padding:8px 10px;border-radius:8px;
  border:1px solid var(--line-strong);background:var(--input);color:var(--ink);
}
.msg-edit-input:focus{outline:none;border-color:var(--accent)}
.msg-edit-hint{font-size:11px;color:var(--ink-soft)}
/* 降级警告必须显眼：它说明"这次编辑会让块变成纯文字"，是数据损失提示 */
.msg-edit-warn{font-size:11px;line-height:1.5;color:#c98a2e;border-left:2px solid #c98a2e;padding-left:8px}
.msg-edit-err{font-size:12px;color:#d9534f}
.msg-edit-bar{display:flex;gap:6px;justify-content:flex-end}


/* =====================================================================
 * 工作流编排 · 对话式编排界面
 *
 * 设计方向（沿用本项目的 Claude 风格 token，不另立配色）：
 *   编排区就是一条「对话流」——每个节点是对话流里的一条消息，
 *   用户消息在右、AI 消息在左，与左边栏形成标准的两栏对话布局。
 *   编排 = 写一段会被真正执行的对话，所以它就该长得像对话。
 *
 * 刻意遵守的几条：
 *   - 不再「卡片套卡片」：整个消息序列是一条连续纵轴，节点之间靠间距与
 *     一条纵向连接线表达顺序，而不是给每个节点套边框。
 *   - 层级用字重与颜色表达，不靠字号堆砌；辅助说明只在需要处出现，
 *     常驻的长副标题删掉，改为 hover/focus 时才出现的轻提示。
 *   - 空状态必须给出「为什么 + 怎么做」，不是一句「暂无数据」。
 *   - 所有交互元素都有 hover/active/focus/disabled 四态，禁用态可解释。
 * ===================================================================== */

.orch-page{overscroll-behavior-y:none}

/* ---------- 骨架 ---------- */
/*
 * ★ position:relative 是**必须的**（2026-09-28 实测踩到的 bug）★
 *   左侧工作流列表的拖拽手柄（.orch-side-resizer）是 .orch-side 的**兄弟节点**
 *   （模块用 insertAdjacentElement('afterend') 插在它后面），
 *   并且是 position:absolute; right:-3px —— 相对谁定位取决于**最近的定位祖先**。
 *   本行原先没有 position，于是手柄越过 .orchestrator 一直找到
 *   body.orch-page（定位祖先），最后贴在了**视口右缘**：
 *   实测 1600 宽窗口下手柄在 x=1586 而不是左栏右缘的 x≈264，
 *   表现为"左栏中间那条分割线根本抓不到 / 抓的是窗口最右边那条"。
 *   无头浏览器里因为视口差异偶然能拖动，有头下必然失效——
 *   这正是"必须真浏览器验证"的价值。
 */
.orchestrator{display:flex;flex:1 1 auto;min-width:0;min-height:0;height:100%;position:relative}
body.orch-page .shell{height:100vh;height:100dvh}

/* ---------- 左栏：工作流列表 ---------- */
.orch-side{
  /*
   * 宽度由 CSS 变量 --orch-side-w 驱动（用户可拖，唯一口径在 layout_widths.js）。
   * 兜底值 264px 与 layout_widths.js 的 def **必须一致**：兜底值在 JS
   * 未加载完的那一帧生效，不一致就会看到"打开页面时分栏跳一下"。
   * 264 也**刻意等于本栏改动前的固定宽度**——首次使用观感不变。
   */
  width:var(--orch-side-w,264px);flex:none;background:var(--bg-deep);border-right:1px solid var(--line);
  display:flex;flex-direction:column;min-height:0;
}

/*
 * ============================================================================
 * 左侧边栏（工作流列表 / 历史）的折叠态 —— 首帧正确性的落点
 * ============================================================================
 *
 * 用户 2026-10-01 要求："左侧边栏，就是历史，可以折叠展开，状态持久"。
 *
 * ★ 为什么规则写成「html 的 data 属性 → 后代」而不是在 JS 里改元素 ★
 *   与右栏面板踩过的坑**完全同类**（见 orch-panels-init.js 顶部与
 *   style.css 的右栏面板段）：显隐/占位一旦等 JS，用户刷新时就会看到
 *   "先按展开态画一帧、再跳成折叠"，而左栏一折一展会让编排区横向跳
 *   300 多像素 —— 那是刷新时最容易看见的一种抖。
 *   引导脚本 public/orch-side-init.js 在 <head> 里同步执行（先于样式表），
 *   所以用它的投影驱动 CSS，首帧就是用户上次留下的那一态，全程零跳动。
 *   与分栏宽度的 --orch-*-w 完全同构：都是"引导脚本投影 → CSS 消费"。
 *
 * ★ 为什么限定在宽屏（min-width:761px）★
 *   窄屏（≤760px）下这一栏是**抽屉**（position:fixed + .open，见下面的
 *   @media (max-width:760px) 段），"折不折"在那里没有意义：它的显隐由
 *   抽屉自己表达，与用户偏好是两件事。若在这里也吃这条规则，
 *   请求打开抽屉时会被 data 属性按 0 宽度锁死 —— 表现为"点 ☰ 没反应"。
 *   断点取 761（与 .orch-side 变抽屉的那个断点同源），不另立一套。
 *
 * ★ 为什么用 width:0 而不是 display:none ★
 *   与设置区折叠同一个理由（见 style.css 的 html[data-orch-settings-collapsed="1"] .orch-cols 段）：
 *   这一栏在 flex 流里占一个位置，display:none 会让**后面的兄弟节点
 *   （手柄）跟着换位**，将来若在它前后再放东西，列序一变就是一次
 *   静默错位。用 width:0 + overflow:hidden 把它原地压扁，
 *   "谁在哪"这个结构永远不变。
 *
 * ★ 为什么保留 --orch-side-w 不动 ★
 *   折叠是**此刻**的取舍，不是"把宽度改成 0 存起来"。变量保持用户拖出来的
 *   值，展开时原样回来 —— 用户收一次再展开，看到的是自己调过的宽度，
 *   而不是被打回默认值（那会让人以为"折叠把我的设置弄丢了"）。
 *   手柄同理只是隐藏、不销毁：它仍挂在原处，展开后立即可用。
 */
@media (min-width: 761px){
  html[data-orch-side-open="0"] .orch-side{
    /* 仍在 flex 流里，只是被压成 0 宽；不得用 display:none（会长出上面的问题） */
    width:0;min-width:0;border-right:0;overflow:hidden;visibility:hidden;
  }
  html[data-orch-side-open="0"] .orch-side-resizer{
    /* 同设置区：留在原处，只是不可见、不可抓（不销毁，展开后立即可用） */
    visibility:hidden;pointer-events:none;
  }
  /*
   * ☰「展开工作流列表」：宽屏下它**只在左栏收起时出现**。
   *
   * 同一个按钮在两种形态下各有一个明确含义，且**同一时刻只有一种语义**：
   *   · 窄屏（≤760px）：抽屉的开关，常驻（见下面的媒体查询）；
   *   · 宽屏（≥761px）：折叠后把左栏找回来的唯一入口，收起时才出现。
   * 收起后如果没有这个入口，用户就只能靠清 localStorage 才能把左栏找回来。
   *
   * 为什么不让它常驻：展开态下左栏本来就看得见，此刻再摆一个 ☰
   * 只会与左栏里的"收起"按钮形成两个方向相反的开关（用户得读两遍才知道
   * 哪个是哪个）。宁可让它在不需要时**彻底不出现**。
   */
  html[data-orch-side-open="0"] #orchMenu{display:block}
}
.orch-side .brand{
  position:relative;display:flex;align-items:center;justify-content:space-between;gap:8px;
  padding:16px 14px 12px;
}
/*
 * 移动端抽屉自己的关闭入口。
 * 抽屉是 fixed + z-index:var(--z-side-drawer,48)，**盖住整个页面（含顶栏）**；
 * 主顶栏里的 #orchMenu 只适合负责"打开抽屉"（它此刻已经在抽屉底下，
 * 点不到也不该点到），不能在抽屉打开后再负责"关闭抽屉"。
 * 抽屉内部的关闭按钮随抽屉一起移动，层级与可见范围始终一致。
 */
.orch-side-drawer-close{display:none!important;border:0!important;background:transparent!important;color:var(--ink-soft)!important;font-size:25px!important;line-height:1!important;width:auto!important;height:auto!important;padding:1px 5px!important;cursor:pointer}
.orch-side-drawer-close:hover{color:var(--ink)!important}
.orch-side .brand b{font-size:14px;font-weight:600;letter-spacing:-.01em;color:var(--ink-strong)}
.orch-side .brand button{
  width:28px;height:28px;flex:none;display:grid;place-items:center;
  border:1px solid var(--line);border-radius:8px;background:var(--panel);
  color:var(--ink);font-size:15px;line-height:1;
  transition:border-color var(--dur-fast,150ms) ease,background var(--dur-fast,150ms) ease;
}
.orch-side .brand button:hover{border-color:var(--line-strong);background:var(--panel-2)}
.orch-side .brand button:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
.orch-side .caption{
  font-size:11px;color:var(--ink-faint);letter-spacing:.12em;text-transform:uppercase;
  padding:2px 16px 8px;
}
.wf-list{flex:1;overflow:auto;padding:0 10px 16px;-webkit-overflow-scrolling:touch}
.wf-item{display:flex;align-items:stretch;border-radius:8px;margin-bottom:1px}
.wf-item:hover{background:var(--panel)}
.wf-item-title{
  flex:1;min-width:0;text-align:left;border:0;background:transparent;padding:8px 10px;border-radius:8px;
  font-size:13px;color:var(--ink-soft);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  transition:color var(--dur-fast,150ms) ease;
}
.wf-item-title.active{background:var(--panel);color:var(--ink-strong);font-weight:600}
.orch-side-foot{margin-top:auto;padding:10px 14px 14px;border-top:1px solid var(--line-soft)}
.orch-back{
  display:block;width:100%;padding:8px 10px;border-radius:8px;border:1px solid var(--line);
  background:var(--panel);color:var(--ink-soft);font-size:12.5px;text-align:center;
  text-decoration:none;box-sizing:border-box;
  transition:border-color var(--dur-fast,150ms) ease,color var(--dur-fast,150ms) ease;
}
.orch-back:hover{border-color:var(--line-strong);color:var(--ink)}

/* ---------- 主区骨架 ---------- */
.orch-main{flex:1;min-width:0;display:flex;flex-direction:column;min-height:0}
.orch-head{
  height:56px;flex:none;display:flex;align-items:center;gap:8px;padding:0 16px;
  border-bottom:1px solid var(--line);background:var(--bg);
}
#orchMenu{display:none;border:0;background:transparent;font-size:19px;color:var(--ink);padding:2px 6px;line-height:1}
.wf-name{
  flex:1;min-width:0;font-size:15px;font-weight:600;letter-spacing:-.01em;color:var(--ink-strong);
  border:1px solid transparent;border-radius:8px;padding:6px 10px;background:transparent;outline:none;
  transition:border-color var(--dur-fast,150ms) ease,background var(--dur-fast,150ms) ease;
}
.wf-name::placeholder{color:var(--ink-faint);font-weight:400}
.wf-name:hover:not(:disabled){border-color:var(--line);background:var(--input)}
.wf-name:focus{border-color:var(--line-strong);background:var(--input)}
.wf-name:disabled{cursor:default}
.wf-status{
  font-size:12px;color:var(--ink-faint);white-space:nowrap;overflow:hidden;
  text-overflow:ellipsis;max-width:34%;flex:none;
}

/* ---------- 按钮：四态齐全 ---------- */
.orch-btn{
  font-size:13px;padding:7px 13px;border:1px solid var(--line);border-radius:8px;
  background:var(--input);color:var(--ink);white-space:nowrap;text-decoration:none;
  display:inline-flex;align-items:center;gap:6px;box-sizing:border-box;
  transition:border-color 150ms ease,background 150ms ease,color 150ms ease,opacity 150ms ease;
}
.orch-btn:hover:not(:disabled){border-color:var(--line-strong);background:var(--panel-2)}
.orch-btn:active:not(:disabled){transform:translateY(.5px)}
.orch-btn:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
.orch-btn.small{font-size:12.5px;padding:6px 11px}
.orch-btn.primary{background:var(--accent);color:var(--accent-ink);border-color:var(--accent);font-weight:500}
.orch-btn.primary:hover:not(:disabled){background:var(--accent-hover);border-color:var(--accent-hover)}
.orch-btn.danger:hover:not(:disabled){border-color:var(--danger);color:var(--danger)}
/*
 * 禁用态必须「可读 + 可解释」：
 * 不是灰到看不见，而是降低对比并配合 title 说明原因（见 JS 里的 disabledReason）。
 */
.orch-btn:disabled{opacity:.45;cursor:not-allowed}

/* =====================================================================
 * 按钮图标化（用户 2026-10-01 要求："移动端把这些都缩小，按钮都用图标表示"）
 * =====================================================================
 *
 * 结构（**唯一权威**，全站图标化按钮都必须是这个结构）：
 *     <button class="... btn-iconable"><span class="btn-glyph">⟳</span><span class="btn-label">文字</span></button>
 *
 * ★ 为什么必须拆成两个 span，而不是"窄屏用 CSS 把文字藏掉" ★
 *   纯文本节点的按钮做不到"只留图标"：CSS 没法单独隐藏文本节点，
 *   只能整个按钮一起没。拆开之后"显示哪个"完全交给 CSS，
 *   JS 只负责往里写内容 —— 于是同一个 DOM 在两种形态下都能用，
 *   不需要第二份节点（复制一份必然两边状态不同步，这是本项目的既有禁令）。
 *
 * ★ .btn-glyph 在**所有**形态下都显示 ★
 *   桌面端也保留图标：它让同一排按钮的"识别锚点"一致（鼠标与手机上找同一个符号），
 *   而且缩小后不显得空。手机上则只留它。
 *
 * ★ 图标用字形（emoji / 几何符号）而不是引入图标字体 ★
 *   项目没有图标依赖，也不打算为几个按钮新增一套。字形的好处是零成本、
 *   与文字同字体；代价是不同平台字形略有差异 —— 对这几个通用符号可以接受。
 *   每个图标化按钮都必须有 aria-label（图标本身对读屏无意义），
 *   这条由 JS/HTML 各自保证，且与 title 同源。
 */
.orch-btn.btn-iconable .btn-glyph{
  flex:none;line-height:1;display:inline-flex;align-items:center;justify-content:center;
}
/*
 * ★ 热区扩展机制（唯一实现，全站图标按钮共用）★
 *
 * 把"可点面积"与"视觉尺寸"解耦：
 *   · 按钮盒子做成 --tap-visual（紧凑，决定界面轻重）；
 *   · 用绝对定位的伪元素向外撑到 --tap（决定手指点得中）。
 * 伪元素不参与布局、不影响观感，只扩大命中区域。
 *
 * 为什么用 ::after 而不是 padding：
 *   padding 会同时把视觉盒子撑大（那就又回到"图标很大"）。
 *   伪元素可以是纯热区、零视觉 —— 这是唯一能做到"看着小、点着准"的办法。
 *
 * 为什么热区是正方形 --tap 而不是"沿某一边多给一点"：
 *   手指落点相对图标是各方向的，只补上下仍会在左右边缘点空。
 *   统一正方形，判据只有一个。
 *
 * 仅在窄屏生效（见下方 760px 段）：桌面端鼠标精度高、不需要热区，
 * 而且桌面按钮本来就是紧凑的文字按钮。
 */
.btn-iconable{position:relative}
.btn-iconable::after{
  content:'';position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  width:var(--tap);height:var(--tap);
}
.orch-btn.btn-iconable .btn-label{min-width:0;overflow:hidden;text-overflow:ellipsis}

/* ---------- 滚动区：一条对话流 ---------- */
/*
 * 滚动区：内容固定最大宽度 + 居中，用 margin 而不是对称 padding。
 * 对称 padding 会让 flex 子项被拉伸到「视口宽 - 两侧 padding」，
 * 内容窄的时候看起来就是被推到一边、另一侧空一条（上一版的观感问题）。
 */
.orch-scroll{
  flex:1;overflow:auto;min-height:0;box-sizing:border-box;
  padding:24px 20px 120px;
  -webkit-overflow-scrolling:touch;
  display:flex;flex-direction:column;gap:30px;
}
/*
 * 内容列宽度：取值要兼顾「读得舒服」与「不留下假空白」。
 * 880px 在 1170px 的可视区里会左右各留 145px，看起来像内容被挤到一边；
 * 提到 1080px 后两侧余量约 45px，既是留白又不是空缝。
 */
.orch-scroll > *{
  width:100%;max-width:780px;box-sizing:border-box;margin-left:auto;margin-right:auto;flex:none;
}

/* ---------- 区块标题：克制，不抢戏 ---------- */
.sec{display:flex;flex-direction:column;gap:12px}
/*
 * 分区标题行。
 * 标题必须 flex:none + white-space:nowrap：
 *   它是 flex 容器里的一个可收缩项，在窄卡片（停靠态面板只有 420px）里
 *   会被压到只能放下两个汉字，于是「工具设置」竖着排成两行——
 *   看起来像排版坏了，实际是 flex 收缩导致的换行。
 *   标题是固定短语，本来就不该参与收缩。
 */
/*
 * 区块标题行。
 *
 * ★ 必须允许换行（flex-wrap:wrap），且每个子项都要有 min-width:0。
 *
 * 为什么（这是"两栏宽度可拖拽"暴露出来的真实缺陷，不是为改而改）：
 *   这一行原先是不换行的单行 flex：.sec-title 与 .sec-head-actions 都是
 *   flex:none（不可压缩），于是**唯一可压缩的就是中间的 .sec-hint**。
 *   在右栏被拖窄（用户明确要的功能）之后，提示文案被压到只剩几十像素，
 *   中文会在每个字之间折行，渲染成一列竖排的字符柱——
 *   截图实测：右栏 280px 时「每次测试跑完自动存一个；也可随时手动存」
 *   竖成 11 行单字列，比没有提示更糟（它同时把标题与按钮挤到两边）。
 *
 *   结构性修复：给标题行**允许换行**的语义（窄了就整体换到下一行），
 *   并给提示文案 min-width:0 + 可控的省略。这样"窄"只影响排布，
 *   不会把文字压成竖柱——任何把这一行塞进窄容器的场景都自动正确，
 *   而不是针对快照面板打一个 width 补丁。
 */
.sec-head{display:flex;align-items:center;gap:8px;min-height:20px;flex-wrap:wrap;row-gap:6px}
.sec-title{
  font-size:11.5px;font-weight:600;color:var(--ink-faint);
  letter-spacing:.1em;text-transform:uppercase;display:flex;align-items:center;gap:7px;
  /*
   * flex:none + nowrap：标题是**导航锚点**（"我在看哪一块"），
   * 它被截断或折行都会让人认不出这一块是什么。
   * 因此它保持完整尺寸，靠 flex-wrap 让整行换行来腾空间——
   * 这比"把标题挤成两行"或"省略成『消...』"都更可读。
   */
  flex:none;white-space:nowrap;
}
.sec-title::before{content:'';width:3px;height:11px;border-radius:2px;background:var(--line-strong)}
.sec-hint{
  font-size:12px;color:var(--ink-faint);opacity:0;transform:translateX(-2px);
  transition:opacity 150ms ease,transform 150ms ease;pointer-events:none;
  /*
   * min-width:0 让它**可以被压缩**（否则它会把同行其它元素顶出去），
   * 但同时禁止它在字与字之间折行：提示文案宁可整块换到下一行、
   * 或整体省略，也不能渲染成一列竖排汉字（见 .sec-head 的说明）。
   * flex:1 1 auto 让它优先占据剩余空间并在需要时缩小。
   */
  flex:1 1 auto;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.sec:hover .sec-hint,.sec:focus-within .sec-hint{opacity:1;transform:none}
/*
 * 区块标题行右侧的动作区（目前是「测试」按钮 + 它的状态文案）。
 *
 * 按钮放进标题行而不是独占一排：它是这一块的「就地验证」动作，
 * 与这一块的配置同处一行，视线不用跨过整块内容。
 */
.sec-head-actions{
  /*
   * 动作区自己不压缩（按钮被压扁会点不准），但允许它整块换行到下一行
   * （由 .sec-head 的 flex-wrap 实现）。flex:none 在这里是刻意的：
   * 宁可换行，也不要一排被挤成图标大小的按钮。
   */
  margin-left:auto;display:flex;align-items:center;gap:8px;flex:none;
}
/* 测试状态属于编排正文，独立于粘性工具条；长错误完整换行，不撑宽页面。 */
#testNote{
  flex:none;min-width:0;box-sizing:border-box;
  font-size:11.5px;color:var(--ink-faint);line-height:1.5;
  white-space:normal;overflow-wrap:anywhere;
}
#testNote:empty{display:none}
#testNote.running{color:color-mix(in srgb, var(--accent) 80%, var(--ink-faint))}
.sec-body{
  border:1px solid var(--line-soft);border-radius:12px;background:var(--panel);
  padding:14px 16px;
}

/*
 * =====================================================================
 * 可折叠区块标题行（目前只有「系统提示词」用它）
 * =====================================================================
 *
 * 用户 2026-10-01 要求："系统提示词默认在移动端折叠……让消息区域的展示空间更多"。
 *
 * 折叠体的显隐**完全由 <html data-orch-sys-open> 决定**（一个状态，一个出口）。
 * 值由 public/orch-sys-init.js 在 <head> 里同步投影，所以首帧就是正确的那一态，
 * 不存在"先展开画一帧、再折起来"的跳动 —— 与左栏折叠、右栏 tab 同一套做法。
 *
 * 为什么不给 .sec-body 加 class：那会变成"首帧靠 CSS、之后靠 JS"两套机制，
 * 切换的那一帧必然出现空档（JS 还没跑到，两块都按默认显示）。
 */
html[data-orch-sys-open="0"] #sysFoldBody{display:none}

/*
 * 折叠标题行：整行就是一个按钮。
 *
 * 用 <button> 而不是 <div role=button>：原生按钮自带键盘、焦点、
 * 读屏语义（Enter/Space 激活），不必手写一套等价物 —— 手写的那套
 * 总会漏掉某项（通常是 Space 或焦点样式）。
 * 折叠按钮只负责系统提示词；工作流运行操作由独立工具条承载。
 */
.sec-head-foldable{padding:0}
.sec-fold-btn{
  display:flex;align-items:center;gap:8px;flex:1 1 auto;min-width:0;
  background:none;border:0;padding:0;margin:0;font:inherit;color:inherit;
  text-align:left;cursor:pointer;
}
/*
 * 焦点可见：键盘用户必须看得出焦点在哪。
 * 只在 :focus-visible 上画（鼠标点击不显示），避免鼠标用户看到多余的描边。
 */
.sec-fold-btn:focus-visible{
  outline:2px solid color-mix(in srgb, var(--accent) 60%, transparent);
  outline-offset:3px;border-radius:6px;
}
/*
 * caret：三角形用 border 画（不引字体图标、不占文字节点）。
 * 展开朝下、收起朝右 —— 朝向由 data 属性投影，首帧就正确，
 * 不需要等 JS 转一下（那一下转动会被看见）。
 */
.sec-fold-caret{
  flex:none;width:0;height:0;
  border-left:5px solid transparent;border-right:5px solid transparent;
  border-top:6px solid var(--ink-faint);
  transition:transform 150ms ease;
}
html[data-orch-sys-open="0"] .sec-fold-caret{transform:rotate(-90deg)}
/*
 * 收起时把标题的"竖线"提到前面（视觉上仍是这一块的标题），
 * 并让提示文案保持隐藏（它由 .sec:hover 揭示，收起态下没有悬停意义）。
 */
.sec-fold-btn .sec-title{flex:none}
/* ---------- 系统提示词：文件芯片 ---------- */
.orch-filebox{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.orch-chips{display:flex;flex-wrap:wrap;gap:6px;flex:1;min-width:180px}
.orch-chip{
  display:inline-flex;align-items:center;gap:6px;font-size:12.5px;padding:5px 8px 5px 11px;
  border-radius:8px;border:1px solid var(--line);background:var(--panel-2);color:var(--ink-soft);
  max-width:100%;
}
.orch-chip span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:300px}
.orch-chip button{
  border:0;background:transparent;color:var(--ink-faint);font-size:14px;line-height:1;
  padding:0 1px;border-radius:4px;
}
.orch-chip button:hover{color:var(--danger);background:transparent}
.orch-chip-empty{font-size:12.5px;color:var(--ink-faint)}
/*
 * 默认文件星标。
 * 用强调色而不是普通文字色：它表达"状态"（此文件对所有新会话生效），
 * 与文件名这一"内容"应当在视觉上分开，否则用户扫一眼分不清哪个字是文件名。
 * 用 flex 的 gap 自然与文件名拉开距离，不额外加 margin（避免与 .orch-chip 的
 * gap 叠加成不等距）。
 */
.chip-star{font-size:11px;line-height:1;color:var(--accent);flex:none}
/*
 * 有默认文件的标签整体描边用强调色，让"哪些是默认"在一排标签里一眼可见，
 * 不必逐个去找那颗星。仅靠星形符号在长文件名旁边容易被忽略。
 */
.orch-chip.is-default{border-color:color-mix(in srgb, var(--accent) 45%, var(--line))}

/*
 * 触屏（手指输入）下的标签：把"点主干出菜单"这件事变成可发现、可命中的目标。
 *
 * 用 @media (pointer:coarse) 而不是宽度断点，与 JS 侧 coarsePointer() 同一条权威：
 * 宽度说的是版面够不够，输入方式说的是有没有右键。用宽度会出现两类错误——
 * 宽屏触屏设备（平板横屏）菜单打不开，窄窗桌面（鼠标）却被夺走左键。
 *
 * 三件事，都必要：
 *   · 加大点击高度：手指落点远不如鼠标精确，12.5px 字号的小胶囊很难稳定命中；
 *   · -webkit-touch-callout/user-select：长按不再弹系统选字菜单与放大镜，
 *     否则它会盖住我们要给的菜单（两个菜单抢同一个手势，用户只会觉得乱）；
 *   · cursor:pointer：没有 hover 的设备上，这是"这里可以点"的唯一提示。
 */
@media (pointer:coarse){
  .orch-chip{
    padding:9px 10px 9px 12px;
    font-size:13.5px;
    cursor:pointer;
    -webkit-touch-callout:none;
    -webkit-user-select:none;
    user-select:none;
    /* 点按的视觉反馈（触屏没有 hover，用 :active 顶上） */
    transition:background .12s ease,border-color .12s ease;
  }
  .orch-chip:active{background:var(--panel);border-color:var(--line-strong)}
  /* × 的点击区同样放大，且与文件名拉开距离，避免误触（删掉 vs 弹菜单） */
  .orch-chip button{
    font-size:16px;line-height:1;padding:2px 6px;margin-left:2px;
    -webkit-touch-callout:none;
  }
}

/* ---------- 变量 ---------- */
.var-rows{display:flex;flex-direction:column;gap:2px}
.var-row{
  display:flex;align-items:center;gap:10px;font-size:13px;
  padding:7px 8px;border-radius:8px;
}
.var-row:hover{background:var(--panel-2)}
.var-row .var-name{
  font-family:var(--mono);font-size:12px;color:var(--ink);min-width:110px;
  display:flex;align-items:center;gap:6px;
}
.var-row .var-name .undeclared{
  font-size:10px;padding:1px 5px;border-radius:4px;background:var(--panel-2);
  border:1px solid var(--line);color:var(--ink-faint);font-family:var(--font);
}
.var-row input[type=text]{
  flex:1;min-width:60px;font-size:13px;padding:6px 10px;border:1px solid var(--line);
  border-radius:8px;background:var(--input);color:var(--ink);outline:none;
  transition:border-color 150ms ease;
}
.var-row input[type=text]:focus{border-color:var(--accent)}
.var-row .var-del{
  border:0;background:transparent;color:var(--ink-faint);font-size:15px;padding:0 5px;
  border-radius:6px;line-height:1;
}
.var-row .var-del:hover{color:var(--danger)}
.var-req{
  display:flex;align-items:center;gap:5px;font-size:12px;color:var(--ink-soft);
  white-space:nowrap;cursor:pointer;user-select:none;
}
.var-add{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:10px;
  padding-top:12px;border-top:1px solid var(--line-soft)}
.var-add input[type=text]{
  font-size:13px;padding:7px 11px;border:1px solid var(--line);border-radius:8px;
  background:var(--input);color:var(--ink);outline:none;min-width:140px;
  transition:border-color 150ms ease;
}
.var-add input[type=text]:focus{border-color:var(--accent)}
.var-warn{color:var(--danger);font-size:12.5px;margin-top:10px;display:flex;align-items:center;gap:6px}

/* =====================================================================
 * 消息序列：一条真正的对话流
 *
 * 每条消息 = 头像 + 气泡。用户消息靠右（自己说的话），AI 消息靠左（对方的回答），
 * 与聊天软件一致的方位直觉。节点之间用纵向细线 + 序号节点表达「先后顺序」，
 * 让「这会按顺序执行」这件事一眼可见，而不需要靠文字解释。
 * ===================================================================== */
.thread{position:relative;display:flex;flex-direction:column;gap:4px}
/* 纵轴：贯穿所有消息的顺序线 */
/* 顺序线：贯穿所有消息，头像正好压在这条线上成为「节点」 */
.thread::before{
  content:'';position:absolute;left:15px;top:30px;bottom:30px;width:1px;
  background:var(--line);pointer-events:none;
}
.thread-empty{
  border:1px dashed var(--line);border-radius:12px;padding:28px 20px;text-align:center;
  background:var(--panel-2);
}
.thread-empty-title{font-size:13.5px;font-weight:600;color:var(--ink);margin-bottom:6px}
.thread-empty-desc{font-size:12.5px;color:var(--ink-faint);line-height:1.7;margin-bottom:14px}
.thread-empty-desc kbd{
  font-family:var(--mono);font-size:11px;padding:1.5px 6px;border-radius:5px;
  border:1px solid var(--line-strong);background:var(--panel);color:var(--ink-soft);
}
.thread-empty-actions{display:flex;gap:8px;justify-content:center;flex-wrap:wrap}

/*
 * 消息行布局：用户消息靠右、AI 消息靠左。
 *
 * 上一版两条都用同一个 flex 容器，结果用户消息虽然用了强调色头像，
 * 整个气泡仍然贴着左边缘、和 AI 消息挤在同一列，读者无法一眼分辨「谁在说」。
 * 这里用 flex-direction: row-reverse 把用户消息整行翻转，
 * 气泡改为按内容自适应宽度（不再拉伸占满），方位直觉与聊天软件一致。
 */
.t-row{display:flex;width:100%;gap:12px;position:relative;padding:9px 0;align-items:flex-start}
/*
 * 方位用 row-reverse + 取消拉伸来实现：
 *   行内只有「头像」和「内容」两块；内容不拉伸（flex:0 1 auto），
 *   剩下的空间自然落在另一端，于是用户消息贴右、AI 消息贴左。
 *
 * 关键是 max-width 要相对「整行」而不是相对 body 自己——
 * 否则 row-reverse 下会把 body 顶到最右侧、留出一条空缝。
 */
/*
 * ===== 为什么不是「用户靠右 / AI 靠左」=====
 *
 * 试过 zigzag（用户右、AI 左）。在宽画布 + 窄气泡下，每一步都要横跨
 * 一千多像素换边，眼睛得来回跳，「第 3 步说了什么」反而更难找。
 *
 * 编排的本质是「一条按顺序执行的流水线」，不是两个人在对话——
 * 所以改成：所有消息共用同一条左侧轨道（头像成为顺序线上的节点），
 * 用头像颜色 + 角色标签区分谁在说，用纵向连接线表达先后。
 * 保留了聊天式的心智模型（头像 + 气泡 + 顺序），但不拿方位当角色信号。
 */
.t-body{flex:1 1 0;width:0;min-width:0;padding-top:1px;display:flex;flex-direction:column;align-items:stretch}
.t-body > .t-bubble{width:100%;max-width:none;align-self:stretch;box-sizing:border-box}
.t-meta{width:100%}
.t-avatar{
  flex:none;width:31px;height:31px;border-radius:50%;display:grid;place-items:center;
  font-size:10.5px;font-weight:650;letter-spacing:.02em;user-select:none;
  border:1px solid var(--line-strong);background:var(--bg);color:var(--ink-soft);
  /* 盖住纵轴，让头像成为顺序线上的「节点」 */
  position:relative;z-index:1;
}
.t-row.is-user .t-avatar{background:var(--accent);border-color:var(--accent);color:var(--accent-ink)}
.t-row.is-ai .t-avatar{background:var(--panel);color:var(--ink-strong)}
.t-meta{display:flex;align-items:center;gap:8px;margin-bottom:6px;min-height:19px;white-space:nowrap}
.t-name{font-size:12px;font-weight:600;color:var(--ink-soft);white-space:nowrap;flex:none}
.t-seq{
  font-family:var(--mono);font-size:10.5px;color:var(--ink-faint);flex:none;
  border:1px solid var(--line);border-radius:5px;padding:0 5px;line-height:16px;
}
/* 「最后一个 AI 消息就是输出」这件事要在最后一格上说清楚 */
.t-row.is-last-ai .t-bubble{
  border-color:color-mix(in srgb, var(--accent) 45%, var(--line));
  background:color-mix(in srgb, var(--accent) 4%, var(--panel));
}
.t-actions{display:flex;gap:5px;opacity:0;transition:opacity 150ms ease;margin-left:auto}
.t-row:hover .t-actions,.t-row:focus-within .t-actions{opacity:1}
.t-actions button{
  border:1px solid var(--line);background:var(--panel);color:var(--ink-faint);
  border-radius:6px;font-size:11px;padding:2px 7px;line-height:16px;
  transition:border-color 150ms ease,color 150ms ease;
}
/* AI 行包含单条/到底两个运行入口；列宽可拖窄，动作在自身行内换行，不撑宽正文。 */
.is-ai .t-meta{flex-wrap:wrap;white-space:normal}
.is-ai .t-actions{flex-wrap:wrap;min-width:0;max-width:100%}
.is-ai .t-actions [data-retry-seq]{white-space:nowrap}
.t-actions button:hover:not(:disabled){border-color:var(--line-strong);color:var(--ink)}
.t-actions button:disabled{opacity:.35;cursor:not-allowed}
/*
 * 图标按钮（四个「插入消息」）与文字按钮（上移/下移/删除/裁掉后续）同排。
 *
 * 只做两件事：定宽对齐 + 图标字形居中。其余（边框/配色/悬停/禁用）全部沿用
 * 上面的 .t-actions button 规则 —— 不复制一套，否则将来调按钮观感
 * 一定会漏掉这一排（"四个按钮看着跟别的不一样"就是这么来的）。
 *
 * 定宽是必须的：图标字形宽度不一（↥ ⤒ 与 ↑）、汉字是两个字符宽，
 * 不锁宽度这一排会参差不齐，悬停显隐时还会因为宽度变化而抖动。
 */
.t-actions button.is-icon{
  width:24px;padding:2px 0;text-align:center;
  font-size:12px;line-height:16px;
}
/*
 * 执行时刻徽标（用户 2026-10-01 要求）。
 *
 * 它是"这条消息在实际运行中被发出去的时刻"，一条**只读的事实记录**，
 * 不是用户写的内容、也不是运行态标记。因此：
 *   · 挂在行尾（.t-row 的直接子元素），与 .t-body 并列——塞进 .t-bubble 里
 *     会让它看起来是正文的一部分，而 .t-bubble 是可编辑的；
 *   · 用与消息标签同一档视觉重量（小字号、弱色、等宽数字），
 *     它是一条元信息，不该与用户写的字争注意力；
 *   · 缩进与 .t-body 对齐：它是这条消息的旁注，左对齐到内容列才读得出归属。
 */
.t-row > .t-stamp-badge{
  position:absolute;
  top:6px;right:0;
  margin:0;
  font-size:11px;line-height:1.5;
  font-variant-numeric:tabular-nums;
  color:var(--ink-faint);
  user-select:text;
}
/*
 * 图标按钮的「已开启」态（当前只有用户消息的「最大高度」开关用它）。
 *
 * 这个开关描述的是一个**持续生效**的状态（当前这条消息被限高），
 * 与上移/下移那种一次性动作不同 —— 一次性动作不该有"开着"的样子。
 * 所以给它一个明确的开启态：强调色描边 + 强调色字形，
 * 让用户扫一眼就知道"现在不是全文"。
 * 不靠悬停来提示状态：悬停是瞬时的，而状态是持续的。
 */
.t-actions button.is-icon.is-on{
  border-color:color-mix(in srgb, var(--accent) 45%, var(--line));
  color:var(--accent);
}
.t-bubble{border-radius:12px;padding:12px 14px;border:1px solid var(--line-soft);background:var(--panel)}
/* 消息正文使用主题中性色，角色由头像与标题标识。 */
.t-row.is-user .t-bubble{
  background:var(--panel-2);
  border-color:var(--line);
}
/* AI 气泡：结果槽，用虚线边表达「这是待填的位置」 */
.t-row.is-ai .t-bubble{border-style:dashed}

/* 用户消息输入：看起来就是聊天输入框，随内容增高 */
.t-input{
  width:100%;border:0;background:transparent;color:var(--ink);outline:none;
  resize:none;font:13.5px/1.75 var(--font);padding:0;min-height:44px;overflow:hidden;display:block;
}
.t-input::placeholder{color:var(--ink-faint)}

/* =====================================================================
 * 用户消息的「最大高度」（折叠态）
 *
 * 状态权威：public/orch-msg-height-init.js（唯一读写 localStorage、
 * 投影 --orch-user-msg-max-height 的地方）。这里只负责把变量用出去，
 * 不判断"当前是哪种状态"，也不写死任何高度数字 —— 上限值只在那个模块里。
 *
 * 折叠时：超过上限的部分在框内上下滚动（用户原话"超过的就上下滚动就好了"）。
 * 展开时：变量值是 none，等于没有上限，回到"随内容增高"的原有行为。
 * ===================================================================== */
.t-row.is-user .t-input{
  max-height:var(--orch-user-msg-max-height,none);
  /*
   * ===== 为什么两态都要预留同一道滚动条槽（实测得到的结论，勿删）=====
   *
   * 这是本次实现里唯一一个"看起来多余、但删掉一定出错"的声明。
   *
   * 实测（有头 Chromium，中文正文）：滚动条**占 15px**。若只在折叠态出现滚动条，
   * 内容盒宽度就从 900 变成 885 —— 于是同一段文字在「展开态 1 行」
   * 与「折叠态 2 行」之间跳变。逐字符实测复现：
   *     64 字 → 两态都 1 行（一致）
   *     65 字 → 展开 1 行 / 折叠 2 行   ← 折行点被滚动条改变了
   *     66 字 → 展开 1 行 / 折叠 2 行   ← 同上
   *     67 字以后两态又一致（都折成 2 行）
   * 也就是说：**点一下开关，正文的换行位置会变**。用户会以为
   * "折叠/展开把我写的内容动了"，这比"多一条滚动条"严重得多。
   *
   * 解法是让"可用内容宽度"与折叠与否**无关**：两态都预留同一道槽
   *（scrollbar-gutter:stable），于是 65/66 字这类边界案例两态表现一致
   * （实测复验：64–70 字逐字符比对全部 same=true）。
   *
   * 代价：不折叠时右侧也留一条 15px 的空槽。这是刻意的取舍 ——
   * 换行稳定性是正确性问题，多 15px 空白是观感问题。
   * 另外 overflow-y 用 auto：预留了槽但没有溢出时不会真的出现滚动条。
   */
  overflow-y:auto;
  scrollbar-gutter:stable;
  /*
   * 框内滚到底后继续滚，滚动手势**不冒泡给页面**。
   * 不写这一句时，长消息里滚到底会把整个编排列表一起往上拽，
   * 用户想继续看这条消息的最后几行却先被带走了页面位置。
   */
  overscroll-behavior:contain;
}

/* AI 消息：内容全部在输入编辑器里，不存在第二个结果容器 */
.t-row.is-ai .t-bubble{background:var(--panel)}
.ai-badge{
  display:inline-flex;align-items:center;gap:5px;font-size:11px;color:var(--ink-faint);
  margin-bottom:6px;
}

/* =====================================================================
 * 临时会话 · 内联标记
 *
 * 用户明确要求：标记不独占一行、不折叠展开，就是一个约一个字宽的小图标，
 * 点它弹窗编辑。这样主会话消息的文本流完全自由，标记想插哪儿插哪儿。
 * ===================================================================== */
.cchip{
  display:inline-flex;align-items:center;gap:2px;
  vertical-align:baseline;
  margin:0 1px;padding:0 4px;
  border:1px solid color-mix(in srgb, var(--accent) 45%, var(--line));
  border-radius:5px;
  background:color-mix(in srgb, var(--accent) 10%, transparent);
  color:var(--accent);
  font-size:11.5px;font-weight:600;line-height:17px;
  cursor:pointer;user-select:none;
  transition:background 120ms ease,border-color 120ms ease;
}
.cchip:hover{
  background:color-mix(in srgb, var(--accent) 20%, transparent);
  border-color:var(--accent);
}
.cchip-icon{font-size:10px;line-height:1}
.cchip-num{font-size:10.5px;font-variant-numeric:tabular-nums}

/*
 * AI 结果方块。
 *
 * 与临时会话标记刻意做得不一样：
 *   - 临时会话 ▣ 是实心主色块（「这里要跑一个小会话」）；
 *   - AI 结果 ▤ 是虚线中性块，带文字「AI结果」（「这里要塞一段已有回答」）。
 * 两种东西语义完全不同，长得像会直接导致误插。虚线 = 引用已有内容，
 * 实线 = 要执行的动作，这个约定也用在别处（比如 AI 消息气泡用虚线）。
 */
.aichip{
  display:inline-flex;align-items:center;gap:3px;
  vertical-align:baseline;
  margin:0 1px;padding:0 5px;
  border:1px dashed var(--line-strong, var(--line));
  border-radius:5px;
  background:color-mix(in srgb, var(--ink-faint) 8%, transparent);
  color:var(--ink-soft);
  font-size:11.5px;font-weight:600;line-height:17px;
  cursor:default;user-select:none;
}
.aichip-icon{font-size:10px;line-height:1;opacity:.75}
.aichip-label{font-size:10.5px;letter-spacing:.2px}
.aichip-num{font-size:10.5px;font-variant-numeric:tabular-nums;opacity:.85}

/* 容器弹窗里的用户消息编辑器：contenteditable 需要自己模拟占位符 */
.cmodal-msg{
  width:100%;min-height:78px;max-height:220px;overflow:auto;
  border:1px solid var(--line);border-radius:8px;background:var(--input);
  color:var(--ink);font-size:12.5px;line-height:1.75;padding:9px 11px;
  outline:none;white-space:pre-wrap;overflow-wrap:anywhere;
  font-family:inherit;
}
.cmodal-msg:focus{border-color:var(--accent)}

/* 变量没填值：会原样发出去，必须显眼 */
.var-empty{
  font-size:10.5px;color:var(--danger);white-space:nowrap;
  border:1px solid color-mix(in srgb, var(--danger) 35%, transparent);
  background:color-mix(in srgb, var(--danger) 8%, transparent);
  border-radius:5px;padding:1px 6px;
}
.var-declare{
  border:1px solid var(--line);background:var(--panel);color:var(--ink-faint);
  border-radius:5px;font-size:10.5px;padding:1px 6px;margin-left:6px;
  cursor:pointer;font-family:inherit;
}
.var-declare:hover{border-color:var(--accent);color:var(--accent)}

/*
 * 工作流块。
 *
 * 三种块必须一眼区分，因为它们的代价完全不同：
 *   ▣ 实心主色 = 临时会话（跑一次 AI 调用）
 *   ▤ 虚线中性 = AI 结果（零成本，只是引用）
 *   ⬒ 实线强调 = 工作流块（会跑一整套别的工作流，可能很多次调用）
 * 最贵的用最显眼的边框，让人插的时候就知道这里要花 token。
 */
.wfchip{
  display:inline-flex;align-items:center;gap:4px;
  vertical-align:baseline;
  margin:0 1px;padding:0 6px;
  border:1px solid color-mix(in srgb, var(--accent) 65%, var(--line));
  border-radius:5px;
  background:color-mix(in srgb, var(--accent) 14%, transparent);
  color:var(--accent);
  font-size:11.5px;font-weight:600;line-height:17px;
  cursor:pointer;user-select:none;max-width:280px;
}
.wfchip:hover{background:color-mix(in srgb, var(--accent) 24%, transparent)}
.wfchip-icon{font-size:10px;line-height:1}
.wfchip-label{
  font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  max-width:170px;
}
.wfchip-state{
  font-size:9.5px;font-weight:500;opacity:.8;
  border-left:1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  padding-left:4px;
}
/*
 * 变量块。
 * 四种块的符号各自独立，扫一眼就知道插的是什么：
 *   ▣ 临时会话（实心主色，要跑 AI）  ▤ AI 结果（虚线中性，引用）
 *   ⬒ 工作流（实线强调，跑一整套）    ▽ 变量（点线青色，纯文本替换）
 * 变量是最轻的一种（不产生任何调用），所以用最弱的存在感。
 */
.varchip{
  display:inline-flex;align-items:center;gap:3px;
  vertical-align:baseline;
  margin:0 1px;padding:0 5px;
  border:1px dotted color-mix(in srgb, var(--ok, #3f9c5a) 65%, var(--line));
  border-radius:5px;
  background:color-mix(in srgb, var(--ok, #3f9c5a) 9%, transparent);
  color:color-mix(in srgb, var(--ok, #3f9c5a) 85%, var(--ink));
  font-size:11.5px;font-weight:600;line-height:17px;
  cursor:default;user-select:none;max-width:200px;
}
.varchip-icon{font-size:10px;line-height:1}
.varchip-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:160px}
/*
 * 带清洗的变量块：把虚线边框换成实线并加一个「洗」小标。
 * 必须与普通变量块看得出区别——否则用户完全不知道该块输出的
 * 只是 JSON 里的一个字段，而不是变量原值。
 */
.varchip.is-clean{border-style:solid;cursor:pointer}
.varchip.is-clean:hover{background:color-mix(in srgb,var(--ok,#3f9c5a) 18%,transparent)}
.varchip-clean{
  font-size:8.5px;line-height:1;padding:1px 3px;border-radius:3px;
  background:color-mix(in srgb,var(--ok,#3f9c5a) 22%,transparent);
  color:color-mix(in srgb,var(--ok,#3f9c5a) 90%,var(--ink));font-weight:700;
}

.wfchip.is-running{animation:cchipblink 1.1s ease-in-out infinite}
.wfchip.is-done{border-color:var(--ok,#3f9c5a);background:color-mix(in srgb,var(--ok,#3f9c5a) 12%,transparent);color:var(--ok,#3f9c5a)}
.wfchip.is-error{border-color:var(--danger);background:color-mix(in srgb,var(--danger) 12%,transparent);color:var(--danger)}

/* 二级菜单（工作流列表）略微下沉，与一级形成层次 */
.slash-sub{box-shadow:0 16px 38px rgba(0,0,0,.26)}
/*
 * 预览态：一级焦点移动时右侧显示有什么，但键盘焦点还没进去。
 * 用更淡的观感区分「只是给你看」与「现在由 ↑↓ 控制」，
 * 否则用户会以为焦点已经在二级里了。
 */
.slash-sub.is-preview{opacity:.72;box-shadow:0 10px 24px rgba(0,0,0,.16)}
.slash-item.is-group .slash-hint{opacity:.65;font-size:11px}

/* 工作流块弹窗里的变量表 */
.wb-vars{display:flex;flex-direction:column;gap:7px}
.wb-var{
  display:grid;grid-template-columns:minmax(90px,150px) 1fr auto;gap:8px;align-items:start;
  border:1px solid var(--line-soft);border-radius:8px;padding:7px 8px;
}
.wb-var.is-missing{border-color:color-mix(in srgb, var(--danger) 45%, var(--line));
  background:color-mix(in srgb, var(--danger) 5%, transparent)}
.wb-var-name{
  font-size:12px;color:var(--ink);font-weight:600;padding-top:5px;
  overflow:hidden;text-overflow:ellipsis;word-break:break-all;
}
.wb-req{
  font-size:9.5px;font-weight:600;color:var(--danger);margin-left:5px;
  border:1px solid color-mix(in srgb,var(--danger) 35%,transparent);
  border-radius:4px;padding:0 4px;vertical-align:middle;
}
.wb-var-input{
  min-height:30px;border:1px solid var(--line);border-radius:6px;background:var(--input);
  color:var(--ink);font-size:12px;line-height:1.7;padding:5px 8px;outline:none;
  white-space:pre-wrap;overflow-wrap:anywhere;
}
.wb-var-input:focus{border-color:var(--accent)}
.wb-var-del{
  border:0;background:transparent;color:var(--ink-faint);cursor:pointer;
  font-size:14px;line-height:1;padding:6px 4px;font-family:inherit;
}
.wb-var-del:hover{color:var(--danger)}
.wb-var-add{display:flex;gap:6px;align-items:center;margin-top:2px}
.wb-var-add input{
  flex:1;border:1px solid var(--line);border-radius:6px;background:var(--input);
  color:var(--ink);font-size:12px;padding:5px 8px;outline:none;font-family:inherit;
}
.wb-note{font-size:11.5px;color:var(--ink-faint);line-height:1.7}
.modal-foot-gap{flex:1}

/* ---------- 变量清洗弹窗（▽ 插入变量） ---------- */
/*
 * 三种模式用并排的单选块（radio 卡片），而不是下拉：
 *   用户是在"选一种做法"，三种做法应当同时可见、可比较，
 *   下拉会把它们藏起来，反而多一次点击才知道有什么可选。
 */
.vb-modes{display:flex;gap:8px;flex-wrap:wrap}
.vb-mode{
  display:flex;align-items:center;gap:6px;font-size:12.5px;color:var(--ink-soft);
  border:1px solid var(--line);border-radius:8px;padding:6px 10px;cursor:pointer;
  background:var(--input);user-select:none;transition:border-color 150ms ease,color 150ms ease;
}
.vb-mode:hover{border-color:var(--line-strong)}
.vb-mode:has(input:checked){border-color:var(--accent);color:var(--ink);
  background:color-mix(in srgb,var(--accent) 9%,transparent)}
.vb-mode input{accent-color:var(--accent)}
/* 模式对应的字段区：与上面的选项留出清晰间隔，读起来像"展开了这一种" */
.vb-mode-body{margin-top:9px;padding:9px 10px;border:1px solid var(--line-soft);
  border-radius:8px;background:var(--panel-2);display:flex;flex-direction:column;gap:7px}
.vb-locator{display:flex;gap:14px;flex-wrap:wrap;font-size:12px;color:var(--ink-soft)}
.vb-locator label{display:flex;align-items:center;gap:5px;cursor:pointer;user-select:none}
.vb-locator input{accent-color:var(--accent)}
.vb-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:12px;color:var(--ink-faint)}
.vb-row > label{min-width:88px}
.vb-row input[type=text],.vb-row input[type=number]{
  flex:1;min-width:120px;border:1px solid var(--line);border-radius:7px;background:var(--input);
  color:var(--ink);font-size:12.5px;padding:6px 9px;outline:none;font-family:inherit;
}
.vb-row input[type=text]:focus,.vb-row input[type=number]:focus{border-color:var(--accent)}
.vb-row .vb-mini{flex:0 0 96px;min-width:96px}
.vb-row .vb-inline{min-width:0}
/* 预览：等宽字体 + 保留换行，让 JSON 取出来的子对象/多行文本能照原样看 */
.vb-preview{
  font-family:var(--mono);font-size:12px;line-height:1.7;color:var(--ink);
  background:var(--input);border:1px solid var(--line);border-radius:8px;
  padding:8px 10px;min-height:44px;max-height:200px;overflow:auto;
  white-space:pre-wrap;overflow-wrap:anywhere;
}
.vb-modes + .vb-mode-body + .vb-mode-body{margin-top:9px}
.vb-warn{font-size:11.5px;color:var(--danger);margin-top:6px;line-height:1.6}
.vb-summary{font-size:11.5px;color:var(--ink-faint);margin-top:6px}
.vb-summary span{color:var(--ink-soft);font-family:var(--mono)}

/* ---------- 价格设置 ---------- */
.price-grid{display:flex;gap:14px;flex-wrap:wrap}
.price-item{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--ink-soft)}
.price-label{min-width:52px;color:var(--ink-faint)}
.price-item input{
  width:110px;border:1px solid var(--line);border-radius:7px;background:var(--input);
  color:var(--ink);font-size:12.5px;padding:6px 9px;outline:none;font-family:inherit;
  font-variant-numeric:tabular-nums;
}
.price-item input:focus{border-color:var(--accent)}
.price-item input:disabled{opacity:.45;cursor:not-allowed}
.price-unit{font-size:11px;color:var(--ink-faint)}
/*
 * 默认价格开关行：与上面三个输入框同属"价格设置"一个区块，
 * 用一条细线把它与输入框分开——它是**另一个作用域**的动作
 *（改的是以后新建的会话，不是当前会话），不划线容易看成"第 4 个价格字段"。
 */
.price-default-row{margin-top:11px;padding-top:11px;border-top:1px dashed var(--line)}
.price-foot{margin-top:9px}
.price-note{font-size:11.5px;color:var(--ink-faint);line-height:1.7}
.price-note.is-off{color:var(--ink-faint)}

/* ---------- 花费显示 ---------- */
.cchip.has-cost[data-cost]:hover::after{
  content:attr(data-cost);
  position:absolute;margin-left:4px;margin-top:-20px;
  background:var(--panel);border:1px solid var(--line);border-radius:5px;
  padding:2px 6px;font-size:10.5px;color:var(--ink-soft);white-space:nowrap;z-index:5;
}

/* 单次调用的用量明细行：挂在每条消息下方，三项 + 合计 */
.cost-line-wrap{margin-top:7px}
.cost-line-tag{
  display:block;font-size:10.5px;color:var(--ink-faint);
  margin-bottom:3px;font-weight:600;
}
.cost-line{
  display:flex;flex-wrap:wrap;gap:6px;
}
.cl-item{
  display:inline-flex;align-items:baseline;gap:5px;
  border:1px solid var(--line-soft);border-radius:6px;
  padding:3px 7px;font-size:11px;line-height:1.5;
  background:color-mix(in srgb, var(--panel) 55%, transparent);
  font-variant-numeric:tabular-nums;
}
.cl-item.is-total{
  border-color:color-mix(in srgb, var(--accent) 40%, var(--line));
  background:color-mix(in srgb, var(--accent) 7%, transparent);
}
.cl-item.is-unknown{color:var(--ink-faint);font-style:italic}
.cl-k{color:var(--ink-faint);font-size:10.5px}
.cl-tok{color:var(--ink-soft)}
.cl-money{color:var(--ink);font-weight:600;min-width:44px;text-align:right}
.cl-item.is-total .cl-money{color:var(--accent)}

/* 费用汇总表：token 与金额两列对齐，方便核对 */
.cost-tbl{margin-top:8px;display:flex;flex-direction:column;gap:2px}
.cost-tr{
  display:grid;grid-template-columns:52px 1fr 92px;gap:8px;align-items:baseline;
  font-size:11.5px;color:var(--ink-soft);font-variant-numeric:tabular-nums;
}
.cost-tr.is-head{font-size:10.5px;color:var(--ink-faint)}
.cost-tr.is-head span:nth-child(2),
.cost-tr span:nth-child(2){text-align:right}
.cost-tr.is-head span:nth-child(3),
.cost-tr span:nth-child(3){text-align:right}
.cost-tr.is-total{
  margin-top:4px;padding-top:5px;border-top:1px solid var(--line-soft);
  color:var(--ink);font-weight:600;
}
.cost-tr.is-total span:nth-child(3){color:var(--accent)}

/* 费用明细面板：跟在最终结果下面 */
.cost-panel{
  margin-top:10px;border:1px solid var(--line);border-radius:10px;
  background:color-mix(in srgb, var(--panel) 60%, transparent);
  padding:10px 12px;
}
.cost-head{display:flex;align-items:baseline;justify-content:space-between;gap:10px}
.cost-label{font-size:11.5px;font-weight:600;color:var(--ink-soft)}
.cost-total{
  font-size:15px;font-weight:700;color:var(--accent);
  font-variant-numeric:tabular-nums;
}
.cost-total.is-unknown{font-size:12px;font-weight:500;color:var(--ink-faint)}
.cost-sum{margin-top:5px;font-size:11px;color:var(--ink-faint);line-height:1.7}
.cost-rows{
  margin-top:8px;padding-top:8px;border-top:1px solid var(--line-soft);
  display:flex;flex-direction:column;gap:4px;
  max-height:190px;overflow:auto;
}
.cost-row{display:flex;align-items:baseline;gap:10px;font-size:11.5px;color:var(--ink-soft)}
.cost-who{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cost-tk{color:var(--ink-faint);font-variant-numeric:tabular-nums;font-size:10.5px;cursor:help}
.cost-money{color:var(--ink);font-variant-numeric:tabular-nums;min-width:62px;text-align:right}
.cost-note{margin-top:8px;font-size:10.5px;color:var(--ink-faint);line-height:1.6}

/* ---------- 插入块菜单（/ 打开，见 slash-menu.js） ----------
 *
 * ⚠️ 定位的**唯一权威是 slash-menu.js 的 computeRect()**。
 * 这里只负责外观，一行坐标/尺寸都不写——理由是踩过的坑：
 *   浏览器原生锚点定位（anchor-name / position-anchor / position-area /
 *   position-try-fallbacks）在 iOS 键盘弹起时**不可靠**：锚点（输入框）
 *   本身已被键盘顶到可视视口之外，任何"贴着锚点"的规则都会算到看不见的地方；
 *   而它翻转与否还取决于各引擎不一致的内部分支，实测 WebKit 上就没翻上来。
 *   更糟的是它会与 JS 的校正量（translate / 内联 max-height）同时生效，
 *   两套坐标语义互相打架，最终位置无法用代码推理。
 * 所以：删掉全部锚点定位规则与居中兜底（transform:translate(-50%,-50%)），
 * 位置/宽度上限/高度上限全部由 JS 以**可视视口**为基准算好落在内联样式上。
 *
 * 保留 position:fixed 只作为"JS 还没算出坐标"那一瞬的默认展示方式；
 * JS 一定会在首帧绘制前写入 left/top（它是先隐藏插入、量完再亮）。
 */
.slash-menu{
  position:fixed;z-index:var(--z-slash,80);left:0;top:0;
  min-width:250px;max-width:min(340px,calc(100vw - 24px));
  border:1px solid var(--line);border-radius:10px;
  background:var(--panel);box-shadow:0 14px 34px rgba(0,0,0,.22);
  padding:6px;display:flex;flex-direction:column;gap:2px;
  overflow:auto;
}

.slash-head{
  font-size:11px;font-weight:600;color:var(--ink-faint);
  padding:5px 8px 6px;border-bottom:1px solid var(--line-soft);margin-bottom:3px;
}
/*
 * 菜单项 = 一行两槽。两槽的职责在这里**唯一声明**，别处不再各写一份：
 *
 *   左槽 .slash-main（名称，如「▧ 文件」）
 *       本征宽度、永不压缩、永不换行。名称折行（「文件」变成「文/件」）
 *       是纯粹的读障碍：它把一行选项撑成两行，扫读节奏断掉，
 *       而且折行位置由"右侧说明有多长"决定 —— 同一项在不同宽度下
 *       长得不一样。名称是识别项，不是内容，必须恒为一行。
 *
 *   右槽 .slash-hint（说明）
 *       吃掉剩余宽度、内部永不换行，超出部分靠**自身横向滚动**，
 *       而不是折成多行把选项撑高。短说明仍贴右（justify-content:
 *       space-between 的自然结果），长说明才收缩成滚动窗口 ——
 *       这条规则同时覆盖"装得下"与"装不下"两种情况，没有分支、没有魔法宽度。
 *
 * 为什么是 flex:0 1 auto 而不是 flex:1：
 *   grow=1 会让短说明被拉伸成一个全宽盒子，文字在盒内左对齐，
 *   于是"说明贴右"这个既有观感被改掉。shrink=1 + basis=auto 才是
 *   "装得下就保持自身宽度贴右，装不下才让位并滚动"。
 */
.slash-item{
  display:flex;align-items:baseline;justify-content:space-between;gap:10px;
  flex-wrap:nowrap;
  width:100%;text-align:left;border:0;background:transparent;color:var(--ink);
  font-size:12.5px;font-family:inherit;padding:7px 8px;border-radius:7px;cursor:pointer;
}
.slash-item.is-active{background:color-mix(in srgb, var(--accent) 12%, transparent)}
.slash-item.is-disabled{opacity:.45;cursor:not-allowed}
.slash-main{flex:0 0 auto;white-space:nowrap;font-weight:600}
/*
 * 说明槽是**一个独立的横向滚动容器**（与顶栏 header 同一套做法）：
 * 隐藏滚动条避免在 24px 高的行里多出一条灰线，但滚动能力始终在
 * （滚轮 / 触控板横滑 / 触屏拖动都生效）。
 *
 * 悬停或键盘选中时给出两条可滚动提示，两者都只在"用户正在看这一项"时出现：
 *   · 右缘渐隐遮罩（mask-image）—— 明示"右侧还有字被切掉了"；
 *   · 细滚动条 —— 明示"这块本身可以左右拨动"。
 * 不用 JS 参与：可见性判据就是 CSS 的 hover/active 状态，没有第二套状态。
 */
.slash-hint{
  flex:0 1 auto;min-width:0;
  font-size:10.5px;color:var(--ink-faint);
  white-space:nowrap;
  overflow-x:auto;overflow-y:hidden;
  -webkit-overflow-scrolling:touch;
  scrollbar-width:none;
}
.slash-hint::-webkit-scrollbar{display:none}
.slash-item:hover .slash-hint,
.slash-item.is-active .slash-hint,
.slash-hint:focus-visible{
  /* 细滚动条：视觉上是一根 2px 的提示线，不抢内容 */
  scrollbar-width:thin;
  /* 右缘渐隐：只在可能被裁切时才需要，所以只在这几个状态出现 */
  -webkit-mask-image:linear-gradient(to right,#000 calc(100% - 14px),transparent);
          mask-image:linear-gradient(to right,#000 calc(100% - 14px),transparent);
}
.slash-item:hover .slash-hint::-webkit-scrollbar,
.slash-item.is-active .slash-hint::-webkit-scrollbar,
.slash-hint:focus-visible::-webkit-scrollbar{
  display:block;height:2px;
}
.slash-item:hover .slash-hint::-webkit-scrollbar-thumb,
.slash-item.is-active .slash-hint::-webkit-scrollbar-thumb,
.slash-hint:focus-visible::-webkit-scrollbar-thumb{
  background:var(--line-strong);border-radius:2px;
}
.slash-foot{
  font-size:10.5px;color:var(--ink-faint);
  padding:6px 8px 3px;border-top:1px solid var(--line-soft);margin-top:3px;
}

/* 运行中的标记：就地闪烁，跑完变绿勾 */
.cchip.is-running{
  animation:cchipblink 1.1s ease-in-out infinite;
  border-color:var(--accent);
}
@keyframes cchipblink{
  0%,100%{background:color-mix(in srgb, var(--accent) 10%, transparent);opacity:.7}
  50%{background:color-mix(in srgb, var(--accent) 30%, transparent);opacity:1}
}
.cchip.is-done{
  border-color:var(--ok,#3f9c5a);
  background:color-mix(in srgb, var(--ok,#3f9c5a) 12%, transparent);
  color:var(--ok,#3f9c5a);
}
.cchip.is-error{
  border-color:var(--danger);
  background:color-mix(in srgb, var(--danger) 12%, transparent);
  color:var(--danger);
}
/* 结果跑出来后悬停可见（用户选的 blink_check 带悬停看输出） */
.cchip[data-result]:hover::after{
  content:attr(data-result);
  position:absolute;left:50%;bottom:calc(100% + 6px);transform:translateX(-50%);
  max-width:340px;white-space:pre-wrap;
  padding:7px 9px;border-radius:8px;
  background:var(--panel);border:1px solid var(--line-strong);
  box-shadow:0 8px 24px rgba(0,0,0,.16);
  font-size:11.5px;font-weight:400;color:var(--ink);line-height:1.6;
  z-index:60;pointer-events:none;
}

/* ---------- contenteditable 编辑器 ---------- */
.t-input{
  width:100%;
  min-height:44px;
  outline:none;
  font:13.5px/1.75 var(--font);
  color:var(--ink);
  white-space:pre-wrap;
  overflow-wrap:anywhere;
  padding:0;
}

/* 编排区同一条规则（左栏消息序列） */
.t-row.is-ai .t-avatar{background:var(--panel);border-style:dashed;color:var(--ink-soft)}
.t-row.is-ai .t-bubble{border-style:dashed;background:transparent}
.t-row.is-user .t-bubble{box-shadow:none}
/* 浮层里的搜索框：中文必须能输入（详见 JS 里的说明） */
.orch-picker-search{padding:8px 9px;border-bottom:1px solid var(--line-soft);flex:none}
.orch-picker-search input{
  width:100%;box-sizing:border-box;border:1px solid var(--line);border-radius:8px;
  padding:7px 10px;font-size:13px;background:var(--input);color:var(--ink);outline:none;
}
.orch-picker-search input:focus{border-color:var(--accent)}
.orch-picker-search input::placeholder{color:var(--ink-faint)}

.orch-picker{
  position:fixed;z-index:80;
  right:auto;bottom:auto;
  width:min(520px,92vw);
  height:auto;max-height:min(340px,60vh);
  left:auto;top:auto;
}

@media (max-width:760px){
  /*
   * 窄屏左栏 = 抽屉：盖住整页（**包括顶栏**）。
   *
   * 层级用 --z-side-drawer(48) 而不是写死 40：
   * 写死 40 时它会被顶栏（--z-head-overlay=45）盖住，抽屉顶部那行
   * （工作流标题 + ＋新建 + 收起）整行看不见 —— 用户在手机上就无法
   * 新建工作流了（2026-10-06 报的问题）。层级集中到令牌里，
   * 三条约束（> 顶栏浮层、< 底部抽屉、< 弹窗）写在令牌定义处。
   */
  .orch-side{
    position:fixed;top:0;left:-300px;width:290px;height:100%;
    z-index:var(--z-side-drawer,48);
    transition:left .22s var(--ease-out,cubic-bezier(.33,1,.68,1));
    padding-top:env(safe-area-inset-top);box-shadow:6px 0 24px rgba(0,0,0,.14);
  }
  .orch-side.open{left:0}
  /*
   * 编排页的遮罩跟着抽屉一起抬到顶栏之上。
   *
   * .scrim 的基础规则（z-index:29）是给**聊天页**写的：那边侧栏是 30，
   * 遮罩 29 正好压在它下面。编排页的抽屉是 48，若沿用 29，
   * 遮罩就会落在顶栏(45)之下 —— 顶栏既不会被压暗、也还能点，
   * 而它此刻明明在抽屉底下（看得见的按钮点下去没反应，是最难排查的一类）。
   * 所以这里按本页自己的层级给值：紧贴抽屉之下、顶栏之上。
   */
  .orch-page .scrim{z-index:var(--z-side-scrim,47)}
  /* 抽屉打开后，关闭按钮必须在抽屉内部可见；主顶栏按钮只负责再次打开。 */
  .orch-side-drawer-close{display:block!important}
  #orchMenu{display:block}
  /*
   * 抽屉里的工作流条目按 --tap 放大。
   *
   * 桌面是 35px 高的紧凑列表（鼠标够用、一屏能看更多条）；
   * 但手机抽屉是**主要的工作流切换入口**，35px 手指点不准，
   * 而且点错就是切到另一个工作流（代价不是"没反应"而是"改错了对象"）。
   * 只改条目，不改字号 —— 信息密度保持不变，只是每行更好点。
   */
  .wf-item-title{min-height:var(--tap);display:flex;align-items:center;padding:0 10px}
  /*
   * 顶栏的高度/内边距/按钮尺寸**不在这里定**。
   * 原先这里写死 height:50px + 一组按钮尺寸，与后面同断点的两行栅格块
   * 形成两套权威（后写的靠源码顺序赢），一次改动要看两个地方才知道顶栏多高。
   * 顶栏窄屏形态的唯一权威在后面的「编排页顶栏：两行栅格」块里。
   */
  /*
   * 注意：#orchBackTop（「对话」）**不在这里隐藏**。
   * 它隐藏的理由本来是"顶栏放不下 8 个控件"，但窄屏下整条 .head-nav
   * 已经收进 ⋯ 面板（见后面的顶栏栅格块），面板里三项排得下 ——
   * 继续隐藏会让手机上**没有回聊天页的入口**（编排页只能靠浏览器后退），
   * 这是功能缺失，不是省空间。
   */
  .wf-status{display:none}
  /*
   * 窄屏滚动区：把"给消息区让空间"落到具体数值上（用户 2026-10-01 要求）。
   *
   * 三处收紧，都是"弱化其他、突出消息"的直接体现：
   *   · 上内边距 16→10、左右 12→10：手机上首屏最宝贵，顶部留白纯属浪费；
   *   · 块间距 24→14：.sec 之间的空档在手机上被感知为"消息被切碎了"，
   *     而窄屏本来就靠标题行区分区块，不需要那么大的空白来做分隔；
   *   · 底部 100→76：那 100px 是为「滚动到底时最后一条不被悬浮控件挡住」
   *     留的，但消息序列结束后还有 .node-add 两个按钮垫底，
   *     实测 76px 足够让最后一条完整可见（不再压着底部安全区）。
   */
  .orch-scroll{padding:10px 10px 76px;gap:14px}
  /*
   * 窄屏把横向滚动**关掉**（用户 2026-10-01 要求：
   * "要么你就限制那一排的宽度，不要让移动滑动的时候会变得左右滑动"）。
   *
   * 理由不是"藏起溢出"，而是这一层的滚动职责本来就是**只有纵向**：
   *   · 它是一条自上而下的消息序列，横向内容（长文案、宽表格）应当折行或自身内部滚动，
   *     而不是把整页变成一个可以左右拖的画布；
   *   · 一旦出现横向可滚动，手指**竖向**滑动时页面会跟着左右晃（用户反馈的正是这个），
   *     原因是浏览器在判定手势方向前会先允许一点横向位移。
   *
   * 注意这**不是**在掩盖布局溢出：真正会溢出的那个源头（#testNote 的裸 nowrap）
   * 已在上面的 #testNote 规则里从归属上修好（独占一行 + 允许换行）。
   * 这一条是防线：即使将来某个组件又忘了收敛宽度，
   * 表现也只是"那个组件被裁掉"而不是"整页跟着晃"——
   * 至少用户还能正常上下阅读消息。
   * 桌面端不动它：宽屏下横向空间本来就够，且那里没有触摸手势问题。
   */
  .orch-scroll{overflow-x:hidden}
  .t-row{gap:9px}
  .t-body{max-width:none}
  .t-input{min-width:0}
  .t-avatar{width:27px;height:27px;font-size:9.5px}
  .thread::before{left:13px}
  .var-row .var-name{min-width:74px}
  .var-row{flex-wrap:wrap}
  .t-actions{opacity:1}
  .orch-picker{width:94vw}
}

/* =====================================================================
 * 内联文件选择器
 *
 * 取代原来的「＋ 选文件」按钮 + 浮层：
 * 直接给一个输入框，输入即匹配 OB 文件名，↑↓ 选择、回车填入。
 * 少一次点击、少一层浮层，也顺便绕开了浮层定位那类继承坑。
 * ===================================================================== */
.fsel{position:relative;margin-top:10px}
.fsel-input{
  width:100%;box-sizing:border-box;border:1px solid var(--line);border-radius:9px;
  padding:8px 11px;font-size:13px;background:var(--input);color:var(--ink);outline:none;
  transition:border-color 150ms ease;
}
.fsel-input:focus{border-color:var(--accent)}
.fsel-input::placeholder{color:var(--ink-faint)}
.fsel-menu{
  position:absolute;left:0;right:0;top:calc(100% + 4px);z-index:50;
  background:var(--panel);border:1px solid var(--line-strong);border-radius:10px;
  box-shadow:0 10px 30px rgba(0,0,0,.16);overflow:hidden;
  max-height:min(300px,46vh);display:flex;flex-direction:column;
}
.fsel-menu.hidden{display:none}

/*
 * ===== 弹窗内的下拉列表：改为文档流内联（不再绝对定位）=====
 *
 * 问题：.fsel-menu 是 position:absolute，绝对定位元素不参与父容器高度计算。
 * 在块弹窗（.modal-body）里，弹窗刚打开时列表还没搜索、内容为空，
 * 于是 .modal-body 的高度只等于一个搜索框——整块弹出只有一条缝，
 * 列表还被弹出到弹窗外面（截图里的观感就是「弹窗高度莫名其妙地小」）。
 *
 * 处理：弹窗内改用文档流内联，让列表的高度真正参与弹窗的高度计算，
 * 并给一个稳定的最小高度，使「有没有搜索结果」不再改变弹窗尺寸。
 * 弹窗外（会话级系统提示词那一行）保持绝对定位浮层——那里才需要浮层，
 * 因为它不能把页面内容顶下去。
 */
.modal-body .fsel-menu{
  position:static;margin-top:6px;max-height:none;
  box-shadow:none;border-radius:9px;
}
/* 列表内滚动而不是整块弹窗滚动：搜索框与按钮始终可见 */
.modal-body .fsel-list{max-height:300px;overflow:auto}
/* 弹窗高度由「对话框」而不是「当前有没有内容」决定，避免尺寸跳变 */
.modal-body .fsel{min-height:360px}
.fsel-head{
  flex:none;padding:6px 11px;border-bottom:1px solid var(--line-soft);
  font-size:11px;color:var(--ink-faint);background:var(--panel-2);
  display:flex;align-items:center;gap:8px;
}
/*
 * 表头一行放三样东西：[计数] [视图切换] [刷新]。
 * 只有「刷新」贴右（margin-left:auto 挂在它身上），计数与切换留在左侧——
 * 原来把 margin-left:auto 写在 .fsel-head button 上，加了切换按钮之后
 * 会把两个切换键推到中间去，视觉上像三个互不相关的控件。
 *
 * 顺序即优先级：下面这三条必须"先写按钮的通用外观，再写切换键的覆盖"。
 * .fsel-head button 与 .fsel-head .fsel-tab 的选择器权重完全相同（都是
 * 两个类），所以谁在后面谁生效——把覆盖规则写在前面等于没写，
 * 切换键会长成两个各自带边框的独立按钮，分段控件的观感就没了。
 */
.fsel-head button{
  border:1px solid var(--line);background:var(--panel);color:var(--ink-faint);
  border-radius:5px;font-size:10.5px;padding:1px 7px;line-height:15px;
}
.fsel-head button:hover{border-color:var(--line-strong);color:var(--ink)}
.fsel-head .fsel-count{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fsel-tabs{
  flex:none;display:inline-flex;align-items:center;gap:2px;margin-left:2px;
  border:1px solid var(--line);border-radius:6px;padding:1px;background:var(--panel);
}
.fsel-head .fsel-tab{
  margin-left:0;border:0;background:transparent;color:var(--ink-faint);
  border-radius:4px;font-size:10.5px;padding:1px 7px;line-height:15px;cursor:pointer;
}
.fsel-head .fsel-tab:hover{color:var(--ink)}
/* 当前视图：唯一的高亮依据是 .on（由 render 按 st.view 写），不靠 :hover 猜 */
.fsel-head .fsel-tab.on{background:var(--panel-2);color:var(--ink);font-weight:600}
.fsel-head .fsel-head-refresh,#bkModalRoot .fsel-head>button:last-child{margin-left:auto}
.fsel-list{flex:1;min-height:0;overflow:auto;padding:4px}
.fsel-item{
  display:flex;align-items:center;gap:9px;width:100%;text-align:left;
  border:0;background:transparent;padding:6px 9px;border-radius:7px;
  font-size:12.5px;color:var(--ink-soft);cursor:pointer;
}
.fsel-item:hover,.fsel-item.active{background:var(--panel-2);color:var(--ink)}
.fsel-item.active .fsel-name{font-weight:600}
.fsel-name{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.fsel-dir{
  font-size:11px;color:var(--ink-faint);flex:0 0 auto;max-width:48%;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;direction:rtl;text-align:right;
}
.fsel-empty{padding:14px;text-align:center;color:var(--ink-faint);font-size:12.5px;line-height:1.7}
.fsel-foot{
  flex:none;padding:5px 11px;border-top:1px solid var(--line-soft);
  font-size:10.5px;color:var(--ink-faint);background:var(--panel-2);
}
.fsel-foot kbd{
  font:inherit;font-size:10px;border:1px solid var(--line);border-radius:4px;
  padding:0 4px;background:var(--input);
}

/* ---------- 弹窗（新建 / 重命名） ---------- */
.modal-narrow{width:min(460px,100%)}
.modal-head-sub{color:var(--ink-faint);font-size:11.5px;font-weight:400}
.modal-narrow .modal-body{padding:18px}

/* ---------- 设置弹窗：工具/价格/变量 ---------- */
/*
 * 内容比窄弹窗宽（三块配置），比跟踪弹窗窄（没有分栏），
 * 取 760px 这个中间值——与左栏最大宽度一致，放得下价格三列。
 */
.modal-settings{width:min(760px,100%)}
.modal-settings .modal-body{display:flex;flex-direction:column;gap:16px}
/*
 * 设置面板「停靠」变体（仅桌面端 + 用户开启时生效，见 app.js applySettingsDock）。
 *
 * 它解决的是一个具体的遮挡问题：居中模态弹窗会盖住正在看的对话，
 * 而价格/工具这类设置是"边看对话边调"的——弹窗本身不该成为要反复开关的东西。
 *
 * 两个关键点：
 *   1) 去掉遮罩（.modal::before）并把 pointer-events 放开：
 *      停靠态下用户要能继续滚动、选中对话内容，
 *      留着遮罩等于"看起来停靠了，实际还是模态"。面板自身恢复 pointer-events:auto。
 *   2) left/top 由 app.js 现场量出来（相对 #main 定位），不在这里写死：
 *      "聊天区域左上角"取决于侧栏宽度、头部高度、系统提示词条是否展开，
 *      这些都不是常量，写死必然对不上。CSS 只负责"怎么摆"，JS 负责"摆在哪"。
 *
 * 只有 == .modal.docked 才生效；未开启时下面每条规则都不参与，
 * 居中弹窗的行为与之前逐字一致（零回归面）。
 */
.modal.docked{
  position:absolute;
  inset:auto;                 /* 覆盖 .modal 的 inset:0（居中铺满） */
  display:block;
  padding:0;
  pointer-events:none;        /* 让面板之外的区域可以继续操作对话 */
  z-index:40;
}
.modal.docked::before{display:none}   /* 停靠态不需要遮罩，也不该暗化页面 */
.modal.docked .modal-card{
  pointer-events:auto;        /* 面板自身恢复可交互 */
  /*
   * ★ 停靠态不在这里定尺寸 ★
   * 面板是一个矩形，它的两个轴都由 app.js 的 positionDockedSettings() 现场算：
   *   · 宽度卡进消息列左侧的空白带（空白带 - DOCK_GAP）；
   *   · 高度占满消息区那条带（聊天区上沿到下沿 - DOCK_BOTTOM_GAP）。
   * 这两处的输入（侧栏宽、窗口宽、消息列宽、系统提示词条是否展开）没有一个是常量。
   *
   * 这里曾经写着 max-height:min(72vh,720px)，那是**垂直轴上的第二个权威**：
   * 无论父容器多高，面板都被这个上限卡住，于是上下各空一大截——
   * 用户截图报告的"上下高度冗余"就是它。留着 max-width 也是同一个毛病，
   * 只是 420px 恰好等于 JS 的理想宽，多数窗口下看不出来而已。
   *
   * 所以两个轴一起交还给 JS：CSS 只声明"怎么摆"（外观/圆角/阴影），
   * 不声明"摆多大"。JS 写进 style 的尺寸是唯一权威，此处不得再用
   * max-width/max-height 去夹它——两条规则同时想定尺寸时，
   * 哪条生效取决于特异性，实际表现为"某个窗口宽度下面板忽然变矮"这种极难定位的错。
   *
   * 首帧安全性由调用顺序保证：.docked 类与内联尺寸是同一次同步调用里
   * 一起落下的（见 app.js 的 applySettingsDock → positionDockedSettings），
   * 不存在"类已生效、尺寸还没算"的中间帧。
   */
  width:auto;
  max-width:none;
  max-height:none;
  box-shadow:0 12px 32px rgba(0,0,0,.18);
}
/*
 * 停靠态是"卡片有固定高度"的那一个变体（基线弹窗的高度是内容驱动的），
 * 所以"哪一段吸收高度变化"必须在这里一次性说清楚：
 *
 *   头、尾  —— **flex:none**，恒为它们自己的高度。
 *             默认 flex:0 1 auto 允许被压缩，窗口矮时先被挤扁的会是
 *             标题与「关闭」按钮，而真正该滚的内容区反倒拿到了高度。
 *   内容区  —— **flex:1 1 auto; min-height:0**，吃掉剩余高度并自己滚。
 *             · flex:1 1 auto：卡片比内容高时它要撑开
 *               （否则 footer 紧贴内容、底部留空，看着仍像"高度没占满"）；
 *             · min-height:0：flex 项默认 min-height:auto 会阻止收缩，
 *               不加这条，内容更高时 body 撑破卡片，而卡片 overflow:hidden
 *               会把它直接裁掉——overflow:auto 永远不触发，滚动条都出不来。
 *
 * 两条症状同时指向一件事：滚动这个职责必须落到内容区身上。
 * 只写在 .modal.docked 下，居中模态的行为与之前逐字一致（零回归面）。
 */
.modal.docked .modal-head,
.modal.docked .modal-foot{flex:none}
.modal.docked .modal-body{flex:1 1 auto;min-height:0}
/*
 * 停靠态是"常驻一看"的面板，底部操作条不必再撑那么大。
 * （与上面的 flex:none 是同一选择器的两条声明，这里合并写在一起，
 *   不留两处各自定义 .modal.docked .modal-foot —— 一处选择器一条规则。）
 */
.modal.docked .modal-foot{padding:10px 14px}
/*
 * 停靠不可用时的说明（勾了开关但窗口太窄）。
 * 用提示色的小字，与 .sec-hint 的淡化风格区分开——它是"当前状态需要注意"，
 * 不是"这个区块是干什么的"。
 */
.chat-dock-hint{
  margin-top:8px;padding:8px 10px;border-radius:9px;
  border:1px solid var(--line);background:var(--panel-2);
  font-size:11.5px;line-height:1.6;color:var(--ink-soft);
}
.chat-dock-hint.hidden{display:none}
/* 底部操作条（wbModal 同款，桌面端本来缺这条基线样式） */
.modal-foot{
  display:flex;align-items:center;justify-content:flex-end;gap:8px;
  padding:12px 18px;border-top:1px solid var(--line);
}
/* 设置入口常驻顶栏最前，与 ☰ 同高对齐，比其他 orch-btn 稍紧凑 */
.orch-settings-btn{padding:5px 11px;background:var(--panel);border-color:var(--line);font-weight:500}
/*
 * 设置区收起时，顶栏这个按钮是**唯一**的重新打开入口。
 * 因此它不能只靠"按下"这一瞬的反馈——区域已经在屏幕上消失了，
 * 用户需要一眼看出"设置被收起来了、点这里能找回来"。
 * 用实心强调色表示"当前是关闭状态，点它开启"，与开启态的中性底区分开。
 */
.orch-settings-btn.is-off{
  background:color-mix(in srgb,var(--accent) 16%,var(--panel));
  border-color:color-mix(in srgb,var(--accent) 45%,var(--line));
  color:var(--accent);
}
.mfield{display:flex;flex-direction:column;gap:7px;font-size:12.5px;color:var(--ink-soft)}
.mfield input{
  width:100%;box-sizing:border-box;border:1px solid var(--line);border-radius:9px;
  padding:9px 12px;font-size:14px;background:var(--input);color:var(--ink);outline:none;
  transition:border-color 150ms ease;
}
.mfield input:focus{border-color:var(--accent)}
.merr{color:var(--danger);font-size:12.5px;margin-top:10px}
.merr.hidden{display:none}
/*
 * 弹窗里的中性说明行（与 .merr 同层同尺寸，只差颜色语义）。
 *
 * 与 .merr 分开而不是"用 .merr 加一个修饰类"：错误与说明是两种语义
 * （一个是"你这次提交没通过"，一个是"这里是这件事的背景"），
 * 共用一个类会让以后调错误色的人顺手把说明也染红，或者反过来，
 * 让"到底有错没错"在样式层就分不清楚。
 */
.mhint{color:var(--ink-soft);font-size:12.5px;margin-top:10px}
.mhint.hidden{display:none}
.modal-actions{
  display:flex;justify-content:flex-end;gap:8px;
  padding:12px 18px;border-top:1px solid var(--line);background:var(--bg);
}

/* ---------- 模型管理弹窗（顶部模型按钮右键） ---------- */
.model-list{display:flex;flex-direction:column;gap:6px;max-height:320px;overflow:auto}
.model-row{
  display:flex;align-items:center;gap:8px;padding:8px 10px;border:1px solid var(--line);
  border-radius:9px;background:var(--panel-2);
}
.model-row.is-editing{border-color:var(--accent)}
.model-row-main{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}
.model-row-name{font-size:13.5px;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.model-row-meta{font-size:11.5px;color:var(--ink-faint);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.model-row-tag{font-size:11px;color:var(--accent);border:1px solid var(--accent);border-radius:6px;padding:1px 6px;flex:none}
.model-row .icon-btn{flex:none}
.model-empty{font-size:12.5px;color:var(--ink-faint);padding:14px 4px;text-align:center}
.model-form{display:flex;flex-direction:column;gap:12px;margin-top:14px;padding-top:14px;border-top:1px solid var(--line)}
.model-form.hidden{display:none}

/*
 * 「创建为编排」弹窗里的说明行。
 * 它有两种语义：默认是"会搬什么"的说明，会话正在生成时变成**警告**
 * （这个动作现在做不了及其原因）。两种语义用同一个元素 + 一个 is-warn 类表达，
 * 而不是准备两段文案去互相覆盖——覆盖顺序一乱就会留下错误的旧文案。
 */
.to-wf-info{font-size:12px;line-height:1.6;color:var(--ink-soft);margin-top:10px}
.to-wf-info.is-warn{color:#c98a2e}

/*
 * 顶部警示条（转换有信息损失时使用）。
 *
 * 为什么不用状态栏：状态栏是单行、会被下一条提示覆盖，
 * 而"有 N 条旧消息的块已退化成纯文本"是要用户停下来看的信息。
 * 固定在顶部且必须手动关闭，才能保证它真的被看见。
 */
.warn-banner{
  position:fixed;left:50%;transform:translateX(-50%);top:14px;z-index:80;
  display:flex;align-items:flex-start;gap:10px;max-width:min(680px,92vw);
  padding:10px 12px;border-radius:10px;font-size:12.5px;line-height:1.6;
  background:var(--panel);color:var(--ink);
  border:1px solid #c98a2e;box-shadow:0 8px 26px rgba(0,0,0,.22);
}
.warn-banner.hidden{display:none}
.warn-banner-x{
  flex:none;border:none;background:transparent;color:var(--ink-soft);
  font-size:15px;line-height:1;cursor:pointer;padding:0 2px;
}
.warn-banner-x:hover{color:var(--ink)}

/* 菜单（右键 / 长按 / 点击展开的下拉共用同一套元素与样式） */
.ctxmenu{
  position:fixed;z-index:90;min-width:150px;padding:4px;
  background:var(--panel);border:1px solid var(--line-strong);border-radius:9px;
  box-shadow:0 10px 30px rgba(0,0,0,.18);
  /*
   * 下拉菜单需要接受键盘（↑/↓/Enter）与点选，菜单本身必须是可聚焦容器。
   * outline:none 是因为聚焦态已经由 .is-focus 在项上表达，
   * 再画一圈容器描边会与项的高亮打架。
   */
  outline:none;
}
.ctxmenu button{
  display:flex;align-items:center;gap:8px;
  width:100%;text-align:left;border:0;background:transparent;
  padding:7px 10px;border-radius:6px;font-size:12.5px;color:var(--ink);cursor:pointer;
}
.ctxmenu button:hover{background:var(--panel-2)}
.ctxmenu button.danger{color:var(--danger)}
.ctxmenu button.danger:hover{background:color-mix(in srgb, var(--danger) 10%, var(--panel))}
/*
 * 键盘落点 .is-focus 与鼠标悬停必须长得一样：
 * 用户不该因为"现在用的是键盘还是鼠标"看到两种不同的高亮。
 */
.ctxmenu button.is-focus{background:var(--panel-2)}
.ctxmenu button:disabled{opacity:.45;cursor:default}
/*
 * 当前值标记（选择型菜单）：左侧一条强调色竖线 + 加粗。
 * 不用 ✓ 字符是为了让"当前项"在所有字体下都清晰，
 * 而且能被 ::before 精确控制位置（不挤占 label 的对齐）。
 */
.ctxmenu button.is-current{font-weight:600;position:relative}
.ctxmenu button.is-current::before{
  content:'';position:absolute;left:2px;top:50%;transform:translateY(-50%);
  width:2px;height:14px;border-radius:2px;background:var(--accent);
}
.ctxmenu button.is-current{padding-left:12px}
.ctxmenu-item-main{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ctxmenu-item-hint{
  flex:none;font-size:11px;color:var(--ink-faint);font-weight:400;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:120px;
}
/* 下拉菜单通常比右键菜单宽（要放模型名 + 显示名） */
.ctxmenu.ctxmenu-dropdown{min-width:200px;max-width:min(320px,92vw)}

/* 管理弹窗里"共用端点与密钥"的常驻说明 */
.model-note{
  margin:12px 0 0;font-size:11.5px;line-height:1.6;color:var(--ink-faint);
  padding:8px 10px;border-radius:8px;background:var(--panel-2);
}
.model-note b{color:var(--ink-soft);font-weight:600}

/* =====================================================================
 * 两栏编排：左＝编排控件（写什么），右＝运行预览（看到什么）
 *
 * 为什么分栏：原先「运行」在整页最底部，工作流一长就得来回滚——
 * 改一句、滚到底、看结果、再滚回来，是最高频的操作路径却最费劲。
 * 分栏后左右常驻可视，改完立刻能看结果。
 * ===================================================================== */
.orch-cols{
  flex:1;min-height:0;display:grid;
  /*
   * 定位基准：消息导航（#msgNavHost，与两栏同级）要绝对定位压在**编排区**上。
   * 它是覆盖层，因此基准必须是这个 grid 容器本身；写在别处会让导航
   * 相对别的盒子定位（表现就是"导航跑到右栏去了"）。
   */
  position:relative;
  /*
   * ===== 两栏宽度分配（用户可拖拽，桌面端）=====
   *
   * 权威链（**唯一**，不要在别处再写一个数字）：
   *   settings.orch_split_width
   *     ⇄ /api/layout/orch-split-width（口径在 orch_split_width.js）
   *     ⇄ public/orch-split-resize.js 写 --orch-snap-w
   *     → 这里消费 var(--orch-snap-w,400px)
   *
   * 为什么是「左固定吸收 / 右定宽」而不是两栏各给百分比：
   *   用户拖的是一条边界，它的位置必须是**绝对的像素值**（"快照栏 360px"
   *   在任何窗口宽度下含义都一样）；而左栏（编排区，页面主任务）
   *   用 minmax(0,1fr) 吃掉剩余空间，窗口变宽时多出来的空间自动归左栏。
   *   若两栏都写百分比，窗口一变形两栏会同时缩放，用户"记住"的分配比例
   *   就与他的实际手感对不上了。
   *
   * minmax(0,1fr) 里的 0 是必需的：默认 min-width:auto 会让左栏被
   * 内容（长文件名、长 URL）撑开，拖拽结果被内容顶回去，表现为"拖了没反应"。
   *
   * 分隔条占一条独立的网格轨道（7px），它在两栏之间"夹"出一条可抓的缝：
   *   · 不做成 column-gap（那会让两栏之间出现无法抓取的空白，
   *     而且 gap 是背景色，不是可交互元素）；
   *   · 也不做成绝对定位浮在边界上（右栏有 border-left 与自己的滚动条，
   *     浮层会和它们抢命中区）。独立轨道让命中区与视觉边界严格重合。
   *
   * 注意：position:relative 与网格列数**必须同时存在**——
   *   前者是消息导航覆盖层的定位基准（它要压在编排区上，
   *   写在别处会让导航相对别的盒子定位、看起来"跑到右栏去了"）；
   *   后者是两栏宽度分配。两者描述的是同一个容器的不同职责，缺一即坏。
   */
  /*
   * ===== 四段宽度分配（桌面端，全站唯一口径在 layout_widths.js）=====
   *
   *   [工作流列表 .orch-side] │ [设置区] │ [编排区] │ [右栏]
   *        var(--orch-side-w)   var(--orch-set-w)  1fr   var(--orch-snap-w)
   *
   * 三条边界各占一条独立的 7px 网格轨道（不在列宽里挤，见下方说明）。
   *
   * 每段的兜底值与 layout_widths.js 的 def **必须一致**：
   *   · 兜底值在 JS 未加载完的那一帧生效，不一致就会看到"分栏跳一下"。
   *   · 契约测试会核对这四组数字（见 layout_widths.contract.test.js）。
   *
   * 为什么只有编排区是 1fr：它是页面主任务，应该吃掉所有剩余空间；
   * 其余三段都是"宽度有含义"的辅助栏（用户记住的是绝对像素值，
   * 而不是"占多少比例"）。窗口变宽时多出来的空间全部归编排区。
   *
   * ----------------------------------------------------------------------
   * ★ 网格模板只能有**一处**声明（2026-10-03 结构性修复）★
   * ----------------------------------------------------------------------
   * 这里曾经是"每个折叠态各写一份完整 grid-template-columns"：
   *   · 基础态        set-w 7px 1fr 7px snap-w
   *   · 设置区折叠     0px  0px 1fr 7px snap-w
   *   · 右栏折叠      set-w 7px 1fr 0px  0px
   * 三条规则各自**完整**描述了五条轨道。这在只有两种折叠时看着还行，
   * 但它把"每段占多宽"这件**每段自己的事**，写成了"整个网格的事"：
   *   ① 组合丢失：折叠维度是**彼此独立**的（左栏 / 设置区 / 右栏各一个开关），
   *      但每条规则只覆盖了"只有我自己折叠"这一种情形。真同时折叠时
   *      两条规则都在命中，靠**特异性**决出胜者（html[a] .orch-cols 的
   *      特异性 (0,2,1) 高于 html[data-orch-settings-collapsed="1"] .orch-cols 的 (0,2,0)）——
   *      于是"设置区已折叠"这一条被整条丢弃，设置区那条轨道退回
   *      var(--orch-set-w,320px)。用户实测症状：三个面板全收起后，
   *      编排区左边仍空着一条 320px 的**死轨道**（内容不可见、宽度照占），
   *      于是"收缩之后中间区并没有变宽"。而这条规则冲突**不会报任何错**。
   *   ② 组合爆炸：n 个可折叠段要写 2^n 份完整模板。加第四个折叠维度时，
   *      漏写一份的表现与 ① 完全一样——静默的空间浪费，没人能归因。
   *
   * 正确结构：**每段自己声明自己那条轨道**，容器只负责把它们串起来。
   * 折叠 = 把自己那条轨道置 0，其余一概不动。于是：
   *   · 任意组合都自动成立（组合数从声明里消失）；
   *   · "这一段多宽"只有一个落点，改它不会碰到别的段；
   *   · 加一个新折叠维度 = 新声明一组自己的轨道变量，老规则一行不改。
   *
   * 轨道变量按**网格顺序**排列（手柄轨道紧跟在它依附的那一列之后，
   * 与 layout-resize.js 的 BOUNDARIES 表同一顺序——那里规定
   * "手柄永远插在它所依附的那一列的右边"）。
   */
  --orch-set-track:var(--orch-set-w,320px);
  --orch-set-resizer-track:7px;
  /* 编排区永远是 1fr：它是唯一"吃掉剩余空间"的那一段，不是可折叠项 */
  --orch-edit-track:minmax(0,1fr);
  --orch-split-resizer-track:7px;
  --orch-snap-track:var(--orch-snap-w,400px);
  grid-template-columns:
    var(--orch-set-track)
    var(--orch-set-resizer-track)
    var(--orch-edit-track)
    var(--orch-split-resizer-track)
    var(--orch-snap-track);
}
/*
 * 各段折叠：**只把自己那两条轨道（内容 + 它右缘的手柄）置 0**。
 *
 * 为什么手柄那条也要置 0：折叠的是"这一段"，而手柄是这一段的边界。
 * 留下 7px 手柄轨道就是留下一条谁都抓不到、却实打实占宽的死缝——
 * 与 320px 死轨道是同一个毛病的小号版本。
 *
 * ★ 为什么不能用 display:none（2026-09-28 实测踩到的真实 bug）★
 *   网格列的位置是按"子元素出现顺序"分配的。`display:none` 会让元素
 *   整个退出网格流，后面的兄弟节点就**顶上来认领前面的轨道**：
 *   收起设置区后实测 .orch-col-edit 被挤到第 1 条（0px）轨道上 →
 *   编排区塌成 40px，右栏反而吃掉 1fr。用户一点"收起设置"，编排区就没了。
 *
 *   正确做法：轨道置 0（列结构恒定），元素仍在流里，
 *   于是"谁在哪条轨道"永远不变；再用 overflow:hidden + min-width:0
 *   把它压到看不见。
 *
 *   这也解释了为什么**列数必须恒定**：手柄是靠"插在某一列之后"定位的，
 *   列序号一变，手柄就会挂到别的轨道上。
 */
html[data-orch-settings-collapsed="1"] .orch-cols{
  /* 设置区：内容轨道 + 它右缘的手柄轨道，一起压成 0 */
  --orch-set-track:0px;
  --orch-set-resizer-track:0px;
}
html[data-orch-settings-collapsed="1"] .orch-cols > .orch-col-set{
  /* 仍在网格流里，只是被压成 0 宽；不得用 display:none（会长出上面的 bug） */
  min-width:0;
  padding:0;
  border:0;
  overflow:hidden;
  visibility:hidden;
}
html[data-orch-settings-collapsed="1"] .orch-cols > .orch-set-resizer{
  /* 同上：留在自己那条 0px 轨道上，只是不可见、不可抓 */
  visibility:hidden;
  pointer-events:none;
}
.orch-col{min-width:0;min-height:0;overflow:auto;-webkit-overflow-scrolling:touch;
  padding:24px 20px 80px;display:flex;flex-direction:column;gap:30px}
/*
 * 底部留白至少要装得下最后一个块，否则"跳到最后一条"永远到不了位。
 *
 * 这是本次加导航时**实测暴露**的真实缺陷（不是导航引入的，是它让缺陷变得可见）：
 *   滚动容器的可滚距离 = scrollHeight − clientHeight，而最后一条消息到列表底端
 *   还有 80px 内边距。若某条消息（含附件）高于 80px，它**永远无法滚到视口顶部**：
 *   实测最后一条高 247px，跳过去时缺 509px，于是"跳到第 10 条"落在第 7 条附近，
 *   高亮也跟着算错 —— 用户点了没反应/跳错地方，且没有任何报错。
 *
 * 修法不是让导航自己补偿（那会变成第二份几何权威，且只修好导航这一个入口），
 * 而是让容器本身有足够的下方空间：用 min-height 给内容末尾撑出一个视口高度，
 *   · 任何一条消息都能滚到顶部（导航跳转、以及将来别处的跳转都成立）；
 *   · 也顺带解决了"滚到底时最后一个块贴着屏幕下缘"的观感问题。
 * 用 min-height 而不是再加 padding：padding 会让整个列表底部多一条空白，
 * 而 min-height 只保证"至少有这么多可滚空间"，内容本来就更长时完全不生效。
 */
.orch-col-edit::after{content:'';display:block;flex:none;min-height:60vh}

/*
 * ============ 编排区的内容列宽：**与消息导航完全无关** ============
 *
 * 这是本文件里唯一决定编排区内容列（#sec-* / #nodeList / 各 .t-* 块）宽度的规则。
 *
 * ★ 结构性不变量（用户 2026-09-29 裁决，勿再破坏）★
 *   消息导航是一条**覆盖层控件**，它对布局的影响必须恒等于零：
 *   无论它是显还是隐、是静默态还是展开态，本规则算出来的都必须是同一个宽度。
 *   换句话说：导航的形态/可见性**不是**内容列宽度的输入。
 *
 * 为什么必须写在这里说清楚——这里曾经有一条「导航走廊」（--orch-nav-lane）：
 *   它让内容列按导航状态收窄（静默 −96px、展开 −276px），即把导航的
 *   **外观态**当成了主内容的布局输入。实测后果（用户截图 + 探针复核）：
 *     vw=1920  内容列 left 868→958、width 447→267
 *     vw=1024  width 630→450（节点列表高度 12326→15684，正文整体重排）
 *     vw=761   width 367→187
 *   仅仅把鼠标移开导航（悬停收起）就会让整页正文横向位移——一个浮层的
 *   悬停态在改变文档流，这是职责边界被打破的典型症状。
 *
 *   当时引入它的理由是"编排区里导航与正文没有足够横向空间"。但那个前提
 *   本身是错的：聊天页的导航从一开始就是覆盖层（absolute 出流），
 *   与 .warn-banner / .scroll-bottom-btn / .toolbar-status 同一条原则——
 *   旁路控件无论显隐都不得改变主内容盒子。编排页没有理由是例外。
 *
 * 不重叠由**几何**解决，不由**布局**解决（这是两件事，别混）：
 *   导航壳层的横向锚点由 msg-nav.js 的 layoutHost 按现场量取写入
 *   （区域右缘偏移 + 区域滚动条宽度 + 间隙），宽度由 renderShell 按展开态写入。
 *   CSS 不猜、不写百分比、也不需要给它留位。
 *   窄窗里两者是否会重叠，由浮层的既有让开手段处理：
 *   ⋯ 里的「显示导航」整条关闭、点空白/导航自身收成 48px 横线列。
 *   重叠是覆盖层的正常语义（聊天页同款），位移不是。
 *
 * 清理要求（可被契约测试核对）：
 *   本文件（以及任何其它样式表）**不得再出现 --orch-nav-lane**，
 *   也不得有以 pl-nav-* 类名或导航尺寸作为布局输入的任何规则。
 *   留一个恒为 0px 的变量不是"更保险"，而是给下一次越界预留落点。
 *
 * 下限 200px 的来历：编排区宽度 = 窗口 − 工作流列表 − 设置区 − 右栏，
 * 而后三段都是用户可拖的绝对像素值（--orch-side-w / --orch-set-w / --orch-snap-w），
 * 于是窗口较窄时编排区可能真的只剩几十像素。此时"铺满这一列"（width:100%）
 * 会让每一行文字都只剩一两个字宽、卡片被压成竖条——比横向滚动更难用，
 * 因为它看起来"有内容"却读不了。给一个最小可读宽度、让编排区自己横向滚动，
 * 用户至少能拖动看清；这是**内容列自身**的下限，与导航毫无关系。
 * 注意 min-width 只对 .t-row 这类块级子元素生效，对内联的 sec-head 之类无影响：
 * 它们本来就是块级、会跟着列宽走。
 */
.orch-col-edit > *{
  width:100%;
  /*
   * 上限来自 :root 的单一权威变量（默认 100% = 铺满版面宽）。
   * 曾经这里写死 min(760px,100%)：760 对版面无感知，用户收起面板腾出的
   * 空间内容一概不要，"收起"于是毫无意义（见 --orch-col-max 的说明）。
   */
  max-width:var(--orch-col-max,100%);
  min-width:200px;
  margin:0 auto;flex:none;
}

/*
 * 右栏（版本历史 / 快照）。
 *
 * ★ 为什么是"一个 flex 列 + 顶部 tab 条"而不是"两条 .sec 竖排" ★
 *   用户 2026-10-01 要求：两块放进同一个容器、用 tab 切换、一次只显示一个
 *   （"这样其中一个高度就很大，很舒服"）。
 *   结构上就是：tab 条 flex:none 钉在顶部，被选中的 pane flex:1 吃掉剩余
 *   全部高度 —— 高度链必须从 .orch-col-run 一路贯通到列表的 overflow:auto，
 *   断在任一层（比如某层没写 min-height:0）都会让列表**永远不出现内部滚动**，
 *   而是把整列顶出视口：看起来像"右栏不能滚"，实际是高度链断了。
 *
 * ★ 为什么子元素的宽度规则要保留（原来那两条 margin:0 auto;flex:none）★
 *   它们是"右栏内容撑满、不参与收缩"的既有约定，tab 条与 pane 都适用。
 *   flex:none 在这里是必要的：否则 pane 会被 tab 条挤扁而不是各自占满。
 */
.orch-col-run{
  background:var(--bg-deep);
  display:flex;flex-direction:column;min-height:0;
  /*
   * ★ 右栏不用 .orch-col 那套内边距（这是本次改造必须收掉的一处）★
   *   .orch-col 的 `padding:24px 20px 80px` 是为"一整条可滚动的块序列"设计的：
   *   左右 20px 是内容留白，底部 80px 是"最后一块还能滚到顶"的兜底
   *   （理由见 .orch-col-edit::after 那段）。
   *   但右栏现在是「固定 tab 条 + 一个自己滚动的 pane」：
   *     · 底部 80px 变成 pane 下方的一条**空缝** —— 白白吃掉高度，
   *       而"其中一个高度就很大"正是本次要的效果；
   *     · 左右 20px 会把 tab 条挤窄，而 tab 条是这一栏的标题行，
   *       它应当与左栏的标题行一样贴边（视觉上"这一栏从这里开始"）。
   *   高度归 pane、留白归 pane 的正文，右栏本体不再参与这两件事。
   */
  padding:0;gap:0;
}
.orch-col-run > *{width:100%;max-width:100%;margin:0 auto;flex:none}

/*
 * 右栏顶部 tab 条 = 这一栏的标题行。
 *
 * 它同时装着：两个 tab + 消息导航开关 + 收起整栏。三者都属于"这一栏现在
 * 长什么样"，所以同处一行（理由见 bianpai.html 那段的说明）。
 *
 * 与窄屏抽屉的 .sheet-tabs 是**同一套 tab 语言**（同一个选中药丸、
 * 同样 flex:none 的条）：用户在不同形态下看到的 tab 应当长得一样，
 * 否则"点在哪儿是切换视图"这件事要重学一遍。
 */
.orch-run-head{
  flex:none;display:flex;align-items:center;gap:6px;
  padding:10px 12px 8px;border-bottom:1px solid var(--line);
}
.run-tabs{display:flex;gap:4px;flex:1;min-width:0}
.run-tab{
  display:inline-flex;align-items:center;gap:5px;
  border:0;background:transparent;color:var(--ink-faint);font:inherit;
  font-size:12.5px;padding:6px 10px;border-radius:8px;cursor:pointer;
  white-space:nowrap;
}
.run-tab:hover{background:var(--panel-2);color:var(--ink-soft)}
.run-tab.is-active{color:var(--ink-strong);font-weight:600;background:var(--panel-2)}
.run-tab:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
/*
 * tab 条上的小图标按钮（当前只有导航开关）。
 * 比 tab 更轻（它是个开关，不是导航），但同样要够大能点。
 */
.run-icon-btn{
  flex:none;display:inline-flex;align-items:center;justify-content:center;gap:4px;
  border:1px solid transparent;border-radius:8px;background:transparent;
  color:var(--ink-faint);font:inherit;font-size:12px;padding:6px 8px;cursor:pointer;
  white-space:nowrap;
}
.run-icon-btn:hover{border-color:var(--line);background:var(--panel);color:var(--ink)}
.run-icon-btn:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
/* 导航开着 = 处于"已启用"的持续状态（与 ⋯ 里那个开关同一套视觉） */
.orch-pref-toggle.is-on{color:var(--accent);border-color:color-mix(in srgb,var(--accent) 40%,var(--line))}

/*
 * pane：被选中的那一个吃掉整栏剩余高度。
 *
 * flex:1 + min-height:0 是这一整条高度链的关键一环（见 .orch-col-run 的说明）：
 * 少了它，pane 会被内容撑到自然高度，列表的内部滚动条永远不出现。
 */
.orch-col-run .run-pane{flex:1;min-height:0}
.orch-col-run .run-pane > .sec-body,
.orch-col-run .run-pane > .sec-body-run{
  flex:1;min-height:0;display:flex;flex-direction:column;
  /*
   * ★ 这里必须能滚、且必须是**这一层**滚 —— 不许把滚动交给 .orch-col-run ★
   *   若让整列滚，tab 条会跟着滚走（那就不是"这一栏的标题"了，
   *   用户往下看几条版本之后会不知道自己在哪个 tab）。
   *   所以滚动归属正文容器，tab 条永远钉在顶部。
   */
  overflow:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;
  /*
   * 留白从右栏本体搬到这里：右栏已经 padding:0（见 .orch-col-run 的说明），
   * 所以滚动体的左右留白与底部兜底必须在这里补上 ——
   * 否则列表会贴着栏边、且最后一条滚到底时贴屏幕下缘。
   */
  padding:14px 16px 60px;
}

/*
 * ---------- 两栏之间的拖拽分隔条（桌面端） ----------
 *
 * 视觉取向：**静默**。它平时只是两栏之间一条极淡的分隔线（与原来的
 * border-left 视觉等重，用户不会觉得界面里突然多了一个控件）；
 * 指向它时整条线亮起并显示出「抓取」光标与更明显的对比色——
 * 与本项目的消息导航条、滚动条同一套语言：不指向时安静，指向时才亮。
 *
 * 命中区（7px）比视觉线（1px）宽得多，这是刻意的：
 * 1px 的线在触控板/鼠标上几乎抓不住，用户会觉得"这个功能时灵时不灵"。
 * 命中区宽、视觉细，是拖拽手柄的通行做法。
 */
/*
 * =====================================================================
 * 分隔条（拖拽手柄）—— 全站共用的基础样式
 * =====================================================================
 *
 * 四类边界（.side-resizer / .orch-side-resizer / .orch-set-resizer /
 * .orch-split-resizer）共用下面这套基础外观，各自只覆盖"定位方式"
 * （绝对定位在宿主右缘 vs 占一条网格轨道）。
 *
 * 为什么基础样式只写一份：分隔条的视觉必须全站一致（同一条 1px 线、
 * 同样的悬停高亮、同样的聚焦反馈）。四处各写一遍的必然结果是
 * 某一条明显长得不一样，而用户会以为"这条不能拖"。
 */
.layout-resizer,
.orch-split-resizer{
  position:relative;
  min-width:0;min-height:0;padding:0;border:0;background:transparent;
  cursor:col-resize;touch-action:none;
  /* 分隔条自身不需要滚动，去掉任何继承来的行为 */
  overflow:visible;
}
/*
 * 编排页工作流列表栏的手柄：绝对定位在宿主右缘。
 *
 * ★ 注意它与 .orch-set-resizer 的定位方式**必须不同**，理由是结构不同 ★
 *   · .orch-side 是 flex 布局里的一列（不在网格中），它的右缘没有"网格轨道"
 *     可以放东西，所以手柄绝对定位在宿主右缘。
 *   · .orch-set 是 .orch-cols 网格里的第一列，它的右缘**正好有一条 7px 轨道**。
 *     手柄应当**占住那条轨道**（作为普通网格项），而不是绝对定位浮上去：
 *     浮上去的话轨道仍然存在（7px 空白照留），而手柄压在这条空白上，
 *     两侧列的实际间距会变成 7px + 视觉错觉，拖拽命中区也错位。
 *   所以下面把 .orch-set-resizer 单独按"网格项"声明（它复用
 *   .layout-resizer 的视觉样式，只覆盖定位方式）。
 */
.orch-side-resizer{
  /*
   * 定位：贴住**工作流列表栏的右缘**，而不是容器的右缘。
   *
   * ★ 为什么用 left 而不是 right（2026-09-28 实测踩到的 bug）★
   *   手柄是 .orch-side 的**兄弟节点**（模块插在它之后），
   *   所以 `right:-3px` 参照的是整个 .orchestrator 的右缘 ——
   *   实测手柄跑到 x=1586（视口右边），左栏那条分割线根本抓不到，
   *   无头下偶然可拖、有头下必失效。
   *
   *   改成 left 之后，偏移量**就是那一栏的宽度**（同一个 CSS 变量）：
   *   手柄位置与列宽由一个来源决定，列宽一变（拖拽/读回/双击复位）
   *   手柄自动跟到位，不会出现"线在旧位置"的中间态。
   *   减去 3px 让 7px 命中区以那条 1px 边框线为中心。
   */
  position:absolute;top:0;left:calc(var(--orch-side-w,264px) - 3px);width:7px;height:100%;z-index:5;
  cursor:col-resize;background:transparent;border:0;padding:0;
  touch-action:none;
}
.orch-side-resizer:hover,.orch-side-resizer.is-dragging{
  background:color-mix(in srgb,var(--accent) 55%,transparent);
}
/*
 * 设置区的手柄：占住网格里那条 7px 轨道。
 * 视觉与命中区由 .layout-resizer 提供（同一条 1px 线、同一个 col-resize 光标），
 * 这里只声明"它是一个不参与内容排版的网格轨道内容"。
 */
.orch-set-resizer{position:relative;z-index:5}
/* 视觉线：居中在命中区里，只有 1px */
.layout-resizer::before,
.orch-split-resizer::before{
  content:'';position:absolute;top:0;bottom:0;left:50%;width:1px;
  transform:translateX(-50%);
  background:var(--line);
  transition:background-color .15s ease,width .15s ease;
}
.layout-resizer:hover::before,
.layout-resizer.is-dragging::before,
.orch-split-resizer:hover::before,
.orch-split-resizer.is-dragging::before{
  width:2px;
  background:color-mix(in srgb,var(--accent) 65%,var(--line));
}
/* 键盘可达：聚焦时也要有可见反馈（它本质上是一个可交互控件） */
.layout-resizer:focus-visible{outline:none}
.layout-resizer:focus-visible::before,
.orch-split-resizer:focus-visible{outline:none}
.orch-split-resizer:focus-visible::before{
  width:2px;background:var(--accent);
}
/* 拖拽期间：屏蔽文本选中与 iframe 抢事件，让指针移动全程被手柄接管 */
body.is-orch-split-dragging{cursor:col-resize;user-select:none}
body.is-orch-split-dragging iframe{pointer-events:none}

/* =====================================================================
 * 设置区（桌面端常驻列；窄屏时 #sec-* 搬回弹窗）
 *
 * 它是 .orch-cols 的第一列，宽度 var(--orch-set-w)（口径在 layout_widths.js）。
 * 内部结构与右栏同构：固定的表头 + 可滚动的正文。
 * 同构是有意的——两个都是"常驻的辅助面板"，用户对它们的滚动/表头行为
 * 应该有同一套预期，不该一个表头跟着滚、另一个不跟着。
 * ===================================================================== */
.orch-col-set{
  /* 与其它列同样的可滚动契约（min-width:0 防止子内容把列撑开，见 .orch-cols 说明） */
  min-width:0;min-height:0;overflow:hidden;
  display:flex;flex-direction:column;
  background:var(--bg);border-right:1px solid var(--line);
  /*
   * 定位基准：本列的拖拽手柄绝对定位在它的右缘。
   * 手柄插在本列之后（同一网格里的独立轨道），所以它不会与本列重叠。
   */
  position:relative;
}
.orch-set-head{
  flex:none;display:flex;align-items:center;gap:8px;
  padding:14px 12px 10px 16px;border-bottom:1px solid var(--line);
}
.orch-set-title{font-size:13px;font-weight:600;color:var(--ink-strong)}
.orch-set-sub{flex:1;min-width:0;font-size:11.5px;color:var(--ink-faint);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/*
 * 「收起某一段」的小图标按钮（设置区、左侧边栏…）。
 *
 * ★ 为什么是一个共用类而不是每段各写一份 ★
 *   设置区的 #orchSetHide 与左栏的 #orchSideHide 是**同一种交互**
 *   （把这一段收起来，把宽度让回编排区），只是收的对象不同。
 *   各写一份的失败模式是：改了其中一处的手感（尺寸/hover/边框），
 *   另一处还是老的，用户在同一个页面上遇到两种"收起"按钮。
 *   同一件事只有一条规则 —— 收哪一段由 id 决定，长什么样由这里决定。
 */
.orch-sec-hide{
  flex:none;width:24px;height:24px;display:grid;place-items:center;
  border:1px solid transparent;border-radius:6px;background:transparent;
  color:var(--ink-faint);font-size:15px;line-height:1;
}
.orch-sec-hide:hover{border-color:var(--line);background:var(--panel);color:var(--ink)}
/*
 * 正文：可滚动，且**同一时刻只有一个滚动条归属它**。
 * min-height:0 是必需的——flex 子项的默认 min-height:auto 会按内容撑高，
 * 让这个容器永远不出现内部滚动条，而是把整列顶出视口。
 */
.orch-set-body{
  flex:1;min-height:0;overflow:auto;-webkit-overflow-scrolling:touch;
  padding:14px 14px 60px;display:flex;flex-direction:column;gap:22px;
}
/*
 * 设置区里的 section 收敛间距与内边距：
 * 弹窗里它们靠 .modal-body 的节奏排布，搬进窄列后那套留白显得松散，
 * 且"标题 + 说明"两行会占掉过多高度。
 */
.orch-set-body .sec{gap:10px}
.orch-set-body .sec-head{display:flex;flex-direction:column;gap:3px}
.orch-set-body .sec-hint{white-space:normal;line-height:1.55}
/*
 * 工具行在窄列里必须能换行：工具目录是两列网格（名称 + 右侧复选框），
 * 列宽不足时会溢出——而 .orch-set-body 的 overflow:auto 会给出一个
 * **横向滚动条**，看起来像布局坏了。这里让右侧控件换到下一行。
 */
.orch-set-body .tool-catalog{--tool-cols:1}
/*
 * 设置区折叠时由 html[data-orch-settings-collapsed="1"] .orch-cols 负责隐藏（见 .orch-cols 段落）。
 * 这里只处理"折叠后如何再打开"：折叠后设置区整列不可见，
 * 顶栏按钮成为唯一的入口（见 .orch-settings-btn 的 is-off 态）。
 */

/*
 * 窄屏下的分隔条必须消失（规则写在下面的 @media (max-width:1080px) 段里，
 * 与右栏退出布局同处）——它们是同一件事：没有两栏，就没有两栏之间的边界。
 */

/*
 * 快照 pane 与版本 pane 同构：flex:1 吃掉整栏剩余高度。
 * （.run-pane 的通用规则在 .orch-col-run 那段，这里只补它自己的缺口。）
 */
.sec-run{flex:1;min-height:0;display:flex;flex-direction:column}
.sec-body-run{flex:1;min-height:0;display:flex;flex-direction:column;gap:10px}

/*
 * ★ 右栏本身不得滚动（与 .orch-col 的 overflow:auto 相反）★
 *
 *   .orch-col 是通用列样式，带 overflow:auto。对左栏（编排区）来说那是对的：
 *   它就是一整条可滚动的消息流。
 *   但右栏现在是「顶部一条固定的 tab 条 + 下面一个可滚动的 pane」：
 *     · 若整列可滚，tab 条会跟着滚走 —— 用户往下看几条版本之后，
 *       既不知道自己在哪个 tab，也够不到收起按钮；
 *     · 而且"整列可滚 + pane 也可滚"会产生嵌套滚动容器，
 *       滚轮到底时该滚哪一个由浏览器猜，手感是"有时滚不动"。
 *   所以右栏的滚动归属**只有 pane 的正文**（见 .orch-col-run .run-pane > .sec-body）。
 *   把 overflow 设回 hidden 是结构性的：它把"滚动归谁"这件事钉死成唯一答案。
 */
.orch-col-run{overflow:hidden}
/*
 * 快照面板的包裹层（与 .ver-panel-main 完全同构，理由见版本面板那段注释）：
 * 它是移动端抽屉搬移同一份 DOM 的节点，桌面态也必须自己撑起高度链，
 * 否则 .snap-list 的 overflow:auto 同样会失效。
 */
.snap-panel-main{display:flex;flex-direction:column;flex:1 1 auto;min-height:0}

/* =====================================================================
 * 右栏「快照」列表
 *
 * 这是工作流页右栏现在的本职：记录「每次测试跑完时，当时看到的是什么」。
 * 它取代了历史上的「运行调试 · 聊天流」——那个视图与左栏「测试」的就地显示
 * 职责重复，而且刷新就没了（用户想留着待会儿对比时无从下手）。
 *
 * 排版取向：一行一条、信息密度高、扫一眼就能分清「哪次、成没成、几条结果」。
 * 时间与状态是最常被用来定位条目的两个字段，因此排在名字正下方。
 * ===================================================================== */
.snap-list{
  flex:1 1 auto;min-height:0;overflow:auto;
  display:flex;flex-direction:column;gap:8px;
  padding:2px 2px 16px;
}
/* 条目不被压缩：滚动的是列表内容，不是条目本身（与 .ver-row 同一条不变量） */
.snap-row{flex:none}
.snap-empty{
  margin:auto;text-align:center;color:var(--ink-faint);font-size:12.5px;line-height:1.9;
  padding:20px;
}
.snap-empty b{display:block;color:var(--ink-soft);font-size:13.5px;margin-bottom:6px;font-weight:600}

/* 一条快照：[主体(名字/来源/状态/时间)] [删除] */
.snap-row{
  display:flex;align-items:stretch;gap:0;
  border:1px solid var(--line-soft);border-radius:9px;background:var(--panel);
  overflow:hidden;
}
.snap-row:hover{border-color:var(--line-strong)}
.snap-main{
  flex:1;min-width:0;text-align:left;cursor:pointer;font-family:inherit;
  border:0;background:transparent;color:var(--ink);
  padding:9px 10px;display:flex;flex-direction:column;gap:5px;
}
.snap-main:hover{background:var(--panel-2)}
.snap-top{display:flex;align-items:center;gap:7px;min-width:0}
.snap-name{
  font-size:12.5px;font-weight:600;color:var(--ink-strong);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;
}
.snap-kind{
  flex:none;font-size:10px;line-height:1;padding:3px 6px;border-radius:5px;
  border:1px solid var(--line);color:var(--ink-faint);background:var(--bg);
}
.snap-kind.is-auto{color:var(--accent);border-color:color-mix(in srgb,var(--accent) 40%,transparent);background:color-mix(in srgb,var(--accent) 10%,transparent)}
.snap-kind.is-timestamp{color:#2f7d58;border-color:color-mix(in srgb,#2f7d58 40%,transparent);background:color-mix(in srgb,#3fa87a 12%,transparent)}
.snap-kind.is-manual{color:#a9631a;border-color:color-mix(in srgb,#a9631a 40%,transparent);background:color-mix(in srgb,#f0a63c 12%,transparent)}
.snap-meta{display:flex;align-items:center;gap:9px;font-size:10.5px;color:var(--ink-faint);flex-wrap:wrap}
.snap-status{
  font-size:10px;line-height:1;padding:3px 6px;border-radius:5px;font-weight:600;
}
.snap-status.is-ok{color:var(--ok,#3f9c5a);background:color-mix(in srgb,var(--ok,#3f9c5a) 14%,transparent)}
.snap-status.is-error{color:var(--danger);background:color-mix(in srgb,var(--danger) 14%,transparent)}
.snap-status.is-aborted{color:#a9631a;background:color-mix(in srgb,#f0a63c 16%,transparent)}
.snap-status.is-idle{color:var(--ink-faint);background:var(--panel-2)}
.snap-time{font-variant-numeric:tabular-nums}
.snap-del{
  flex:none;border:0;border-left:1px solid var(--line-soft);background:transparent;
  color:var(--ink-faint);font-size:11.5px;padding:0 11px;cursor:pointer;font-family:inherit;
}
.snap-del:hover{color:var(--danger);background:color-mix(in srgb,var(--danger) 10%,transparent)}

/* ---------- 快照详情弹窗 ---------- */
#snapDetailBody{display:flex;flex-direction:column;gap:16px}
.snap-detail-head{display:flex;align-items:center;gap:9px;font-size:11.5px;color:var(--ink-faint);flex-wrap:wrap}
.snap-sec{display:flex;flex-direction:column;gap:8px}
.snap-sec-title{
  font-size:12px;font-weight:600;color:var(--ink-soft);
  border-bottom:1px solid var(--line-soft);padding-bottom:6px;
}
.snap-results{display:flex;flex-direction:column;gap:9px}
.snap-res{border:1px solid var(--line-soft);border-radius:8px;background:var(--panel)}
.snap-res-head{
  display:flex;align-items:center;gap:8px;padding:7px 10px;
  border-bottom:1px solid var(--line-soft);
}
.snap-res-n{font-size:11.5px;font-weight:600;color:var(--ink-soft)}
.snap-res-st{font-size:10px;line-height:1;padding:3px 6px;border-radius:5px;background:var(--panel-2);color:var(--ink-faint)}
.snap-res-st.is-done{color:var(--ok,#3f9c5a)}
.snap-res-st.is-error{color:var(--danger);background:color-mix(in srgb,var(--danger) 12%,transparent)}
.snap-res-st.is-running{color:var(--accent)}
.snap-res-body{
  padding:9px 10px;font-size:12px;line-height:1.75;color:var(--ink);
  white-space:pre-wrap;overflow-wrap:anywhere;max-height:var(--orch-msg-max-h);overflow:auto;
}
.snap-res-body.is-error{color:var(--danger)}
.snap-vars{display:flex;flex-direction:column;gap:5px}
.snap-var{display:flex;gap:9px;font-size:12px;align-items:baseline}
.snap-var-k{flex:none;min-width:110px;font-weight:600;color:var(--ink-soft);overflow-wrap:anywhere}
.snap-var-v{color:var(--ink);overflow-wrap:anywhere;white-space:pre-wrap}
.snap-def{
  border:1px solid var(--line-soft);border-radius:8px;background:var(--panel);padding:8px 10px;
}
.snap-def summary{cursor:pointer;font-size:12px;font-weight:600;color:var(--ink-soft)}
.snap-def-pre{
  margin:9px 0 0;max-height:340px;overflow:auto;font-size:11.5px;line-height:1.6;
  background:var(--code,var(--panel-2));padding:9px;border-radius:7px;
  white-space:pre-wrap;overflow-wrap:anywhere;
}
.sec-body-run .orch-output{flex:1;min-height:120px;max-height:none;margin-top:0}
#runNote{font-size:12px;color:var(--ink-faint);line-height:1.6;min-height:0}
#runNote:empty{display:none}
.wf-steps{overflow:auto;max-height:46%;flex:none}

/* ---------- 工作流绑定的模型选择（编排运行工具条） ----------
 *
 * 位置：编排区独立运行工具条内；只写当前工作流的模型绑定。
 * 这一段样式只描述"它是这一行里的一个紧凑控件"，不描述它在哪——
 * 位置是结构的事，归 HTML；外观归 CSS。两者混在一起就会重演
 * "想换个位置却要连带改外观"的耦合。
 *
 * 尺寸按动作区口径（与 .orch-btn.small 同高）：它现在和「清空结果 / 测试」
 * 并排，高矮不一会让整行看起来是拼起来的。
 */
.wf-model{
  display:flex;align-items:center;gap:6px;flex:none;
  font-size:11.5px;color:var(--ink-faint);white-space:nowrap;
}
.wf-model select{
  font-size:12px;padding:5px 8px;border-radius:8px;max-width:190px;
  border:1px solid var(--line);background:var(--input);color:var(--ink);
  outline:none;cursor:pointer;font-family:inherit;
}
.wf-model select:focus{border-color:var(--accent)}
.wf-model select:disabled{opacity:.45;cursor:not-allowed}
/*
 * 「还没选模型」的状态：控件本身要显眼，因为它是**运行的前提**。
 * 不在这里做任何"自动替用户选一个"的兜底——那正是被删掉的静默回落。
 */
.wf-model.is-unset select{border-color:var(--warn,var(--danger));color:var(--danger)}
/*
 * 「绑定的模型已下线」：如实显示成红框，并在下拉里补一个 `X（已下线）` 选项，
 * 与"没选"是两种不同的状态，因此各有各的类名，不共用一种颜色暗示。
 */
.wf-model.is-unknown select{border-color:var(--danger);color:var(--danger)}

/*
 * 窄屏（≤1080）：编排页退回**单列**，设置区与右栏都退出布局。
 *
 * ★ 设置区在这里的行为是"整列消失"，而不是"变窄的列"★
 *   窄屏的宽度本来就紧张，再切出一列设置区会让编排区只剩几百像素。
 *   设置内容改由**弹窗**承载——而且它承载的是**同一批 #sec-* 节点**
 *   （由 orchestrator.js 的 syncSettingsHome 搬移），不是另做一份窄屏表单。
 *   这也解释了为什么 .orch-col-set 的隐藏与 .orch-set-resizer 的隐藏
 *   必须写在一起：没有这一列，就没有它右缘的边界。
 *
 * 左侧工作流列表栏的宽度变量在窄屏下同样失效（下面的 .orch-side 规则
 * 用固定宽度接管）；layout-resize.js 在窄屏会移除这些变量与手柄，
 * 两处口径一致（断点值 1080 与 layout-resize.js 的 desktopQuery 对应）。
 */
@media (max-width:1080px){
  .orch-cols{grid-template-columns:1fr;grid-template-rows:auto minmax(0,1fr)}
  .orch-col-run{border-left:0;border-top:1px solid var(--line);order:-1}
  .orch-col-run > *{max-width:760px}
  .orch-col{padding:18px 12px 60px;gap:22px}
  .sec-body-run .orch-output{min-height:140px}
  .wf-steps{max-height:none}
  /*
   * 设置区整列 + 它右缘的手柄一起消失；
   * 工作流列表栏右缘的手柄同理（窄屏下它是全宽的一列，没有"两栏"可分配）。
   */
  .orch-col-set,.orch-set-resizer,.orch-side-resizer{display:none}
}

/* =====================================================================
 * 账号信息块（▦）与账号信息弹窗
 *
 * 视觉上与另外四种块都不撞：
 *   ▣ 临时会话（实心主色）、▤ AI结果（虚线中性）、⬒ 工作流、▽ 变量（点线绿）；
 *   ▦ 账号信息用**琥珀色实线**——它是「一段被带进来的外部数据」，
 *   既不是要执行的动作，也不是引用本流水线里的回答。
 * ===================================================================== */
.accchip{
  display:inline-flex;align-items:center;gap:3px;
  vertical-align:baseline;
  margin:0 1px;padding:0 5px;
  border:1px solid color-mix(in srgb, #c98a1f 55%, var(--line));
  border-radius:5px;
  background:color-mix(in srgb, #c98a1f 12%, transparent);
  color:color-mix(in srgb, #c98a1f 88%, var(--ink));
  font-size:11.5px;font-weight:600;line-height:17px;
  cursor:pointer;user-select:none;max-width:260px;
  transition:background 120ms ease,border-color 120ms ease;
}
.accchip:hover{background:color-mix(in srgb, #c98a1f 22%, transparent);border-color:#c98a1f}
.accchip-icon{font-size:10px;line-height:1}
.accchip-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:150px}
.accchip-state{
  font-size:9.5px;font-weight:600;opacity:.72;letter-spacing:.02em;
  border-left:1px solid color-mix(in srgb, #c98a1f 40%, transparent);
  padding-left:4px;margin-left:1px;
}

/*
 * 运行区里的「纯内联」块：账号信息 ▦ / 文件 ▧。
 *
 * 为什么单独一个类而不是复用 .accchip：
 * 这是**只读展示**——运行区里它不可点、没有状态，只回答"这里内联了什么"。
 * 编排区的 .accchip 是可编辑方块（带 hover、点击打开弹窗），
 * 两者语义不同，共用一个类会让"运行区能不能点"变成看运气。
 * 用弱化的边框/底色与容器标记 ▣ 区分开视觉权重。
 */
.bkflowchip{
  display:inline-flex;align-items:center;gap:3px;
  vertical-align:baseline;
  margin:0 1px;padding:0 5px;
  border:1px solid color-mix(in srgb, #c98a1f 40%, var(--line));
  border-radius:5px;
  background:color-mix(in srgb, #c98a1f 8%, transparent);
  color:color-mix(in srgb, #c98a1f 80%, var(--ink));
  font-size:11.5px;font-weight:600;line-height:17px;
  user-select:none;max-width:220px;
}
.bkflowchip-icon{font-size:10px;line-height:1}
.bkflowchip-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* AI 消息的固定内容编辑器 */
.t-ai-fixed-wrap{
  border:1px dashed var(--line-strong, var(--line));
  border-radius:8px;padding:7px 9px 6px;
  background:var(--panel-2);
}
.t-ai-fixed-wrap.has-fixed{
  border-style:solid;
  border-color:var(--line);
  background:var(--panel-2);
}
.t-input.t-ai-fixed{
  min-height:46px;border:0;background:transparent;padding:0;
  font-size:12.5px;line-height:1.75;outline:none;
  white-space:pre-wrap;overflow-wrap:anywhere;
}
.t-ai-fixed-hint{
  margin-top:5px;font-size:10.8px;color:var(--ink-faint);line-height:1.5;
}
.t-ai-fixed-wrap.has-fixed .t-ai-fixed-hint{color:color-mix(in srgb, var(--accent) 75%, var(--ink-faint))}
/* 账号信息弹窗里的内容选择 */
.accm-account{min-height:40px}
.accm-files{display:flex;flex-direction:column;gap:10px;max-height:320px;overflow:auto;padding-right:2px}
.accm-group{border:1px solid var(--line-soft);border-radius:8px;padding:8px 10px}
.accm-group-head{display:flex;align-items:center;gap:10px;justify-content:space-between}
.accm-group-title{font-size:12px;font-weight:600;color:var(--ink)}
.accm-count{font-size:11px;color:var(--ink-faint);font-variant-numeric:tabular-nums}
.accm-note{font-size:11px;color:var(--ink-faint);margin:4px 0 6px;line-height:1.5}
.accm-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:4px 12px}
.accm-item{display:flex;align-items:flex-start;gap:6px;font-size:12px;color:var(--ink-soft);
  padding:3px 4px;border-radius:6px;cursor:pointer;line-height:1.5}
.accm-item:hover{background:color-mix(in srgb, var(--accent) 7%, transparent)}
.accm-item.is-missing{color:var(--ink-faint)}
.accm-item.is-missing::after{content:'';}
.accm-item input{margin-top:2px}

/* AI 固定内容可能很长（可以内联整份账号信息），给一个上限避免把编排页撑爆。
 * 高度数字引用 --orch-msg-max-h（唯一权威在 orch-msg-height-init.js），
 * 与用户消息的折叠高度是同一个数字 —— 两者描述同一件事：编排页里一块长正文能看多高。 */
.t-input.t-ai-fixed{max-height:var(--orch-msg-max-h);overflow:auto}
/*
 * =====================================================================
 * AI 消息的「运行结果」区（与上面的输入框是两个独立载体）
 *
 * 分工（结构与语义一致，不要合并）：
 *   .t-ai-fixed-wrap = 输入：用户手写的固定内容，决定运行时是否跳过模型；
 *   .t-ai-result-wrap= 输出：运行产出的回答（含工具调用），只读、只由运行事件写入。
 *
 * 视觉上必须一眼可分（实线框 + 有标签 + 只读底色），否则用户分不清
 * 「这是我写的」还是「这是跑出来的」——那正是本次要修的那类混淆。
 * ===================================================================== */
.t-ai-result-wrap{
  margin-top:7px;
  border:1px solid var(--line-soft);
  border-radius:8px;padding:7px 9px 6px;
  background:color-mix(in srgb, var(--ink-faint) 3%, transparent);
}
.t-ai-result-label{
  display:flex;align-items:center;justify-content:space-between;gap:8px;
  font-size:10.5px;color:var(--ink-faint);margin-bottom:4px;letter-spacing:.3px;
}
.t-ai-result-copy{
  border:1px solid var(--line-soft);border-radius:5px;padding:2px 7px;
  background:transparent;color:var(--ink-faint);font:inherit;cursor:pointer;
}
.t-ai-result-copy:hover{border-color:var(--line-strong);background:var(--panel);color:var(--ink)}
.t-ai-result-copy.done{border-color:var(--accent);color:var(--accent)}
/*
 * 结果区：**默认消息渲染**，不是编辑框。
 *
 * 用户明确要求（2026-09-24）：默认就按对话消息元素渲染（正文段落 + 工具调用），
 * 只有双击才进入编辑模式。因此这里的两条规则分别描述两种形态，
 * 而"现在是哪一种"只由一个属性决定（data-result-mode，由 JS 的
 * setResultMode 唯一写入）——样式不自己猜、也不另记一份状态。
 *
 *   data-result-mode="view"（默认）：显示 .msg-flow，隐藏编辑元素
 *   data-result-mode="edit"        ：显示编辑元素，隐藏元素流
 */
.t-ai-result{
  font-size:12.5px;line-height:1.75;color:var(--ink);
  border-radius:6px;cursor:text;transition:background .12s ease, box-shadow .12s ease;
}
/*
 * 展示态：长正文与长工具参数各自滚动。
 *
 * ★ 高度上限**默认关闭**（用户 2026-09-24 明确要求："默认情况下运行结果不设置
 *   高度上限，这样我看得清楚些，但是可以设置一个开关来恢复限制"）★
 *
 * 因此这里引用的不再是"统一的 320px 上限"，而是**本块专属**的变量
 * --orch-result-max-height：它的值只有两种，'none' 或 var(--orch-msg-max-h)，
 * 由 public/orch-result-height-init.js（唯一权威）在启动时与开关切换时写入。
 *
 * 为什么不直接用 --orch-msg-max-h：那个变量的语义是"编排页里一块长正文的
 * 统一上限"，它同时还约束着用户消息与固定内容框；把它改成 none 会连带把
 * 那两处也展开——那是另一个决定，用户没有做，也不该由这一处替他做。
 * 结果区有自己的默认值，就必须有自己的变量；数字本身仍然只有一份
 *（var(--orch-msg-max-h)），不存在第二个 320。
 *
 * scrollbar-gutter 仍然保留：限制开启时会出滚动条，预留槽位才能保证
 * 开关两态下正文换行位置一致（同一类实测结论，见用户消息那条规则的注释）。
 */
.t-ai-result .msg-flow{
  max-height:var(--orch-result-max-height);
  overflow:auto;
  scrollbar-gutter:stable;
  overscroll-behavior:contain;
}
.t-ai-result:hover{background:color-mix(in srgb, var(--accent) 4%, transparent);}
/*
 * 没跑过时结果区不占地方：留一行说明，避免出现一个莫名的空框。
 * 判据是"元素流里一个元素都没有"——运行中还没有任何内容时同样适用。
 */
.t-ai-result .msg-flow:empty::before{
  content:'还没有运行结果';
  color:var(--ink-faint);font-size:11.5px;
}
/* 编辑态与展示态必须受**同一个**变量约束：同一块内容的两种形态，
 * 开关一开一关的高度表现当然要一致（引 --orch-result-max-height，见上）。 */
.t-ai-result-edit{
  white-space:pre-wrap;overflow-wrap:anywhere;
  max-height:var(--orch-result-max-height);overflow:auto;
  scrollbar-gutter:stable;
}
/*
 * 形态切换（唯一判据是 data-result-mode）：
 * 非编辑态下编辑元素不参与布局，编辑态下元素流整块让位。
 * 用 display 而不是 visibility：编辑元素是 contenteditable，
 * 留在布局里会让 Tab 键聚焦到一个看不见的输入框上。
 */
.t-ai-result[data-result-mode="view"] .t-ai-result-edit{display:none}
.t-ai-result[data-result-mode="edit"] .msg-flow{display:none}
/*
 * 编辑态的样子：实线主色边框 + 内衬，让它明确"我现在可以打字"。
 * 这条规则挂在结果区自己身上（而不是编辑元素上），因为"在编辑"这件事
 * 描述的是整个结果区此刻的形态。
 */
.t-ai-result.is-editing{
  background:var(--bg);padding:4px 6px;
  box-shadow:0 0 0 1.5px color-mix(in srgb, var(--accent) 60%, transparent);
}
/* 提示语：可编辑但不被发现 = 不可用。默认形态下用一行小字说明"双击可改" */
.t-ai-result[data-result-mode="view"]::after{
  content:'双击可修改这条结果';
  display:block;font-size:10.5px;color:var(--ink-faint);margin-top:4px;
  opacity:0;transition:opacity .12s ease;
}
.t-ai-result:hover[data-result-mode="view"]::after{opacity:1}
/*
 * 运行状态用边框说明（挂在 .t-ai-result-wrap 上，由 setRunState 设置）：
 *   running：正在产出（实线主色）
 *   error  ：这一步跑失败了（危险色）
 *   done   ：已定稿（默认外观即可，不再另写样式——同一状态只描述一次）
 */
.t-ai-result-wrap[data-run-state="running"]{
  border-color:color-mix(in srgb, var(--accent) 55%, var(--line));
  background:color-mix(in srgb, var(--accent) 4%, transparent);
}
.t-ai-result-wrap[data-run-state="error"]{
  border-color:var(--danger);
  background:color-mix(in srgb, var(--danger) 6%, transparent);
}
.t-ai-result-error{
  margin-top:8px;padding:7px 9px;border:1px solid color-mix(in srgb,var(--danger) 50%,var(--line));
  border-radius:6px;color:var(--danger);background:color-mix(in srgb,var(--danger) 9%,transparent);
  font-size:11px;line-height:1.45;white-space:pre-wrap;
}
/*
 * 编排页结果区里的工具调用元素：与聊天页的 .tool-call **同一套实现**。
 *
 * 为什么不再单独一套 .rmsg-tool：结果区现在按对话消息元素渲染，
 * 里面的工具卡片就是"一次工具调用"这种东西在本宿主的呈现。
 * 另写一套的代价已经在历史上付过一次（同一个工具在两个页面长得不一样，
 * 描述还常驻版面撑高行高）。所以这里只做一件事——把 .msg-flow 在本页
 * 需要的尺寸补上（编排页字号更小、卡片更紧凑），结构仍由 app.js 产出。
 */
.t-ai-result .msg-flow .md{font-size:12.5px}
.t-ai-tools .msg-flow{max-height:none}

/* =====================================================================
 * 文件块（▧）
 *   与另外五种块都不撞：▣ 临时会话、▤ AI结果、▦ 账号信息、⬒ 工作流、▽ 变量。
 *   蓝色实线——「带进来的外部正文」，与琥珀色的账号信息同属「外部数据」但来源不同。
 * ===================================================================== */
.filechip{
  display:inline-flex;align-items:center;gap:3px;
  vertical-align:baseline;
  margin:0 1px;padding:0 5px;
  border:1px solid color-mix(in srgb, #3b6ea5 55%, var(--line));
  border-radius:5px;
  background:color-mix(in srgb, #3b6ea5 11%, transparent);
  color:color-mix(in srgb, #3b6ea5 88%, var(--ink));
  font-size:11.5px;font-weight:600;line-height:17px;
  cursor:pointer;user-select:none;max-width:260px;
  transition:background 120ms ease,border-color 120ms ease;
}
.filechip:hover{background:color-mix(in srgb, #3b6ea5 20%, transparent);border-color:#3b6ea5}
.filechip-icon{font-size:10px;line-height:1}
.filechip-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:200px}
/*
 * 目录提示词芯片（▤）。
 * 与三种既有块同构（同样的字号/圆角/内边距），只换色系：
 * 紫色系与「文件」的蓝、「账号信息」的琥珀、「临时会话」的主色都不同，
 * 一条消息里同时出现几种块时能靠颜色一眼区分。
 */
.promptchip{
  display:inline-flex;align-items:center;gap:3px;
  vertical-align:baseline;
  margin:0 1px;padding:0 5px;
  border:1px solid color-mix(in srgb, #7a5ea8 55%, var(--line));
  border-radius:5px;
  background:color-mix(in srgb, #7a5ea8 12%, transparent);
  color:color-mix(in srgb, #7a5ea8 85%, var(--ink));
  font-size:11.5px;font-weight:600;line-height:17px;
  cursor:pointer;user-select:none;max-width:260px;
  transition:background 120ms ease,border-color 120ms ease;
}
.promptchip:hover{background:color-mix(in srgb, #7a5ea8 20%, transparent);border-color:#7a5ea8}
.promptchip-icon{font-size:10px;line-height:1}
.promptchip-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:200px}
.promptchip-state{font-weight:400;opacity:.75}

/* 目录提示词弹窗：目录行 / 模板 / 预览三段式 */
.pmodal-row{display:flex;gap:8px;align-items:center}
.pmodal-row select{
  flex:1;min-width:0;font-size:13px;padding:7px 10px;border:1px solid var(--line);
  border-radius:8px;background:var(--input);color:var(--ink);outline:none;
}
.pmodal-tpl{
  width:100%;box-sizing:border-box;min-height:120px;resize:vertical;
  border:1px solid var(--line);border-radius:8px;padding:9px 11px;
  font:12.5px/1.7 var(--mono);background:var(--input);color:var(--ink);outline:none;
}
.pmodal-tpl:focus{border-color:var(--accent)}
.pmodal-head{display:flex;align-items:baseline;gap:6px}
.pmodal-meta{font-size:11.5px;font-weight:400;color:var(--ink-faint)}
.pmodal-preview{
  max-height:240px;overflow:auto;margin:0;
  border:1px solid var(--line);border-radius:8px;padding:9px 11px;
  background:var(--panel-2);color:var(--ink-soft);
  font:11.5px/1.6 var(--mono);white-space:pre-wrap;overflow-wrap:anywhere;
}

/*
 * 状态提示：出流的浮层，永不参与消息区几何。
 *
 * 结构性事故（2026-09-22 用户截图报告「出现和消失都会让消息区域变抖」）
 * ----------------------------------------------------------------------------
 * 此前它是 <main> 列布局里的一个 flex:none 流内块，靠 min-height:38px 撑出一条
 * 38px 高的带子，空态再 display:none 收起。于是**每一次显隐**（一次发送至少三次：
 * 「发送中…」→「已提交…」→「完成」淡出清空）都把下面的 #chat（flex:1）整体
 * 上下平移 38~44px —— 实测像素证据：空态 chatTop=178，显示态 chatTop=222。
 *
 * 根因不是"文案不合适"，而是**位置与职责错配**：一条瞬时的、旁路性质的反馈，
 * 被放进了承载主内容几何的文档流里，于是它的存在与否改变了主内容的盒子。
 * 只删掉某一句话是打补丁 —— 下一条 setStatus 照样抖。
 *
 * 修法：把状态提示移出文档流（position:absolute，相对已 position:relative 的
 * <main>），它无论显示还是隐藏都不再改变 #chat / .composer 的任何盒子。
 * 这与本仓库既有结论同源：
 *   · .msg-nav-host —— 「absolute 出流 → 不改变 #chat 与 .composer 的任何盒子」；
 *   · .warn-banner  —— 顶部警示用 fixed，显示/隐藏不影响布局；
 *   · .scroll-bottom-btn —— 出流是刻意的，显隐不能让输入框或消息区动一下。
 * 既然这三处都因同一条理由出流，状态提示没有理由继续留在流里。
 *
 * 想当然的替代方案（都只是遮掩，不采用）：
 *   · 保留流内但"少抖两次"（节流/防抖/合并提示）—— 位移只是变少变慢，没消除；
 *   · visibility:hidden / opacity:0 占位 —— 透明但仍占 38px，等于常驻空条；
 *   · 固定 38px 常驻高度 —— 又回到用户当初否掉的"空占一行"。
 *
 * 锚点几何：贴在 <main> 顶部、按内容列右缘对齐，浮在消息区之上。
 * top 用一个既定的安全值（header 高 56px / 移动端 48px，见下方媒体查询里的覆盖），
 * 不依赖 JS 现场测量 —— 提示条本身不需要跟随任何动态高度，跟着走反而引入新的耦合。
 *
 * 指针：默认不吃事件（pointer-events:none），避免一条浮层挡住消息区的点击/选中；
 * 只有真的显示出内容时才打开指针事件（见下一条规则），让长错误文本可被选中复制。
 *
 * 属性名带 status- 前缀而不是直接叫 data-<空>：后者在本文件被明令禁止
 *（见 block_commit.contract.test.js——它曾是输入框占位符的驱动状态，
 * 同名会让那套已删除的机制看起来还活着）。
 */
.toolbar-status{
  position:absolute;z-index:15;
  top:calc(56px + 10px);
  right:max(20px,calc((100% - var(--content-col))/2));
  max-width:min(var(--content-col),calc(100% - 40px));
  width:max-content;
  margin:0;padding:8px 12px;
  border:1px solid var(--line-strong);border-bottom:1px solid var(--line-strong);
  border-radius:10px;background:var(--panel);
  box-shadow:0 6px 20px rgba(0,0,0,.14);
  pointer-events:none;
}
.toolbar-status[data-status-empty="false"]{pointer-events:auto}
.toolbar-status[data-status-empty="true"]{display:none}

/* 弹窗里的文件选择器：菜单直接在文档流里展开，不做绝对定位浮层 */
#filePicker .fsel-menu{position:static;margin-top:6px;max-height:320px}
#filePicker .fsel-input{width:100%;box-sizing:border-box}

/* =====================================================================
 * 版本与草稿（工作流编排）
 *
 * 这一组样式的共同约束：**状态必须一眼可辨**。
 * 用户在这个页面上要做的最关键的判断是「我刚才改的东西到底存下来没有」，
 * 所以徽章、预览横幅、版本行的类型标签都要有明确且互不混淆的视觉语言：
 *   绿＝与版本一致   琥珀＝已修改（草稿已落库）   红＝还没落库
 *   蓝＝只读预览     灰＝已删除（回收站）
 * ===================================================================== */

/* ---------- 草稿徽章 ---------- */
.draft-badge{
  flex:none;font-size:11.5px;line-height:1;padding:5px 9px;border-radius:999px;
  border:1px solid transparent;white-space:nowrap;max-width:230px;overflow:hidden;text-overflow:ellipsis;
}
.draft-badge:empty{display:none}
.draft-badge.is-clean{color:var(--ink-faint);border-color:var(--line);background:var(--input)}
.draft-badge.is-dirty{
  color:#a9631a;border-color:color-mix(in srgb,#a9631a 40%,transparent);
  background:color-mix(in srgb,#f0a63c 16%,transparent);
}
.draft-badge.is-unsaved{
  color:var(--danger);border-color:color-mix(in srgb,var(--danger) 45%,transparent);
  background:color-mix(in srgb,var(--danger) 12%,transparent);
}
.draft-badge.is-preview{
  color:var(--accent);border-color:color-mix(in srgb,var(--accent) 45%,transparent);
  background:color-mix(in srgb,var(--accent) 12%,transparent);
}

/* ---------- 只读预览横幅 ---------- */
.preview-banner{
  border:1px solid color-mix(in srgb,var(--accent) 45%,var(--line));
  background:color-mix(in srgb,var(--accent) 8%,var(--panel));
  border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;
}
.preview-banner.hidden{display:none}
.preview-banner-main{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
.preview-tag{
  font-size:11px;padding:3px 8px;border-radius:999px;font-weight:600;
  background:var(--accent);color:var(--accent-ink);
}
.preview-name{font-weight:600;color:var(--ink-strong);font-size:13.5px;word-break:break-all}
.preview-meta{font-size:12px;color:var(--ink-faint)}
.preview-diff{font-size:12.5px;color:var(--ink-soft);line-height:1.6}
.preview-banner-actions{display:flex;gap:8px;flex-wrap:wrap}

/* 预览态下被改动的区块标一条边：区块级提示，不做逐字高亮（理由见 JS 注释） */
.sec.is-diff{position:relative}
.sec.is-diff::before{
  content:'';position:absolute;left:-11px;top:6px;bottom:6px;width:3px;border-radius:2px;
  background:color-mix(in srgb,var(--accent) 75%,transparent);
}

/* ---------- 版本历史面板 ---------- */
/*
 * 右栏面板的高度链（**唯一**权威，改任何一环都要顺着读一遍）：
 *
 *   .orch-col-run（列，flex）
 *     ├ .orch-run-head    flex:none（tab 条，永远钉在顶部，不参与滚动）
 *     └ .run-pane         flex:1 + min-height:0（被选中的那块吃掉整栏剩余高度）
 *         └ .sec-body     flex 列 + min-height:0（允许被压缩，否则撑破父级）
 *             └ .ver-panel-main   ← 移动端抽屉的搬移节点，必须是 flex 列且 min-height:0
 *                 ├ .ver-toolbar  flex:none（筛选与搜索，永远按内容占高）
 *                 └ .ver-list     flex:1 + min-height:0 + overflow:auto（**唯一滚动体**）
 *                     └ .ver-row  flex:none（条目不被压缩，滚动的是列表内容）
 *
 * 为什么必须逐环写清楚：`.ver-panel-main` / `.snap-panel-main` 是为移动端抽屉
 * 「搬移同一份 DOM」而加的内层包裹。它们原先**只在 .sheet-pane 作用域里有规则**，
 * 桌面态完全没有任何规则 —— 于是高度链在这里断掉：flex 的 min-height:0 传不下去，
 * `.ver-list` 的 overflow:auto 失效（没有可约束的高度），列表以内容高度（实测 1606px）
 * 溢出 381px 的父容器，直接盖住下方的快照面板。表现为「点历史版本后布局混乱」。
 *
 * 反向也要防住：如果只给包裹层加 min-height:0 而不给 .ver-row 加 flex:none，
 * 被压缩的就会变成**条目本身**（实测每行被挤到 13px、名字高度归零）。
 * 正确语义是：滚动容器压缩的是**滚动内容**，不是内容里的每一项。
 *
 * ★ 2026-10-01：max-height:44% 已删除（这是本次改造的关键一处）★
 *   那条上限是「两块上下堆叠」时代的产物：版本上限 44%、快照吃掉剩下的，
 *   两块各得一点高度、都不至于把对方挤没。
 *   换上 tab 之后它的含义完全变了 —— 它变成"即使只有版本这一块，
 *   也不许它长过右栏的 44%"，于是下面永远空着一大半（实测：
 *   右栏 844px 高，版本列表只有 162px）。用户要的正是"其中一个高度就很大"，
 *   而这个上限就是挡在那句话前面的东西。
 *   现在由 .run-pane{flex:1} 表达"被选中的那个占满整栏"，不再需要任何百分比上限。
 */
.sec-versions{flex:1;display:flex;flex-direction:column;min-height:0}
.sec-versions.hidden{display:none}
.sec-versions .sec-body{display:flex;flex-direction:column;gap:8px;min-height:0}
.ver-panel-main{display:flex;flex-direction:column;gap:8px;flex:1 1 auto;min-height:0}
.ver-toolbar{display:flex;flex-direction:column;gap:7px;flex:none}
.ver-filters{display:flex;flex-wrap:wrap;gap:5px}
.ver-chip{
  font-size:11.5px;line-height:1;padding:5px 9px;border-radius:999px;cursor:pointer;
  border:1px solid var(--line);background:var(--input);color:var(--ink-soft);
  transition:border-color 150ms ease,color 150ms ease,background 150ms ease;
}
.ver-chip:hover{border-color:var(--line-strong);color:var(--ink)}
.ver-chip.active{background:var(--accent);border-color:var(--accent);color:var(--accent-ink)}
.ver-search{
  width:100%;box-sizing:border-box;font-size:12.5px;padding:7px 10px;border-radius:8px;
  border:1px solid var(--line);background:var(--input);color:var(--ink);outline:none;font-family:inherit;
}
.ver-search:focus{border-color:var(--accent)}
.ver-list{overflow:auto;min-height:0;flex:1 1 auto;display:flex;flex-direction:column;gap:6px;padding-right:2px}

.ver-row{
  display:flex;align-items:stretch;gap:6px;border:1px solid var(--line);border-radius:10px;
  background:var(--input);overflow:hidden;flex:none;
}
.ver-row.is-base{border-color:color-mix(in srgb,var(--accent) 40%,var(--line))}
.ver-row.active{background:color-mix(in srgb,var(--accent) 12%,var(--input));border-color:var(--accent)}
.ver-main{
  flex:1;min-width:0;display:flex;flex-direction:column;gap:3px;align-items:flex-start;
  text-align:left;border:0;background:transparent;padding:8px 10px;cursor:pointer;font:inherit;color:inherit;
}
.ver-main:hover{background:var(--panel-2)}
.ver-name{
  font-size:12.5px;font-weight:600;color:var(--ink-strong);width:100%;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.ver-meta{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.ver-kind{
  font-size:10.5px;line-height:1;padding:3px 6px;border-radius:5px;
  border:1px solid var(--line);color:var(--ink-faint);background:var(--panel);
}
.ver-kind.kind-manual{color:#2f7d58;border-color:color-mix(in srgb,#2f7d58 40%,transparent);background:color-mix(in srgb,#3fa87a 12%,transparent)}
.ver-kind.kind-quick{color:var(--accent);border-color:color-mix(in srgb,var(--accent) 40%,transparent);background:color-mix(in srgb,var(--accent) 10%,transparent)}
.ver-kind.kind-auto{color:#a9631a;border-color:color-mix(in srgb,#a9631a 40%,transparent);background:color-mix(in srgb,#f0a63c 12%,transparent)}
.ver-time{font-size:11px;color:var(--ink-faint)}
.ver-base-tag{
  font-size:10.5px;line-height:1;padding:3px 6px;border-radius:5px;
  background:color-mix(in srgb,var(--accent) 18%,transparent);color:var(--accent);
}
.ver-note{
  font-size:11.5px;color:var(--ink-soft);width:100%;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.ver-stats{font-size:11px;color:var(--ink-faint)}
.ver-acts{display:flex;flex-direction:column;flex:none;border-left:1px solid var(--line)}
.ver-act{
  border:0;background:transparent;color:var(--ink-soft);font-size:11.5px;padding:0 9px;flex:1;
  cursor:pointer;white-space:nowrap;font-family:inherit;
}
.ver-act:hover{background:var(--panel-2);color:var(--ink)}
.ver-act.danger:hover{color:var(--danger);background:color-mix(in srgb,var(--danger) 10%,transparent)}

/* ---------- 版本命名弹窗 ---------- */
.sv-hint{font-size:11.5px;color:var(--ink-faint);line-height:1.6;margin-top:2px}
/*
  跨工作流保存模块（版本命名弹窗内）。
  勾选行复用 .mfield 的排版语汇（同一字号/同一颜色梯度），只额外处理
  「勾选框与文字基线对齐」这一点——不引入第二套表单视觉。
*/
.sv-cross-save{
  display:flex;align-items:center;gap:8px;
  font-size:12.5px;color:var(--ink-soft);cursor:pointer;user-select:none;
}
.sv-cross-save input{width:auto;margin:0;accent-color:var(--accent);cursor:pointer}
.sv-cross-fields{display:flex;flex-direction:column;gap:9px}
.sv-cross-fields select{
  width:100%;padding:9px 10px;font-size:13px;font-family:inherit;
  background:var(--panel-2);color:var(--ink);
  border:1px solid var(--line);border-radius:8px;
}
.sv-cross-fields select:focus{outline:none;border-color:var(--accent)}
.sv-cross-fields select:disabled{opacity:.6;cursor:not-allowed}

/* ---------- 只读预览：编辑控件整体不可写 ---------- */
#nodeList.is-readonly .t-actions{display:none}
#nodeList.is-readonly .t-input,
#varRows.is-readonly input{cursor:default}
#nodeList.is-readonly .t-input:hover{border-color:transparent}

@media (max-width:1080px){
  /*
   * 窄屏：两块正文都搬进底部抽屉（.sheet-pane），各自独立成页，
   * 不存在"谁抢谁高度"的问题 —— 所以这里不再需要任何高度/顺序上的修正。
   * （原先这里的 `max-height:none;order:1` 是"上下堆叠"时代的补救；
   *   那个 max-height 已随 tab 改造整体删除，这条也就一并归零。）
   */
  .sec-versions{order:1}
  .ver-row{flex-direction:column}
  .ver-acts{flex-direction:row;border-left:0;border-top:1px solid var(--line)}
  .ver-act{padding:7px 9px}
  /*
   * 窄屏徽章：不再靠 max-width 截断（见 orchestrator.js 的说明），
   * 文案本身就是短的，所以这里**不设 max-width**——设了反而会在
   * 名称很长时把徽章也一起切掉，制造第二个"看不全"的地方。
   * 真正需要收缩的是工作流名（下面那条），徽章保持"完整显示短文案"。
   */
  .draft-badge{flex:none;max-width:none}
  /*
   * 工作流名：一排布局里唯一可以被压缩的东西（越长越没用，
   * 看不全也不影响任何操作），所以让出空间的第一顺位是它。
   */
  .orch-head .wf-name{text-overflow:ellipsis}
  /*
   * 工作流名的**最小可读宽度**：它是唯一允许被压缩的格子，但不能压到 0 ——
   * 压到 0 时用户看不到自己在编辑哪一个工作流，那是功能缺失而不是省空间。
   * 6em ≈ 五个汉字，足够辨认"是哪一个"；再短就只剩省略号了。
   */
  .orch-head .wf-name{min-width:6em}
}

/* 工作流列表项上的「有草稿未存版本」圆点 */
.wf-item-title{position:relative}
.wf-dirty-dot{
  display:inline-block;width:6px;height:6px;border-radius:50%;margin-left:6px;vertical-align:middle;
  background:#e09b3d;flex:none;
}

/*
 * ===== 工作流列表项上的「运行指示器」（运行中转圈 / 刚完成绿勾）=====
 *
 * 结构：.wf-item 内的一个 span（名字按钮的兄弟节点，位于行右端）。
 * 状态类只有两个，且互斥，由 syncRunIndicators 从运行期投影写入：
 *   .is-running → 转圈（正在执行）
 *   .is-done    → 绿色对勾 + 亮一下（刚跑完，窗口结束后自动消失）
 *
 * 尺寸固定为一个正方形槽位（16px）：两种状态共用同一块位置，
 * 状态切换时**不会引起列表行宽/文字位置跳动** —— 那正是"指示器一闪，
 * 名字就跳一下"这类廉价感的来源。指示器始终占位、只是内容在变。
 */
.wf-run-ind{
  flex:none;
  width:16px;height:16px;
  margin:auto 8px auto 4px;   /* 竖直居中（auto 上下），右端留 8px 内边距 */
  position:relative;
  display:block;
  color:var(--ok,#3f9c5a);
  /* 默认（'none'）完全不可见：不用 display:none，否则状态切换会引发布局跳动 */
  opacity:0;
  transition:opacity 160ms ease;
}
.wf-run-ind.is-running,.wf-run-ind.is-done{opacity:1}

/*
 * 运行中：一个缺口圆环持续旋转。
 *
 * 用 conic-gradient + mask 画环，而不是 border 技巧：border 的圆环在
 * 16px 这种尺寸下抗锯齿很差（看起来像模糊的方块），而 conic 环在任何缩放下都干净。
 * 缺口（透明的那一段）让"它在转"可被观察到 —— 完整的环旋转起来是静止的。
 */
.wf-run-ind.is-running::before{
  content:'';
  position:absolute;inset:0;
  border-radius:50%;
  background:conic-gradient(from 0deg, transparent 0turn,
    color-mix(in srgb, var(--accent) 35%, transparent) .35turn,
    var(--accent) .95turn, transparent 1turn);
  -webkit-mask:radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 2px));
  mask:radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 2px));
  animation:wf-run-spin .8s linear infinite;
}
@keyframes wf-run-spin{to{transform:rotate(1turn)}}

/*
 * 刚完成：绿色对勾「画入」（从 0 放大到 1 并轻微过冲，像笔尖落下），
 * 同时整枚指示器"亮一下"（一圈放大淡出的绿色光晕）。
 *
 * 对勾用两个 border 边（左 + 下）旋转 -45° 画成，而不是插 SVG：
 * 它是纯装饰（宿主已 aria-hidden），形状就是两条线，用伪元素即可，
 * 不为它引入一个 DOM 节点；也因此用 scale 表达"画入"，
 * 而不是 SVG 的 stroke-dashoffset（这里根本没有 stroke 可画）。
 * 两段动画都只播一次（forwards），时长与 RUN_DONE_VISIBLE_MS 的可见窗口对齐。
 */
.wf-run-ind.is-done::before{
  content:'';
  position:absolute;left:1px;top:4px;
  width:12px;height:6px;
  border-left:2.5px solid currentColor;
  border-bottom:2.5px solid currentColor;
  transform:rotate(-45deg) scaleY(1);
  transform-origin:center;
  opacity:0;
}
.wf-run-ind.is-done::after{
  content:'';
  position:absolute;inset:-4px;
  border-radius:50%;
  background:radial-gradient(circle, color-mix(in srgb, var(--ok,#3f9c5a) 30%, transparent) 0%, transparent 70%);
  opacity:0;
}
@keyframes wf-run-check{
  0%{transform:rotate(-45deg) scale(0);opacity:0}
  55%{transform:rotate(-45deg) scale(1.12);opacity:1}
  100%{transform:rotate(-45deg) scale(1);opacity:1}
}
@keyframes wf-run-flash{
  0%{opacity:0;transform:scale(.6)}
  35%{opacity:.9;transform:scale(1.15)}
  100%{opacity:0;transform:scale(1.5)}
}
/*
 * 动画只在 is-done 存在时播放一次。用 `animation` 而非 transition：
 * 它是"发生了一次"的事件型动效（画勾 + 亮一下），不是状态间的渐变。
 */
.wf-run-ind.is-done::before{animation:wf-run-check 260ms cubic-bezier(.2,.9,.3,1.2) 40ms forwards}
.wf-run-ind.is-done::after{animation:wf-run-flash 620ms ease-out 120ms forwards}

/* 减少动效偏好：保留状态可见性（转圈/绿勾仍然显示），只是不再旋转/闪光 */
@media (prefers-reduced-motion:reduce){
  /* 操作排的淡入淡出是纯观感，减少动态偏好下直接切换 */
  .msg-toolbar{transition:none}
  .wf-run-ind.is-running::before{animation:none}
  .wf-run-ind.is-done::before{animation:none;opacity:1;transform:rotate(-45deg) scale(1)}
  .wf-run-ind.is-done::after{animation:none;opacity:0}
  .wf-run-ind{transition:none}
}
/* 数量为 0 的版本分类：保留可发现性，但明显降权 */
.ver-chip.is-empty{opacity:.5}
.ver-chip.is-empty:hover{opacity:.85}

/* 日志列表里「这次跑的是哪一版」的一行小字 */
.run-ver{font-size:11px;color:var(--ink-faint);margin-top:2px;display:flex;align-items:center;gap:4px}
.run-ver-tag{
  display:inline-flex;align-items:center;justify-content:center;width:13px;height:13px;border-radius:3px;
  background:color-mix(in srgb,var(--accent) 16%,transparent);color:var(--accent);font-size:9.5px;font-weight:700;
}
.run-ver-link{color:var(--accent);text-decoration:none;border-bottom:1px dashed color-mix(in srgb,var(--accent) 50%,transparent)}
.run-ver-link:hover{border-bottom-style:solid}
.tool-catalog{display:grid;gap:8px}

/*
 * 工具行 = 网格：[工具名 + 会写入 + 描述 ⓘ + 提示词图标] | 右侧控件列……
 *
 * 右侧列数由 tool-row.js 在行上写成 --tool-cols（等于真实右列数）：
 *   聊天页两个作用域复选框（本会话 / 新会话默认）→ 2 列；
 *   编排页只有一个启用复选框 → 1 列。
 * 为什么不让两侧各写自己的 grid-template：
 *   列数必须与真实列数一致，否则复选框会落到错误的那一列（对不上表头）。
 *   由行上的变量统一推导，任何页面都不可能写出列数不符的模板。
 *
 * 为什么用 grid 而不是 flex + space-between：
 *   复选框必须**纵向对齐成列**（表头也要对齐），否则每一行的复选框
 *   会随名称长度左右浮动，用户根本对不上表头，也就分不清哪个是哪个。
 *   固定列宽（92px）让对齐由布局保证，而不是靠内容长度碰巧一致。
 */
.tool-row{
  display:grid;
  /*
   * 右侧列宽两档：1 列的行给 68px（只有勾选框，不需要聊天页那种两列表头宽度），
   * 2 列的行给 92px（要与表头对齐）。
   * 用 --tool-cols 选档，避免把"几列"这件事在 CSS 里再写死一遍。
   */
  grid-template-columns:minmax(0,1fr) repeat(var(--tool-cols,2), var(--tool-col-w,92px));
  align-items:center;gap:10px;
  border:1px solid var(--border,#ddd);border-radius:8px;padding:10px;
}
/* 调用方覆盖策略是说明 + 开关，不复用目录工具行的多列网格。 */
.tool-override-policy{
  display:flex;align-items:center;gap:12px;
  border:1px solid var(--border,#ddd);border-radius:8px;padding:10px;
  cursor:pointer;
}
.tool-override-policy > span{flex:1;min-width:0}
.tool-override-policy b{display:block;font-size:inherit;line-height:1.5}
.tool-override-policy small{display:block;color:var(--ink-faint);line-height:1.5;margin-top:3px}
.tool-override-policy > input[type=checkbox]{flex:none;width:18px;height:18px;margin:0}
.tool-row-one-col{--tool-col-w:68px}
.tool-row-main{min-width:0;display:flex;align-items:center;gap:6px;flex-wrap:wrap}

/*
 * 信息图标（ⓘ）：把长说明从"常驻版面"变成"悬停/聚焦才看"。
 *
 * 视觉上刻意做得**低调而可发现**：
 *   圆形描边 + 淡色，不与标题争视线；悬停变强调色，表示"这里可以点/看"。
 * 尺寸用固定 14px：它是图表元素，比正文小一号才像标注而不是内容。
 * cursor:help 是这台平台上"有额外说明"的标准光标语义。
 * 不用 emoji（ⓘ/ℹ️）：各平台字形差异大，且彩色 emoji 会破坏面板的整体克制感。
 */
.info-mark{
  display:inline-flex;align-items:center;justify-content:center;
  width:14px;height:14px;flex:none;border-radius:50%;
  border:1px solid var(--line-strong);color:var(--ink-faint);
  font-size:9.5px;font-weight:700;font-style:italic;line-height:1;
  cursor:help;user-select:none;transition:border-color 120ms ease,color 120ms ease;
}
.info-mark:hover,.info-mark:focus-visible{border-color:var(--accent);color:var(--accent);outline:none}
/*
 * 「会写入」标记：只出现在 effect=write 的工具行上。
 *
 * 用警示色而不是普通灰标签：这一行与只读行在**行为上**是不同性质的
 * （一个会改数据、一个不会），视觉上必须能一眼分开——
 * 用户是照着这个面板决定"要不要给 AI 这个能力"的。
 * 刻意做小：它是提示，不是警告横幅；醒目但不抢走标题。
 */
.tool-effect-write{
  display:inline-flex;align-items:center;flex:none;
  padding:1px 6px;border-radius:999px;font-size:10.5px;line-height:1.5;
  border:1px solid color-mix(in srgb,var(--danger) 45%,transparent);
  color:var(--danger);
  background:color-mix(in srgb,var(--danger) 8%,transparent);
}
/*
 * 「查看嵌入提示词」图标：**黄色**信息图标（与自带描述 ⓘ 明确区分）。
 *
 * 为什么必须与 .info-mark 长得不一样：
 *   同一条工具行上已经有一个 ⓘ（挂在 description 上，讲"这个工具是干什么的"）。
 *   如果查看图标也是同款灰圆点，用户会以为点哪个都一样——
 *   而它们给的是两类东西：人写的简介 vs 真正发给模型的声明原文。
 *   黄色是这里的识别特征：一眼就知道这个点开的是"嵌入提示词原文"。
 *
 * 尺寸/形状与 .info-mark 保持一致（14px 圆形），保证行内排版不跳动；
 * 差异只在颜色与可点击态上。注意它是 <button>，所以要把浏览器默认
 * 的按钮外观（背景、边框、内边距、字体）全部归零，只留这套图标样式——
 * 否则会得到一个带原生立体边框的方块，与行内其它图元完全不是一路。
 */
/*
 * 面板的"嵌套层"：面板（含工具提示词查看器）是**从弹窗内部**打开的，
 * 那时它必须压在弹窗之上。
 *
 * ★ 这里不写 #toolPromptPanel 这样的单点规则。
 *   历史写法就是 `#toolPromptPanel{z-index:var(--z-modal-child,65)}`——
 *   它把"某一档层级"变成了某个具体元素的知识：改令牌要记得改它、
 *   下一个"从弹窗里打开的面板"又要再加一条规则，漏一处就重现
 *   "点开看不见/点不动"的老故障（查看器落在弹窗背后就是这么来的）。
 *   现在层级判断只有一个来源（modal-stack.js 的 markNested），
 *   CSS 只认 .is-nested 这一个类，数值只来自令牌。
 *
 * 遮罩比面板低 1：两者是同一次打开的同一层，先后关系必须可读，
 * 不能靠 DOM 顺序（背景遮罩若压住面板，整个面板就点不动了）。
 */
.panel.is-nested{z-index:var(--z-modal-child,65)}
.panel-backdrop.is-nested{z-index:calc(var(--z-modal-child,65) - 1)}

/*
 * 工具调用卡片里的封面图块。
 *
 * 每篇一块：上面一行"标题 · 阅读数"，下面是封面缩略图（复用 .msg-images）。
 * 为什么要分块而不是并成一片：判断封面影响时，用户最需要的是
 * "哪张图属于哪篇、那篇阅读多少"——混在一起就失去了这个对应关系，
 * 而对应关系正是这个功能全部的意义。
 */
.tool-call-cover{
  margin:0 0 10px;
  padding:8px;
  border:1px solid var(--line-soft);border-radius:8px;
  background:var(--panel-2);
}
.tool-call-cover:last-child{margin-bottom:0}
.tool-call-cover-cap{
  font-size:11.5px;color:var(--ink-soft);margin-bottom:6px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
/* 卡片里的封面不必太大：它是"给你认一眼"，点开才是看大图 */
.tool-call-cover .msg-images{grid-template-columns:repeat(auto-fill,minmax(88px,1fr));gap:6px}
.tool-call-cover .msg-image img{max-height:120px;object-fit:cover}

.tool-prompt-mark{
  display:inline-flex;align-items:center;justify-content:center;
  width:16px;height:16px;flex:none;padding:0;
  border-radius:50%;
  border:1px solid color-mix(in srgb,var(--warn) 70%,transparent);
  background:color-mix(in srgb,var(--warn) 18%,transparent);
  color:var(--warn);
  font:700 italic 10px/1 var(--font);
  cursor:pointer;user-select:none;
  transition:background 120ms ease,border-color 120ms ease,color 120ms ease;
}
.tool-prompt-mark:hover,.tool-prompt-mark:focus-visible{
  background:color-mix(in srgb,var(--warn) 34%,transparent);
  border-color:var(--warn);
  color:var(--ink-strong);
  outline:none;
}
/*
 * 深色下用同一个 --warn 令牌即可：令牌本身在主题里已经换成提亮的黄
 * （见 :root[data-theme="dark"]），这里不再另写一套颜色——
 * 否则"深色下图标是什么颜色"会有两处定义，改一处必然不同步。
 */

.tool-row input[type=checkbox]{width:18px;height:18px}
/* 复选框所在的两列：居中，且整列可点（label 包住复选框） */
.tool-col{display:flex;align-items:center;justify-content:center;cursor:pointer}
/*
 * 表头：与工具行严格同列，让"哪个勾选框属于哪个作用域"无需解释。
 * 与 .tool-row 共用同一套列变量（--tool-cols / --tool-col-w），
 * 否则表头与行的列宽会各写一个数字，改一处必然对不齐。
 * 只有聊天页有两列表头；编排页只有一个启用框，不需要表头。
 */
.tool-head{
  display:grid;
  grid-template-columns:minmax(0,1fr) repeat(var(--tool-cols,2), var(--tool-col-w,92px));
  align-items:center;gap:10px;
  padding:0 10px;
  font-size:11px;color:var(--ink-faint);letter-spacing:.04em;
}
.tool-head-col{text-align:center;white-space:nowrap}
@media (max-width:760px){
  /*
   * 手机上固定的 92px 会把名称列挤没：缩小列宽并缩小字号。
   * 不改成上下堆叠——表头与列的对应关系一旦断开，两个复选框又会看混。
   * 这里只覆盖变量值：列数仍由行自身的 --tool-cols 决定，
   * 不会因为媒体查询而把"几列"这件事写死成 2。
   */
  .tool-row{--tool-col-w:68px;gap:6px}
  .tool-head{--tool-col-w:68px;gap:6px;font-size:10px}
}
/*
 * AI 节点上的「历史工具调用」（来自「会话 → 编排」转换）。
 * 里面画的是**与聊天页同一套**工具元素（.tool-call，见 public/app.js 的
 * makeToolCallEl）——两种调用是同一种事实，视觉上不该看起来像两种机制。
 * 这里只补一个分组标题与内边距，不再单独定义一套卡片外观。
 */
.t-ai-tools{margin:8px 0 0;padding:8px 10px;border-radius:8px;border:1px dashed var(--line);background:var(--panel)}
.t-ai-tools-title{font-size:11px;color:var(--ink-soft);margin-bottom:4px}

/* ---------- 消息内工具调用元素（聊天页与编排页运行结果**共用**）----------
 * 类名是 .tool-call。它从"聊天页专用"变成了唯一的工具卡片实现：
 * 编排页的运行结果区按对话消息元素渲染（用户明确要求），里面的工具卡片
 * 就是这一套——不再是另写一份"编排页的样子"。
 * 因此这里的尺寸规则必须对两个宿主都成立：卡片本身不依赖所在区块的宽度，
 * 只依赖字号与行距（两页字号差半个像素，由各自容器的 font-size 决定）。
 */
/*
 * 正文/工具按真实顺序交织的流容器。
 * 纵向排列 + 统一间距：每一段正文与每一个工具元素都是它的直接子元素，
 * 顺序即真实发生顺序（由服务端的 segments 决定，前端不排序）。
 */
.msg-flow{display:flex;flex-direction:column;gap:10px}
/* 流里的正文段：段落之间的间距由容器的 gap 统一给，这里不再各写 margin */
.msg-flow .md{margin:0}
.msg-flow .md+.md{margin-top:2px}
.tool-call{
  border:1px solid var(--line);border-radius:var(--radius);
  background:var(--panel-2);overflow:hidden;
}
.tool-call-head{
  display:flex;align-items:center;gap:8px;width:100%;
  padding:7px 11px;border:0;background:transparent;cursor:pointer;
  font:inherit;font-size:12.5px;color:var(--ink-soft);text-align:left;
}
.tool-call-head:hover{background:var(--panel)}
/* 折叠箭头：纯 CSS 三角，与思考容器同一套语言 */
.tool-call-caret{
  flex:none;width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;
  border-top:5px solid var(--ink-faint);transition:transform .16s ease;
}
.tool-call.open .tool-call-caret{transform:rotate(180deg)}
/*
 * 状态点：进行中/完成/失败三色，一眼分辨这次调用到底成没成。
 *
 * white-space:nowrap —— 工具名必须整体可读。它是这一行的**主语**
 *（"百家号账号 完成 第 1 轮 · 3 条"），一旦被折成两行（"百家号账 / 号"），
 * 这一行就不再像一个句子，而且状态点会与名字分家。
 * 现在的工具名最长的也就四五个字，不换行不会挤压元信息；
 * 真正长的是元信息那一段（它已经 nowrap + 靠右）。
 */
.tool-call-name{font-weight:600;color:var(--ink);display:flex;align-items:center;gap:7px;white-space:nowrap}
.tool-call-name::before{
  content:'';flex:none;width:7px;height:7px;border-radius:50%;background:var(--ink-faint);
}
.tool-call[data-status=running] .tool-call-name::before{background:#e6a23c}
.tool-call[data-status=success] .tool-call-name::before{background:#3fbf6f}
.tool-call[data-status=failed] .tool-call-name::before{background:#e05252}
.tool-call-state{font-size:11.5px;color:var(--ink-faint);flex:none;white-space:nowrap}
.tool-call-meta{font-size:11.5px;color:var(--ink-faint);margin-left:auto;white-space:nowrap}
.tool-call-body{
  display:none;padding:0 11px 10px;border-top:1px solid var(--line-soft);
}
.tool-call.open .tool-call-body{display:block;padding-top:8px}
.tool-call-label{font-size:11.5px;color:var(--ink-faint);margin:6px 0 3px}
.tool-call-pre{
  margin:0;max-height:260px;overflow:auto;white-space:pre-wrap;word-break:break-all;
  font:12px/1.6 var(--mono);background:var(--panel);color:var(--ink-soft);
  padding:7px 9px;border-radius:7px;border:1px solid var(--line-soft);
}
.tool-call-pre.is-error{color:var(--danger);border-color:var(--danger)}
/*
 * 「被工作流调用」的跳转入口（工具卡片内的一个按钮）。
 *
 * 用按钮而不是链接：它的行为是页面内切换（打开那条运行记录），不是导航，
 * 用 <a> 会让"右键新标签打开/复制链接"这些浏览器行为给出错误预期
 * （这里根本没有一个可打开的 URL）。
 * 视觉上做成可点的行内按钮（描边 + 强调色），让人一眼看出"这里能点进去"。
 */
.tool-call-workflow{
  display:block;width:100%;text-align:left;cursor:pointer;
  font:12px/1.6 var(--mono);
  padding:6px 9px;border-radius:7px;
  border:1px solid color-mix(in srgb,var(--accent) 45%,transparent);
  background:color-mix(in srgb,var(--accent) 7%,transparent);
  color:var(--accent);
}
.tool-call-workflow:hover{background:color-mix(in srgb,var(--accent) 13%,transparent)}
/* 设置弹窗里的工具说明行 */
.chat-tool-note{font-size:12px;color:var(--ink-faint);margin-top:8px;line-height:1.6}

/* =====================================================================
 * 工具设置子模块（tools.settings 的可视化，如「工作流调用」选允许调哪些工作流）
 *
 * 结构由 public/tool-row.js 的 buildSettings 产出（两页共用唯一实现）。
 * 这里只负责外观，不参与"哪些工具有设置"的判断。
 *
 * 它跨在整行之下（grid-column:1/-1）：设置区是"这一行工具的参数"，
 * 不是第三列——放进列里会让复选框列被挤窄，表头随之对不上。
 * ===================================================================== */
.tool-settings{
  grid-column:1/-1;
  margin-top:2px;padding:9px 10px;
  border:1px solid var(--line-soft);border-radius:8px;
  background:var(--panel);
}
.tool-settings-label{font-size:12px;font-weight:600;color:var(--ink-soft);display:flex;align-items:center;gap:6px}
/*
 * 可折叠子模块的标题行：整行是按钮（命中区覆盖全行）。
 * 只在 schema 声明 collapsible 时才有这个类（见 tool-row.js），
 * 所以不可折叠的块不会长出一个假的可点区域。
 */
.tool-settings-label.is-collapsible{
  cursor:pointer;user-select:none;border-radius:6px;padding:2px 4px;margin:-2px -4px;
  transition:background var(--dur-fast,150ms) ease;
}
.tool-settings-label.is-collapsible:hover{background:var(--panel-2)}
.tool-settings-label.is-collapsible:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
/*
 * 三角：折叠态朝右、展开态朝下，与全站折叠开关同一套语言
 *（见 .snap-toggle-caret 等处）。
 */
.tool-settings-caret{
  flex:none;width:10px;font-size:10px;color:var(--ink-faint);line-height:1;
  transition:transform var(--dur-fast,150ms) ease;
}
.tool-settings.is-folded .tool-settings-caret{transform:rotate(0deg)}
.tool-settings:not(.is-folded) .tool-settings-caret{transform:rotate(90deg)}
/*
 * 折叠体：收起时整体不显示。
 *
 * ★ 用 display:none 而不是高度 0 ★
 *   这里的折叠对象是**一块独立的卡片内容**（不是布局里的一列），
 *   没有"后续兄弟节点会顶上来认领轨道"这种问题（那是网格列的场景，
 *   见 html[data-orch-settings-collapsed="1"] .orch-cols）。display:none 同时顺带解决掉
 *   "隐藏但可 Tab 进去"的可达性问题 —— 高度 0 + overflow:hidden 的元素
 *   仍然会被 Tab 聚焦，读屏用户会在一个看不见的列表里走一圈。
 */
.tool-settings.is-folded .tool-settings-fold{display:none}
.tool-settings-hint{font-size:11.5px;color:var(--ink-faint);margin-top:3px;line-height:1.6}
.tool-settings-list{margin-top:7px;display:flex;flex-direction:column;gap:6px}
.tool-settings-item{
  display:grid;grid-template-columns:auto minmax(0,1fr);gap:4px 8px;align-items:center;
  padding:6px 8px;border:1px solid var(--line-soft);border-radius:7px;background:var(--bg);
  cursor:pointer;
}
/* 复选框与名字同一行；描述跨整行（它往往是一整段说明） */
.tool-settings-item .tool-settings-desc{grid-column:1/-1}
.tool-settings-name{font-size:12.5px;color:var(--ink-soft);min-width:0;overflow-wrap:anywhere}
/*
 * 描述在这里是**常驻正文**（与工具行的 ⓘ 相反，理由见 tool-row.js 的说明）：
 * 勾选时用户需要一次看全部描述再决定，逐个悬停是无法完成这个任务的。
 */
.tool-settings-desc{font-size:11.5px;color:var(--ink-faint);line-height:1.6;white-space:pre-wrap;margin-left:22px}
.tool-settings-desc.is-empty{font-style:italic;color:var(--warn,var(--ink-faint))}
.tool-settings-empty{font-size:12px;color:var(--ink-faint)}
.tool-settings-note{font-size:11.5px;color:var(--ink-faint);margin-top:7px}
.tool-settings-err{font-size:12px;color:var(--danger)}

/* =====================================================================
 * 聊天页输入框：块编辑器（与编排页同一套块）
 *
 * 输入框从 textarea 换成 contenteditable 之后，块是行内原子元素，
 * 所以这里只补「在聊天输入框里看起来对」的尺寸与占位，不重新定义块本身——
 * 块的配色 / 形状全部来自编排页那套 .accchip / .filechip / .cchip。
 * ===================================================================== */
.composer .composer-input{
  min-height:64px;
  max-height:220px;
  overflow:auto;
  font:14px/1.7 var(--font);
  border:0;
  padding:0;
}
.composer .composer-input:focus{outline:none}
/* 块在聊天输入框里比编排区略大一点：这里的字号是 14px，块要跟得上 */
.composer .composer-input .accchip,
.composer .composer-input .filechip,
.composer .composer-input .cchip{
  font-size:12px;
  line-height:19px;
  vertical-align:baseline;
}

/* 气泡里的临时会话标记（不是正文，只是「这条消息先跑过一次子调用」的痕迹） */
.msg-containers{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}
.msg-container-tag{
  font-size:11px;padding:2px 8px;border-radius:999px;
  border:1px dashed color-mix(in srgb, var(--accent) 55%, var(--line));
  color:color-mix(in srgb, var(--accent) 80%, var(--ink-soft));
  background:color-mix(in srgb, var(--accent) 7%, transparent);
}

/* =====================================================================
 * 账号信息块弹窗里的「构建」工具区
 *
 * 原来这些按钮在顶部工具栏；用户要求把账号相关入口全部收进 / 菜单的
 * 账号信息块弹窗，所以这里给它们一个紧凑、不与勾选列表打架的排布。
 * ===================================================================== */
.accm-account-row{display:flex;gap:8px;margin-bottom:8px}
.accm-account-pick{
  flex:1;min-width:0;font:12.5px var(--font);color:var(--ink);
  border:1px solid var(--line);border-radius:8px;padding:6px 8px;background:var(--input);
}
.accm-account-pick:focus{border-color:var(--accent);outline:none}
.accm-extra{display:flex;flex-direction:column;gap:10px}
.accm-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.accm-rowlimit{
  display:flex;align-items:center;gap:8px;font-size:12px;color:var(--ink-soft);
}
.accm-rowlimit input{
  width:96px;border:1px solid var(--line);border-radius:8px;padding:5px 8px;
  background:var(--input);color:var(--ink);font:12px var(--font);
}
.accm-tool-status{font-size:11.5px;color:var(--ink-faint);min-height:16px;line-height:1.5}
.accm-tool-status.is-error{color:var(--danger)}
.accm-built{font-size:11.5px;color:var(--ink-faint);line-height:1.5}
.accm-rerun .acc-batch-bar{margin:6px 0 4px}

/* =====================================================================
 * 移动端深度优化（2026-09-16）
 *
 * 用 iPhone 14 尺寸实测发现的问题（不是猜的）：
 *   输入框字号 14px  → iOS 聚焦时会**自动放大整个页面**，
 *                      而且放大后不会自动缩回，用户就得手动捏合；
 *   列表项高 31px    → 手指点不准（人机指南要求 ≥44px 触控目标）；
 *   底部按钮高 35px  → 同上；
 *   弹窗贴边          → 刘海屏/圆角会压到内容。
 *
 * 这里统一在一个媒体查询里解决，而不是散落到各处——
 * 移动端的问题是成组的（触控目标尺寸 + 安全区 + 防缩放），
 * 分散写必然会出现"改了列表项忘了按钮"这种漏网。
 * ===================================================================== */
@media (max-width:760px){
  /*
   * 防 iOS 自动放大：输入类控件的**字号必须 ≥16px**。
   * 注意 text-size-adjust 管不了这件事——那个管的是"横竖屏切换时
   * 浏览器自己调整文字大小"，与"聚焦时放大"是两回事（这里踩过）。
   * 只覆盖输入类元素，不动正文排版（正文变大反而降低信息密度）。
   *
   * ⚠ 这里的选择器**必须带 !important**（或写成逐组件的高特异度列表）。
   * 原因是一个静默失效：这条规则原先写成裸 `input,textarea,select{font-size:16px}`，
   * 特异度是 0,0,1；而组件自己的规则几乎都是类选择器，例如
   *   .wf-model select{font-size:12px}      → 特异度 0,1,1
   *   .fsel-input{font-size:13px}           → 特异度 0,1,0
   * 媒体查询**不提供特异度加成**，所以无论这条规则写在文件多后面，
   * 它都赢不了那些类选择器 —— 实测 .wf-model select 仍然算出 12px，
   * 而 12px 会让 iPhone 一点就放大整页。
   *
   * 用 !important 而不是把每个组件逐个列出来：后者的失效方式是
   * "新加一个输入框组件时忘了往列表里补名字"，而这正是上面那种静默失效的成因。
   * 语义上这里也确实该强制——"手机上输入控件不得小于 16px"是平台约束，
   * 不该被任何组件级排版偏好覆盖。
   */
  input,textarea,select,
  [contenteditable="true"]{
    font-size:16px !important;
  }
  /*
   * 行高与最小高度单独给：上面只管字号，若行高仍按小字号计算，
   * 16px 字会被挤在 20px 的行盒里显得别扭。只对真正需要多行的输入生效。
   */
  .composer-input,.t-input[contenteditable="true"]{line-height:1.7}

  /*
   * 块弹窗：铺满可用高度、四角留出安全区，并把底部操作条做成
   * 拇指能舒服够到的位置。
   * dvh 而不是 vh：移动浏览器地址栏收放会改变可视高度，
   * 用 vh 会让弹窗底部被地址栏切掉一截（iOS 上尤其明显）。
   */
  .modal{padding:0;align-items:flex-end}
  .modal-card{
    width:100%;max-width:none;max-height:92dvh;
    border-radius:16px 16px 0 0;
    padding-bottom:env(safe-area-inset-bottom,0px);
  }
  .modal-head{padding:14px 16px 10px}
  .modal-head .icon-btn{width:44px;height:44px;font-size:20px}
  .modal-body{padding:12px 16px}
  .modal-foot{
    padding:10px 16px calc(10px + env(safe-area-inset-bottom,0px));
    gap:8px;flex-wrap:nowrap;
  }
  /* 底部按钮统一加大到拇指可点（引用 --tap，不写字面量） */
  .modal-foot .orch-btn{min-height:var(--tap);font-size:15px;flex:1 1 auto}
  /* 「删除这个块」是破坏性操作，不需要和确定键争同样宽度 */
  .modal-foot .orch-btn[data-bk-del]{flex:0 0 auto;padding:0 14px;font-size:14px}

  /* 文件列表项：31px → 48px，并让路径能换行而不是被挤没 */
  .fsel-item{min-height:48px;padding:10px 12px;font-size:14px;align-items:center}
  .fsel-item .fsel-name{font-size:14px}
  .fsel-item .fsel-dir{font-size:11.5px}
  /* 列表本身给足高度，让它成为可滚动区域而不是被压成一条缝 */
  .modal-body .fsel{min-height:auto}
  .modal-body .fsel-list{max-height:46dvh}
  .fsel-input{min-height:var(--tap);padding:10px 12px}
  /* 底部键盘提示在触屏上没有意义（没有物理方向键），去掉省一行高度 */
  .fsel-foot{display:none}

  /*
   * 顶栏与工具条上的小按钮同样按触控目标放大。
   *
   * 一律引用 --tap（44px），不再写 38px / 46px 这类"看着差不多"的字面量：
   * 触控下限是一个**平台约束**（Apple HIG / Material 都是 44~48px），
   * 它只有一个权威。散落的 38px 会让"这一排按钮能点中、那一排点不中"，
   * 而且以后想调整时根本搜不干净。
   */
  .view-toggle{min-height:var(--tap);display:inline-flex;align-items:center;padding:0 10px}
  header#logHead,.log-head{flex-wrap:wrap}
  .orch-btn.small{min-height:var(--tap)}

  /*
   * / 菜单在手机上的适配。
   *
   * 约束：屏幕只有 390px，而菜单要容纳"图标+名称+说明"。
   * 原来的 max-width 是 min(340px, calc(100vw-24px))，在 390px 屏上取到 340px，
   * 而输入框宽度也是 ~340px、左边缘并不为 0 —— 于是菜单向右伸展就会出屏
   *（用户截图里被裁掉的那一块）。
   *
   * 这里把宽度收到 calc(100vw - 40px)：左右各留 20px，无论锚点在哪一侧
   * 都保证整块菜单能完整落在视口内。
   */
  .slash-menu{
    min-width:0;
    max-width:calc(100vw - 40px);
    width:calc(100vw - 40px);
  }
  /* 菜单项按触控目标放大（与文件列表同一标准） */
  .slash-item{min-height:52px;padding:11px 12px;align-items:center}
  .slash-main{font-size:15px}
  /*
   * 触屏上不显示说明：说明槽靠"悬停"揭示可横向滚动，而触屏没有悬停
   * （只有按下），于是说明会以被裁切的状态常驻——既读不全，又在 52px
   * 的触控行里留下一个能横向拖动的小窗口，和"点这一项"抢同一个手势。
   * 图标+名称已经足够区分这几个块，说明留给桌面端。
   */
  .slash-hint{display:none}
  .slash-head{font-size:12px;padding:8px 12px 6px}
  .slash-foot{display:none}
  /*
   * 二级菜单在手机上不做左右换边（没有横向空间）。
   * 它跟随一级菜单展开——具体是"叠在下面"还是"翻到上面"，
   * 同样由 slash-menu.js 的 computeRect() 按可视视口算，这里不写坐标。
   */
}

/* =====================================================================
 * 触屏长按：会话条目与消息 ==== 上下文菜单的第二个入口
 *
 * 背景：消息操作按钮（复制 / 重试 / 分支 / 编辑）与侧栏会话的「创建为编排 /
 * 删除」原本只有"鼠标悬停出现"或"桌面右击"两条路径。触屏没有 hover、
 * 也没有右键——于是这些能力在手机上等于不存在。长按是移动端对应手势的
 * 唯一权威实现（见 public/ctx-menu.js 的 bindLongPress）。
 *
 * CSS 在这里只负责三件事，全部是"手势能不能成立"的前提，不含任何定位逻辑：
 *   ① 按住期间的视觉反馈（.longpress-holding）：500ms 内必须让用户看出来
 *      "再按一会儿就有了"，否则他会以为没反应而松手，功能等于不存在；
 *   ② 压制系统的长按选字菜单与放大镜（-webkit-touch-callout / user-select）：
 *      否则系统的菜单会盖住我们的菜单，两个菜单抢同一个手势；
 *   ③ 提示可点（cursor:pointer + 按压反馈）：没有 hover 的设备上，
 *      这是"这里可以按"的唯一提示。
 *
 * 用 (pointer:coarse) 而不是宽度断点，与 JS 侧 CtxMenu 的长按判定同一条权威
 *（宽度说的是版面够不够，输入方式说的是有没有右键，两者不等价）。
 * ===================================================================== */
@media (pointer:coarse){
  /*
   * 会话条目：长按弹菜单，点按仍然是"打开会话"。
   * 因此这里**不**设置 user-select:none 的全局压制之外的东西——
   * 会话标题是应用生成的短文本（不是要复制的正文），压制系统选字是安全的。
   */
  .session-item,
  .wf-item{
    -webkit-touch-callout:none;
    -webkit-user-select:none;
    user-select:none;
    cursor:pointer;
    border-radius:8px;
    transition:background .12s ease;
  }
  /*
   * 按压反馈与"长按进行中"必须区分开：
   *   :active  = 手指刚按下（可能是普通点按）——浅色即可；
   *   .longpress-holding = 计时器正在跑（再按住就到菜单）——加深 + 描边，
   *   让用户看到"它在等我按完"。
   * 只有 :active 的话，用户无法知道继续按住会有新东西出现。
   */
  .session-item:active,
  .wf-item:active{background:var(--panel)}
  .session-item.longpress-holding,
  .wf-item.longpress-holding{
    background:var(--panel);
    box-shadow:inset 0 0 0 1px var(--accent);
  }
  /* 触屏下条目本身的点击区要够高（手指落点远不如鼠标精确） */
  .session-item .session-title{padding:11px 10px;font-size:14px}
  /* 窄屏上 id 仍然完整显示：它是功能本体（复制/核对），不随标题一起被截断 */
  .session-item .session-id{font-size:10px}

  /*
   * 消息气泡：长按弹操作菜单。
   *
   * 这里**不能**整块 user-select:none —— 消息正文是用户最想复制的东西，
   * 压制选中会让"长按选字"这条系统路径失效，那是真实的损失。
   * 所以只压制"长按弹出系统 callout"（那是要抢我们手势的那个），
   * 保留文字可选中：用户在正文上长按仍可拖拽选字（系统行为），
   * 而我们自己的菜单不会因此被夺走——菜单的打开不需要 preventDefault
   * 系统行为（见 ctx-menu.js 的说明）。
   */
  /* 消息触摸只负责选择操作栏，不给正文附加按压背景、描边或过渡。
     长按计时由 ctx-menu 管理，不再把手势状态投影成整条消息高亮。 */
  .msg{
    -webkit-touch-callout:none;
    -webkit-tap-highlight-color:transparent;
    cursor:pointer;
  }
  /* 与桌面共用点击选中规则，只扩展触屏命中范围。 */
  .msg-toolbar{min-height:40px}
  .msg-action{width:40px;height:40px;padding:0}

}

/*
 * 上下文菜单在触屏上的尺寸。
 *
 * 菜单现在多了一个入口（长按），在手机上会被频繁使用，而原有尺寸是按
 * 鼠标设计的：12.5px 字号、7px 内边距，单行高度约 28px——手指很难稳定命中，
 * 相邻两项还会误触（"编辑"点成"删除"这类后果不可逆）。
 * 触屏下统一放大到接近系统菜单的尺度，并让危险项与常规项之间有可分辨的间距。
 *
 * 这里只改尺寸，不改定位：定位的唯一权威仍是 ctx-menu.js 的 open()
 *（贴边收敛在那里做，CSS 不参与，否则两套坐标语义会打架）。
 */
@media (pointer:coarse){
  .ctxmenu{min-width:190px;padding:6px;border-radius:12px}
  .ctxmenu button{
    padding:11px 12px;font-size:14px;border-radius:9px;
    /* 触摸目标最小高度：低于这个值相邻项会互相误触 */
    min-height:44px;display:flex;align-items:center;
  }
  /* 禁用项与可选区分开：触屏没有 hover，hover 色给不了这个信息 */
  .ctxmenu button:disabled{opacity:.42}
  /* 危险项与上一项拉开距离：删除是不可逆的，不该紧贴着别的项 */
  .ctxmenu button.danger{margin-top:4px;border-top:1px solid var(--line-soft);border-radius:0 0 9px 9px}
}

/* ============================================================================
 * 回收站面板（public/recycle-bin.js 的唯一配套样式）
 *
 * 为什么是固定定位的弹层而不是嵌进侧栏：
 *   回收站条目可能很多、且要在两个页面里长得一模一样。
 *   嵌进侧栏会被窄栏宽挤成一行一个字，还会随页面不同而变样。
 * z-index 取 70：高于侧栏/遮罩（29–40）与普通弹层（60），
 * 低于 / 菜单（80）——回收站是「就地看一眼」的短动作，
 * 不该盖住用户正在输入的补全菜单。
 * ========================================================================== */
.rb-mask{position:fixed;inset:0;z-index:70;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;padding:20px}
.rb-panel{
  width:min(680px,100%);max-height:min(78vh,720px);display:flex;flex-direction:column;
  background:var(--panel);border:1px solid var(--line);border-radius:14px;
  box-shadow:0 24px 60px rgba(0,0,0,.32);overflow:hidden;
}
.rb-head{display:flex;align-items:center;gap:10px;padding:14px 16px;border-bottom:1px solid var(--line)}
.rb-head b{font-size:14px}
.rb-close{margin-left:auto;background:none;border:0;color:var(--ink-soft);font-size:18px;line-height:1;cursor:pointer;padding:2px 6px;border-radius:8px}
.rb-close:hover{background:var(--line-soft);color:var(--ink)}
.rb-retention{display:flex;align-items:center;gap:8px;padding:10px 16px;border-bottom:1px solid var(--line-soft);font-size:12px;color:var(--ink-soft);flex-wrap:wrap}
.rb-retention-label{white-space:nowrap}
.rb-retention-input{width:64px;padding:4px 6px;border:1px solid var(--line);border-radius:7px;background:var(--bg);color:var(--ink);font-size:12px}
.rb-retention-value{font-weight:600;color:var(--ink)}
.rb-hint{font-size:11.5px;color:var(--ink-faint)}
.rb-hint.is-bad{color:var(--danger)}
.rb-list{overflow:auto;min-height:0;padding:6px}
.rb-empty{padding:24px 16px;text-align:center;color:var(--ink-faint);font-size:12.5px}
.rb-item{display:flex;align-items:center;gap:10px;padding:10px 12px;border-radius:10px}
.rb-item:hover{background:var(--bg)}
.rb-item-main{min-width:0;flex:1}
.rb-item-title{font-size:13px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.rb-item-meta{font-size:11.5px;color:var(--ink-faint);margin-top:2px}
.rb-item-acts{display:flex;gap:6px;flex-shrink:0}
.rb-act{
  padding:5px 10px;font-size:12px;border:1px solid var(--line);border-radius:8px;
  background:var(--panel);color:var(--ink);cursor:pointer;white-space:nowrap;
}
.rb-act:hover{background:var(--line-soft)}
.rb-act:disabled{opacity:.5;cursor:default}
.rb-act.danger{color:var(--danger);border-color:color-mix(in srgb,var(--danger) 35%,transparent)}
.rb-act.danger:hover{background:color-mix(in srgb,var(--danger) 12%,transparent)}
/*
 * 顶栏的「一键清理」：与标题同排，靠右侧聚拢（.rb-close 有 margin-left:auto，
 * 所以这里只需给定自身位置即可，顺序为 [标题] [一键清理] [×]）。
 * 用 danger 语气：它是全站破坏面最大的动作，必须在视觉上就能被认出来。
 */
.rb-empty-all{margin-left:auto;font-size:12px;padding:5px 10px}
/* 触屏：条目改成两行（标题一行、按钮一行），窄屏横向排不下两个按钮 */
@media (hover:none){
  .rb-item{align-items:flex-start;flex-direction:column;gap:8px}
  .rb-item-acts{width:100%}
  .rb-act{flex:1;padding:8px 10px}
}
/* 侧栏底部的回收站入口：与「返回对话」同级的常驻小入口 */
.rb-entry{
  display:flex;align-items:center;gap:6px;width:100%;
  padding:7px 10px;font-size:12px;color:var(--ink-soft);
  background:none;border:1px solid transparent;border-radius:8px;cursor:pointer;text-align:left;
}
.rb-entry:hover{background:var(--panel);color:var(--ink);border-color:var(--line-soft)}
.rb-badge{
  margin-left:auto;min-width:18px;padding:0 5px;border-radius:9px;
  background:var(--line-soft);color:var(--ink-soft);font-size:11px;text-align:center;
}
.rb-badge.hidden{display:none}

/* ============================================================================
 * 站内确认弹窗（public/confirm-dialog.js 的唯一配套样式）
 *
 * 为什么 z-index 比回收站(70)还高：
 *   回收站面板自己会触发确认（清空回收站 / 彻底删除），确认框必须压在被它
 *   触发的那层之上，否则用户点了按钮却看不到任何东西。
 *   取 90：高于回收站(70)与普通弹窗(60/65)，低于图片查看器的 9999
 *   （lightbox 是"临时全屏看东西"，理应压住一切）。
 *
 * 为什么单独一套类名（cf-*）而不是复用 .modal：
 *   .modal 的宽度是 min(1000px,100%)，而确认框是窄条；复用会让它要么被撑得
 *   很宽、要么需要在调用点额外覆盖宽度（又一处"每个调用点自己调"）。
 *   独立的 .cf-panel 把"确认框就该这么宽、这么排版"这件事收在一处。
 * ========================================================================== */
.cf-mask{
  position:fixed;inset:0;z-index:90;
  background:rgba(0,0,0,.5);
  display:flex;align-items:center;justify-content:center;padding:20px;
}
.cf-panel{
  width:min(400px,100%);
  background:var(--panel);border:1px solid var(--line);border-radius:14px;
  box-shadow:0 22px 60px rgba(0,0,0,.34);
  padding:20px 20px 16px;
  animation:cf-in .14s ease-out;
}
@keyframes cf-in{from{opacity:0;transform:translateY(6px) scale(.985)}to{opacity:1;transform:none}}
@media(prefers-reduced-motion:reduce){.cf-panel{animation:none}}
.cf-title{font-size:15px;font-weight:600;color:var(--ink-strong);margin-bottom:10px}
.cf-body{font-size:13px;line-height:1.7;color:var(--ink-soft)}
.cf-para{margin-bottom:6px;overflow-wrap:anywhere}
.cf-para:last-child{margin-bottom:0}
.cf-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:18px}
.cf-btn{
  padding:8px 16px;font-size:13px;border-radius:9px;cursor:pointer;
  border:1px solid var(--line);background:var(--panel);color:var(--ink);
}
.cf-btn:hover{background:var(--line-soft)}
.cf-btn.primary{background:var(--accent);border-color:var(--accent);color:var(--accent-ink);font-weight:600}
.cf-btn.primary:hover{background:var(--accent-hover);border-color:var(--accent-hover)}
/*
 * 危险动作：确认按钮用危险色而不是主色。
 * 视觉语气必须与"不可逆"对齐——用户在按下之前就该感到"这一下很重"，
 * 而不是按完才知道。
 */
.cf-btn.primary.danger{background:var(--danger);border-color:var(--danger);color:#fff}
.cf-btn.primary.danger:hover{background:color-mix(in srgb,var(--danger) 82%,#000);border-color:transparent}
@media (max-width:400px){
  .cf-actions{flex-direction:column-reverse}
  .cf-btn{width:100%;padding:10px}
}

/* ============================================================================
 * 图片附件（粘贴传图）—— 唯一实现：public/attachments.js
 * ============================================================================
 *
 * 三条结构约定（与 JS 侧的不变量对应，改样式前先读）：
 *   ① 附件条 .att-strip 是数据（composerAttachments / node.images）的**投影**，
 *      它自己不持有状态；所以样式只描述"怎么显示一张图"，不描述"有几张"。
 *   ② 消息气泡里的 .msg-images 与附件条用**同一套格子尺寸**：
 *      发给别人看时（气泡）和自己编辑时（附件条）应该长得一样，
 *      否则用户会怀疑"我看到的和发出去的是不是同一张"。
 *   ③ 缩略图一律 object-fit:cover + 固定方格：图片比例千差万别，
 *      让它们撑开布局会让聊天记录随图片高度跳动。
 */

/* ---------- 附件条 ---------- */
.att-host{width:100%}
.att-host:empty{display:none}
.att-strip{
  display:flex;flex-wrap:wrap;gap:8px;align-items:center;
  padding:8px 0 2px;
}
/* 拖入时用一个清晰的目标态，而不是只改光标 */
.att-strip.att-drop{
  outline:2px dashed color-mix(in srgb, var(--accent) 55%, transparent);
  outline-offset:4px;border-radius:10px;
}
.att-cell{
  position:relative;width:64px;height:64px;flex:none;
  border:1px solid var(--line);border-radius:9px;overflow:hidden;
  background:var(--panel-2);
}
.att-thumb{
  width:100%;height:100%;object-fit:cover;display:block;cursor:zoom-in;
  /* 图片加载前不留白闪：底色与面板一致 */
  background:var(--panel-2);
}
.att-del{
  position:absolute;top:2px;right:2px;width:18px;height:18px;line-height:16px;
  border:0;border-radius:50%;padding:0;cursor:pointer;
  background:rgba(0,0,0,.62);color:#fff;font-size:13px;
  display:grid;place-items:center;
  /* 默认半隐，悬停/聚焦才完全显现：不遮挡缩略图内容，又能被发现 */
  opacity:.72;transition:opacity 120ms ease,background 120ms ease;
}
.att-cell:hover .att-del,.att-del:focus-visible{opacity:1;background:var(--danger)}
.att-add{
  width:64px;height:64px;flex:none;
  border:1px dashed var(--line-strong);border-radius:9px;
  background:var(--panel-2);color:var(--ink-faint);
  font-size:20px;line-height:1;cursor:pointer;
  transition:border-color 120ms ease,color 120ms ease;
}
.att-add:hover{border-color:var(--accent);color:var(--accent)}
/* 编排页：附件条与编辑器之间的间距（.t-bubble 内部） */
.node-atts{margin-top:8px}

/* ---------- 消息气泡里的图片 ---------- */
.msg-images{
  display:flex;flex-wrap:wrap;gap:8px;margin:8px 0 2px;
}
.msg-image{
  margin:0;width:96px;height:96px;flex:none;
  border:1px solid var(--line);border-radius:10px;overflow:hidden;
  background:var(--panel-2);
}
.msg-image img{
  width:100%;height:100%;object-fit:cover;display:block;cursor:zoom-in;
}

/* ---------- 大图查看（lightbox） ---------- */
.att-lightbox{
  position:fixed;inset:0;z-index:9999;
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;
  /* 深色遮罩在两种主题下都成立：它是"临时进入查看态"，不参与主题配色 */
  background:rgba(18,17,16,.88);
  backdrop-filter:blur(3px);
  padding:32px 20px;
}
.att-lightbox.hidden{display:none}
.att-lightbox-img{
  max-width:min(96vw,1600px);max-height:calc(100vh - 130px);
  object-fit:contain;border-radius:10px;
  box-shadow:0 18px 60px rgba(0,0,0,.5);
}
.att-lightbox-cap{
  color:#e8e4de;font-size:12.5px;max-width:min(90vw,900px);
  text-align:center;overflow-wrap:anywhere;
}
.att-lightbox-close{
  position:absolute;top:16px;right:18px;
  width:34px;height:34px;border-radius:50%;border:0;cursor:pointer;
  background:rgba(255,255,255,.14);color:#fff;font-size:20px;line-height:1;
  display:grid;place-items:center;
}
.att-lightbox-close:hover{background:rgba(255,255,255,.26)}

/* ---------- 编辑态里的附件区 ---------- */
.msg-edit-atts{margin-top:8px}
.msg-edit-atts .att-host:empty{margin:0}

/* =====================================================================
 * 移动端形态层 · 折叠态 / 顶栏收纳 / 底部抽屉
 *
 * 这一节解决的是「iPhone 上竖向空间怎么用」的问题，不是某个面板的问题。
 * 三条规则贯穿始终：
 *   ① 常驻面板不得与「消息序列」抢竖向空间（消息序列才是这个页面的主任务）；
 *   ② 移动端所有列表型浮层共用一个抽屉（唯一实现，见 orchestrator.js 注释）；
 *   ③ 触控目标 ≥ --tap（44px），底部让开 iPhone 横条（safe-area）。
 * ===================================================================== */

/*
 * ---------- 快照面板头的折叠开关：**已整体移除** ----------
 *
 * 这里原先有 .snap-toggle（面板头做成的折叠按钮）。
 * 用户 2026-10-01 裁决：右栏改成 tab 之后取消各自的折叠开关 ——
 * 它变成了**第二套控制同一件事的开关**（"快照折不折" 与 "当前选中哪个 tab"
 * 会互相打架，表现为"tab 选了快照、面板却是折着的（一片空白）"）。
 * 折叠现在只有一个答案：tab 选谁，谁就占满整栏。
 *
 * ★ 样式也一并删掉，不留空壳 ★
 *   留着的话，"这个类还存在"会让下一个人误以为折叠态还活着，
 *   而任何新建节点若不小心带上它就会长出一个点不动的按钮。
 */

/* 条数徽标：折叠态下唯一的「值不值得点开」信号 */
.snap-badge{
  display:inline-flex;align-items:center;justify-content:center;
  min-width:17px;height:17px;padding:0 5px;border-radius:9px;
  background:var(--accent);color:var(--accent-ink);
  font-size:10.5px;font-weight:650;line-height:1;font-variant-numeric:tabular-nums;
}
.snap-badge.hidden{display:none}

/* 动作条被搬进抽屉后的占位（避免面板头留下空洞） */
.sec-head-actions.is-vacated{display:none}

/* ---------- 自动快照的按日分组 ---------- */
.snap-group{border:1px solid var(--line-soft);border-radius:9px;background:var(--panel);overflow:hidden}
.snap-group-head{
  display:flex;align-items:center;gap:7px;width:100%;text-align:left;
  border:0;background:transparent;color:var(--ink-soft);font:inherit;font-size:12px;
  padding:9px 10px;cursor:pointer;min-height:var(--tap);
}
.snap-group-head:hover{background:var(--panel-2)}
.snap-group-caret{font-size:10px;color:var(--ink-faint);flex:none}
.snap-group-count{margin-left:auto;font-size:10.5px;color:var(--ink-faint)}
.snap-group-body{display:flex;flex-direction:column;gap:8px;padding:0 8px 8px}
.snap-group-body.hidden{display:none}

/* ---------- 顶栏收纳（移动端 ⋯ 溢出菜单） ---------- */
.orch-actions{display:flex;align-items:center;gap:6px}
.orch-more-btn{display:none}
.orch-snap-entry{display:none;align-items:center;gap:5px}
/*
 * 「显示右侧栏」（顶栏）。
 *
 * ★ 它的显隐**两支都写在媒体查询里**，不写"裸的默认值" ★
 *   这一条踩过一次真实的坑（本次改造中实测）：如果写成
 *   "裸规则默认可见 + 窄屏媒体查询隐藏"，两支的**特异度完全相同**
 *   （都是 .orch-head #orchRightToggle），胜负就完全由样式表里的
 *   **先后顺序**决定 —— 而窄屏那支属于"顶栏收纳"那一段，位置在文件靠后，
 *   于是"隐藏"在所有宽度下都赢，按钮在桌面上也被一直藏着，且不报任何错。
 *   把两支都放进各自的媒体查询，顺序就再也影响不到结论：
 *   同一宽度下永远只有一支在生效。这类"看不出错在哪"的失配，
 *   只能靠结构（而不是靠记住别乱放）来避免。
 *
 * 为什么宽屏必须有它：右栏收起后，这是**唯一**能把右栏找回来的入口。
 * 为什么窄屏不能有：窄屏右栏整列退出布局（.orch-col-run{display:none}），
 * 内容由底部抽屉承载（⋯ 面板里的「快照」/「版本历史」），
 * "整栏折不折"无从谈起 —— 与"那一列不存在"是同一条口径的两面。
 * 断点 1080 与 .orch-cols 的窄屏网格、layout-resize.js 的 desktopQuery 同源。
 */
@media (min-width:1081px){
  .orch-head #orchRightToggle{display:inline-flex;align-items:center}
}
@media (max-width:1080px){
  .orch-head #orchRightToggle{display:none}
}

/* ---------- 底部抽屉（bottom sheet）· 唯一宿主 ---------- */
.sheet{
  position:fixed;inset:0;z-index:var(--z-sheet,50);
  display:flex;flex-direction:column;justify-content:flex-end;
}
.sheet.hidden{display:none}
.sheet-mask{
  /*
   * 遮罩是抽屉里**最低的一层**。
   *
   * z-index:0 是显式声明而不是靠"它写在 .sheet-panel 之前"这个 DOM 顺序：
   * 面板自己有 z-index:1，一旦有人给遮罩也加个 z-index（或调整 DOM 顺序），
   * 两者的上下关系就从"隐含"变成"看运气"。这里把层级写死成两层：
   *   遮罩 z-index:0（铺满整个 .sheet，只有面板没盖住的上方那条是可见的）
   *   面板 z-index:1（从底部升起，盖住遮罩的下半部分）
   * 于是"点上方那条黑边关抽屉"在结构上一定成立。
   */
  position:absolute;inset:0;z-index:0;
  background:rgba(0,0,0,.45);
  animation:sheet-fade 180ms ease-out;
}
@keyframes sheet-fade{from{opacity:0}to{opacity:1}}
.sheet-panel{
  position:relative;z-index:1;
  display:flex;flex-direction:column;
  /*
   * 用 dvh 而不是 vh：iOS Safari 上 70vh 是「含地址栏」的高度，
   * 抽屉底部会被浏览器 chrome 顶掉一截（表现为最后一条快照看不全）。
   * dvh 跟着可视视口走；不支持 dvh 的老浏览器仍由上一行的 vh 兜底。
   */
  max-height:70vh;height:70vh;
  max-height:70dvh;height:70dvh;
  background:var(--panel);color:var(--ink);
  border-radius:16px 16px 0 0;
  box-shadow:0 -8px 32px rgba(0,0,0,.22);
  /*
   * 底部让开 iPhone 的 Home 指示条：只有安全区变量，不写死像素——
   * 不同机型横条高度不同，写死必然在某些机型上被横条压住。
   */
  padding-bottom:env(safe-area-inset-bottom);
  animation:sheet-up 220ms cubic-bezier(.33,1,.68,1);
  /* 下拉关闭时用 transform 跟手，所以这里声明过渡（松手弹回用） */
  transition:transform 200ms cubic-bezier(.33,1,.68,1);
}
@keyframes sheet-up{from{transform:translateY(100%)}to{transform:translateY(0)}}

/* 抓取手柄：既是「可下拉关闭」的视觉提示，也是下拉手势的命中区 */
.sheet-grip{
  display:flex;align-items:center;justify-content:center;
  padding:9px 0 5px;flex:none;cursor:grab;touch-action:none;
}
.sheet-grip span{display:block;width:38px;height:4px;border-radius:2px;background:var(--line-strong)}
.sheet-head{
  display:flex;align-items:center;gap:8px;flex:none;
  padding:2px 12px 8px;border-bottom:1px solid var(--line-soft);
}
.sheet-tabs{display:flex;gap:4px;flex:1}
.sheet-tab{
  display:inline-flex;align-items:center;gap:5px;
  border:0;background:transparent;color:var(--ink-faint);font:inherit;
  font-size:13px;padding:8px 12px;border-radius:8px;cursor:pointer;
  min-height:var(--tap);   /* HIG：底部 tab 必须够大 */
}
.sheet-tab.is-active{color:var(--ink-strong);font-weight:600;background:var(--panel-2)}
/* 关闭键要够大好点：icon-btn 默认偏小 */
.sheet-head .icon-btn{width:var(--tap);height:var(--tap);font-size:20px;flex:none}
.sheet-body{
  flex:1;min-height:0;display:flex;flex-direction:column;
  /* 抽屉内部自己滚；列表长短都不影响外层 */
  overflow:auto;-webkit-overflow-scrolling:touch;
  /* 阻止滚动链传到背后的页面（iOS 上手感最明显的一个坑） */
  overscroll-behavior:contain;
  padding:10px 12px;
}
.sheet-pane{min-height:0}
.sheet-pane.hidden{display:none}
.sheet-foot{
  flex:none;display:flex;gap:8px;padding:9px 12px;
  border-top:1px solid var(--line-soft);
}
.sheet-foot:empty,.sheet-actions:empty{display:none}
.sheet-actions{display:flex;gap:8px;flex:1}
.sheet-actions .orch-btn{flex:1;min-height:var(--tap)}

/* 抽屉打开时锁背景滚动：只锁 body，不动 #nodeList 的内部滚动位置 */
body.sheet-open{overflow:hidden;touch-action:none}

/* 把版本面板的正文搬进抽屉后，去掉桌面容器的内边距 */
.sheet-pane .ver-panel-main{display:flex;flex-direction:column;gap:8px}
.sheet-pane .snap-list{overflow:visible;padding:0}

/* =====================================================================
 * 窄屏（<=1080px，与 JS 的 SHEET_MQ 同一条判据）
 * ===================================================================== */
@media (max-width:1080px){
  /*
   * ================= 编排页顶栏：两行栅格（结构，不是间距） =================
   *
   * 修的是一个真实的结构缺陷（iPhone 14 · 390px 实测；761–1080px 同样命中）：
   *   .orch-head 是**单行 flex**，8 个控件自然宽合计 577px，
   *   而 .orch-head 没有横向滚动，于是右侧三个控件（⋯ / 快照 / 对话·编排·日志）
   *   的右缘落在 400 / 470 / 569 —— 全部在视口外，且**没有任何办法够到**。
   *   这不是"排得挤"，是顶栏根本没有承载这些控件的空间。
   *
   * 为什么不是给顶栏加 overflow-x:auto：
   *   触屏上的横向滚动条不可见，用户看不出"右边还有东西"，
   *   等于把"永久不可达"换成"藏起来不可达"，问题只是换了个长相。
   *
   * 断点取 1080px 而**不是** 760px：顶栏那一行（8 个控件 + 页面导航）
   * 的自然宽度实测约 817px，所以 761–1080px 这段"平板/窄桌面"区间同样放不下，
   * 实测 900px 时 .head-nav 的右缘落在 1020px（出屏 120px）、
   * 1000px 时落在 1020px（出屏 20px）—— 与手机上是同一个缺陷。
   * 1080px 也正是本页既有的形态判据（JS 的 SHEET_MQ 与两栏折叠同源），
   * 复用它而不新造一个阈值：顶栏放不下与右栏该折叠发生在同一个宽度，
   * 两者本来描述的就是"这个页面已经进入窄形态"。
   *
   * 结构性解法：窄屏把顶栏从"一行塞不下"改成**两行显式栅格**——
   *   第一行：导航/身份（☰ · 设置 · 工作流名 · 草稿态 · 保存）
   *   第二行：动作与出口（⋯ · 快照 · 对话/编排/日志）
   * 每行都只放得下自己那一组，冲突从"宽度不够"变成"不需要宽度"。
   * 栅格轨道用 minmax(0,1fr)，任何一格都不会把整行撑破。
   *
   * DOM 与桌面端**完全一致**（同一批节点，只换布局），
   * 不搬动节点、不复制一份 —— 复制必然出现"两边状态不同步"。
   */
  .orch-head{
    /*
     * z-index 在这里是**结构必需**，不是微调：
     * 之前 .orch-head 只有 position:relative、没有 z-index ⇒ 不产生层叠上下文，
     * 于是它内部 ⋯ 面板的 z-index **只在 head 自己的层叠顺序里有效**，
     * 而 .orch-cols 在 DOM 里排在 head 之后，整体压在上面 ——
     * 实测点开 ⋯ 后面板 display:flex、几何正常（t=104..258），**却完全看不见**。
     * 这类"DOM 里存在、几何也对，就是看不到"的坑只能靠建立层叠上下文解决，
     * 调面板自己的 z-index 是没用的（它在错误的层里比大小）。
     */
    position:relative;z-index:var(--z-head-overlay);
    display:grid;height:auto;
    /*
     * ============ 一排栅格（用户 2026-09-23 要求压缩） ============
     *
     * 之前是两排：`menu | name | save` + `more | badge | acts`，
     * 实测吃掉 101px（屏高 664px 的 15%）。逼出第二排的**唯一**原因是
     * 草稿徽章那串长文字（「已修改 · 基于「09-22 …」」实测 150px）。
     *
     * 压成一排的机制有两处，缺一不可：
     *   ① 徽章改成**短形态**（见 .draft-badge 的窄屏规则）——它已经有完整的
     *      title 提示，可见文字从长句压成两三个字不丢信息；
     *   ② 工作流名允许被压缩（minmax(0,1fr) + 省略号）——它是这一排里
     *      唯一"越长越没用"的东西（长名称看不全也不影响操作），
     *      所以让出空间的第一顺位是它。
     *
     * 轨道顺序按**用户扫视顺序**排：左→右 = 菜单、名称、状态、
     * 快照、更多、设置。徽章紧跟名称（都是"这块东西是什么"），
     * 动作类集中在右侧（都是"能对它做什么"）。
     *
     * 保留 grid 而不是退回 flex：栅格让每一格的归属显式，
     * 且下方"极窄屏回退两排"只需再写一条 grid-template-areas。
     */
    /*
     * 名称轨道用 minmax(<最小可读宽>, 1fr) 而**不是** minmax(0,1fr)：
     * 0 会让这一列在空间紧张时被挤成 0px（实测 320/390px 下都是 0px），
     * 用户就看不见自己在编辑哪个工作流了 —— 功能缺失，不是省空间。
     * 给一个下界（6em ≈ 五个汉字）后，栅格不会把名称压没；
     * 真正需要收缩时优先牺牲的是名称的**内容**（省略号），而不是宽度下界。
     *
     * ============ 一排只放五格：menu | name | badge | qsave | more ============
     *
     * 实测（390px 视口）：全部七个控件的自然宽合计 461px，而可用只有 374px，
     * 一排**结构上装不下**。逐个给元素设 min-width 只是把溢出从 A 推到 B
     *（实测：给名称下界后溢出 44px，给徽章下界后换成名称被压成 0px），
     * 那是打补丁，不是解决。
     *
     * 结构性的答案是**减少这一排的格数**。判据是"有没有独立入口的必要"：
     *   ☰        展开工作流列表 —— 页面级导航，必须常驻
     *   名称     当前编辑对象   —— 必须常驻（否则不知道在改谁）
     *   状态徽章 存没存         —— 必须常驻（唯一的状态权威显示）
     *   快速保存 主操作         —— 必须一键可达
     *   ⋯       其余动作的入口 —— 必须常驻（它是收纳层的门）
     * 而下面两个与 ⋯ 的职责**同族**（都是"打开一层去做别的事"），
     * 因此并入 ⋯ 面板，不占常驻格：
     *   设置 —— ⚙ 打开设置弹窗（低频、配一次就不动）
     *   快照 —— 打开快照抽屉（查看型，且有数量徽标提示）
     * 并入后实测只需 322px，390px 下名称还有富余，320px 下也留得住 66px。
     *
     * 注意：桌面（>1080px）**不受影响** —— 那边一行放得下，两个控件照常常驻。
     * 这条规则只在窄屏栅格里生效。
     */
    grid-template-columns:auto minmax(6em,1fr) auto auto auto;
    grid-template-areas: "menu name badge qsave more theme";
    align-items:center;
    column-gap:6px;
    padding:6px 8px;
  }
  /* 每一格的归属是显式的：DOM 顺序与视觉顺序解耦，靠 area 落位 */
  /*
   * ☰ 展开工作流列表。桌面端它是「裸图标按钮」（无边框、小内边距），
   * 在手机上必须是一个完整的触控目标：--tap × --tap。
   * 只给宽高而不加边框/底色，保住桌面那种"安静"的观感。
   */
  /*
   * ☰ 展开工作流列表。
   *
   * 视觉取 --tap-visual、热区补到 --tap（与全站图标按钮同一套）：
   * 它是手机上打开工作流列表的唯一入口，可点性绝不能降，
   * 但它同时只是一行的"首页符号"，用 44px 方块会显得笨重。
   */
  .orch-head #orchMenu{
    grid-area:menu;position:relative;
    min-width:var(--tap-visual);min-height:var(--tap-visual);
    display:grid;place-items:center;padding:0;
  }
  .orch-head #orchMenu::after{
    content:'';position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
    width:var(--tap);height:var(--tap);
  }
  /*
   * 设置与快照在窄屏**移出常驻格**，改为 ⋯ 面板的两个段落（见下）。
   * 它们是同一个 DOM 节点，不是复制品 —— 只是位置归属变了。
   */
  .orch-head #orchSettingsBtn{display:none}
  .orch-head #orchSnapEntry{display:none}
  .orch-head #wfName{grid-area:name;min-width:0}
  .orch-head #draftBadge{grid-area:badge;justify-self:start;margin:0}
  .orch-head #wfQuickSave{grid-area:qsave;justify-self:end}
  .orch-head #orchMore{grid-area:more;justify-self:end}
  /*
   * #orchThemeBtn 的轨道也必须是显式的：窄屏它是 display:none（随导航进 ⋯ 面板），
   * 但若不给它 aria/轨道归属，将来谁把它改回常驻时它会落进"隐式自动轨道"，
   * 在窄屏凭空多出一列、把这一排撑破 —— 而那时错误只会表现为"顶栏右边出屏"。
   * 显式轨道的成本是一行，收益是这类漂移不可能发生。
   */
  /*
   * 快照入口与页面导航（对话/编排/日志）**分别占行**，不抢同一格。
   * 实测：两者共格时快照 301–369、导航 288–382 互相压住，
   * 点下去命中的是哪个取决于 DOM 顺序 —— 这类"看得见却点错"的坑
   * 不能靠调 justify 解决，只能让它们各自有位置。
   * 快照是可带徽标的动态宽度，导航是固定三项，本就该分行。
   */
  /* 快照入口的常驻位由上面的 display:none 关闭；展开态见 ⋯ 面板段 */
  /*
   * 页面导航（对话/编排/日志）：窄屏**收进 ⋯ 面板**，不占常驻行。
   *
   * 为什么不给它自己的行：实测三行顶栏吃掉约 250px 竖向空间（屏高 664px，
   * 占 38%），而这三个链接是"离开本页"的低频动作 —— 常驻的代价远大于收益。
   *
   * 为什么收进 ⋯ 而不是新造一个"更多"入口：⋯ 面板本来就是本页窄屏
   * **低频道动作的唯一归宿**（保存…/版本历史/删除都在里面）。
   * 再开一个平行入口就是第二个权威，用户得记住"哪个动作在哪一层"。
   *
   * 实现是**同一个 DOM 节点的位置归属变化**：桌面端 .head-nav 在顶栏，
   * 窄屏把 .orch-actions 面板撑成 .head-nav 的宿主（见下），
   * 不复制链接、不改 JS —— 复制一份必然两边状态不同步（aria-current 等）。
   */
  .orch-head .head-nav{
    grid-area:nav;
    display:none;              /* 常驻位隐藏；它随后被"搬进"展开的 ⋯ 面板 */
  }
  /*
   * 主题按钮在窄屏（≤1080px）同样**不占常驻格**：常驻格里每一格都要靠
   * "减少格数"换来（见上面的栅格说明），而主题是全局外观、低频动作，
   * 与设置/快照同族 —— 同族的东西归宿相同，因此它随导航一起进 ⋯ 面板，
   * 但在面板里**排在最前**（它是全局控件，不是本页动作）。
   * 这是同一个 DOM 节点的位置归属变化，不是复制。
   */
  .orch-head #orchThemeBtn{
    grid-area:theme;
    display:none;              /* 常驻位隐藏；展开的 ⋯ 面板里显示（文件末尾的分段段） */
  }
  /*
   * ⚠ 只认 #orchThemeBtn，**不写 .theme-toggle 通配**：
   *   通用外观 .theme-toggle 是三页共用的（无媒体查询），而"窄屏进面板"是
   *   编排页独有结构。混在一起会让日志页那颗主题按钮在 ≤1080px 凭空消失
   *   —— 它是 .log-head 的普通子项，那里根本没有"面板"可进。
   *   同一类名在两种结构里含义不同时，必须让结构规则按 id 落到具体页面。
   */
  /*
   * ⋯ 展开时导航显示在面板正下方，与面板连成一块。
   * 两者都是绝对定位、都以 head 底边为基准，导航再用 --orch-actions-h 让开面板，
   * 因此"面板多高"只在一个常量里描述，不靠运行时测量。
   */
  /*
   * 引导卡（主题）→ 动作面板 → 设置 / 快照 / 导航，**拼成一张卡片**：
   * 相邻两段的交界共用一条 1px 分割线，整张卡只有最上面一段有上圆角、
   * 最下面一段（导航）有下圆角，投影只在导航块口下沿投一次。
   *
   * 为什么圆角/边框必须集中写在这一处：这是由**多个节点共同实现**的一副外观，
   * 若各段自己写自己的圆角，改一处就会出现"某一层还是方的"这种错位
   *（历史实测：面板与导航曾错位 2px、快照与导航互相压住）。
   *
   * 2026-10-17：卡片最上面一段换成**主题引导卡**（见文件末尾的分段段），
   * 因此这里不再有上圆角；引导卡自己没有圆角，圆角由它的上一段决定 ——
   * 首段是卡片里唯一"自己的外观由自己圆角"的例外，见该段规则。
   */
  .orch-head:has(#orchActions.is-open) .orch-actions{
    border-radius:0;border-bottom:0;
  }
  .orch-head:has(#orchActions.is-open) .head-nav{
    display:flex;
    position:absolute;right:8px;
    /* 紧接面板下沿：--orch-actions-h 是面板的确定高度（见上面的定义），
       再减去 1px 让两块共用一条分割线而不是两条贴在一起。
       （100% 以下各段的最终落点由文件末尾「⋯ 面板的分段」段统一重写，
         那里是"面板里到底有几段"的唯一权威。） */
    top:calc(100% + 4px + var(--orch-actions-h) - 1px);
    z-index:var(--z-head-overlay);
    flex-direction:column;align-items:stretch;gap:2px;
    background:var(--panel);border:1px solid var(--line);border-top:0;
    border-radius:0 0 10px 10px;
    box-shadow:0 10px 28px rgba(0,0,0,.18);padding:6px;padding-top:8px;min-width:170px;
  }
  /* 导航三项与面板里的动作按钮**同款**：同一张卡片里不该出现两种行高/字号 */
  .orch-head:has(#orchActions.is-open) .head-nav .view-toggle{
    min-height:var(--tap);justify-content:flex-start;padding:0 10px;font-size:13px;border-radius:8px;
  }
  /* 导航与动作之间补一条分隔线，表达"上面是操作、下面是离开本页" */
  .orch-head:has(#orchActions.is-open) .head-nav::before{
    content:'';display:block;height:1px;background:var(--line);margin:0 0 6px;
  }

  /*
   * 顶栏按钮统一成触控尺寸。--tap 由 :root 定义（全站唯一权威），
   * 这里不再写 34px 这种"看起来差不多"的魔法数。
   */
  /*
   * 顶栏按钮（☰ / 快速保存 / ⋯）。
   *
   * 尺寸同样分两层：视觉用 --tap-visual，热区由各自的 ::after 撑到 --tap。
   * 顶栏是手机上最挤的一行，44px 见方会让它占掉屏高的 8% 还显得笨重 ——
   * 而顶栏按钮的**可点性**不能降（☰ 是打开工作流列表的唯一入口），
   * 所以走的还是"看着小、热区够"这一条。
   */
  .orch-head .orch-btn{min-height:var(--tap-visual);font-size:12px;padding:0 10px}
  .orch-more-btn{display:inline-flex;align-items:center;justify-content:center;min-width:var(--tap-visual)}
  .orch-snap-entry{display:inline-flex}
  /* 工作流名占满中间格：它是这一行里唯一需要弹性宽度的东西 */
  .orch-head .wf-name{width:100%;font-size:16px}
  /*
   * 模型下拉在编排区工具行里，桌面高 31px（鼠标够用）。
   * 手机上 38px 仍然点不准 —— 它是"这条流水线用哪个模型"的唯一入口，
   * 点不中就没法改。按 --tap 放大，并让文字在框内垂直居中。
   */
  /*
   * 模型下拉：与同排的图标按钮**同高**（--tap-visual），视觉上齐平。
   * 它保留文字（"现在用哪个模型"是身份信息，换成图标没有意义），
   * 所以宽度随内容受 max-width 约束（见移动到末尾的那段）。
   */
  .wf-model select{min-height:var(--tap-visual);height:var(--tap-visual);padding:0 8px}

  /*
   * ⋯ 面板常驻在 .orch-head 的栅格里（绝对定位，展开不撑高顶栏），
   * 与下面的页面导航构成**同一层浮层**，两者 z-index 同族（见 --z-head-overlay）。
   *
   * --orch-actions-h 是它展开后的**确定高度**：3×--tap + 2×4(gap) + 2×6(padding) + 2(border)。
   * 面板内容固定（就是这三个动作按钮）所以是常量而不是测量值；
   * 若将来增删动作，必须同步改这里 —— 页面导航的落点依赖它。
   */
  .orch-head{--orch-actions-h:calc(var(--tap) * 3 + 4px * 2 + 6px * 2 + 2px)}
  .orch-actions{
    display:none;position:absolute;right:8px;top:calc(100% + 4px);
    z-index:var(--z-head-overlay);
    flex-direction:column;align-items:stretch;gap:4px;
    background:var(--panel);border:1px solid var(--line);border-radius:10px;
    box-shadow:0 10px 28px rgba(0,0,0,.18);padding:6px;min-width:170px;
  }
  .orch-actions.is-open{display:flex}
  .orch-actions .orch-btn{min-height:var(--tap);justify-content:flex-start;text-align:left}
  /*
   * 「显示导航」开关的开启态。
   *
   * 它与其它面板按钮的区别是：那些是**一次性动作**（点了就走），
   * 这一个是**持续状态**（现在开不开）。所以只有它需要"开/关"两种长相，
   * 也只有它对读屏暴露 aria-checked —— 用背景与左侧色条表达当前是开着的，
   * 不靠颜色单独传达（色条 + 背景 + 文字三者同时变）。
   */
  /*
   * ★ 这一段原先给"显示导航"开关做 ⋯ 面板里的开关长相（整行高亮 + 左色条）★
   *   用户 2026-10-01 把该开关挪进了右栏 tab 条，而右栏在窄屏整列不参与布局
   *   —— 也就是说，窄屏上根本没有这个开关了。
   *   留着这条规则没有消费者，只会让下一个人以为"这里还有个开关"。
   *   右栏 tab 条上那个开关的长相由 .run-icon-btn.is-on 负责（见上）。
   */

  /* iPhone 上没有 hover，触控目标一律放大到 --tap */
  .orch-btn{min-height:var(--tap)}
  /*
   * AI 结果区的「复制」：桌面是 37×23 的小 chip（鼠标够用），
   * 手机上必须按 --tap。它挂在结果区标题行上，加宽不影响排版。
   */
  .t-ai-result-copy{min-height:var(--tap);min-width:var(--tap);padding:0 10px;font-size:13px}
  .snap-main{min-height:var(--tap)}
  .snap-del{min-width:var(--tap)}
  .ver-act{min-height:var(--tap)}

  /*
   * 右栏不再承担「常驻面板」职责：版本与快照都搬进抽屉。
   * 这里让 orch-col-run 不占高度——它若还留着，就会在消息序列上方
   * 压出一块空白（这正是用户说的「积压空间」）。
   */
  .orch-cols{
    grid-template-columns:minmax(0,1fr);
    grid-template-rows:minmax(0,1fr);
  }
  .orch-col-run{display:none}
  /*
   * 设置区与它右缘的手柄同时退出（与右栏同理：没有分栏，就没有分栏间的边界）。
   * 设置内容此时走弹窗（同一批 #sec-* 节点）。
   */
  .orch-col-set,.orch-set-resizer,.orch-side-resizer{display:none}
  .orch-col{padding:14px 12px calc(60px + env(safe-area-inset-bottom));gap:20px}
  /*
   * 分隔条必须与右栏同时消失：没有两栏，就没有两栏之间的边界。
   * 与 orch-split-resize.js 的 DESKTOP_QUERY（min-width:1081px）互补，
   * 两处口径必须严格互斥——不一致时会出现"看不见但能拖"的幽灵手柄。
   */
  .orch-split-resizer{display:none}
}

@media (max-width:760px){
  /*
   * ================= 消息工具条：换行（结构，不是缩字号） =================
   *
   * 实测根因：.t-actions 是**不换行的单行 flex**，10 个按钮自然宽 326px，
   * 而消息列在 390px 屏上只有约 300px（还要给头像/序号让位），
   * 于是右缘冲到 600px：删除 / 裁掉后续 全在视口外，手指够不到。
   *
   * 窄屏本来就常显这排按钮（opacity:1），所以它必须真正可点。
   * 结构性解法：让它**换行**，用 flex-wrap 把"一行放不下"变成"排到下一行"。
   * 不缩字号（缩到 10px 仍然溢出，只是把溢出的量变小，问题还在）。
   */
  .t-actions{flex-wrap:wrap;margin-left:0;width:100%;row-gap:5px;column-gap:5px;justify-content:flex-start}
  /*
   * 文字按钮（↑ / ↓ / 删除 / 裁掉后续）宽度随文字，双向箭头「↑↓」只有 33px
   * ——够不到 --tap。统一给一个最小宽度，一排按钮左右对齐也更整齐。
   */
  /*
   * 节点操作条（🕒 / ↕ / ↑ / ↓ / 插入 / ⋯ / 删除 / 裁掉后续）。
   *
   * ★ 这里同样分成"热区"与"视觉尺寸"两件事 ★
   *   上一版这一排也是 --tap 见方，实测截图里三个图标各占一个 44px 方块，
   *   比消息气泡的标题行还高 —— 而它们是**次要操作**，不该比内容更抢眼。
   *   现在：视觉 --tap-visual，热区仍由 ::after 撑到 --tap。
   *
   * 文字按钮（↑ ↓ 删除 裁掉后续）保留文字，所以宽度随内容、只给 min-width：
   * 它们不是图标按钮，没有可省的字（"删除"省掉就认不出了）。
   */
  .t-actions button{min-height:var(--tap-visual);min-width:var(--tap-visual);padding:0 9px;justify-content:center}
  .t-actions button.is-icon{width:var(--tap-visual);padding:0}
  /* 图标按钮的热区扩展：与 .btn-iconable 同一机制、同一份尺寸权威 */
  .t-actions button.is-icon{position:relative}
  .t-actions button.is-icon::after{
    content:'';position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
    width:var(--tap);height:var(--tap);
  }
  /*
   * 工具条换行后，元信息行（头像/名称/序号/工具条）必须整体变成纵向栈：
   * 原来 .t-meta 是 nowrap 的单行，工具条换行会把这一行撑得更宽，
   * 换行等于白做。让 .t-meta 允许换行，工具条独占第二行。
   */
  .t-meta{flex-wrap:wrap;white-space:normal;row-gap:6px}
  .t-meta > .t-actions{flex:1 1 100%}
  /*
   * ★ 按钮排满一行时"不要被拉开"（用户 2026-10-01 反馈后修正）★
   *
   * .t-actions 是 flex 且 width:100%（上面那条，为的是能换行），
   * 于是它的 justify-content 默认值会让按钮沿整行铺开、相互之间留出大缝 ——
   * 实测截图里 🕒 / ↕ / ⋯ 三个按钮各占 34px 却被拉开成横跨整行，
   * 看起来像"三个大按钮"，用户的原话就是"这些图标还是很大"。
   * 视觉尺寸明明已经缩到 34px，**让它们显得大的其实是间距**。
   *
   * 靠左紧排、间距收到 5px：按钮恢复成"一小簇次要操作"该有的样子，
   * 而右侧空出来的地方本来也没有内容需要占（这排按钮是这条消息的附属动作）。
   * 与 .orch-actions（顶栏 ⋯ 面板）刻意不同：那边是菜单项，要拉满点击宽度；
   * 这边是图标簇，挤在一起才好找。
   */
  .t-meta > .t-actions{justify-content:flex-start;column-gap:5px}
}

/*
 * ============ ⋯ 面板的分段（窄屏）：动作 / 设置 / 快照 / 页面导航 ============
 *
 * 顶栏一排只留五格（menu | name | badge | qsave | more，见上面栅格的说明），
 * 「设置」与「快照」的常驻位在窄屏被关闭，改由这里在 ⋯ 面板中显示。
 *
 * 为什么用"面板分段"而不是各写各的浮层：
 *   它们本来就是**同一层收纳**（用户点 ⋯ 一次性看到所有低频动作），
 *   各自绝对定位会得到四块互相对不齐的卡片（实测过：面板与导航曾经
 *   错位 2px、快照与导航互相压住）。这里统一由 --orch-actions-h 这一条
 *   高度常量串起来，每段紧接上一段，纵向连成一张卡片。
 *
 * 高度的唯一权威是 --orch-actions-h（面板本体高度）。分段用它的整数倍压栈：
 *   段 1  动作（保存…/版本历史/删除/显示导航）  = --orch-actions-h
 *   段 2  设置                                  紧随其后
 *   段 3  快照                                  紧随其后
 *   段 4  页面导航（对话/编排/日志）             紧随其后
 * 因此"面板里多一个按钮就要改这里"这件事是显式的（改 --orch-actions-h
 * 的定义处），而不是靠运行时测量去猜位置。
 */
@media (max-width:1080px){
  /*
   * 面板现在是**四段**：动作 + 设置 + 快照 + 页面导航。
   * 动作段本身是 4 个按钮（保存…/版本历史/删除/显示导航）。
   */
  .orch-head{--orch-actions-h:calc(var(--tap) * 4 + 4px * 3 + 6px * 2 + 2px)}
  /* 分段共用的外观：与面板同宽、同底色、彼此之间不留缝，连成一张卡片 */
  .orch-head:has(#orchActions.is-open) #orchSettingsBtn,
  .orch-head:has(#orchActions.is-open) #orchSnapEntry{
    display:inline-flex;align-items:center;justify-content:center;
    position:absolute;right:8px;
    z-index:var(--z-head-overlay);
    min-width:170px;min-height:var(--tap);
    background:var(--panel);border:1px solid var(--line);border-top:0;
    border-radius:0;
  }
  /* 设置紧接动作段（面板本体）之后 */
  .orch-head:has(#orchActions.is-open) #orchSettingsBtn{
    top:calc(100% + 4px + var(--orch-actions-h));
  }
  /* 快照紧接设置段之后 */
  .orch-head:has(#orchActions.is-open) #orchSnapEntry{
    top:calc(100% + 4px + var(--orch-actions-h) + var(--tap));
  }
  /*
   * 页面导航是最后一段：它要再让开"设置 + 快照"两段（各 --tap 高）。
   * 这条把原来写死的 top 改成按段数累加，于是"以后再加一段"
   * 只需在这里多加一个 --tap，不必重算所有数字。
   *
   * 2026-10-17 起，导航块内部还有一条**主题行**（#orchThemeBtn 进来的位置），
   * 见紧接着的分段 list 与末尾的落点表。
   */
  .orch-head:has(#orchActions.is-open) .head-nav{
    top:calc(100% + 4px + var(--orch-actions-h) + var(--tap) * 2 - 3px);
    border-radius:0 0 10px 10px;
  }
  /* 分段里的按钮不该有各自的边框（否则卡片里出现横七竖八的框线） */
  .orch-head:has(#orchActions.is-open) #orchSettingsBtn,
  .orch-head:has(#orchActions.is-open) #orchSnapEntry{
    border-left:0;border-right:0;border-radius:0;
  }
  /*
   * ============ 主题行：卡片的最上一段，位于动作段之上 ============
   *
   * 为什么在动作段**之上**而不是之下：卡片的分段是一条从"全局"到"本页"的
   * 单向序 —— 主题是全局外观、动作是本页动作、导航是离开本页。
   * 排在动作（也就是本页动作层）下面，用户会以为主题是这一页私有的开关。
   *
   * 落点用 -1px 与动作段共用一条分割线（与导航块同一手法）：
   * 两个盒子各自画边框，就会在交界处出现两条 1px 线，看起来像两张卡。
   */
  /*
   * ============ 主题行：卡片的最上一段 ============
   *
   * 为什么在动作段**之上**而不是之下：卡片的分段是一条从"全局"到"本页"的
   * 单向序 —— 主题是全局外观、动作是本页动作、导航是离开本页。
   * 排在动作层下面，用户会以为主题是这一页私有的开关。
   *
   * 为什么是"整行按钮"而不是一颗 34px 图标：分段的每一段都是整宽行
   *（设置 / 快照 / 导航同宽），中间夹一颗小圆钮会让首段看起来是"多出来的
   *  一点东西"，而且它比相邻行窄，卡片上沿就出现一处凹口。
   *
   * 几何：上沿 = 面板顶边 - 自身高 + 1px。
   * 直接写 -7px（也就是"长在面板上面"）会冒到顶栏下沿之外盖住顶栏，
   * 所以整张卡片必须**从顶栏下沿往下**连续排开：
   *   顶栏底 → 4px 空档 → 主题行 → 动作面板（共用 1px 分割线）
   *   → 设置 → 快照 → 导航（卡片下沿，投影只在这里投一次）。
   */
  .orch-head:has(#orchActions.is-open) #orchThemeBtn{
    display:inline-flex;align-items:center;justify-content:flex-start;
    position:absolute;right:8px;
    top:calc(100% + 4px + var(--orch-actions-h) - var(--tap));
    z-index:var(--z-head-overlay);
    /* 整行按钮：宽度由右缘对齐卡片，文字靠左，与下面各段的行内起点对齐 */
    right:8px;left:auto;width:auto;height:auto;min-height:var(--tap);
    /*
     * ⚠ 必须 flex:none：绝对定位的盒子仍会按内容的 shrink-to-fit 宽收缩，
     *   一旦它参与收缩，宽度就被压到只够一个字形 —— 实测"☾ 切到深色"
     *   只剩一个 ☾ 露在右缘，看起来像"文字没渲染"，实则是盒子被压扁了。
     */
    flex:none;
    min-width:170px;box-sizing:border-box;
    /* 首段负责卡片上圆角与上边框：整张卡只有一个"上面"，就是它 */
    padding:0 12px;gap:6px;font-size:13px;
    border:1px solid var(--line);border-bottom:0;border-radius:10px 10px 0 0;
    background:var(--panel);
    /* 本行**不投影**：投影只在卡片下沿（导航块）投一次 */
  }
}

/* 横条/刘海机型：顶栏也避开顶部安全区 */
@supports (padding: env(safe-area-inset-top)){
  .orch-head{padding-top:env(safe-area-inset-top)}
}

/* =====================================================================
 * 日志页顶栏：主题按钮永远常驻（用户 2026-10-17 要求）
 * =====================================================================
 *
 * 日志页顶栏在窄屏是"一条不滚动的行"（见 log.css 同段说明），
 * 宽度预算是按项算过的：身份区 + 自动刷新 + 刷新 + ⋯ + 导航。
 * 主题按钮是**恒定的 34px 方块**（.theme-toggle 用 --tap-visual），
 * 这里只补一条：它必须站得住，不许被压缩。
 *
 * 为什么用 id 而不是 .theme-toggle：主题按钮的通用外观归 .theme-toggle，
 * 而"在这条行里不许压缩"是 .log-head 这个容器的约束。
 * 把容器约束写到共用类上，将来聊天页/编排页的主题按钮也会莫名带上它 ——
 * 一处容器的问题挂在通用类上，是"规则跑到了管辖范围之外"。
 */
.log-head > #logThemeBtn{flex:none}
/*
 * 窄屏（≤760px）顶栏是"一条不滚动的行"，宽度预算按项算过。
 * 这颗按钮在那一行里必须按 --tap 放大到可点（与相邻按钮同一口径），
 * 但**不允许把文字省掉**：省了文字就退回"一个 ☾"，语义又变得要靠猜。
 * 它比"刷新"略宽是划算的 —— 主题是全局控件，用户切页前最常碰的就是它。
 */
@media (max-width:760px){
  .log-head > #logThemeBtn{min-height:var(--tap);padding:0 8px;font-size:11px}
}

/* 减少动效偏好：抽屉不做滑入动画（与站内其它动效一致） */
@media (prefers-reduced-motion:reduce){
  .sheet-panel{animation:none;transition:none}
  .sheet-mask{animation:none}
  .snap-toggle-caret{transition:none}
}

/* =====================================================================
 * 设置弹窗（工具 / 价格 / 变量）· 移动端
 *
 * 三块配置在 iPhone 上此前直接沿用桌面排版，有三个真实问题：
 *   ① 弹窗贴满整屏且没有内部滚动边界 → 底部内容被 Home 横条压住点不到；
 *   ② 工具行的复选框只有 18px（桌面用鼠标够用，手指点不中）；
 *   ③ 价格三列在 375px 下被挤成很窄的输入框，数字几乎看不见。
 * 这里按「能用手指操作」重排，不改任何语义。
 * ===================================================================== */
@media (max-width:760px){
  .modal{padding:0;align-items:flex-end}
  .modal-card{
    width:100%;max-width:100%;max-height:92dvh;
    border-radius:16px 16px 0 0;
    padding-bottom:env(safe-area-inset-bottom);
  }
  .modal-settings .modal-body{gap:14px;padding:14px 14px calc(14px + env(safe-area-inset-bottom))}
  /*
   * 复选框放大到可点。
   *
   * 范围是**设置弹窗里的全部复选框**，不只是工具行的：
   * 原先只写 `.tool-row input[type=checkbox]`，于是「变量」区的
   * 「必填」复选框（#varNewReq）漏网，仍是桌面的 13px —— 手指点不中。
   * 这类"漏了一个同类控件"的失效只有真机点过才会发现，
   * 所以规则按**控件类型**收敛（弹窗内所有 checkbox），不按容器逐个列。
   * 视觉仍是小方框（22px），四周由行高撑出更大的命中区。
   */
  #orchSettingsModal input[type=checkbox]{width:22px;height:22px}
  .tool-row{padding:12px 11px;gap:9px}
  .tool-row-main{font-size:13px}
  /* 工具行右侧控件列收窄，把宽度还给工具名（窄屏最缺横向空间） */
  .tool-row{--tool-col-w:52px}
  /*
   * 价格区在桌面是 flex 横排（.price-grid 是 display:flex）。
   * 窄屏让它整行铺满：每个 .price-item 占满一行，输入框不再被挤扁。
   * 用 flex 属性而不是 grid-template —— 改布局模型会与既有规则打架。
   */
  .price-grid{flex-direction:column;gap:9px}
  .price-item{width:100%}
  .price-item input{flex:1;min-width:0}
  /* 变量行允许换行（变量名 + 输入 + 开关挤一行会溢出） */
  .var-row{flex-wrap:wrap}
  /* 弹窗标题栏与关闭键够大 */
  .modal-head{padding:12px 14px}
  .modal-head .icon-btn{min-width:var(--tap);min-height:var(--tap)}
}


/* =====================================================================
 * API 测试弹窗（编排页顶栏「API 测试」）
 *
 * 这个弹窗只做两件事：收一个数量、如实回报 N 条受理结果。所以样式也只需要
 * 覆盖这两块 —— 说明行复用 .mhint 的语义（中性说明）而不是自造，
 * 结果列表是唯一新增的视觉单元。
 *
 * 结果行的颜色语义与全站徽标一致：能/不能是同一件事的两种结局，
 * 用现有 --accent（受理）与 --danger（被拒）两色，不引入新色板。
 * ===================================================================== */
.apitest-note{
  margin:12px 0 0;font-size:12.5px;line-height:1.65;color:var(--ink-soft);
}
.apitest-note b{color:var(--ink);font-weight:600}

.apitest-result{
  margin-top:14px;padding-top:12px;border-top:1px solid var(--line-soft);
}
.apitest-result.hidden{display:none}
.apitest-head{font-size:13px;font-weight:600;color:var(--ink-strong);margin-bottom:8px}
.apitest-sum.ok{color:var(--accent)}
.apitest-sum.bad{color:var(--danger)}
/*
 * 结果列表：最多 50 行，限高滚动。
 * 不展开全部而不滚动 —— 50 行会把弹窗撑得比屏幕还高，底部的
 * 「去日志页」按钮就再也够不到了，而那正是此刻最该点的东西。
 */
.apitest-list{
  display:flex;flex-direction:column;gap:4px;max-height:220px;overflow:auto;
}
.apitest-item{
  display:flex;align-items:baseline;gap:8px;padding:5px 8px;border-radius:7px;
  background:var(--panel-2);font-size:12.5px;min-width:0;
}
.apitest-item.is-ok{border-left:2px solid var(--accent)}
.apitest-item.is-bad{border-left:2px solid var(--danger)}
.apitest-idx{
  flex:none;min-width:18px;color:var(--ink-faint);font-variant-numeric:tabular-nums;
  font-size:11.5px;text-align:right;
}
.apitest-label{flex:none;color:var(--ink);font-family:var(--mono);font-size:11.5px}
.apitest-item.is-bad .apitest-label{color:var(--danger)}
.apitest-err{
  flex:1;min-width:0;color:var(--ink-faint);font-size:11.5px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.apitest-foot{margin-top:10px;display:flex;justify-content:flex-end}
/* 链接做成按钮外观：它是"下一步去哪"，与弹窗里的动作按钮同一视觉层级 */
.apitest-foot .orch-btn{text-decoration:none;display:inline-flex;align-items:center}

/* =====================================================================
 * 移动端信息密度：图标化 + 紧凑化（用户 2026-10-01 要求）
 * =====================================================================
 *
 * ★ 这一段为什么放在文件**最末尾**、而不是紧跟按钮基础规则 ★
 *   CSS 同特异性时后写的赢。而 .wf-model / .node-add / .orch-btn.small
 *   这些基础规则与它们的移动端规则散布在文件各处（4000+ 行）。
 *   把这一段放在按钮基础规则附近，就会被后面那些基础规则覆盖 ——
 *   实测踩到：规则写了、语法也对，但在 5997 行那条 `.wf-model select{...}`
 *   之后生效，max-width 根本没起作用，而界面上看不出来"哪条没生效"。
 *   放在文件末尾、且只在这一个断点段内声明，是唯一能保证"这些就是最终值"的位置。
 *
 *   这不算补丁：它仍然只有一份权威（本条媒体查询），
 *   只是把"谁最终说了算"用文件顺序明确下来 —— 与项目里
 *   `.orch-head` 那条"窄屏形态的唯一权威在后面的栅格块里"是同一处理。
 *
 * 断点用 760px，与左栏抽屉、系统提示词折叠、页面侧 isSideDrawer() 同源。
 * （顶栏栅格用 1080px 是另一件事——那条管"顶栏放不放得下"，见其说明。）
 */
@media (max-width:760px){
  /*
   * ---- 一、按钮图标化 ----
   *
   * 结构：[glyph][label]，窄屏只留 glyph。文字藏掉后必须靠 aria-label
   * 提供无障碍名字（每个图标化按钮都在 HTML/JS 里写了，与 title 同源）。
   *
   * 宽度取 --tap 收成方形：一排图标按钮等宽才整齐；若宽度由内容决定，
   * "清空结果"与"测试"会一宽一窄，看起来像两个不同级别的动作。
   * 高度同样取 --tap —— 这是**不能**为紧凑牺牲的一条：手机上点不中
   * 比占地方更糟。缩小的是宽度，不是可点面积。
   */
  .orch-btn.btn-iconable .btn-label{display:none}
  /*
   * 视觉尺寸取 --tap-visual（不是 --tap）。
   * 上一版这里写的是 --tap，于是每个图标按钮都是 44×44 —— 用户反馈"图标还是很大"。
   * 热区由基础层的 .btn-iconable::after 撑到 --tap，所以点得中这一点没有被牺牲。
   * 高度用 min-height 而不是 height：文字（aria 隐藏但仍在流里）极端情况下
   * 不该把按钮压扁，给它一个下界即可。
   */
  .orch-btn.btn-iconable{
    width:var(--tap-visual);min-height:var(--tap-visual);
    padding:0;justify-content:center;gap:0;
  }
  .orch-btn.btn-iconable .btn-glyph{font-size:15px}

  /*
   * ---- 二、动作区紧凑化 ----
   *
   * 模型下拉是这一排里**唯一必须显示文字**的控件："现在用哪个模型"是身份信息，
   * 换成图标就没有意义了。所以它保留（但收窄），其余按钮让位。
   * "模型"那个 label 文字在手机上省掉（下拉里的模型名已经自解释）。
   */
  .sec-head-actions{gap:6px}
  .wf-model{gap:4px}
  .wf-model > span{display:none}
  .wf-model select{max-width:104px}

  /*
   * ---- 三、消息序列底部的新增按钮 ----
   * 图标化后两个按钮并排在一条线上，比原来各占半行更省竖向空间。
   */
  .node-add{gap:8px}
}

.system-text-dialog{width:min(720px,calc(100vw - 32px));max-height:85dvh;overflow:auto;padding:24px;border:1px solid var(--line);border-radius:12px;background:var(--panel);color:var(--ink)}
.system-text-head,.system-text-actions{display:flex;align-items:center;gap:12px;justify-content:space-between}
.system-text-actions{justify-content:flex-end;margin-top:20px}
.system-text-dialog label{display:block;margin-top:20px;font-size:13px}
/*
 * 标题输入框沿用正文编辑器的外观口径（边框、圆角、填充、聚焦描边同源），
 * 但它是单行 input：不继承 .system-text-editor 的 contain/overflow 规则。
 * 单列出来写，而不是给 .system-text-editor 加 input 分支，是因为正文那套规则
 * 是为 contenteditable 写的，套到 input 上只会互相干扰。
 */
.system-text-title{display:block;width:100%;margin-top:8px;box-sizing:border-box;padding:8px 10px;border:1px solid var(--line);border-radius:8px;background:var(--input);color:var(--ink);font-size:14px;font-family:inherit}
.system-text-title:focus{outline:2px solid var(--accent);outline-offset:2px}
.system-text-title::placeholder{color:var(--ink-soft)}
.system-text-editor{display:block;width:100%;margin-top:8px;border:1px solid var(--line);border-radius:8px;background:var(--input);color:var(--ink);font-size:14px;overflow:hidden;contain:layout style}
.system-text-editor:focus-within{outline:2px solid var(--accent);outline-offset:2px}
.system-text-editor:empty::before{content:attr(data-placeholder);color:var(--ink-soft);pointer-events:none}
.system-text-editor:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.system-text-editor[aria-invalid="true"]{border-color:var(--danger)}
.system-text-dialog p{font-size:12px;line-height:1.6;color:var(--ink-soft)}
.system-text-dialog button{border:1px solid var(--line);border-radius:6px;padding:6px 12px;background:var(--panel-2);color:var(--ink);cursor:pointer}
.system-text-dialog button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

/* Message output updates preserve the visible message explicitly; do not let
   browser scroll anchoring select transient tool/text descendants as anchors. */
.orch-col-edit { overflow-anchor: none; }

/* Schema 驱动的工具配置字段，沿用工具设置布局与主题。 */
.tool-settings-field{display:flex;flex-direction:column;align-items:stretch;gap:8px;min-width:0;margin:10px 0;color:var(--ink-soft);font-size:13px}
.tool-settings-field-checkbox{flex-direction:row;align-items:center;min-height:44px;cursor:pointer}
.tool-settings-field-checkbox input[type=checkbox]{flex:none;margin:0;accent-color:var(--accent)}
.tool-settings-field textarea,
.tool-settings-field input[type=number]{display:block;width:100%;box-sizing:border-box;padding:10px 12px;border:1px solid var(--line);border-radius:8px;background:var(--input);color:var(--ink);font:inherit;line-height:1.7}
.tool-settings-field textarea{min-height:140px;resize:vertical}
.tool-settings-field input[type=number]{min-width:0;min-height:44px;font-variant-numeric:tabular-nums}
.tool-settings-field textarea:focus-visible,
.tool-settings-field input[type=number]:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.tool-settings-field input:disabled,
.tool-settings-field textarea:disabled{opacity:.6;cursor:not-allowed}
.tool-settings[hidden],.tool-settings-field[hidden]{display:none}

/* 引导员阅读视图：只保留标题/关闭与一个独立的大消息滚动区。 */
#orchGuideContextBtn[hidden]{display:none}
#orchGuideContextBtn{min-width:44px;min-height:44px}
.guide-context-card{width:min(1000px,96vw);height:90dvh;max-height:90dvh}
.guide-context-messages{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;padding:24px 32px}
.guide-context-message{max-width:850px;margin:0 auto 28px;overflow-wrap:anywhere}
.guide-context-role{font-size:12px;font-weight:600;color:var(--ink-faint);margin-bottom:10px}
.guide-context-messages .msg-flow{font-size:16px;line-height:1.75}
.guide-context-state{color:var(--ink-soft);line-height:1.7}
.guide-context-card .modal-head .icon-btn{min-width:44px;min-height:44px}
@media(max-width:760px){
  .guide-context-modal{padding:4px;padding-top:max(4px,env(safe-area-inset-top));padding-bottom:max(4px,env(safe-area-inset-bottom));box-sizing:border-box}
  .guide-context-modal .guide-context-card{width:100%;height:100%;max-height:100%;border-radius:10px}
  .guide-context-card .modal-head{padding:8px 12px}
  .guide-context-messages{padding:18px 16px;padding-left:max(16px,env(safe-area-inset-left));padding-right:max(16px,env(safe-area-inset-right))}
}

/* SettingsRegion owns the device/orientation decision; CSS only projects it.
   On narrow landscape iPads, keep the existing right-hand sheet behaviour and
   expose only the settings column + its own resize boundary beside the editor. */
html[data-orch-settings-layout="modal"] .orch-cols{
  --orch-set-track:0px;
  --orch-set-resizer-track:0px;
}
html[data-orch-settings-layout="modal"] .orch-col-set,
html[data-orch-settings-layout="modal"] .orch-set-resizer{visibility:hidden;pointer-events:none}
@media(max-width:1080px){
  html[data-orch-settings-layout="region"] .orch-cols{
    grid-template-columns:var(--orch-set-track) var(--orch-set-resizer-track) minmax(0,1fr);
    grid-template-rows:minmax(0,1fr);
  }
  html[data-orch-settings-layout="region"] .orch-col-set{display:flex;grid-column:1;grid-row:1}
  html[data-orch-settings-layout="region"] .orch-set-resizer{display:block;grid-column:2;grid-row:1}
  html[data-orch-settings-layout="region"] .orch-col-edit{grid-column:3;grid-row:1}
}

/* 聊天侧栏顶部的管理入口，复用回收站入口的主题与交互状态。 */
.session-manage-entry { padding: 4px 12px 8px; }
.session-manage-entry .rb-entry { width: 100%; min-height: 44px; justify-content: center; }

/* Virtual documents grow with content up to the shared viewport-aware cap.
   CodeMirror owns scrolling; the message shell only clips its rounded boundary. */
.t-input.virtual-message-editor{
  padding:0;white-space:normal;overflow:hidden;min-width:0;
  max-height:none;scrollbar-gutter:auto;
}
.virtual-message-editor .cm-editor{max-width:100%}
.virtual-message-editor .cm-scroller{overscroll-behavior:contain}

/* Tool instance forms share the existing in-page dialog theme. */
.tool-instance-dialog{box-sizing:border-box}
.tool-instance-dialog input:not([type=checkbox]),.tool-instance-dialog select,.tool-instance-dialog textarea{box-sizing:border-box;min-height:44px;background:var(--panel-2);color:var(--ink);border:1px solid var(--line);border-radius:6px;padding:8px;font:inherit}
.tool-instance-dialog textarea{min-height:90px;resize:vertical}
.tool-instance-dialog [role=alert]{color:var(--danger,var(--ink))}
/* 编排页排序与启用共享固定右栏；实例管理操作仍独立，不撑高普通工具行。 */
.tool-row.tool-row-with-actions{--tool-col-w:90px}
.tool-row-controls{display:flex;align-items:center;justify-content:flex-end;gap:10px}
.tool-row-controls .tool-col{flex:none}
.tool-row-controls input[type=checkbox]{margin:0}
.tool-reorder{display:inline-flex;align-items:center;gap:2px}
.tool-reorder-button{
  display:inline-flex;align-items:center;justify-content:center;
  width:28px;height:28px;min-height:0;padding:0;
  border:1px solid transparent;border-radius:5px;
  background:transparent;color:var(--ink-faint);cursor:pointer;
}
.tool-reorder-button:hover:not(:disabled){background:var(--panel-2);color:var(--ink);border-color:var(--line)}
.tool-reorder-button:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
.tool-reorder-button:disabled{opacity:.3;cursor:default}
.tool-reorder-button svg{display:block;flex:none;pointer-events:none}
.tool-instance-actions{grid-column:1/-1;display:flex;flex-wrap:wrap;gap:8px;margin-top:0}
.tool-instance-actions button{min-height:36px;cursor:pointer}
.tool-instance-dialog::backdrop{background:rgba(0,0,0,.6)}
