/* ──────────────────────────────────────────────────────────────────────
   对话页。
   分两层，中间那条线要清楚：

   - 上半段（.sw-convo / .sw-msg / .sw-composer）是**这个应用的对话界面**：会话抬头、
     日期分隔、左右气泡、工具痕迹、输入区。
   - 下半段（.sw-md 及以下）是**气泡里那段 Markdown**，照着 vercel/ai-chatbot 现在用的
     Streamdown 那套排：代码块带语言条和复制/下载、表格自己横向滚、mermaid 出图、
     公式走 KaTeX。它跟「聊天」无关，抽出去照样能用在别处。

   颜色一律走 theme.css 的令牌，深浅两套自动跟着翻。
   ────────────────────────────────────────────────────────────────────── */

/* ── 会话身份行（长在 .gw-head 里，见 app.js 的 chatHeadInline） ────────
   外壳、内边距、下边框都由 .gw-head 给，这里只管里面那几件东西怎么排。 */
.sw-convo-avatar {
  width: 36px;
  height: 36px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  background: var(--secondary);
  color: var(--secondary-foreground);
}
/* flex: 1 把中间这段撑开，右边的操作按钮才会被顶到最右；min-width: 0 是省略号
   能生效的前提——不写的话 flex 子项按内容宽度算，长名字会把按钮挤出顶栏。 */
.sw-convo-id { min-width: 0; flex: 1; }
.sw-convo-title { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
.sw-convo-name {
  font-family: var(--font-heading);
  font-size: 15px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* 在线灯。三态各占一个色相，扫一眼就能分开：
     离线 = 中性灰、在线 = 绿（theme.css 的 --color-ok-*）、正在处理 = 品牌赭红 + 呼吸。
   绿是「一切正常」的通用约定，用别的颜色表达它，等于要求每个人先学一遍图例。 */
.sw-convo-live {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  flex: none;
  padding: 2px 9px 2px 7px;
  border-radius: 999px;
  background: var(--secondary);
  color: var(--secondary-foreground);
  font-size: 11.5px;
  font-weight: 500;
  white-space: nowrap;
}
.sw-convo-live i {
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: var(--color-neutral-500);
}
.sw-convo-live[data-live='1'] { background: var(--color-ok-100); color: var(--color-ok-800); }
.sw-convo-live[data-live='1'] i { background: var(--color-ok-500); }
/* 正在处理：留在品牌赭红上，跟「在线」区分开——它是个短暂状态，不是「正常」。 */
.sw-convo-live[data-live='busy'] { background: var(--color-accent-100); color: var(--color-accent-800); }
.sw-convo-live[data-live='busy'] i { background: var(--color-accent-500); animation: sw-pulse 1.4s ease-in-out infinite; }
/* 机器心跳迟了 / 失联。**形状跟机器页那两盏灯对齐**（见 app.css 的 .satu-linkdot）：
   迟了是实心琥珀带闪，失联是空心粗环。同一件事在两个页面上长同一个样，人才认得出
   它们说的是同一台机器。

   失联**不能**借赭红那一格：那是「正在处理」，绿→赭红→绿是它的正常节奏，而这一格
   要说的是「它现在谁也够不着」。 */
.sw-convo-live[data-live='stale'] { background: var(--color-warn-100); color: var(--color-warn-800); }
.sw-convo-live[data-live='stale'] i { background: var(--color-warn-500); animation: sw-pulse 1.6s ease-in-out infinite; }
.sw-convo-live[data-live='offline'] { background: var(--color-neutral-200); color: var(--foreground); }
.sw-convo-live[data-live='offline'] i {
  width: 8px;
  height: 8px;
  box-sizing: border-box;
  background: transparent;
  border: 2px solid var(--destructive);
}
@keyframes sw-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }
@media (prefers-reduced-motion: reduce) {
  .sw-convo-live[data-live='busy'] i { animation: none; }
  .sw-convo-live[data-live='stale'] i { animation: none; }
}
.sw-convo-meta {
  margin: 1px 0 0;
  font-size: 12px;
  color: var(--muted-foreground);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sw-convo-actions { position: relative; display: flex; align-items: center; gap: var(--space-2); flex: none; }
.sw-convo-actions .btn { padding: 7px 14px; font-size: 13px; }
.sw-convo-actions .btn-icon { padding: 7px; }

/* 消息流外壳。只为「回到底部」提供定位上下文——按钮要浮在消息流底边上方，钉在
   .gw-chat-main 上的话会落到输入区后面去。 */
.sw-threadwrap { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; }

/* ── 内容的最大宽度 ────────────────────────────────────────────────────
   消息流和输入区共用同一条宽度上限，并且居中。宽屏上不设上限的话，一行正文会被
   拉到一千几百像素，眼睛扫到行尾再跳回下一行的行首就容易串行；输入框同时也被拉
   成横贯整屏的一条，人刚发出去的那句在屏幕中间，正在写的那句在屏幕两端，视线要
   横着走一整屏才对得上。两处用同一个值，气泡和输入框才在同一条竖直轴上。

   上限加在**里面的内容**上，不加在 .sw-thread / .sw-composer 这两个外壳上：
   外壳继续占满，滚动条才照旧贴着窗口右沿（把上限加在滚动容器自己身上，滚动条会
   跟着缩到内容边上去）；左右的最小留白也还是外壳那份 padding 在给，窄屏上内容
   贴边的问题不会因为这条上限冒出来。 */
.gw-chat { --sw-chat-max: 920px; }
.sw-thread > *,
.sw-composer > * {
  /* border-box 不能省：这一层里有自带边框和内边距的（输入框那个盒子、底下那行提示），
     content-box 下 width: 100% 是算在**内容**上的，边框和 padding 再往外加，于是窄屏上
     它比外壳的可用宽度还宽一圈，右边直接顶出去。 */
  box-sizing: border-box;
  width: 100%;
  max-width: var(--sw-chat-max);
  margin-inline: auto;
}

/* ── 消息流 ────────────────────────────────────────────────────────── */
.sw-thread {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  padding: var(--space-4) var(--space-6) var(--space-2);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
/* 日期分隔。只有一行居中的淡字，不画线——消息流里已经有气泡的边界在分组了，
   再加一条横贯全宽的线，视觉重量比它要传达的信息（换了一天）重得多。 */
.sw-daydiv {
  display: flex;
  justify-content: center;
  margin-block: var(--space-1);
  font-size: 12px;
  color: var(--muted-foreground);
}

/* 上下文边界（自动压缩、/compact、/new）。
 *
 * **这一条真的画线**，和上面那条日期分隔不一样。理由正是那条注释的反面：日期换了
 * 只是个背景信息，而这里是「往上翻的那些，模型这一轮看不见了」——它比气泡之间的
 * 任何间隔都更该被看见。线的两侧留给字，颜色压到 border 级，不去抢正文。 */
.sw-ctxdiv {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-block: var(--space-3);
  color: var(--muted-foreground);
  font-size: 12px;
}
.sw-ctxdiv::before,
.sw-ctxdiv::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--border);
}
/* /new 比压缩硬：压缩还留着摘要，重置什么都不留。线也跟着实一档。 */
.sw-ctxdiv[data-mark='reset']::before,
.sw-ctxdiv[data-mark='reset']::after { background: var(--foreground); opacity: 0.28; }
.sw-ctxdiv-text { flex: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.sw-msg { display: flex; gap: var(--space-3); align-items: flex-start; }
.sw-msg[data-role='user'] { flex-direction: row-reverse; }
.sw-msg-avatar {
  width: 36px;
  height: 36px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  background: var(--secondary);
  color: var(--secondary-foreground);
  transition: background 0.25s ease, color 0.25s ease;
}
/* 这一条还在写：头像自己呼吸（data-live 由 chat.js 的 updateRow 挂上）。
   为什么值得单独做一个动效：三点省略号只在还没吐字的那一段有，一开口就没了；而一轮
   真花时间的活恰恰是「已经写了几段、后面还在跑」的那种——那时候屏幕上除了读秒那个数字
   没有任何东西在动，人分不出它是在跑还是已经答完了。

   两层叠着，都很轻：底色换成品牌赭红那一档（和顶栏在线灯的 busy 态同一个色系，扫一眼
   就是同一个意思），外面一圈光晕一涨一消。**不做旋转、不做弹跳**——它在屏幕左边一直
   待着，幅度大一点就变成一个不停晃的东西，读正文时挡视线。 */
.sw-msg[data-live='1'] .sw-msg-avatar {
  background: var(--color-accent-100);
  color: var(--color-accent-700);
  animation: sw-avatar-live 1.9s ease-in-out infinite;
}
/* 图标本身跟着淡一下，跟外圈错开半拍——两层同相位的话看着就是一整块在闪。 */
.sw-msg[data-live='1'] .sw-msg-avatar svg {
  animation: sw-pulse 1.9s ease-in-out infinite;
  animation-delay: 0.35s;
}
@keyframes sw-avatar-live {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-accent-500) 34%, transparent); }
  60% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--color-accent-500) 0%, transparent); }
}
/* 关了动效的人也要看得出状态：动画去掉，但那一档底色留着——它本身就说明「在跑」。 */
@media (prefers-reduced-motion: reduce) {
  .sw-msg[data-live='1'] .sw-msg-avatar,
  .sw-msg[data-live='1'] .sw-msg-avatar svg { animation: none; }
}
/* 气泡列最宽 72%：再宽长段落一行装太多字，眼睛回行会丢；再窄代码块和表格就挤了。 */
.sw-msg-col { min-width: 0; max-width: 72%; display: flex; flex-direction: column; gap: 6px; }
.sw-msg[data-role='user'] .sw-msg-col { align-items: flex-end; }

.sw-bubble {
  position: relative;
  min-width: 0;
  max-width: 100%;
  box-sizing: border-box;
  padding: 11px var(--space-4) 9px;
  border-radius: 16px;
  font-size: 14.5px;
  line-height: 1.75;
  overflow-wrap: anywhere;
}
.sw-bubble[data-role='user'] {
  background: var(--primary);
  color: var(--primary-foreground);
  border-bottom-right-radius: 6px;
}
.sw-bubble[data-role='assistant'] {
  background: var(--card);
  color: var(--card-foreground);
  border: 1px solid var(--border);
  border-bottom-left-radius: 6px;
}
/* 用户气泡里的链接和行内代码要在赭红底上还看得清——继承色 + 半透明底，
   照搬浅色底那一套的话是深红字压深红底。 */
.sw-bubble[data-role='user'] a { color: inherit; text-decoration: underline; }
.sw-bubble[data-role='user'] code[data-md='inline-code'] {
  background: color-mix(in srgb, var(--primary-foreground) 20%, transparent);
  color: inherit;
  border-color: transparent;
}

/* 时间戳。两侧一样，都在气泡**外面**——压进气泡里的话，同一条消息里就有了两种
   字号两种颜色，而时间只是旁注。列的对齐方向（sw-msg-col）自然把它推到各自那一边。 */
.sw-time { font-size: 11px; color: var(--muted-foreground); font-variant-numeric: tabular-nums; }

/* 工具痕迹 / 引用来源。靠间距和药丸本身的底色跟正文分开，不画分割线——
   气泡本来就是一个视觉容器，里面再切一刀是双重分组。 */
.sw-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 10px;
}
/* 下面这几个类都自己写了 display，会盖掉 [hidden] 自带的 display:none（作者样式赢 UA
   样式，见 app.css 的 .gw-aside-stack[hidden]）。少了这一行，hidden 了还占着位置。 */
.sw-chips[hidden] { display: none; }
.sw-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  max-width: 100%;
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--secondary);
  color: var(--secondary-foreground);
  font-size: 11.5px;
  line-height: 1.6;
}
.sw-chip svg { flex: none; opacity: 0.75; }
.sw-chip > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 还在跑的工具：文字降一档，点一下呼吸灯，跑完了自己变回来。 */
.sw-chip[data-state='running'] { color: var(--muted-foreground); }
.sw-chip[data-state='running'] svg { animation: sw-pulse 1.4s ease-in-out infinite; }
.sw-chip[data-state='error'] { background: var(--color-accent-100); color: var(--color-accent-800); }
@media (prefers-reduced-motion: reduce) {
  .sw-chip[data-state='running'] svg { animation: none; }
}

/* 正在思考：三个点。这是**空气泡**，不是状态栏文字——回答一开始吐字它就地变成正文，
   位置不跳。 */
.sw-typing { display: inline-flex; align-items: center; gap: 4px; padding: 3px 0; }
.sw-typing i {
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: var(--muted-foreground);
  animation: sw-bounce 1.2s ease-in-out infinite;
}
.sw-typing i:nth-child(2) { animation-delay: 0.15s; }
.sw-typing i:nth-child(3) { animation-delay: 0.3s; }
@keyframes sw-bounce { 0%, 60%, 100% { opacity: 0.28; } 30% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .sw-typing i { animation: none; opacity: 0.6; }
}

/* 回到底部。人往上翻看历史时才出现——见 app.js 的 stickToBottom。 */
.sw-jump {
  position: absolute;
  left: 50%;
  bottom: 14px;
  transform: translateX(-50%);
  z-index: 4;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 14px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--popover);
  color: var(--secondary-foreground);
  box-shadow: var(--shadow-md);
  font-family: var(--font-body);
  font-size: 12.5px;
  cursor: pointer;
}
.sw-jump:hover { background: var(--secondary); }
.sw-jump[hidden] { display: none; }

/* ── 输入区 ────────────────────────────────────────────────────────── */
.sw-composer { flex: none; padding: var(--space-2) var(--space-6) var(--space-4); }
.sw-composer-box {
  /* 选单绝对定位在它上沿（.sw-pickbox），所以这里要是定位上下文。 */
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-3) var(--space-2);
  background: var(--popover);
  border: 1px solid var(--input-border);
  border-radius: var(--radius-md);
  transition: border-color 120ms ease, box-shadow 120ms ease;
}
/* 焦点环画在盒子上而不是 textarea 上：视觉上「输入框」是整个盒子，
   光标在里面时高亮的也该是整个盒子。 */
.sw-composer-box:focus-within {
  border-color: var(--ring);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 22%, transparent);
}
.sw-composer-row { display: flex; align-items: center; gap: var(--space-2); }
.sw-composer-row .sw-spacer { flex: 1; }
/* 输入区左边那排图标按钮（目前只有附件）。圆形、无底色，跟右边实心的主操作分开：
   它们是「顺手加点东西」，不是「把这条发出去」。 */
.sw-iconbtn {
  width: 32px;
  height: 32px;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--muted-foreground);
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
}
.sw-iconbtn:hover { background: var(--secondary); color: var(--secondary-foreground); }
.sw-iconbtn svg { display: block; }

/* 待发送的附件。排在输入框上面而不是下面——它们是这条消息的一部分，视线从附件
   往下走到正文再走到发送，顺序跟消息最终的样子一致。 */
.sw-files { display: flex; flex-wrap: wrap; gap: 6px; }
.sw-files[hidden] { display: none; }
.sw-file {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  padding: 3px 4px 3px 10px;
  border-radius: 999px;
  background: var(--secondary);
  color: var(--secondary-foreground);
  font-size: 12px;
}
.sw-file > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sw-file-x {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--muted-foreground);
  cursor: pointer;
}
.sw-file-x:hover { background: var(--color-neutral-400); color: var(--color-neutral-800); }

/* 发送 / 停止。圆形图标按钮——它是这一屏唯一的主操作，圆形加实心底色让它在一堆
   方角的输入框和文字按钮里一眼可辨，也不用为「发送」两个字留一块随语言变宽的位置。 */
.sw-send {
  position: relative;
  width: 34px;
  height: 34px;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: var(--primary);
  color: var(--primary-foreground);
  box-shadow: var(--shadow-2xs);
  cursor: pointer;
  transition: background 120ms ease;
}
.sw-send:hover { background: var(--color-accent-600); }
.sw-send:active { background: var(--color-accent-700); }
.sw-send svg { display: block; }
/* 停止态。
 *
 * 意图是「中性、退到主操作后面」，但**不能用 --color-neutral-***：那套色阶在暗色主题
 * 里是翻过来的（--color-neutral-800 在暗色下是 #e0dcd3，几乎是白的），于是这颗按钮在
 * 暗色界面上变成一颗刺眼的白球，比它要让位的主操作还抢眼。
 *
 * 改用语义色 --secondary / --border：这一组两套主题都朝同一个方向走，和消息里的工具
 * 药丸、附件按钮是同一种「次要控件」的底色，停止态因此看着像本来就在那儿的东西。 */
.sw-send[data-state='stop'] {
  background: var(--secondary);
  color: var(--secondary-foreground);
  box-shadow: inset 0 0 0 1px var(--border);
}
.sw-send[data-state='stop']:hover {
  background: var(--muted);
  color: var(--foreground);
  box-shadow: inset 0 0 0 1px var(--muted-foreground);
}
.sw-send[data-state='stop']:active { background: var(--secondary); }
/* 正在停止。
 *
 * **停止不是当场生效的**：按下去只是把中止下发出去，Bot 收到、把手上那一步收尾，
 * `turn/end` 才回来（见 chat.js 的 abortChat）。中间快则几百毫秒，撞上一个正跑着的
 * 工具就是十几秒——这一段以前按钮一点变化都没有，人只能靠反复点来确认自己点上了。
 *
 * 转圈画在按钮**外沿**（::after 一圈环），图标留在原地只是淡下去：动的是「这件事
 * 正在发生」，而不是又变成了另一个能按的动作。禁用态不改底色——一颗突然变灰的按钮
 * 读起来像「不能用」，这里要说的是「已经收下了，等着」。 */
.sw-send[data-state='stopping'] {
  background: var(--secondary);
  color: var(--muted-foreground);
  box-shadow: inset 0 0 0 1px var(--border);
  cursor: default;
}
.sw-send[data-state='stopping']::after {
  content: '';
  position: absolute;
  top: -3px;
  right: -3px;
  bottom: -3px;
  left: -3px;
  border-radius: 999px;
  border: 2px solid transparent;
  border-top-color: var(--color-accent-500);
  animation: sw-spin 700ms linear infinite;
}
.sw-send[data-state='stopping'] svg { animation: sw-pulse 1.4s ease-in-out infinite; }
/* 动效敏感的人：环留着（它标着「正在进行」，位置和含义都还在），只是不转。 */
@media (prefers-reduced-motion: reduce) {
  .sw-send[data-state='stopping']::after {
    animation: none;
    border-color: var(--border);
    border-top-color: var(--color-accent-500);
  }
  .sw-send[data-state='stopping'] svg { animation: none; opacity: 0.6; }
}

/* 工具痕迹的悬浮窗。挂在 body 上，位置由 placeToolPop 算（见 chat.js）。 */
.sw-toolpop {
  position: fixed;
  z-index: 60;
  max-width: 520px;
  padding: 10px 12px;
  border-radius: var(--radius-md);
  background: var(--popover);
  color: var(--popover-foreground);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-lg);
  font-size: 12px;
  line-height: 1.55;
}
.sw-toolpop[hidden] { display: none; }
.sw-toolpop-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.sw-toolpop-name {
  font-weight: 600;
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}
.sw-toolpop-state {
  padding: 1px 8px;
  border-radius: 999px;
  background: var(--secondary);
  color: var(--secondary-foreground);
  font-size: 11px;
}
.sw-toolpop-state[data-state='error'] { background: var(--color-accent-100); color: var(--color-accent-800); }
.sw-toolpop-k {
  margin: 8px 0 4px;
  font-size: 11px;
  color: var(--muted-foreground);
}
.sw-toolpop-k:first-of-type { margin-top: 0; }
.sw-toolpop-wait { color: var(--muted-foreground); }
/* 结果可能很长，所以给它自己的滚动区，而不是让浮层长到屏幕外面去。 */
.sw-toolpop pre {
  margin: 0;
  max-height: 220px;
  overflow: auto;
  padding: 8px 9px;
  border-radius: var(--radius-sm);
  background: var(--muted);
  color: var(--foreground);
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 11.5px;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
}
/* ── 高风险确认卡 ──────────────────────────────────────────────────────
   摆在工具药丸底下，说的就是那颗还在转的药丸在等什么。

   **用边框和留白分块，不用警告色刷底。** 这不是一次故障，是一道边界；刷成红的，人
   第一反应是「出错了」，然后开始学着忽略它——而这张卡片一旦被习惯性忽略，这个开关
   就等于没有。左边那条竖线用品牌色，够醒目，也说得清「这里在等你」。 */
.sw-approvals { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; }
.sw-approvals[hidden] { display: none; }
.sw-approval {
  border: 1px solid var(--border);
  border-left: 3px solid var(--primary);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  background: var(--background);
  font-size: 12.5px;
}
.sw-approval-head {
  display: flex;
  align-items: center;
  gap: 6px;
  font-weight: 600;
  color: var(--foreground);
}
.sw-approval-head svg { flex: none; color: var(--primary); }
.sw-approval-why { margin-top: 4px; color: var(--muted-foreground); }
.sw-approval-tool { margin-top: 6px; }
.sw-approval-tool code {
  padding: 1px 6px;
  border-radius: var(--radius-sm);
  background: var(--secondary);
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 11.5px;
}
/* 参数要摆出来，但不能占掉半屏：批的是「发给谁、写了什么」，看的是头几行。 */
.sw-approval-args {
  margin: 6px 0 0;
  max-height: 160px;
  overflow: auto;
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  background: var(--muted);
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 11.5px;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
}
.sw-approval-acts { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
/* 卡片里的按钮比页面上的小一号：它长在气泡里，用标准尺寸会把整条消息撑变形。 */
.sw-approval-acts .btn { padding: 5px 12px; font-size: 12px; }
/* 点完到终态事件回来之间。按钮留在原地但点不动——挪走或者变灰闪一下，都会让人
   以为自己点空了。 */
.sw-approval[data-busy='1'] .sw-approval-acts { pointer-events: none; opacity: 0.5; }

/* 转人工的交接卡（见 docs/handoff.md）。骨架整套继承 .sw-approval——两张卡在同一条
   气泡里，长得不一样只会让人以为它们是两种不同性质的东西；它们的性质是一样的：
   **这里有一件事在等你**。区别只在左边那条竖线的颜色（交接是第二品牌色，说的是
   "换个人来"而不是"要不要放行"）和多出来的那个输入框。 */
.sw-handoffs { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; }
.sw-handoffs[hidden] { display: none; }
.sw-handoff { border-left-color: var(--color-accent-2-600, var(--primary)); }
.sw-handoff .sw-approval-head svg { color: var(--color-accent-2-600, var(--primary)); }
/* 卡上最大的那行字是「要人做什么」，不是「为什么卡住」：接手的人先要知道自己该干嘛。 */
.sw-handoff-ask { margin-top: 4px; font-size: 13.5px; line-height: 1.5; color: var(--foreground); }
.sw-handoff-who { margin-top: 6px; font-size: 12px; color: var(--muted-foreground); }
.sw-handoff-note { margin-top: 8px; width: 100%; box-sizing: border-box; font-size: 12.5px; resize: vertical; }
.sw-handoff-note-done { margin-top: 8px; font-size: 12px; color: var(--muted-foreground); }
.sw-handoff[data-busy='1'] .sw-approval-acts { pointer-events: none; opacity: 0.5; }
/* 已经闭合的收成一行灰字。留着是因为「这件事上次是怎么处理的」要看得见，
   但它不该再占着一张卡的位置。 */
.sw-handoff-done {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 2px;
  font-size: 12px;
  color: var(--muted-foreground);
}
.sw-handoff-done svg { flex: none; width: 12px; height: 12px; }

/* 有结论的确认收成一颗药丸，和工具痕迹排在一起（.sw-toolchip 那套样式全都继承）。
   这里只把盾牌调到和工具图标一样大——它原本是给卡片抬头用的，14px 在药丸里偏大。 */
.sw-approvalchip svg { width: 12px; height: 12px; }

/* ── 发信那张定制卡 ────────────────────────────────────────────────────
   排得像一封信：左边一列标签，右边是内容；主题一行、正文一块。**不做成表单控件的
   堆叠**——那看着像「填一张表」，而人在这儿干的事是「读一遍再改两句」。 */
.sw-mail { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; }
.sw-mail-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
.sw-mail-body { display: flex; flex-direction: column; gap: 4px; }
/* 标签列固定宽：三格标签对齐，眼睛沿着一条竖线往下扫。 */
.sw-mail-k {
  flex: none;
  width: 44px;
  font-size: 11.5px;
  color: var(--muted-foreground);
}
.sw-mail-v {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12.5px;
}
.sw-mail-input, .sw-mail-text { font-size: 12.5px; }
/* 单行那格保持药丸：`.input` 全站就是这个样子（主题、口令、名字都一样），
   在这儿单独改成方角反而突兀。 */
.sw-mail-input { flex: 1; min-width: 0; padding: 5px 12px; }
/**
 * 正文那格**要把 .input 的药丸圆角换掉**。
 *
 * `.input` 的 `border-radius: 999px` 是给单行控件用的——高度一固定，999px 就是两头
 * 半圆。多行 textarea 高上去之后，同一个值会把边框摊成一个椭圆，正文从弧线里溢出来
 * （截图里就是那样）。textarea 要的是矩形圆角，和 `textarea.satu-code`（soul.md、
 * Skill 编辑器）同一个口径。
 *
 * 字体不跟 `.satu-code` 那条走等宽：那几处编辑的是代码和提示词，而这儿是一封要发出去
 * 的信——读的是话，不是字符对齐。
 */
.sw-mail-text {
  width: 100%;
  border-radius: var(--radius-md);
  padding: 8px 12px;
  resize: vertical;
  line-height: 1.6;
}
/* HTML 正文默认给人看邮件效果，源码折进下面。预览固定白底，因为绝大多数邮件模板按
   白色阅读面设计；外层即使是深色主题，也不反转邮件自身的颜色。iframe 不接收点击，
   避免批准时误点模板里的链接；真正的隔离与断网约束仍由 sandbox + CSP 承担。 */
.sw-mail-preview {
  display: block;
  box-sizing: border-box;
  width: 100%;
  min-height: 240px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: #fff;
  pointer-events: none;
}
.sw-mail-source { margin-top: 2px; }
.sw-mail-source > summary {
  width: max-content;
  color: var(--muted-foreground);
  font-size: 11.5px;
  cursor: pointer;
  user-select: none;
}
.sw-mail-source[open] > summary { margin-bottom: 5px; }
/* 悬浮窗里按表单摆的那几格。 */
.sw-toolpop-field { margin-top: 4px; }
.sw-toolpop-field > span { font-size: 11px; color: var(--muted-foreground); }

/* 药丸本身要看得出「可以看详情」。 */
.sw-toolchip { cursor: help; }
.sw-toolchip:hover, .sw-toolchip:focus-visible { background: var(--muted); }
.sw-composer-tip {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin-top: 7px;
  padding: 0 2px;
  font-size: 11.5px;
  color: var(--muted-foreground);
}
.sw-composer-tip .sw-spacer { flex: 1; }
/* 正在停止的那一句。这一行平时是灰的背景信息（「Enter 发送」之类），而这一句是人刚
   做的那个动作的回执——抬一档到正文色，加一个不晃眼的呼吸，人的余光才接得住它。
   读秒本身就在动（见 chat.js 的 tickStop），所以这里不再加任何位移。 */
#chat-tip[data-hint='stopping'] {
  color: var(--foreground);
  font-variant-numeric: tabular-nums;
  animation: sw-fade 1.6s ease-in-out infinite;
}
@keyframes sw-fade { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } }
@media (prefers-reduced-motion: reduce) {
  #chat-tip[data-hint='stopping'] { animation: none; }
}
/* ── 上下文占比。药丸长在提示行右端，浮层从它头顶展开 ──────────────────
   为什么往上开：输入框已经贴着窗口底边，往下没有地方；而且往上开的话，浮层盖住的是
   消息流最下面那几行——刚读过的内容，比盖住输入框和发送键代价小。 */
.sw-ctx { position: relative; flex: none; display: inline-flex; }
.sw-ctx[hidden] { display: none; }
.sw-ctx-chip {
  padding: 2px 7px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  font-family: inherit;
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
  color: var(--muted-foreground);
  cursor: pointer;
  white-space: nowrap;
}
.sw-ctx-chip:hover, .sw-ctx-chip[aria-expanded="true"] { background: var(--secondary); color: var(--color-text); }
/* 七成起换色。满了才报警等于没报——那时候已经在丢前面的对话了。 */
.sw-ctx-chip[data-level="mid"] { color: var(--color-warn-500); }
.sw-ctx-chip[data-level="high"] { color: var(--color-accent-600); font-weight: 600; }

.sw-ctxpop {
  position: absolute;
  right: 0;
  bottom: calc(100% + 8px);
  z-index: 15;
  width: 272px;
  box-sizing: border-box;
  padding: var(--space-3);
  background: var(--popover);
  color: var(--popover-foreground);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  cursor: default;
}
.sw-ctxhead { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); font-size: 12px; color: var(--muted-foreground); }
.sw-ctxhead b { font-size: 13px; font-variant-numeric: tabular-nums; color: var(--color-text); }
/* 条本身就是那张明细表的图例：每一段的颜色和下面每一行的圆点一一对应。 */
.sw-ctxbar { display: flex; height: 6px; margin: var(--space-3) 0; border-radius: 999px; background: var(--color-neutral-300); overflow: hidden; }
.sw-ctxbar i { display: block; height: 100%; }
.sw-ctxlist { display: flex; flex-direction: column; gap: 5px; }
.sw-ctxrow { display: grid; grid-template-columns: 8px 1fr auto 44px; gap: var(--space-2); align-items: center; font-size: 12px; }
.sw-ctxdot { width: 8px; height: 8px; border-radius: 2px; }
.sw-ctxname { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sw-ctxnum, .sw-ctxpct { font-variant-numeric: tabular-nums; color: var(--muted-foreground); }
.sw-ctxpct { text-align: right; }
.sw-ctxnote { margin: var(--space-3) 0 0; padding-top: var(--space-2); border-top: 1px solid var(--border); font-size: 11px; line-height: 1.5; color: var(--muted-foreground); }

.sw-composer-tip kbd {
  font-family: inherit;
  font-size: 11px;
  padding: 1px 5px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--card);
}

@media (max-width: 760px) {
  .sw-thread { padding: var(--space-3) var(--space-4) var(--space-2); }
  .sw-composer { padding: var(--space-2) var(--space-4) var(--space-3); }
  .sw-convo { padding: var(--space-3) var(--space-4); }
  .sw-msg-col { max-width: 86%; }
  .sw-composer-tip > span:first-child { display: none; }
  /* 浮层比窄屏还宽的话右边会被切掉，而被切掉的正好是百分比那一列。 */
  .sw-ctxpop { width: min(272px, calc(100vw - var(--space-8))); }
}

/* ══════════════════════════════════════════════════════════════════════
   气泡里的 Markdown。这一段以下跟「聊天」无关。
   ══════════════════════════════════════════════════════════════════════ */
.sw-md { min-width: 0; }
/* 首尾元素不要外边距，否则气泡上下会各多出一截空白。 */
/* syncMd 把正文切成一块一块，每块套一层 .sw-mdblock（见 app.js）。它只是记忆的
   载体，不该带任何自己的间距——间距全由里面的元素给。 */
.sw-mdblock { display: contents; }

.sw-md > :first-child, .sw-mdblock:first-child > :first-child { margin-top: 0; }
.sw-md > :last-child, .sw-mdblock:last-child > :last-child { margin-bottom: 0; }

.sw-md p[data-md='paragraph'] { margin: 0 0 var(--space-3); }
.sw-md h1, .sw-md h2, .sw-md h3, .sw-md h4, .sw-md h5, .sw-md h6 {
  margin: var(--space-4) 0 var(--space-2);
  font-weight: 600;
  line-height: 1.4;
}
.sw-md h1 { font-size: 20px; }
.sw-md h2 { font-size: 18px; }
.sw-md h3 { font-size: 16px; }
.sw-md h4, .sw-md h5, .sw-md h6 { font-size: 14.5px; }

/* 记号放在文字块外面（不是 Streamdown 的 list-inside）。中文段落一行常常要折行，
   记号在里面的话第二行会顶到记号底下，读起来像另起了一条。 */
.sw-md ul, .sw-md ol { margin: 0 0 var(--space-3); padding-left: 1.5em; }
.sw-md ul { list-style: disc; }
.sw-md ol { list-style: decimal; }
.sw-md li { margin: 3px 0; }
.sw-md li > ul, .sw-md li > ol { margin: 3px 0 0; }
.sw-md li > p { margin: 0 0 var(--space-2); }
.sw-md li.sw-task { list-style: none; margin-left: -1.3em; }
.sw-md li.sw-task > input { margin-right: 7px; vertical-align: -1px; accent-color: var(--primary); }

.sw-md blockquote {
  margin: 0 0 var(--space-3);
  padding: 2px 0 2px var(--space-3);
  border-left: 3px solid var(--border);
  color: var(--muted-foreground);
}
.sw-md hr { margin: var(--space-4) 0; border: 0; border-top: 1px solid var(--border); }
.sw-md a { color: var(--color-accent-700); font-weight: 500; text-decoration: underline; text-underline-offset: 2px; }
.sw-md a:hover { color: var(--color-accent-600); }
/*
 * 链接文字后面跟一枚小图标。
 *
 * Bot 列一串东西时（十个视频、十条搜索结果），**每一项自己就是链接**，人要的就是
 * 点进去。而正文里加粗、列表、序号本来就多，一段带下划线的文字混在里面并不显眼——
 * 这枚图标是「这一条点得开」的那个记号，箭头朝外也说明了它开的是新标签页。
 *
 * 跟在文字后面而不是前面：前面会把每一行的起点推开，一列标题就对不齐了。它长在
 * <a> 自己的 ::after 上，所以连它一起是可点的，点哪儿都跳。
 *
 * 用 mask 不用 <img>：颜色跟着 currentColor 走，链接的常态色、hover 色、以及用户
 * 气泡里那套反色，一处都不用再各配一张图。
 */
.sw-md a[data-md='link']::after {
  /* 图标本身。写成变量是因为 -webkit- 和标准写法各要一份，抄两遍必然有一处抄歪。 */
  --sw-icon-external: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M14 4h6v6'/%3E%3Cpath d='M20 4L11 13'/%3E%3Cpath d='M18 14v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4'/%3E%3C/svg%3E");
  content: '';
  display: inline-block;
  width: 0.82em;
  height: 0.82em;
  margin-left: 0.22em;
  vertical-align: -0.06em;
  background-color: currentColor;
  -webkit-mask: var(--sw-icon-external) no-repeat center / contain;
  mask: var(--sw-icon-external) no-repeat center / contain;
}
/* 图片本身就是链接时不加：那张图已经摆在那儿了，角上再挂个小箭头只是脏。 */
.sw-md a[data-md='link']:has(img)::after { content: none; }
.sw-md img { margin: var(--space-2) 0; border-radius: var(--radius-sm); border: 1px solid var(--border); }
.sw-md strong { font-weight: 600; }

.sw-md code[data-md='inline-code'] {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.88em;
  padding: 1.5px 5px;
  border-radius: 5px;
  background: var(--secondary);
  color: var(--secondary-foreground);
  overflow-wrap: anywhere;
}

/* ── 代码块 ────────────────────────────────────────────────────────── */
.sw-code, .sw-mermaid {
  margin: var(--space-3) 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--popover);
  overflow: hidden;
}
.sw-code-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  height: 32px;
  padding: 0 6px 0 var(--space-3);
  background: var(--card);
  border-bottom: 1px solid var(--border);
}
.sw-code-lang {
  flex: 1;
  min-width: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 11.5px;
  text-transform: lowercase;
  color: var(--muted-foreground);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sw-code-tools { display: flex; align-items: center; gap: 2px; flex: none; }
.sw-tool {
  border: 0;
  background: transparent;
  border-radius: 6px;
  padding: 4px 8px;
  font-family: var(--font-body);
  font-size: 11.5px;
  color: var(--muted-foreground);
  cursor: pointer;
  white-space: nowrap;
}
.sw-tool:hover { background: var(--secondary); color: var(--secondary-foreground); }
.sw-tool[data-ok] { color: var(--color-accent-700); }

.sw-code-body {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  overflow-x: auto;
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 12.5px;
  line-height: 1.65;
  tab-size: 2;
}
.sw-code-body code { font: inherit; white-space: pre; }

/* highlight.js 的记号色。不引它自带的主题：那些主题写死了背景和前景，
   在我们这套深浅令牌里会撞。只映射颜色，底色继续用 --popover。 */
.hljs-comment, .hljs-quote { color: var(--muted-foreground); font-style: italic; }
.hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-doctag, .hljs-name { color: var(--color-accent-2-500); }
.hljs-string, .hljs-regexp, .hljs-addition, .hljs-attribute, .hljs-meta .hljs-string { color: var(--color-accent-600); }
.hljs-number, .hljs-symbol, .hljs-bullet, .hljs-variable, .hljs-template-variable, .hljs-link { color: var(--chart-1); }
.hljs-title, .hljs-section, .hljs-title.class_, .hljs-title.function_ { color: var(--color-accent-700); font-weight: 600; }
.hljs-type, .hljs-built_in, .hljs-class .hljs-title { color: var(--color-accent-2-600); }
.hljs-attr, .hljs-selector-attr, .hljs-selector-class, .hljs-selector-id { color: var(--color-accent-2-600); }
.hljs-meta, .hljs-deletion { color: var(--muted-foreground); }
.hljs-emphasis { font-style: italic; }
.hljs-strong { font-weight: 600; }

/* ── 表格 ──────────────────────────────────────────────────────────── */
.sw-table { position: relative; margin: var(--space-3) 0; border: 1px solid var(--border); border-radius: var(--radius-sm); }
/* 横向滚动关在自己这一格里。不关的话一张宽表会把整条气泡撑出容器。 */
.sw-table-scroll { overflow-x: auto; border-radius: var(--radius-sm); }
.sw-table table { width: 100%; border-collapse: collapse; font-size: 13px; }
.sw-table th, .sw-table td { padding: 7px var(--space-3); text-align: left; border-bottom: 1px solid var(--border); white-space: nowrap; }
.sw-table thead th { background: var(--card); font-weight: 600; color: var(--secondary-foreground); }
.sw-table tbody tr:last-child td { border-bottom: 0; }
/* 复制按钮浮在右上角，鼠标进来才出现——常驻会挡住表头第一格。 */
.sw-table-tools { position: absolute; top: 4px; right: 4px; z-index: 2; opacity: 0; transition: opacity 120ms ease; }
.sw-table:hover .sw-table-tools, .sw-table-tools:focus-within { opacity: 1; }
.sw-table-tools .sw-tool { background: var(--popover); border: 1px solid var(--border); }

/* ── Mermaid ───────────────────────────────────────────────────────── */
.sw-mermaid-canvas { padding: var(--space-4); overflow-x: auto; text-align: center; }
.sw-mermaid-canvas svg { max-width: 100%; height: auto; margin: 0 auto; }
.sw-mermaid-canvas[data-state='pending'] { color: var(--muted-foreground); font-size: 12.5px; text-align: left; }
.sw-mermaid-canvas[data-state='err'] { color: var(--muted-foreground); font-size: 12.5px; text-align: left; }
.sw-mermaid-src {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--border);
  overflow-x: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12.5px;
  line-height: 1.65;
}
/* 图和源码同时只露一个，靠 figure 上的 data-view 切。 */
.sw-mermaid[data-view='diagram'] .sw-mermaid-src { display: none; }
.sw-mermaid[data-view='source'] .sw-mermaid-canvas { display: none; }
.sw-mermaid[data-view='source'] .sw-mermaid-src { border-top: 0; }

/* ── 公式 ──────────────────────────────────────────────────────────── */
.sw-math { font-size: 1em; }
/* KaTeX 没加载出来（离线、CDN 被挡）时留在屏幕上的是 TeX 原文。给它一个
   等宽 + 淡底，让人一眼看出「这是段没渲染的公式」，而不是正文写坏了。 */
.sw-math:not([data-done='1']) {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.9em;
  color: var(--muted-foreground);
}
.sw-math-block { margin: var(--space-3) 0; overflow-x: auto; overflow-y: hidden; text-align: center; }
.sw-math-block:not([data-done='1']) { text-align: left; }
.sw-md .katex { font-size: 1.05em; }
.sw-md .katex-display { margin: 0; }

/* 用户气泡（赭红底）里的这些块要换一套底色，不然全是深色压深色。 */
.sw-bubble[data-role='user'] .sw-code,
.sw-bubble[data-role='user'] .sw-mermaid,
.sw-bubble[data-role='user'] .sw-table {
  border-color: color-mix(in srgb, var(--primary-foreground) 28%, transparent);
}
.sw-bubble[data-role='user'] .sw-code,
.sw-bubble[data-role='user'] .sw-mermaid { background: color-mix(in srgb, var(--primary-foreground) 10%, transparent); }
.sw-bubble[data-role='user'] .sw-code-head,
.sw-bubble[data-role='user'] .sw-table thead th { background: color-mix(in srgb, var(--primary-foreground) 12%, transparent); }
.sw-bubble[data-role='user'] .sw-code-lang,
.sw-bubble[data-role='user'] .sw-tool { color: color-mix(in srgb, var(--primary-foreground) 78%, transparent); }
.sw-bubble[data-role='user'] .sw-code-head,
.sw-bubble[data-role='user'] .sw-table th,
.sw-bubble[data-role='user'] .sw-table td,
.sw-bubble[data-role='user'] blockquote { border-color: color-mix(in srgb, var(--primary-foreground) 28%, transparent); }
.sw-bubble[data-role='user'] blockquote { color: color-mix(in srgb, var(--primary-foreground) 82%, transparent); }

/* ── 产出文件与预览 ─────────────────────────────────────────────────────
   产出文件的 chip 长在工具 chip 旁边，但它是**能点的**——所以给边框和 hover，
   跟旁边那些只读的状态 chip 区分开。这是「Bot 生成了什么」唯一的入口。 */
.sw-filechip {
  border: 1px solid var(--border);
  background: var(--background);
  color: var(--foreground);
  cursor: pointer;
  font-family: inherit;
  transition: background 120ms ease, border-color 120ms ease;
}
.sw-filechip:hover { background: var(--secondary); border-color: var(--ring); }
.sw-filechip svg { opacity: 0.6; }
/* 读过的（不是产出的）淡一档：它不是这一轮的成果，只是「它看过这个」。 */
.sw-refchip { border-style: dashed; color: var(--muted-foreground); }
.sw-refchip:hover { color: var(--foreground); }

/* ── 正文里的文件名 ───────────────────────────────────────────────────────
   目录树里那一行、句子里带出来的一个文件名，点开就是预览。

   长成链接而不是药丸：它嵌在正文（常常是代码块）中间，换成药丸会把那一片的对齐
   和行距全撑坏，也就抄不走了。所以只改颜色和下划线，尺寸一概继承外面——
   `font: inherit` 那一条是关键，按钮默认字体在代码块里会突然变成系统字体。 */
.sw-filelink {
  padding: 0;
  border: 0;
  background: none;
  color: var(--color-accent-700);
  font: inherit;
  text-align: inherit;
  text-decoration: underline;
  text-decoration-style: dotted;
  text-underline-offset: 2px;
  cursor: pointer;
}
.sw-filelink:hover { text-decoration-style: solid; }
.sw-filelink:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; border-radius: 2px; }

.sw-file > small { flex: none; color: var(--muted-foreground); font-size: 11px; }

/* 预览层。比别的 modal 宽，因为里面是给人看的内容，不是一张表单。 */
.sw-preview { max-width: min(1040px, 94vw); width: 100%; }
.sw-preview-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
}
.sw-preview-head h2 { font-size: 18px; margin: 0 0 2px; overflow-wrap: anywhere; }
.sw-preview-head p {
  margin: 0;
  font-size: 12px;
  color: var(--muted-foreground);
  overflow-wrap: anywhere;
}
.sw-preview-acts { display: flex; align-items: center; gap: var(--space-2); flex: none; }
.sw-preview-body {
  margin-top: var(--space-4);
  /* 高度写死一档：内容是什么尺寸事先不知道，让层随内容跳会很难受。 */
  height: 70vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--muted);
  border-radius: var(--radius);
  overflow: auto;
}
/* 图片按原始比例缩进框里，不裁切也不拉伸。 */
.sw-preview-img { max-width: 100%; max-height: 100%; object-fit: contain; display: block; }
.sw-preview-frame { width: 100%; height: 100%; border: 0; background: var(--background); border-radius: var(--radius); }
.sw-preview-note {
  margin: 0;
  padding: var(--space-6);
  max-width: 42ch;
  text-align: center;
  font-size: 13px;
  line-height: 1.6;
  color: var(--muted-foreground);
}
.sw-preview-err { color: var(--color-accent-800); }

/* Telegram 的签名预览页复用同一套预览窗口，不挂完整控制台外壳。 */
.sw-channel-preview-page {
  min-height: 100vh;
  min-height: 100dvh;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-6);
  background: var(--color-neutral-200);
}
.sw-channel-preview {
  height: calc(100vh - var(--space-6) - var(--space-6));
  height: calc(100dvh - var(--space-6) - var(--space-6));
}
.sw-channel-preview .sw-preview-title { min-width: 0; }
.sw-channel-preview .sw-preview-body { flex: 1; min-height: 0; height: auto; }
@media (max-width: 680px) {
  .sw-channel-preview-page { padding: 0; }
  .sw-channel-preview {
    height: 100vh;
    height: 100dvh;
    max-height: none;
    border: 0;
    border-radius: 0;
    padding: var(--space-4);
  }
  .sw-channel-preview .sw-preview-head { flex-direction: column; }
  .sw-channel-preview .sw-preview-acts { width: 100%; flex-wrap: wrap; }
  .sw-channel-preview .sw-preview-acts .btn-icon { margin-left: auto; }
}

/* 用户发的图。缩略图排在正文上面——先看见发了什么，再看见说了什么，跟消息本身的
   顺序一致（图片块在正文前面）。 */
.sw-shots { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
.sw-shots[hidden] { display: none; }
/* 气泡里那排「这条消息 @ 了谁」。药丸沿用输入框上方那一种（.sw-mention），发出去前后
   长得一样；只是这里没有那颗 ×——已经发出去的消息改不了了。 */
.sw-mentions-in { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
.sw-mentions-in[hidden] { display: none; }
.sw-mentions-in .sw-mention { padding: 3px 10px; }
/* 用户气泡是强调色底，药丸再用同一族的浅色就糊在一起了——翻过来用气泡里的底色。 */
.sw-bubble[data-role='user'] .sw-mention { background: color-mix(in srgb, var(--color-surface) 88%, transparent); color: var(--color-text); }
.sw-shot {
  width: 108px;
  height: 108px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--muted);
  overflow: hidden;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.sw-shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* 还没取回来、或者太大不自动取的那些，留一个占位而不是塌成一条缝。 */
.sw-shot-ph { color: var(--muted-foreground); opacity: 0.7; }
.sw-bubble[data-role='user'] .sw-shot { border-color: color-mix(in srgb, var(--primary-foreground) 28%, transparent); }
/*
 * 浏览器一步一张的过程截图。和用户发的图共用 .sw-shots / .sw-shot，只改两处：
 *
 * - 摆成 16:10 而不是正方形——拍的是 1280x800 那块屏，方框裁出来的只剩页面正中央
 *   那一小块，十有八九什么都看不出来。
 * - 对齐顶部（object-position）。网页的信息都在上面：标题、导航、第一屏结果。居中裁
 *   会把它们全裁掉，只留一片中段的白。
 */
.sw-steps { margin: 8px 0 0; }
.sw-steps .sw-shot { width: 132px; height: 82px; }
.sw-steps .sw-shot img { object-position: top center; }
/* 没摆下的那些。不是可点的东西，所以别长成可点的样子。 */
.sw-stepmore { color: var(--muted-foreground); align-self: center; }

/* 抬头上那两颗切屏的图标。正看着的那一屏要留个底色——两颗长得一样大、挨在一起，
   不标出来的话，人得先点一下才知道现在开着的是哪一屏。 */
.sw-asidetab[aria-pressed='true'] { background: var(--color-accent-100); color: var(--color-accent-800); }

/* ── 右栏里的工作区文件树 ─────────────────────────────────────────────────
   行是按钮但看起来是一行文字：这一栏窄，任何边框和圆角都会把名字挤到换行。 */
.sw-ws { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.sw-ws-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }
.sw-ws-head h3 {
  margin: 0;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted-foreground);
  line-height: 30px;
}
.sw-ws-tree { list-style: none; margin: 0; padding: 0; }
.sw-ws-row {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 3px 6px 3px 8px;
  border: 0;
  border-radius: var(--radius-sm, 6px);
  background: none;
  color: var(--foreground);
  font: inherit;
  font-size: 12.5px;
  line-height: 1.7;
  text-align: left;
  cursor: pointer;
}
/* 行 + 行末那颗删除。整行本身是按钮，所以那一颗只能是它的兄弟（见 chat.js 的
   wsDelBtn），这一层是把两者摆回同一行的容器。 */
.sw-ws-item { display: flex; align-items: center; min-width: 0; }
.sw-ws-item .sw-ws-row { flex: 1; min-width: 0; }
/* 底色跟着整行走，不只跟着那颗大按钮：鼠标挪到删除上时行还亮着，删的是哪一行才清楚。 */
.sw-ws-item:hover .sw-ws-row { background: var(--secondary); }
.sw-ws-row:focus-visible { outline: 2px solid var(--ring); outline-offset: -2px; }
/* 平时透明但**位置一直占着**：改 display 的话，鼠标一进来名字就被挤短一截，
   读到一半的文件名会当场跳一下。 */
.sw-ws-del {
  flex: none;
  padding: 4px;
  margin-right: 2px;
  color: var(--muted-foreground);
  opacity: 0;
  transition: opacity .12s ease;
}
.sw-ws-item:hover .sw-ws-del,
.sw-ws-del:focus-visible { opacity: 1; }
.sw-ws-del:hover { color: var(--color-accent-700); }
/* 触屏上没有 hover 这回事——不给这一条，那颗按钮在手机上永远不出现。 */
@media (hover: none) {
  .sw-ws-del { opacity: 1; }
}
.sw-ws-caret { flex: none; display: inline-flex; color: var(--muted-foreground); }
/* 名字要能被截断，不能把整栏撑开——工作区里的文件名常常很长。 */
.sw-ws-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sw-ws-row > small { flex: none; color: var(--muted-foreground); font-size: 11px; }
.sw-ws-note { margin: 0; padding: 3px 8px; font-size: 12px; color: var(--muted-foreground); line-height: 1.6; }
.sw-ws-err { color: var(--color-accent-700); }

/* ── 右栏里的内嵌桌面 ─────────────────────────────────────────────────────
   槽是占位，真正的 iframe 在 body 上的常驻层里（见 chat.js 的 syncDesktop）——
   整页重绘换不掉它，桌面才不会每重绘一次就断一次。两者的圆角要对得上。 */
.sw-desk { display: flex; flex-direction: column; gap: 6px; }
.sw-desk-slot {
  /* 席位的屏是 1280x800。比例写死成一样的，预览里就不会有黑边或者被裁。 */
  aspect-ratio: 16 / 10;
  width: 100%;
  /* 这套界面没有全局的 box-sizing 重置。常驻层是照这个槽的 border-box 尺寸摆的，
     两边都得算上那 1px 边框，否则层会比槽宽高各多出 2px，右下角对不齐。 */
  box-sizing: border-box;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--muted);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 11.5px;
  color: var(--muted-foreground);
  overflow: hidden;
}
.sw-desk-cap { margin: 0; text-align: center; font-size: 11.5px; color: var(--muted-foreground); }

.sw-deskl {
  position: fixed;
  /* 20：在页面之上、在 .gw-modal-backdrop（25）之下。开日志那类弹层时，这块屏要
     跟着一起被压到蒙版底下去，不能孤零零地浮在上面。 */
  z-index: 20;
  border-radius: 10px;
  overflow: hidden;
  border: 1px solid var(--border);
  /* 见 .sw-desk-slot：宽高是照槽的 border-box 给的，这里必须同口径。 */
  box-sizing: border-box;
  /* **这几块不跟主题翻。** 它们是「看画面」的表面：底下一块黑屏，上面压白字。
     用 --color-neutral-900 的话，深色主题里它是近白色（#f5f3ee），标题条就成了
     白底白字，什么都看不见。 */
  background: #111;
  box-shadow: var(--shadow-sm);
  /* 列：全屏时标题条占一行、屏占剩下的。**不叠在屏上面**——叠上去挡掉的正好是
     桌面顶栏那一条，而那是要点的地方。 */
  display: flex;
  flex-direction: column;
}
.sw-deskl.is-full {
  z-index: 61;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
}
.sw-deskl-frame { display: block; flex: 1; min-height: 0; width: 100%; border: 0; background: #000; }

/* 预览态的挡片。盖住整块屏：点不进去、也拿不到键盘焦点，所以那块屏在这个状态下
   纯粹是「看」。鼠标移上去才浮出「打开」。 */
.sw-deskl-shield {
  position: absolute;
  inset: 0;
  border: 0;
  padding: 0;
  cursor: pointer;
  background: transparent;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 140ms ease;
  font-family: inherit;
}
.sw-deskl.is-full .sw-deskl-shield { display: none; }
.sw-deskl-shield:hover, .sw-deskl-shield:focus-visible {
  background: rgba(0, 0, 0, 0.42);
}
.sw-deskl-open {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 13px;
  border-radius: 999px;
  background: var(--popover);
  color: var(--color-text);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-sm);
  font-size: 12.5px;
  font-weight: 600;
  opacity: 0;
  transform: translateY(3px);
  transition: opacity 140ms ease, transform 140ms ease;
}
.sw-deskl-shield:hover .sw-deskl-open, .sw-deskl-shield:focus-visible .sw-deskl-open {
  opacity: 1;
  transform: none;
}

/* 全屏时的标题条。**必须一直看得见**：焦点一进 iframe，noVNC 就把键盘抓走了，
   Esc 到不了我们这儿，这颗按钮是唯一确定出得去的路。 */
.sw-deskl-bar {
  display: none;
  flex: none;
  /* 标题条在 DOM 里排在 iframe 后面（iframe 要第一个进树，src 才只加载一次），
     所以位置靠 order 提到顶上，而不是靠改顺序。 */
  order: -1;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: 6px 8px 6px var(--space-4);
  background: #111;
  color: #fff;
  font-size: 12.5px;
}
.sw-deskl.is-full .sw-deskl-bar { display: flex; }
.sw-deskl-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sw-deskl-acts { display: flex; align-items: center; gap: var(--space-2); flex: none; }
/* 「重连」。掉过线之后 noVNC 自己回不来（票过期了），这是人手里唯一确定管用的那一下。 */
.sw-deskl-btn {
  padding: 4px 10px;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, #fff 28%, transparent);
  background: transparent;
  color: #fff;
  font-family: inherit;
  font-size: 12px;
  cursor: pointer;
}
.sw-deskl-btn:hover { background: color-mix(in srgb, #fff 16%, transparent); }
.sw-deskl-close { color: #fff; flex: none; }
.sw-deskl-close:hover { background: color-mix(in srgb, #fff 18%, transparent); }

.sw-deskb {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: rgba(0, 0, 0, 0.62);
  opacity: 0;
  pointer-events: none;
  transition: opacity 200ms ease;
}
.sw-deskb.is-on { opacity: 1; pointer-events: auto; }


/* 席位没上线时，正文中间那块「去部署」。右栏可以收起，正文中间不会——最需要指路的
   那一刻，话得说在人正看着的地方。 */
.sw-nodeploy {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  max-width: 42ch;
  text-align: center;
}
.sw-nodeploy-icon { color: var(--muted-foreground); opacity: 0.55; }
.sw-nodeploy h2 { margin: 0; font-size: 16px; font-weight: 600; color: var(--color-text); }
.sw-nodeploy p { margin: 0; font-size: 13px; line-height: 1.7; color: var(--muted-foreground); }
.sw-nodeploy .btn { margin-top: var(--space-2); }
.sw-nodeploy-hint { font-size: 12px !important; }

/* ── 安装进度 ─────────────────────────────────────────────────────────
   建完 Bot 之后那一屏。条子按**步数**走，不按时间：几步的时长差着一个数量级，
   按时间摊出来的匀速条会在装桌面那一步停住不动（见 chat.js 的 installProgressBody）。
   管家报不出第几步时整根条子不画——一根不动的条比没有条更像卡住了。 */
.sw-install-bar {
  width: 100%;
  height: 4px;
  border-radius: 999px;
  background: var(--muted, rgba(127, 127, 127, 0.18));
  overflow: hidden;
}
.sw-install-bar > span {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--color-accent-500, var(--foreground));
  /* 步与步之间平滑地推过去。跳变会让人以为刷新了一次页面。 */
  transition: width 0.4s ease;
}
.sw-install-step { margin: 0; font-size: 13px !important; color: var(--color-text) !important; }
.sw-install-since { margin: 0; font-size: 12px !important; }

/* 已经发出去、还没收到回执的那一条。淡一档：它是真的在路上，不是画着玩的，所以要
   看得见；但和已经落库的消息不能长得一模一样。 */
.sw-msg[data-pending="1"] { opacity: 0.6; }

/* ── `@` 点名与排队 ───────────────────────────────────────────────────
   三块东西，位置不能混：
   - `.sw-mention` 药丸在输入框**上方**，说的是「这一条还没发出去的消息带着谁」；
   - `.sw-queue` dock 在整个输入框**外面的顶上**，说的是「已经发出去、排着队的那几条」；
   - `.sw-pickbox` 选单浮在输入框上沿，打 `@` 才出来。 */

.sw-mention {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  padding: 3px 4px 3px 10px;
  border-radius: 999px;
  /* 和附件药丸区分开：附件是中性色，点名是强调色——它改的是这一轮的工具表，
     不是随手带个文件。 */
  background: var(--color-accent-100);
  color: var(--color-accent-800);
  font-size: 12px;
}
.sw-mention > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 连接器图标。取不到就 this.remove() 掉，药丸退回纯 `@名字`——一颗破图标比没有更糟。 */
.sw-mention > img { width: 14px; height: 14px; flex: none; border-radius: 3px; object-fit: cover; }
.sw-pick-logo { width: 16px; height: 16px; flex: none; border-radius: 4px; object-fit: cover; }

/* 排队 dock。**一条一行、纯文字、不展开附件**——它是「等会儿要说的话」的提醒，
   不是消息气泡的预演。把气泡那一套搬上来，输入框会被顶掉半屏。 */
/* ── 待办 dock：输入框外面的顶上那块可折叠清单（见 docs/todo-tool.md）────────────
   和 .sw-queue 同一个位置逻辑（都在输入框盒子外面），但**长得像一张卡**而不是一行小字：
   排队那几条是「等会儿要说的话」，扫一眼就过；这一块是这件事此刻的进度，人会停下来看。
   所以给它白底、边框和一点浮起来的阴影，让它从输入区里分出来。 */
.sw-todock {
  margin-bottom: 6px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--popover);
  box-shadow: 0 1px 2px color-mix(in srgb, var(--foreground) 6%, transparent);
  overflow: hidden;
}
.sw-todock[hidden] { display: none; }
.sw-todock-bar { display: flex; align-items: stretch; }
/* 抬头整条都可点：折叠态下这一行就是「展开」这个动作的按钮，只让那个小箭头可点的话，
   命中区域只有 13 px 见方。× 单独在外面——它是另一件事，不能被误点成展开。 */
.sw-todock-head {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 4px 8px 10px;
  border: 0;
  background: transparent;
  font-family: inherit;
  font-size: 12.5px;
  color: var(--foreground);
  text-align: left;
  cursor: pointer;
}
.sw-todock-head:hover { background: var(--muted); }
.sw-todock-head .sw-spacer { flex: 1; }
.sw-todock-icon { flex: none; display: inline-flex; color: var(--muted-foreground); }
.sw-todock-title { flex: none; font-weight: 600; }
.sw-todock-badge {
  flex: none;
  padding: 1px 7px;
  border-radius: 999px;
  font-size: 11px;
  line-height: 1.6;
  background: var(--muted);
  color: var(--muted-foreground);
}
/* 正在执行的那一档用强调色：这块 dock 存在的理由就是「现在做到哪儿了」，
   而「有一条正在跑」和「都停着」是人一眼要分出来的两件事。 */
.sw-todock-badge[data-s='run'] {
  background: var(--color-accent-100);
  color: var(--color-accent-700, var(--foreground));
}
/* 折叠时抬头带一句「在做什么」；展开之后那句话就在下面的清单里，再写一遍是重复。 */
.sw-todock-now {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--muted-foreground);
}
.sw-todock[data-open='1'] .sw-todock-now { display: none; }
.sw-todock-count {
  flex: none;
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
  color: var(--muted-foreground);
}
.sw-todock-chev { flex: none; display: inline-flex; color: var(--muted-foreground); transition: transform 140ms ease; }
.sw-todock[data-open='1'] .sw-todock-chev { transform: rotate(180deg); }
.sw-todock-x {
  flex: none;
  width: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-left: 1px solid var(--border);
  background: transparent;
  color: var(--muted-foreground);
  cursor: pointer;
}
.sw-todock-x:hover { background: var(--muted); color: var(--foreground); }
.sw-todock-list {
  margin: 0;
  padding: 2px 10px 8px;
  border-top: 1px solid var(--border);
  list-style: none;
  /* 清单可以很长（上限 50 条）。给它自己一个滚动区，别把输入框顶出屏幕。 */
  max-height: 34vh;
  overflow-y: auto;
}
.sw-todock-list[hidden] { display: none; }
.sw-todock-item {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 4px 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--foreground);
}
.sw-todock-mark { flex: none; display: inline-flex; padding-top: 2px; color: var(--muted-foreground); }
.sw-todock-task { min-width: 0; word-break: break-word; }
.sw-todock-item[data-s='in_progress'] .sw-todock-mark { color: var(--color-accent-500, var(--foreground)); }
.sw-todock-item[data-s='in_progress'] .sw-todock-task { font-weight: 600; }
/* 做完和取消的都退到背景里去：它们已经不需要人再看一眼了，占着位置只是为了让人知道
   这件事走到了哪一步。取消的额外划一道——「不做了」和「做完了」不能长得一样。 */
.sw-todock-item[data-s='completed'] .sw-todock-task,
.sw-todock-item[data-s='cancelled'] .sw-todock-task { color: var(--muted-foreground); }
.sw-todock-item[data-s='cancelled'] .sw-todock-task { text-decoration: line-through; }

.sw-queue {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-bottom: 6px;
  padding: 4px 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--muted);
}
.sw-queue[hidden] { display: none; }
.sw-queue-row { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted-foreground); }
.sw-queue-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sw-queue-more { font-size: 11px; color: var(--muted-foreground); }
.sw-queue-dot {
  flex: none;
  width: 5px;
  height: 5px;
  border-radius: 999px;
  background: var(--color-accent-500, var(--muted-foreground));
}

.sw-pickbox {
  position: absolute;
  left: 8px;
  right: 8px;
  bottom: 100%;
  margin-bottom: 6px;
  z-index: 5;
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-height: 240px;
  overflow: auto;
  padding: 4px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--popover);
  box-shadow: var(--shadow-lg);
}
.sw-pickbox[hidden] { display: none; }
.sw-pick {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 6px 8px;
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  color: inherit;
  font-size: 13px;
  text-align: left;
  cursor: pointer;
}
.sw-pick:hover, .sw-pick.is-on { background: var(--secondary); }
/* 这一轮在跑时命令点不了。标灰**而不是从选单里拿掉**：拿掉的话人会以为自己记错了
   命令名，而不是「这会儿不能用」——右边那行小字说的就是后者。 */
.sw-pick.is-off { opacity: 0.45; cursor: not-allowed; }
.sw-pick.is-off:hover { background: transparent; }
.sw-pick-name { flex: none; }
.sw-pick small { margin-left: auto; color: var(--muted-foreground); font-size: 11px; }
.sw-pick-empty { padding: 8px; font-size: 12px; color: var(--muted-foreground); }

/* ── 预览弹窗：多种看法 ──────────────────────────────────────────────────
 *
 * 图片、提示、转圈这几种要居中；文本和渲染出来的 Markdown 要**顶部对齐并且能滚**
 * ——一份长文档在居中的框里，人打开看到的是正中间那一段，得往上拖才找得到开头。 */
.sw-preview-body[data-flow='top'] {
  display: block;
  align-items: initial;
  justify-content: initial;
  /* --space-5 不存在（这套只有 1/2/3/4/6/8）。写了它整条 padding 会被判为无效
     直接丢掉，结果就是原文顶着框边——已经这么翻过一次车了。 */
  padding: var(--space-4) var(--space-6);
}
/* iframe（PDF / HTML 预览）要占满整框，所以那一档不吃上面的内边距。
   注意是 `:has(.sw-preview-load)` 不是 `:has(> .sw-preview-frame)`——iframe 现在裹在
   转圈容器里，不再是直接子元素。 */
.sw-preview-body[data-flow='top']:has(.sw-preview-load) { padding: 0; }

.sw-preview-src {
  margin: 0;
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--foreground);
  white-space: pre-wrap;
  word-break: break-word;
}
/* 渲染出来的 Markdown 借聊天气泡那一套排版（.sw-md），这里只管容器。 */
.sw-preview-md { max-width: 78ch; margin: 0 auto; color: var(--foreground); }

/* 「预览 / 原文」。做成一对连体的小段，而不是两颗独立按钮——它们是同一个东西的
   两种看法，不是两个操作。 */
.sw-preview-tabs {
  display: inline-flex;
  padding: 2px;
  border-radius: 999px;
  background: var(--secondary);
  margin-right: var(--space-2);
}
.sw-preview-tab {
  border: 0;
  padding: 4px 12px;
  border-radius: 999px;
  background: transparent;
  color: var(--muted-foreground);
  font-size: 12px;
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
}
.sw-preview-tab:hover { color: var(--foreground); }
.sw-preview-tab[data-on='1'] {
  background: var(--background);
  color: var(--foreground);
  box-shadow: var(--shadow-2xs);
}

/* 内容自己加载完之前的转圈。iframe / img 一 load 就把 data-busy 摘掉（见 chat.js
   的 DONE_JS），转圈跟着消失——CSS 只认这个属性，不需要再管一遍状态。 */
/* 居中同时兼顾两种内容：iframe 自己是 100%×100% 铺满，图片按比例缩在中间。 */
.sw-preview-load {
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}
.sw-preview-load > .sw-preview-spin { display: none; }
.sw-preview-load[data-busy] > .sw-preview-spin {
  display: block;
  position: absolute;
  top: 50%;
  left: 50%;
  margin: -11px 0 0 -11px;
}
.sw-preview-spin {
  display: inline-block;
  vertical-align: -3px;
  width: 22px;
  height: 22px;
  margin-right: var(--space-2);
  border-radius: 50%;
  border: 2px solid var(--border);
  border-top-color: var(--muted-foreground);
  animation: sw-spin 700ms linear infinite;
}
.sw-preview-note > .sw-preview-spin { width: 14px; height: 14px; border-width: 2px; }
@keyframes sw-spin { to { transform: rotate(360deg); } }
/* 动效敏感的人不该被一个转不停的圈晃。留一个静止的环，位置和含义都还在。 */
@media (prefers-reduced-motion: reduce) {
  .sw-preview-spin { animation: none; }
}

/* 正文里就地替换出来的文件药丸（见 chat.js 的 inlineFileChips）。
   它长在一行文字中间，所以要跟着基线走，也不能因为文件名长就把这一行撑开。 */
.sw-filechip-inline {
  vertical-align: -3px;
  margin: 0 1px;
  max-width: min(34ch, 100%);
}

/* ── 日常任务（右栏那一列与点进去的那一屏，见 pages-routines.js）──────── */

.sw-rt { display: flex; flex-direction: column; gap: 6px; }
.sw-rt-head { display: flex; align-items: center; gap: var(--space-2); }
.sw-rt-head h3 { flex: 1; margin: 0; font-size: 12px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--muted-foreground); line-height: 30px; }
.sw-rt-empty { display: flex; flex-direction: column; align-items: center; gap: var(--space-3);
  padding: var(--space-4) var(--space-2); text-align: center; }
.sw-rt-empty p { margin: 0; font-size: 12.5px; line-height: 1.6; color: var(--muted-foreground); }
.sw-rt-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.sw-rt-row { width: 100%; display: flex; align-items: center; gap: var(--space-2); text-align: left;
  padding: 7px 8px; border: 0; border-radius: var(--radius-md); background: transparent; cursor: pointer;
  font-family: var(--font-body); color: var(--color-text); }
.sw-rt-row:hover { background: var(--secondary); }
.sw-rt-text { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.sw-rt-text b { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sw-rt-text em { font-style: normal; font-size: 11.5px; color: var(--muted-foreground);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 左边那个圈说的是「上一次跑得怎么样」：绿 = 好，红 = 上次失败了，灰 = 停用了。 */
.sw-rt-ico { flex: none; display: flex; color: var(--color-accent-2-800); }
.sw-rt-ico[data-state='off'] { color: var(--muted-foreground); }
.sw-rt-ico[data-state='err'] { color: var(--color-accent-800); }

.sw-rt-detail { display: flex; flex-direction: column; gap: var(--space-4); }
.sw-rt-bar { display: flex; align-items: center; gap: var(--space-2); }
.sw-rt-bar > span { flex: 1; text-align: center; font-size: 13px; font-weight: 600;
  /* 返回按钮占了左边一格，标题要真的居中就得在右边留同样宽的一格。 */
  margin-right: 33px; }
.sw-rt-acts { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.sw-rt-acts .btn, .sw-switch > span { white-space: nowrap; }
/* .sw-spacer 的 flex:1 是按父级写的（见上面 composer 那两条），这里要自己声明一次。 */
.sw-rt-acts .sw-spacer, .sw-rt-run .sw-spacer { flex: 1; }
.sw-rt-acts .btn { padding: 7px 12px; font-size: 13px; }
/* 开关。一颗按钮，不是 checkbox：整份右栏都是 data-act 分发的，多一种事件源
   （change 冒泡到 form）只会多一处要记得同步的地方。 */
.sw-switch { display: inline-flex; align-items: center; gap: 8px; padding: 0; border: 0;
  background: transparent; cursor: pointer; font-family: var(--font-body); font-size: 13px;
  color: var(--color-text); }
.sw-switch > i { width: 34px; height: 20px; border-radius: 999px; background: var(--muted);
  position: relative; transition: background .15s ease; }
.sw-switch > i::after { content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px;
  border-radius: 50%; background: var(--popover); box-shadow: var(--shadow-2xs); transition: transform .15s ease; }
.sw-switch[aria-pressed='true'] > i { background: var(--primary); }
.sw-switch[aria-pressed='true'] > i::after { transform: translateX(14px); }

.sw-rt-area { border-radius: var(--radius-md); resize: vertical; min-height: 84px; line-height: 1.6;
  font-family: var(--font-body); }
.sw-rt-triggers { display: flex; flex-direction: column; gap: 6px; }
.sw-rt-trigger { display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  border: 1px solid var(--border); border-radius: var(--radius-md); padding: 6px 6px 6px 10px; }
.sw-rt-sel, .sw-rt-time, .sw-rt-num { font-family: var(--font-body); font-size: 13px; color: var(--color-text);
  background: transparent; border: 0; padding: 2px 0; }
.sw-rt-sel:focus-visible, .sw-rt-time:focus-visible, .sw-rt-num:focus-visible { outline: 2px solid var(--ring); border-radius: 4px; }
.sw-rt-num { width: 3ch; }
.sw-rt-minute { font-size: 13px; color: var(--muted-foreground); }
.sw-rt-del { margin-left: auto; padding: 5px; color: var(--muted-foreground); }
.sw-rt-add { display: inline-flex; align-items: center; gap: 6px; align-self: flex-start; padding: 6px 10px;
  border: 1px dashed var(--border); border-radius: var(--radius-md); background: transparent; cursor: pointer;
  font-family: var(--font-body); font-size: 13px; color: var(--muted-foreground); }
.sw-rt-add:hover { color: var(--color-text); border-color: var(--color-neutral-600); }
.sw-rt-next, .sw-rt-none { margin: 0; font-size: 12px; color: var(--muted-foreground); }

.sw-rt-runs { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.sw-rt-run { display: flex; align-items: center; gap: 8px; padding: 7px 2px; font-size: 13px;
  border-bottom: 1px solid var(--color-divider); }
.sw-rt-run:last-child { border-bottom: 0; }
.sw-rt-runtime { color: var(--color-text); }
.sw-rt-tag { font-size: 11px; color: var(--muted-foreground); border: 1px solid var(--border);
  border-radius: 999px; padding: 0 6px; }
.sw-rt-ok { display: flex; color: var(--color-accent-2-800); }
.sw-rt-bad { display: flex; color: var(--color-accent-800); }
.sw-rt-spin { width: 14px; height: 14px; border-radius: 50%; border: 2px solid var(--border);
  border-top-color: var(--muted-foreground); animation: sw-spin 700ms linear infinite; }
@media (prefers-reduced-motion: reduce) { .sw-rt-spin { animation: none; } }


/* ── 委派卡（docs/delegation.md §13）───────────────────────────────
   摆在气泡最底下：它是这一轮里最重的一块，而上面那颗 delegate_task 药丸只说得出
   「调用中」。行是可点的——摊开才有结论和过程，多数时候人只看状态那一眼。 */
.sw-tasks { margin-top: var(--space-2); }

/* Bot 给自己记下一条方法时的那张小卡（docs/skills.md §13）。刻意做得比委派卡轻：
   它是这一轮的一个副作用，不该盖过人正在读的正文。 */
.sw-skillnotes { margin-top: var(--space-2); display: flex; flex-direction: column; gap: 4px; }
.sw-skillnote {
  display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap;
  padding: 6px 10px; font-size: 12.5px;
  border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--card);
}
.sw-skillnote-name { font-weight: 600; }
.sw-skillnote-verb { color: var(--muted-foreground); }
.sw-skillnote-act {
  margin-left: auto; padding: 0; border: 0; background: none; cursor: pointer;
  font: inherit; color: var(--color-accent-800);
}
.sw-skillnote-act + .sw-skillnote-act { margin-left: 0; }
.sw-task-card {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--muted);
  overflow: hidden;
}
.sw-task-head {
  padding: var(--space-2) var(--space-3);
  font-size: 12px;
  color: var(--muted-foreground);
  border-bottom: 1px solid var(--border);
}
.sw-task-row + .sw-task-row { border-top: 1px solid var(--border); }
.sw-task-line {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  padding: var(--space-2) var(--space-3);
  background: none;
  border: 0;
  text-align: left;
  cursor: pointer;
  color: var(--foreground);
  font: inherit;
}
.sw-task-line:hover { background: var(--accent); }
.sw-task-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex: none;
  background: var(--muted-foreground);
}
.sw-task-dot[data-state='running'] { background: var(--color-accent-600); animation: sw-task-pulse 1.4s ease-in-out infinite; }
.sw-task-dot[data-state='done'] { background: #16a34a; }
.sw-task-dot[data-state='failed'],
.sw-task-dot[data-state='timeout'] { background: var(--color-accent-800); }
.sw-task-dot[data-state='lost'] { background: transparent; box-shadow: inset 0 0 0 2px var(--muted-foreground); }
@keyframes sw-task-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
.sw-task-goal { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sw-task-state,
.sw-task-meta { flex: none; font-size: 12px; color: var(--muted-foreground); }
.sw-task-tier-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0 var(--space-3) var(--space-2) calc(var(--space-3) + 16px);
  font-size: 12px;
  flex-wrap: wrap;
}
.sw-task-tier {
  padding: 0 6px;
  border-radius: 4px;
  background: var(--background);
  border: 1px solid var(--border);
  color: var(--muted-foreground);
  font-family: var(--font-mono, monospace);
}
/* 降过级的要看得出来：不标的话人会以为那一档是主代理自己选的。 */
.sw-task-down { color: var(--color-accent-800); }
.sw-task-why { color: var(--muted-foreground); overflow-wrap: anywhere; }
.sw-task-detail {
  padding: 0 var(--space-3) var(--space-3) calc(var(--space-3) + 16px);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.sw-task-summary { overflow-wrap: anywhere; }
.sw-task-files { display: flex; flex-wrap: wrap; gap: var(--space-1); }
.sw-task-empty { margin: 0; font-size: 12px; color: var(--muted-foreground); }
.sw-task-err { color: var(--color-accent-800); }
.sw-task-trace-btn { align-self: flex-start; font-size: 12px; }
/* 过程是一屏可能很长的东西：给它自己的滚动条，别把对话顶下去。 */
.sw-task-trace {
  max-height: 320px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--background);
  padding: var(--space-2);
  font-size: 12px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.sw-trace-say { overflow-wrap: anywhere; }
.sw-trace-tool { font-family: var(--font-mono, monospace); color: var(--muted-foreground); overflow-wrap: anywhere; }
.sw-trace-out {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  color: var(--muted-foreground);
  border-left: 2px solid var(--border);
  padding-left: var(--space-2);
}
.sw-trace-out[data-failed='1'] { border-left-color: var(--color-accent-800); }
/* 替人发话的那条消息：看板卡 / 日常任务落在主会话里的任务交底。角标挂在气泡上沿，
   CSS 画（rowShell 只跑一次，updateRow 不用管它）。 */
.sw-msg[data-via] .sw-bubble::before {
  display: inline-block;
  margin: 0 0 4px;
  padding: 1px 8px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  line-height: 1.6;
  background: var(--color-accent-100);
  color: var(--color-accent-800);
}
.sw-msg[data-via="kanban"] .sw-bubble::before { content: '看板任务'; }
.sw-msg[data-via="routine"] .sw-bubble::before { content: '日常任务'; }
.sw-msg[data-via="handoff"] .sw-bubble::before { content: '人工交还'; }
.sw-msg[data-via="web"] .sw-bubble::before { content: 'Web'; }
.sw-msg[data-via="telegram"] .sw-bubble::before { content: 'Telegram'; }
/* 派来的活那条正文是**机器拼的简报**（cardBrief / 日常任务的交底），一路 `#`、`##`
   排下来。照 .sw-md 的层级画，一个气泡里就出现 20px、18px、14.5px 三种字号，
   而这些标题只是「这张卡要做什么」这类栏目名，不是文章的章节。字号收成正文一档，
   靠字重和上下留白分栏目——层级还在，字体统一了。 */
.sw-msg:is([data-via="kanban"], [data-via="routine"], [data-via="handoff"]) .sw-bubble .sw-md h1,
.sw-msg:is([data-via="kanban"], [data-via="routine"], [data-via="handoff"]) .sw-bubble .sw-md h2,
.sw-msg:is([data-via="kanban"], [data-via="routine"], [data-via="handoff"]) .sw-bubble .sw-md h3,
.sw-msg:is([data-via="kanban"], [data-via="routine"], [data-via="handoff"]) .sw-bubble .sw-md h4,
.sw-msg:is([data-via="kanban"], [data-via="routine"], [data-via="handoff"]) .sw-bubble .sw-md h5,
.sw-msg:is([data-via="kanban"], [data-via="routine"], [data-via="handoff"]) .sw-bubble .sw-md h6 {
  font-size: inherit;
  line-height: inherit;
  margin: var(--space-3) 0 2px;
}
