/* ===========================================================================
   X-Canvas 基础样式（自原型原样搬运，勿随意改动）
   =========================================================================== */

/* ========== CSS变量系统 ========== */
:root {
  --ease: cubic-bezier(.4,0,.2,1);
  --page: #f5f5f5;
  --grid: #e0e0e0;
  --panel: rgba(255,255,255,.95);
  --card: rgba(255,255,255,.98);
  --card-solid: #ffffff;
  --soft: #f0f0f0;
  --soft-2: #e8e8e8;
  --line: #e0e0e0;
  --line-2: #cccccc;
  --text: #111111;
  --muted: #666666;
  --faint: #999999;
  --shadow: rgba(0,0,0,.08);
  --strong: #111111;
  --strong-text: #ffffff;
  --danger: #dc2626;
  --danger-bg: #fee2e2;
  --success: #059669;
  --warning: #f59e0b;
  --radius-node: 22px;
  --radius-btn: 999px;
  --radius-input: 12px;
  --radius-card: 16px;
  --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans SC', sans-serif;
}
.theme-dark {
  --page: #111111;
  --grid: rgba(255,255,255,.06);
  --panel: rgba(30,30,30,.92);
  --card: rgba(35,35,35,.96);
  --card-solid: #1e1e1e;
  --soft: #1a1a1a;
  --soft-2: #2a2a2a;
  --line: #333333;
  --line-2: rgba(255,255,255,.12);
  --text: #e8e8e8;
  --muted: #888888;
  --faint: #555555;
  --shadow: rgba(0,0,0,.35);
  --strong: #ffffff;
  --strong-text: #111111;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: var(--font); background: var(--page); color: var(--text); height: 100vh; overflow: hidden; -webkit-font-smoothing: antialiased; }
button { font-family: inherit; cursor: pointer; border: none; background: none; color: inherit; }
input, textarea, select { font-family: inherit; color: var(--text); }

/* ========== 全局布局 ========== */
.shell { position: relative; height: 100vh; overflow: hidden; }
.topbar { position: absolute; top: 16px; left: 12px; right: 20px; z-index: 30; display: flex; align-items: flex-start; justify-content: space-between; pointer-events: none; }
.panel { pointer-events: auto; background: var(--panel); border: 1px solid var(--line); border-radius: 24px; box-shadow: 0 16px 44px var(--shadow); backdrop-filter: blur(16px); }

/* ========== 画布导航栏 ========== */
.canvas-nav { width: max-content; max-width: min(360px, calc(100vw - 48px)); padding: 8px 6px 8px 8px; display: flex; align-items: center; gap: 4px; }
/* ---------- 返回栏 / 侧栏：hover 暂时展开 + 点击锁定 ---------- */

/*
 * 返回栏默认收起来，只剩一个返回按钮；hover 临时展出画布名和时间，点击锁定后常开。
 *
 * 之前它永远展开，而侧栏只能 hover 展开、不能锁定——
 * 两条栏的行为不一致，用户往返回栏一打易一定会反应过来。
 *
 * 展开规则写成 :is(:hover, .pinned)，让两者共用一套声明——
 * 否则锁定语义与 hover 会马上分叉，修一处就要改两边。
 */
.canvas-nav { transition: all .25s var(--ease); }
.canvas-nav .canvas-nav-meta { opacity: 0; max-width: 0; overflow: hidden; transition: opacity .2s, max-width .25s var(--ease); }
.canvas-nav:is(:hover, .pinned) .canvas-nav-meta { opacity: 1; max-width: 260px; }
.canvas-nav:is(:hover, .pinned) { box-shadow: 0 8px 40px var(--shadow); }

/* 固定的锁定按钮两侧共用 */
.pin-toggle {
  width: 22px; height: 22px; border-radius: 6px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  color: var(--faint); background: transparent; border: none; cursor: pointer;
  transition: color .15s, background .15s;
}
.pin-toggle svg, .pin-toggle i { width: 13px; height: 13px; }
.pin-toggle:hover { color: var(--text); background: var(--soft); }
.pin-toggle[aria-pressed="true"] { color: var(--strong); background: var(--soft); }
/* 收起时只显示返回按钮；hover / 锁定后才露出固定按钮 */
.canvas-nav .pin-toggle {
  width: 0; margin-left: 0; opacity: 0; pointer-events: none;
  transition: opacity .15s, width .2s var(--ease), margin-left .2s var(--ease);
}
.canvas-nav:is(:hover, .pinned) .pin-toggle {
  width: 22px; margin-left: 4px; opacity: 1; pointer-events: auto;
}
.sidebar .pin-toggle { opacity: 0; transition: opacity .15s; }
.sidebar:is(:hover, .pinned) .pin-toggle { opacity: 1; }
.sidebar-brand {
  font-size: 9px; font-weight: 800; color: var(--faint);
  text-transform: uppercase; letter-spacing: .1em; opacity: 0;
  transition: opacity .2s; margin-top: 4px; text-align: center;
}

.canvas-nav-meta { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.current-canvas-title { max-width: 260px; color: var(--text); font-size: 12px; font-weight: 800; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/*
 * 画布更新时间的副标题。
 *
 * 这个 class 之前**根本没写样式**：DOM 里填的是 toLocaleString() 的完整日期时间，
 * 于是以正文字号正文色渲染在导航条里，一个 36px 高的面板被一行 16px 的日期撑开，
 * 标题和时间字号一样大、时间还不换行，导航条显得又高又乱。
 */
.canvas-time { font-size: 10px; font-weight: 600; color: var(--faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.current-canvas-time { max-width: 260px; color: var(--faint); font-size: 10px; }

/* ========== 工具栏 ========== */
.toolbar { padding: 10px; display: flex; gap: 6px; align-items: center; flex-wrap: nowrap; overflow-x: auto; }
.tool-btn { height: 36px; min-width: 36px; padding: 0 11px; border-radius: var(--radius-btn); display: flex; align-items: center; justify-content: center; gap: 6px; color: var(--muted); background: var(--card-solid); border: 1px solid var(--line); font-size: 11px; font-weight: 700; transition: all .2s var(--ease); white-space: nowrap; flex-shrink: 0; }
.tool-btn:hover { background: var(--soft-2); color: var(--text); }
.tool-btn.active { background: var(--strong); color: var(--strong-text); border-color: var(--strong); }
.tool-btn svg, .tool-btn i { width: 14px; height: 14px; flex-shrink: 0; }
.canvas-nav-btn { height: 32px; width: 32px; min-width: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: var(--muted); background: transparent; transition: all .15s var(--ease); }
.canvas-nav-btn:hover { background: var(--soft-2); color: var(--text); }

/* ========== 画布管理页面 (Gate) ========== */
/*
 * 画布管理页。
 *
 * 这里原本是 align-items: center + overflow-y: auto，是 flex 居中 + 滚动最经典的翻车组合：
 * 内容比容器高时，flex item 会向**上下两侧**同时溢出，而溢出到上方的那部分
 * 根本不在可滚动区域内 —— 于是列表上面的几张画布永远看不见，
 * 往下滚只能滚到下面的溢出部分，看起来像「滚不到顶」。
 *
 * 所以这里不用 align-items 居中。
 *
 * ⚠️ 但也不能图省事写回 align-items: center，那会把上面那个 bug 请回来。
 *
 * ⚠️⚠️ 更隐蔽的是 margin: auto —— 它在**主轴和交叉轴两个方向**都吸收剩余空间。
 * 也就是说子项会被**垂直居中**，于是：
 *   主页（内容最高，几乎没剩余）→ 顶栏贴近顶部
 *   项目页（内容较高，剩余少）  → 顶栏被推下去一点
 *   协作项目（空状态，最矮）    → 剩余最多，顶栏被推到最下面
 * 用户看到的现象就是「顶栏在三个页面里高低不一样」——
 * 同一套壳，切个视图它自己会跳位置。
 * 所以必须是 margin: 0 auto：只做水平居中，垂直一律顶到容器上沿。
 */
.canvas-gate { position: absolute; inset: 0; z-index: 20; display: flex; overflow-y: auto; overscroll-behavior: contain; background: var(--page); }
/*
  ---------------------------------------------------------------------------
  背景：点阵网格 + 顶部光晕
  ---------------------------------------------------------------------------
  为什么是点阵而不是 7lai 的星空
  - 点阵 = 网格，暗示「这里能摆东西」，和产品的本质一致（它就是个画布）；
  - 星空要靠闪烁才有效果，而闪烁会和画布里节点运行态那一整套动画抢注意力；
  - 点阵是纯静态渐变，零 JS、零重绘。
  光晕只给顶部一抹，给「大面积纯黑」一个视觉重心。
  刻意做到很淡（opacity .05 / .10）：背景一旦抢戏，用户的注意力就从
  「开始创作」跑到「看背景」上去了。
*/
.gate-bg { position: absolute; inset: 0; pointer-events: none; }
.gate-bg::before {
  content: ""; position: absolute; inset: 0;
  /* 两个偏移的 radial-gradient 打出 24px 的点阵，视觉上比 24px 密一点 */
  background-image:
    radial-gradient(circle at 1px 1px, color-mix(in srgb, var(--text) 9%, transparent) 1px, transparent 0),
    radial-gradient(circle at 1px 1px, color-mix(in srgb, var(--text) 9%, transparent) 1px, transparent 0);
  background-size: 24px 24px;
  background-position: 0 0, 12px 12px;
}
/* 光晕。用 --strong 跟随主题，浅色主题下会自动变淡。 */
.gate-bg::after {
  content: ""; position: absolute; left: 50%; top: -180px; width: 900px; height: 560px;
  transform: translateX(-50%);
  background: radial-gradient(ellipse at center, color-mix(in srgb, var(--strong) 12%, transparent) 0%, transparent 70%);
}
/* 浅色主题下点阵和光晕都要退到几乎看不见，否则纸白底上会脏 */
body.light .gate-bg::before { opacity: .5; }
body.light .gate-bg::after { opacity: .5; }

/*
  ---------------------------------------------------------------------------
  主内容列：1180px
  ---------------------------------------------------------------------------
  先量了参考页（1920 视口）的实际比例，再按比例搬过来：

    区块          参考页        占列宽      搬到 1180
    输入框        895px         67%   →   800px
    场景卡        960px         72%   →  1180（占满整列）
    最近项目      1330px       100%   →  1180（占满整列）

  三个区块**宽度不同、逐级放宽**，这才是参考页看着「错落有致」的原因。
  上一版把三者一起等比缩小（960 / 760 / 904），结果全挤在中间一条窄带里，
  两侧各空 500px —— 那不是留白，那是塌陷。留白是区块**之间**的，
  不是整页两侧的。

  1180 在 1920 视口下占 61%，比原来的 1080（56%）更宽松，
  窄屏由 max-width + 两侧 28px 内边距自然收敛。

  ⚠️ 上下留白**不在这里**。
  v2.7 曾经在这里写 padding-top: 52，那是把整个壳往下推 ——
  结果顶栏被连带拖下去了（用户明确说过：下沉不含顶栏）。
  现在垂直方向的留白全部交给下面的 .gate-head margin-bottom（顶栏→标题）
  和 .gate-hero margin-bottom（标题→输入框），见各自的注释。
  所以这里 padding-top 必须是 0。
*/
.gate-panel {
  width: 100%; max-width: 1180px; padding: 0 28px 44px; margin: 0 auto;
  flex-shrink: 0; position: relative; z-index: 1;
}
/*
  顶部菜单栏：定死位置。

  ① 水平：width 与 .gate-panel 完全一致，所以左边品牌和右边那排按钮
     在三个视图里都对得整整齐齐，不会因为某个视图内容多就变宽。
  ② 垂直：position: sticky + top: 0，滚动时吸附在容器顶部。
     页面比一屏高的时候（个人项目多的时候就是），不吸附的话顶栏会被
     滚出视野，「固定位置」就成了空话。
  ③ 高度不再参与「整体下沉」—— 上一轮的 padding-top: 52 是把整个壳
     往下推，顶栏也跟着下去了，那是误伤。用户要的是**顶栏贴上沿、
     下面的内容下沉**，所以内边距全部改成给顶栏自己那一条。

  负 margin 抵掉 .gate-panel 的 28px 左右内边距 + 自己补回来：
  这样背景色能铺满整行（否则背景只盖住内容宽度，两侧各露 28px 的缝，
  缝里会透出正在滚过去的项目卡），但内容仍然和上面的列左右对齐。
*/
.gate-head {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  position: sticky; top: 0; z-index: 5;
  margin: 0 -28px 56px; padding: 20px 28px 0;
  /*
    静止时**必须完全透明**。
    之前这里给了半透明底 + backdrop-filter，结果顶栏那条带子上，
    点阵背景和顶部光晕全被盖住 —— 页面中间横着一条纯黑的杠，
    上下两截背景对不上，看起来像「背景丢了」。
    blur(10px) 更是把点阵直接抹平，点本来就只有 9% 不透明度。
    而静止时顶栏背后**本来就没有内容**（正文全在它下面的 margin 里），
    给它上背景纯属自己遮自己。
    真的滚起来之后才有内容从底下穿过，那时才需要 is-stuck 的毛玻璃
    来防止文字和按钮撞在一起 —— 由 gate.js 按 scrollTop 切换。
  */
  background: transparent;
  transition: background .18s var(--ease);
}
.gate-head.is-stuck {
  background: color-mix(in srgb, var(--page) 88%, transparent);
  backdrop-filter: blur(10px) saturate(120%);
}
.gate-head-left { display: flex; align-items: center; gap: 18px; min-width: 0; }
/* 主页 / 项目 切换：胶囊形，参考 7lai 顶部导航 */
.gate-nav { display: flex; gap: 2px; padding: 3px; border-radius: 999px; background: var(--soft); border: 1px solid var(--line-2); }
.gate-nav-btn {
  padding: 5px 15px; border: 0; border-radius: 999px; background: transparent;
  color: var(--muted); font-size: 12px; font-weight: 800; cursor: pointer;
  transition: all .15s var(--ease);
}
.gate-nav-btn:hover { color: var(--text); }
.gate-nav-btn.active { background: var(--card-solid); color: var(--text); box-shadow: 0 1px 4px var(--shadow); }
.gate-brand { font-size: 11px; font-weight: 800; letter-spacing: .14em; color: var(--faint); }
/* 标题回到 30px：参考页的大标题是整页第一视觉，我们之前缩到 27 显得拘谨 */
.gate-slogan { margin: 0; font-size: 30px; font-weight: 800; letter-spacing: -.02em; color: var(--text); line-height: 1.3; text-align: center; }
/* 标题 ↔ 副标题：8 → 14。副标题贴着大标题会像标题的注脚，间距给足它才是独立的一句 */
.gate-sub { margin: 14px 0 0; font-size: 13px; color: var(--muted); line-height: 1.6; text-align: center; }
.gate-count-pill { padding: 1px 8px; border-radius: 999px; background: var(--soft); color: var(--muted); font-size: 10px; font-weight: 800; border: 1px solid var(--line); }
.gate-head-actions { display: flex; gap: 8px; align-items: center; flex-shrink: 0; }
.gate-icon-btn { width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: var(--muted); border: 1px solid var(--line); background: var(--card-solid); transition: all .15s var(--ease); }
.gate-icon-btn:hover { background: var(--soft-2); color: var(--text); }
.gate-icon-btn svg { width: 15px; height: 15px; }
.primary-btn { height: 38px; padding: 0 18px; border-radius: var(--radius-btn); background: var(--strong); color: var(--strong-text); display: flex; align-items: center; justify-content: center; gap: 6px; font-size: 12px; font-weight: 800; transition: all .2s var(--ease); border: none; }
.primary-btn:hover { opacity: .88; }
/*
  小节标题。分隔线上方留 42px 是刻意的 ——
  参考页的场景卡到「最近项目」之间有大约 145px 的空白，
  我们页面更密（没有 Agent 自动剪辑那一大块），按比例取 42px。
  留白是区块**之间**的呼吸，不是把内容缩小来腾地方。
*/
.gate-list-head { display: flex; align-items: center; gap: 8px; margin-top: 42px; padding-top: 20px; border-top: 1px solid var(--line); }
.gate-list-title { font-size: 11px; font-weight: 800; letter-spacing: .09em; text-transform: uppercase; color: var(--faint); }
.gate-more {
  margin-left: auto; display: flex; align-items: center; gap: 4px;
  padding: 4px 10px; border: 0; border-radius: 7px; background: transparent;
  color: var(--muted); font-size: 11px; font-weight: 700; cursor: pointer;
  transition: all .15s var(--ease);
}
.gate-more:hover { color: var(--text); background: var(--soft-2); }
.gate-more svg { width: 13px; height: 13px; }
/*
  主页的最近项目：**固定 3 个槽位**且整体居中。
  固定槽位而不是 auto-fill 网格 —— 主页是「入口」不是「清单」，
  摆 6 张卡会把「这是个内容列表」的印象坐实，那正是要弱化的东西。
  项目页才是清单。
  槽位用 flex + justify-content:center：没有项目时只有 1 个按钮，
  它会自己落到正中，而不是贴在左边。

卡片 350px / 间距 20px：3 张正好 1090px，在 1180 的列里只差 90px。
  这个「差一点点填不满」是关键 —— 之前用 252px 时三张只占 784px，
  列里空出 200px，两侧又是黑边，等于把「两侧空」的问题从页面挪进了区块里。

  比例 16/9（350×197），比 3/2 扁一截。这是照参考页来的：
  它的项目卡约 310×170 ≈ 1.82，和 16/9 的 1.78 几乎一样。
  更重要的是**高度是省出来的空间** —— 每张矮 36px，三张就腾出 108px，
  这些空间给了顶部（标题区），内容整体下沉，页面不再顶到上边框。
  再扁就会挤：我们卡里竖排了图标 + 标题 + meta，169px 内容区装不下两行标题。
*/
.gate-recent-row { display: flex; justify-content: center; gap: 20px; margin-top: 14px; }
.gate-recent-row .canvas-item { width: 350px; flex: 0 0 350px; aspect-ratio: 16/9; }
.gate-create-tile {
  width: 350px; flex: 0 0 350px; aspect-ratio: 16/9;
  border: 1px dashed var(--line); border-radius: var(--radius-card);
  background: transparent; cursor: pointer; color: var(--muted);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px;
  transition: all .18s var(--ease);
}
.gate-create-tile:hover { border-color: var(--strong); color: var(--text); background: var(--soft); }
.gate-create-tile .tile-plus {
  width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
  background: var(--soft); border: 1px solid var(--line);
}
.gate-create-tile .tile-plus svg { width: 17px; height: 17px; }
.gate-create-tile .tile-title { font-size: 13px; font-weight: 750; }
.gate-create-tile .tile-sub { font-size: 10.5px; color: var(--faint); font-weight: 600; }
.gate-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 14px; margin-top: 12px; }
.gate-list-empty { grid-column: 1/-1; text-align: center; padding: 60px 20px; color: var(--muted); font-size: 14px; line-height: 1.9; }
.gate-list-empty-sub { font-size: 12px; color: var(--faint); }
.gate-list-empty-icon { margin-bottom: 12px; color: var(--faint); }
.gate-list-empty-icon svg { width: 40px; height: 40px; }
/* 个人 / 协作分组 */
.gate-scope { display: flex; gap: 4px; }
.gate-scope-btn {
  padding: 7px 16px; border-radius: 9px; cursor: pointer;
  background: var(--soft); border: 1px solid var(--line-2);
  color: var(--muted); font-size: 12px; font-weight: 800;
  transition: all .15s var(--ease);
}
.gate-scope-btn:hover { color: var(--text); }
.gate-scope-btn.active { background: var(--strong); border-color: var(--strong); color: var(--strong-text); }
/* 项目页搜索 */
.gate-project-search-wrap { margin-left: auto; position: relative; display: flex; align-items: center; }
.gate-project-search-wrap svg { position: absolute; left: 9px; width: 13px; height: 13px; color: var(--faint); pointer-events: none; }
.gate-project-search {
  width: 200px; height: 30px; padding: 0 10px 0 28px;
  border: 1px solid var(--line-2); border-radius: 8px;
  background: var(--card-solid); color: var(--text);
  font-size: 12px; font-weight: 600; outline: none; transition: border-color .18s var(--ease);
}
.gate-project-search:focus { border-color: var(--strong); }
.gate-project-search::placeholder { color: var(--faint); font-weight: 550; }
/* 项目页里的「新建画布」占位：和其他卡等宽等高，不占满一行 */
.gate-list .gate-create-tile { width: auto; height: auto; aspect-ratio: 4/3; }
.canvas-item { position: relative; aspect-ratio: 4/3; border: 1px solid var(--line); background: var(--card-solid); border-radius: var(--radius-card); cursor: pointer; transition: all .18s var(--ease); overflow: hidden; }
.canvas-item:hover { border-color: var(--strong); box-shadow: 0 4px 20px var(--shadow); transform: translateY(-2px); }
.canvas-item.active { border-color: var(--strong); box-shadow: 0 0 0 2px var(--strong); }
.canvas-open { width: 100%; height: 100%; padding: 14px; display: flex; flex-direction: column; gap: 8px; }
.canvas-card-icon { width: 34px; height: 34px; border-radius: 10px; display: flex; align-items: center; justify-content: center; background: var(--soft); color: var(--strong); flex-shrink: 0; }
.canvas-card-icon svg { width: 16px; height: 16px; }
.canvas-card-title { font-size: 13px; font-weight: 750; color: var(--text); overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; word-break: break-word; flex: 1; }
/*
  9px + uppercase + .04em 是从英文版式抄来的，中文扛不住：
  9px 的中文在深色底上笔画粘连，uppercase 对中文又完全没有意义。
  提到 11px、去掉 uppercase，是这一批里成本最低的可读性修复。
*/
.canvas-card-meta { margin-top: auto; font-size: 11px; color: var(--faint); font-weight: 600; letter-spacing: .01em; }
.canvas-delete { position: absolute; right: 8px; top: 8px; width: 26px; height: 26px; border-radius: 10px; display: flex; align-items: center; justify-content: center; color: var(--muted); background: var(--card-solid); border: 1px solid var(--line); opacity: 0; transition: all .14s var(--ease); z-index: 2; }
.canvas-item:hover .canvas-delete { opacity: 1; }
.canvas-delete:hover { color: var(--danger); border-color: var(--danger); background: var(--danger-bg); }
.canvas-card-edit { position: absolute; right: 40px; top: 8px; width: 26px; height: 26px; border-radius: 10px; display: flex; align-items: center; justify-content: center; color: var(--muted); background: var(--card-solid); border: 1px solid var(--line); opacity: 0; transition: all .14s var(--ease); z-index: 2; }
.canvas-item:hover .canvas-card-edit { opacity: 1; }
.canvas-card-edit:hover { color: var(--text); background: var(--soft-2); }
/* ---------- 首页 hero ----------
   位置在 gate-head 下面、画布列表上面。它是这个页面唯一的「主动作」：
   列表是回来继续干活的，hero 是开始干活的，两件事的视觉权重不该一样。 */
/*
  ⚠️ 这个 margin-bottom 是整块 hero（输入框 / 场景卡 / 最近项目）**之后**的间距，
  不是「标题 → 输入框」的间距 —— composer 是 .gate-hero 的子元素（见 index.html）。
  想加大标题和下面那整块之间的距离，要改 .gate-composer 的 margin-top。
  v2.9 一度把这里从 26 改成 44，结果只是把页面底部顶下去了，位置完全没动。
*/
.gate-hero { margin: 0 0 26px; }
/*
  输入框：800px 居中，占主列的 68%。
  刻意**比下面那排场景卡窄** —— 参考页也是这样（输入框 895 < 场景卡 960）。
  三块等宽反而会显得像一张表，看不出层次。

  为什么换 textarea：input 只能一行。用户往往要先草稿再回车，
  而 input 里退格清空一段长文本很烦（一次一行，按一次删一个字）。
  textarea 的高度给 min-height，rows 只是初始值 —— 它会随内容长高。
  这里限制 max-height 并允许内部滚动，免得一段长提示词把整页顶下去。
*/
.gate-composer {
  /*
    margin-top 20 → 44：这就是「框内整体下移」要的那一档。
    标题（slogan + sub）和它下面那一整块（输入框 / 场景卡 / 最近项目）
    拉开 44px，视觉上整块内容往下沉了一截，而不是把标题单独往下挪
    —— 那只会让标题去顶顶栏。
  */
  max-width: 800px; margin: 44px auto 0;
  border: 1px solid var(--line); border-radius: 15px;
  background: var(--card-solid); overflow: hidden;
  transition: border-color .2s var(--ease), box-shadow .2s var(--ease);
}
.gate-composer:focus-within { border-color: var(--strong); box-shadow: 0 0 0 3px var(--shadow); }
.gate-composer-input {
  display: block; width: 100%; min-height: 92px; max-height: 230px; resize: none;
  border: 0; background: none; color: var(--text);
  font-family: inherit; font-size: 14.5px; font-weight: 600; line-height: 1.7;
  padding: 17px 20px 6px; outline: none;
}
.gate-composer-input::placeholder { color: var(--faint); font-weight: 500; }
.gate-composer-bar { display: flex; align-items: center; gap: 10px; padding: 0 12px 11px; }
.gate-composer-modes { display: flex; gap: 5px; flex: 1; min-width: 0; }
/*
  三个模式按钮。
  ⚠️ 目前**只有视觉态，行为相同**（都进画布建 generator）——
  预留入口，等生成卡片重做完再接。active 态切换是实的，所以点起来不会像「没反应」。
*/
.gate-mode {
  padding: 6px 13px; border-radius: 8px; cursor: pointer;
  background: var(--soft); border: 1px solid var(--line-2);
  color: var(--muted); font-size: 11.5px; font-weight: 750;
  transition: all .15s var(--ease); white-space: nowrap;
}
.gate-mode:hover { color: var(--text); }
.gate-mode.active { background: var(--strong); border-color: var(--strong); color: var(--strong-text); }
.gate-send {
  width: 33px; height: 33px; border-radius: 9px; border: none; cursor: pointer;
  background: var(--soft-2); color: var(--muted);
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
  transition: all .15s var(--ease);
}
.gate-send:hover { background: var(--strong); color: var(--strong-text); }
.gate-send:disabled { opacity: .4; cursor: default; }
.gate-send svg { width: 15px; height: 15px; }
/*
  「不会替你点生成」这句必须留着（它是防止用户误以为一句话直接出图的唯一说明），
  跟着输入框一起收窄、左对齐到输入框的左边缘 —— 之前它是顶格对齐的，
  和上方居中的输入框对不齐，看着像另一段文字。
*/
.gate-hero-note { max-width: 800px; margin: 9px auto 0; padding-left: 2px; font-size: 10.5px; color: var(--faint); font-weight: 600; }
.gate-hero-label { margin: 24px 0 9px; font-size: 10px; font-weight: 800; letter-spacing: .09em; text-transform: uppercase; color: var(--faint); }

/*
  ---------------------------------------------------------------------------
  场景卡横向跑马灯（从右往左）
  ---------------------------------------------------------------------------
  两端渐隐用外层容器的 ::before/::after 绝对定位遮罩，
  **不用 mask-image**：mask 会把滚过边缘的卡片文字一起糊掉，
  而这里卡片本来就该被裁断，显式遮罩才能只淡背景不淡内容边缘。

  动画挂在 track 上而不是容器上 —— 容器加了 overflow 后再动画，
  会连带触发一次重排；track 动画走的是 transform，走合成器。
*/
.preset-marquee { position: relative; overflow: hidden; margin: 0; padding: 2px 0 4px; }
.preset-marquee::before,
.preset-marquee::after {
  content: ""; position: absolute; top: 0; bottom: 0; width: 48px; z-index: 2; pointer-events: none;
}
.preset-marquee::before { left: 0; background: linear-gradient(to right, var(--page), transparent); }
.preset-marquee::after { right: 0; background: linear-gradient(to left, var(--page), transparent); }
.preset-marquee-track {
  display: flex; width: max-content; padding: 0;
  animation: marquee-left var(--marquee-duration, 42s) linear infinite;
}
/*
  ⚠️ 轨道上**不能有 gap，也不能有 padding**。
  两者都会让「-50% 的位移」偏离「一个分组的宽度」，接缝就会跳：
    轨道总宽 = pad*2 + 2*组宽，-50% = pad + 组宽，
    而第二份接上第一份需要位移恰好 = 组宽 → 差一个 pad。
  gap:12px 时差 6px、padding:4px 时差 4px —— 都是每隔一圈「咯噔」一下。
  所以间距一律放在**分组内部**（见 .preset-marquee-group），
  两端要留白就靠容器两端的渐隐遮罩，不靠轨道自身的 padding。
*/
.preset-marquee-group { display: flex; gap: 12px; padding-right: 12px; }
@keyframes marquee-left {
  /* 0% → -50%：轨道正好由两个等宽分组组成，-50% 精确等于一个分组宽度，
     所以第二份接上第一份时没有接缝。 */
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}
/* 悬停暂停：用户正在读卡片名的时候它不能继续滑走 */
.preset-marquee:hover .preset-marquee-track { animation-play-state: paused; }
/* 前庭敏感的用户：直接停掉，并去掉左移起始位（否则内容会偏在左边） */
@media (prefers-reduced-motion: reduce) {
  .preset-marquee-track { animation: none; }
}
.preset-marquee-track .preset-card { width: 150px; flex: 0 0 150px; cursor: pointer; }
.gate-create-row { display: flex; gap: 8px; margin-top: 16px; }
.gate-name-input { flex: 1; height: 42px; border: 1px solid var(--line); border-radius: var(--radius-input); background: var(--card-solid); color: var(--text); padding: 0 14px; font-size: 13px; font-weight: 600; outline: none; transition: border-color .2s; }
.gate-name-input:focus { border-color: var(--strong); }
.gate-name-input::placeholder { color: var(--faint); }

/* ========== 画布区域 ========== */
/*
 * 画布与坐标层
 * ------------------------------------------------------------------
 * 1) 点阵必须铺满**整个画布视口**，而不是 .world 那块有限矩形。
 *    画在 .world 里虽然能跟着 transform 动，但 .world 只有
 *    4000×4000 左右，往左/上平移后就会露出没有点的空白区域。
 *    现在画在 .board 上，由 JS 把视口平移/缩放同步到 background-position
 *    与 background-size；点阵仍是无限延伸的。
 *
 * 2) 不再使用 40000×40000 的固定巨型层。
 *    Chrome 合成层单边上限约 16384px，超出走分块光栅化，
 *    1.6×10⁹ 像素的层是缩放「整块泛白/闪烁」的高概率来源。
 *    改成跟随视口尺寸 + 内部 offset，元素数量不变但层面积可控。
 */
.board {
  position: absolute; inset: 0; cursor: grab;
  background-color: var(--page);
  background-image: radial-gradient(var(--grid) var(--grid-dot, 1px), transparent var(--grid-dot, 1px));
  background-size: var(--grid-size, 24px) var(--grid-size, 24px);
  background-position: var(--grid-x, 0px) var(--grid-y, 0px);
  overflow: hidden;
}

body.hide-dots .board { background-image: none; }

.board.dragging { cursor: grabbing; }

.world {
  position: absolute; left: 0; top: 0;
  width: var(--world-size, 10000px);
  height: var(--world-size, 10000px);
  transform-origin: 0 0;
}

/* SVG 连线层：不参与 pointer（连线靠 .link-hit 单独命中） */
.links {
  position: absolute; left: 0; top: 0;
  width: var(--world-size, 10000px);
  height: var(--world-size, 10000px);
  overflow: visible;
  pointer-events: none;
  z-index: 1;
}
.link-hit { stroke: transparent; stroke-width: 18; fill: none; pointer-events: stroke; cursor: pointer; }
.link-controls { position: absolute; left: 0; top: 0; width: var(--world-size, 10000px); height: var(--world-size, 10000px); pointer-events: none; z-index: 3; }
.link-delete { position: absolute; width: 22px; height: 22px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transform: translate(-50%, -50%); color: var(--muted); background: var(--card-solid); border: 1px solid var(--line); box-shadow: 0 4px 12px var(--shadow); opacity: 0; pointer-events: none; transition: opacity .14s var(--ease); cursor: pointer; font-size: 12px; }
.link-delete.visible { opacity: 1; pointer-events: auto; }
.link-delete:hover { color: var(--danger); border-color: var(--danger); background: var(--danger-bg); }
#nodes { position: absolute; inset: 0; z-index: 2; pointer-events: none; }
#nodes .node { pointer-events: auto; }

/* ========== 节点 ========== */
.node { position: absolute; min-width: 220px; min-height: 80px; border: 1px solid var(--line); border-radius: var(--radius-node); background: var(--card); box-shadow: 0 12px 36px var(--shadow); overflow: visible; user-select: none; z-index: 2; transition: box-shadow .15s; }
.node:hover { box-shadow: 0 16px 48px var(--shadow); }
.node.selected { outline: 2px solid var(--strong); outline-offset: 2px; }
.node.sized { display: flex; flex-direction: column; }
/*
 * 视口裁剪（见 web/js/canvas/cull.js）。
 *
 * 用 display:none 而不是 visibility:hidden ——
 * 后者保留布局盒，浏览器仍要算它的位置和层级，卡顿一点不减。
 * 也不用 .culled .node-body { display:none }：整卡隐藏收益最大，
 * 而且 head 也不该被合成（阴影 + 边框 + 圆角正是最贵的一层）。
 *
 * ⚠️ 必须写成 #nodes .node.culled，不能只写 .node.culled：
 * .node.sized { display:flex } 与 .node.culled { display:none }
 * 特异性完全相同（都是 0,2,0），谁写在后面谁赢。
 * 而 .sized 恰好对**声明了固定高度**的节点生效 —— 实际占绝大多数 ——
 * 于是裁剪被静默压掉：逻辑判定「16 / 300 可见」，DOM 里却一个都没隐藏，
 * 指标全是 0，看着像「裁剪没用」。加 #nodes 前缀把特异性抬到 (1,2,0) 才是稳的。
 *
 * 之所以不把这些节点从 DOM 里摘掉：body 里有输入框，
 * 销毁会丢焦点 / 光标 / IME 组合输入态（statusbar.js 的注释记录过这个 bug）。
 */
#nodes .node.culled { display: none; }
.node-head { height: 40px; flex: 0 0 40px; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 0 12px; border-bottom: 1px solid var(--line); border-radius: var(--radius-node) var(--radius-node) 0 0; cursor: move; }
.node-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
.node-head .close-btn { width: 22px; height: 22px; border-radius: 6px; display: flex; align-items: center; justify-content: center; color: var(--muted); transition: all .15s; }
.node-head .close-btn:hover { color: var(--danger); background: var(--danger-bg); }
.node-head .props-btn { width: 22px; height: 22px; border-radius: 6px; display: flex; align-items: center; justify-content: center; color: var(--muted); transition: all .15s; }
.node-head .props-btn:hover { color: var(--text); background: var(--soft); }
.node-head .props-btn svg { width: 13px; height: 13px; }
.node-body { padding: 12px; }
.node.sized .node-body { flex: 1; min-height: 0; overflow: auto; }
.resize-handle { position: absolute; right: 7px; bottom: 7px; width: 18px; height: 18px; border-radius: 8px; cursor: nwse-resize; background: var(--card-solid); border: 1px solid var(--line); display: flex; align-items: center; justify-content: center; color: var(--muted); opacity: 0; transition: all .18s var(--ease); z-index: 5; }
.node:hover .resize-handle, .node.selected .resize-handle { opacity: 1; }
.resize-handle::before { content: ""; width: 8px; height: 8px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; }

/* ========== 输入控件 ========== */

/**
 * 裸 input / select / textarea 的兜底样式。
 *
 * 之前只有 .setting-input 有背景和边框，而配置页偏好设置那些是裸元素
 * （selectInput() 里就是 `el('select')`，没有 class）。于是：
 *   背景 = 浏览器默认白、文字 = var(--text)
 * 深色主题下 --text 是浅色，等于**白底浅字**，整个控件等于隐形 ——
 * 「默认模型」四个下拉看不到自己选了哪个。
 *
 * 放在这里而不是给每个调用点加 class：这类问题只要漏一个调用点就会复现，
 * 而原生控件的默认外观不受任何变量控制，只能由我们兜住。
 */
input:not([type="checkbox"]):not([type="radio"]),
select,
textarea {
  background: var(--card-solid);
  color: var(--text);
  border: 1px solid var(--line-2);
  border-radius: var(--radius-input);
  padding: 0 10px;
  font-size: 12px;
  font-weight: 600;
  outline: none;
}
/* 数字输入框的上下箭头在深色下几乎不可见，且点击区域太小 */
input[type="number"] { min-height: 30px; }
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button { opacity: .55; }
/**
 * 下拉箭头：原生 appearance 在深色下要么消失要么是黑块。
 * 用 SVG 内联画一个跟着 --muted 走的箭头，两种主题都清楚。
 */
select {
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
  padding-right: 26px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 9px center;
}

/**
 * 复选框 / 单选框。
 *
 * 之前完全没有样式，就是浏览器原生那个蓝色大方块。深色主题下它显得
 * 又大又突兀，而且和旁边 11px 的文字对不齐。
 *
 * 用 accent-color 而不是 appearance:none —— 前者保留系统原生的绘制
 * （含聚焦环、无障碍的键盘行为），只在颜色和尺寸上收一收。自己画
 * appearance:none 的版本必须自己实现 :focus-visible 和高对比度模式，
 * 那是纯亏。
 */
input[type="checkbox"],
input[type="radio"] {
  width: 15px;
  height: 15px;
  margin: 2px 0 0;
  flex: none;
  accent-color: var(--strong);
  cursor: pointer;
}
/* 悬停时整行高亮，让「这一行可点」这件事看得见 */
.tool-row:hover input[type="checkbox"],
.tool-row:hover input[type="radio"] { accent-color: var(--strong); }

.setting-row { display: flex; flex-direction: column; gap: 4px; margin-bottom: 10px; }
.setting-label { color: var(--faint); font-size: 10px; font-weight: 800; text-transform: uppercase; letter-spacing: .08em; }
.setting-input { width: 100%; height: 34px; border: 1px solid var(--line-2); border-radius: var(--radius-input); background: var(--card-solid); color: var(--text); outline: none; padding: 0 10px; font-size: 12px; font-weight: 600; transition: border-color .2s; }
/* textarea 用 height:34px 会被 rows 属性覆盖成随内容增长。
   节点高度是实测 offsetHeight 算出来的，没有上限的话一段长提示词
   会把节点撑到几千像素高，把适配视图/框选/小地图全带偏。 */
textarea.setting-input { height: auto; max-height: 220px; overflow-y: auto; padding: 8px 10px; line-height: 1.6; resize: vertical; }
.setting-input:focus { border-color: var(--strong); }
textarea.setting-input { height: auto; min-height: 72px; padding: 10px; resize: vertical; line-height: 1.5; }
select.setting-input { cursor: pointer; appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236a8fa8' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 10px center; padding-right: 28px; }
.gen-btn { height: 38px; padding: 0 16px; border-radius: var(--radius-btn); background: var(--strong); color: var(--strong-text); font-size: 12px; font-weight: 800; display: flex; align-items: center; justify-content: center; gap: 6px; transition: all .2s var(--ease); width: 100%; margin-top: 8px; }
.gen-btn:hover { opacity: .88; }
.gen-btn:disabled { opacity: .4; cursor: not-allowed; }
.mode-tabs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 3px; padding: 3px; border-radius: 14px; background: var(--soft); border: 1px solid var(--line-2); margin-bottom: 10px; }
.mode-tabs button { height: 30px; border-radius: 11px; color: var(--muted); font-size: 10px; font-weight: 800; transition: all .15s; }
.mode-tabs button.active { background: var(--strong); color: var(--strong-text); }

/* ========== 图片节点 ========== */
.image-placeholder { width: 100%; aspect-ratio: 1; border: 2px dashed var(--line-2); border-radius: var(--radius-card); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; color: var(--faint); font-size: 11px; font-weight: 600; cursor: pointer; transition: all .2s; }
.image-placeholder:hover { border-color: var(--strong); color: var(--strong); background: rgba(0,180,216,.04); }
.image-placeholder svg { width: 28px; height: 28px; opacity: .5; }
.image-preview { width: 100%; border-radius: var(--radius-card); object-fit: contain; max-height: 280px; background: var(--soft); }

/* ========== Prompt节点 ========== */
/* max-height 是必需的，不是可选的美化：
   节点高度是按 offsetHeight 实测的，而这个 textarea 没有上限 ——
   一段 3000 字的中文提示词会把节点撑到几千像素高，
   于是「适配视图」、框选、小地图全被带偏（fitBounds 依赖 boundsOf）。
   加上限之后多出来的内容在框内滚动，节点高度可控。 */
.prompt-textarea { width: 100%; min-height: 100px; max-height: 340px; overflow-y: auto; border: 1px solid var(--line-2); border-radius: 14px; outline: none; padding: 12px; background: var(--card-solid); font-size: 13px; line-height: 1.6; resize: vertical; color: var(--text); }
.prompt-textarea::placeholder { color: var(--faint); }

/* ---------- 提示词 @ 引用补全 ---------- */
.ref-panel {
  margin-top: 6px; max-height: 190px; overflow-y: auto;
  border: 1px solid var(--line-2); border-radius: 10px;
  background: var(--panel); box-shadow: 0 8px 24px var(--shadow);
}
.ref-item {
  display: flex; flex-direction: column; gap: 1px;
  padding: 6px 9px; cursor: pointer; transition: background .1s;
}
.ref-item + .ref-item { border-top: 1px solid var(--line); }
.ref-item:hover, .ref-item.on { background: var(--soft-2); }
.ref-title {
  font-size: 11px; font-weight: 800; color: var(--strong-text);
  font-family: monospace;
}
.ref-preview {
  font-size: 10px; color: var(--faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ref-empty { padding: 8px 9px; font-size: 10px; color: var(--faint); }

/* ========== Text节点 ========== */
.text-pre { width: 100%; max-height: 240px; overflow-y: auto; background: var(--soft); border: 1px solid var(--line-2); border-radius: 12px; padding: 10px; font-size: 11px; line-height: 1.5; font-family: monospace; white-space: pre-wrap; word-break: break-word; }

/* ========== LLM节点 ========== */
.llm-body { display: flex; flex-direction: column; gap: 10px; }
.llm-provider-tabs { display: grid; grid-template-columns: 1fr 1fr; gap: 2px; padding: 2px; border-radius: 999px; background: var(--soft); border: 1px solid var(--line-2); }
.llm-provider-tabs button { height: 28px; border-radius: 999px; font-size: 10px; font-weight: 700; color: var(--muted); transition: all .15s; }
.llm-provider-tabs button.active { background: var(--strong); color: var(--strong-text); }
.llm-chat { flex: 1; min-height: 120px; max-height: 280px; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; padding: 8px; border: 1px solid var(--line-2); border-radius: 12px; background: var(--soft); }
.llm-bubble { max-width: 85%; padding: 8px 12px; border-radius: 14px; font-size: 12px; line-height: 1.5; word-break: break-word; }
.llm-bubble.user { align-self: flex-end; background: var(--strong); color: var(--strong-text); border-bottom-right-radius: 4px; }
.llm-bubble.assistant { align-self: flex-start; background: var(--card-solid); border: 1px solid var(--line); border-bottom-left-radius: 4px; }

/*
 * Agent 工具调用轨迹。
 *
 * 这是**可信任度的问题**，不只是好看：Agent 改了画布、调了工具，
 * 如果界面上看不见，用户就只能靠猜。用折叠（<details>）是因为
 * 大多数时候不需要看，真要核对时再展开 —— 但「有没有调过、调了什么、
 * 成没成功」这三件事必须一眼可见，所以 summary 上直接带计数。
 */
.llm-tools { border: 1px dashed var(--line-2); border-radius: 10px; background: var(--soft); font-size: 11px; }
.llm-tools > summary { display: flex; align-items: center; gap: 5px; padding: 6px 9px; cursor: pointer; color: var(--muted); font-weight: 600; list-style: none; user-select: none; }
.llm-tools > summary::-webkit-details-marker { display: none; }
.llm-tools > summary:hover { color: var(--text); }
.llm-tools > summary svg { width: 12px; height: 12px; flex: none; }
.llm-tool { display: flex; gap: 6px; padding: 4px 9px; border-top: 1px solid var(--line-2); color: var(--muted); line-height: 1.45; }
.llm-tool .llm-tool-name { flex: none; font-weight: 700; color: var(--text); font-family: var(--mono, ui-monospace, monospace); }
.llm-tool.bad .llm-tool-name { color: var(--danger, #d9534f); }
.llm-tool .llm-tool-sum { min-width: 0; word-break: break-word; }

/*
 * 破坏性操作的授权条。
 *
 * 用琥珀色而不是危险红：要拦的是「删节点」，但 Agent 只是**请求**，
 * 还没发生。渲染成刺眼的红色会让用户条件反射点拒绝，反而达不到目的。
 * 真正执行失败才用红色（见 .llm-notice.bad）。
 */
.llm-confirm { border: 1px solid #d9a441; background: rgba(217, 164, 65, .1); border-radius: 12px; padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; }
.llm-confirm-head { font-size: 12px; font-weight: 700; color: var(--text); line-height: 1.45; }
.llm-confirm-sub { font-size: 11px; color: var(--muted); }
.llm-confirm-btns { display: flex; gap: 8px; }
.llm-confirm-btns .btn { height: 26px; padding: 0 14px; font-size: 12px; }

/* 确认之后的结果回执：用户必须看得到自己那一键盘点了什么 */
.llm-notice { border-radius: 10px; padding: 8px 11px; font-size: 11px; line-height: 1.5; border: 1px solid var(--line-2); background: var(--soft); }
.llm-notice.ok { border-color: rgba(16, 185, 129, .45); }
.llm-notice.bad { border-color: rgba(217, 83, 79, .55); background: rgba(217, 83, 79, .08); }
.llm-notice.rejected { border-color: var(--line-2); opacity: .8; }
.llm-notice-head { font-weight: 700; color: var(--text); }
.llm-notice.bad .llm-notice-head { color: var(--danger, #d9534f); }
.llm-notice-body { margin-top: 4px; color: var(--muted); word-break: break-word; max-height: 90px; overflow-y: auto; }

/*
 * 对话气泡里的图。
 *
 * 关键点：图和文字都在**同一个**气泡里，而不是并排两个元素。
 * 并排的话，气泡宽度会被图撑开、换行全乱，而且「这句话说的就是这张图」
 * 这个对应关系会丢失。
 */
.llm-bubble-text { white-space: pre-wrap; }
.llm-bubble-media { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: 6px; margin-top: 8px; }
.llm-bubble-cell { aspect-ratio: 1; border-radius: 8px; overflow: hidden; border: 1px solid var(--line); cursor: zoom-in; background: var(--soft); }
.llm-bubble-cell img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .15s; }
.llm-bubble-cell:hover img { transform: scale(1.06); }
/*
  气泡图上的动作条（收藏 / 加到画布）。
  贴在图的下沿居中、悬停或聚焦才现身：图本身是要看的，
  按钮常驻会盖住画面，看着也像图上本来就有个东西。
  用 opacity 而不是 display —— display 切换会让图片抖一下。
*/
.llm-bubble-cell { position: relative; }
.llm-bubble-actions {
  position: absolute; left: 50%; bottom: 6px; transform: translateX(-50%);
  display: flex; gap: 6px; z-index: 2;
}
.llm-bubble-actions .rm {
  width: 24px; height: 24px; border-radius: 50%; opacity: 0;
  background: rgba(255, 255, 255, .94); border: 1px solid var(--line);
  display: flex; align-items: center; justify-content: center;
  color: var(--muted); cursor: pointer; transition: opacity .15s, color .15s;
}
.llm-bubble-actions .rm[data-on] { color: var(--strong); }
.llm-bubble-actions .rm svg { width: 13px; height: 13px; }
/* 键盘也要看得见，否则这个按钮对键盘用户等于不存在 */
.llm-bubble-cell:hover .llm-bubble-actions .rm,
.llm-bubble-actions .rm:focus-visible { opacity: 1; }
.llm-chat-empty { color: var(--muted); font-size: 11px; line-height: 1.6; text-align: center; padding: 20px 8px; }

/* 右侧「画布助手」抽屉 */
.agent-drawer { display: flex; flex-direction: column; height: 100%; }
.agent-drawer .llm-chat { max-height: none; flex: 1; }
.agent-drawer-body { padding: 12px 14px; display: flex; flex-direction: column; gap: 8px; border-top: 1px solid var(--line); }

/*
 * 流式气泡：正在生成中的那条回复。
 *
 * 用一个独立元素承载增量，而不是往 messages 里追加 —— renderer 的
 * bodySignature 只看 messages.length，边流边 push 会每帧重建 body，
 * 输入框被销毁、光标丢失。独立气泡改 textContent 即可，零副作用。
 */
.llm-bubble.streaming { position: relative; color: var(--text); }
/* 尾部一个呼吸的点，明确「还在写」而不是「卡住了」 */
.llm-bubble.streaming::after {
  content: ''; display: inline-block; width: 5px; height: 5px; margin-left: 6px;
  border-radius: 50%; background: var(--muted); vertical-align: baseline;
  animation: llm-typing 1s var(--ease) infinite;
}
@keyframes llm-typing { 0%, 100% { opacity: .25; } 50% { opacity: 1; } }
/* 尊重系统的「减少动态效果」设置 */
@media (prefers-reduced-motion: reduce) {
  .llm-bubble.streaming::after { animation: none; opacity: .7; }
}
.llm-input-row { display: flex; gap: 6px; }
.llm-input { flex: 1; height: 36px; border: 1px solid var(--line-2); border-radius: 999px; padding: 0 14px; font-size: 12px; background: var(--card-solid); outline: none; }
.llm-send { width: 36px; height: 36px; border-radius: 50%; background: var(--strong); color: var(--strong-text); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.llm-send:hover { opacity: .85; }

/* ========== Output节点 ========== */
.output-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 8px; }
.output-img-wrap { position: relative; width: 100%; aspect-ratio: 1; border-radius: 12px; overflow: hidden; border: 1px solid var(--line); background: var(--soft); }
.output-img-wrap img { width: 100%; height: 100%; object-fit: contain; }
.output-del { position: absolute; top: 4px; right: 4px; width: 20px; height: 20px; border-radius: 50%; background: rgba(255,255,255,.9); border: 1px solid var(--line); display: flex; align-items: center; justify-content: center; color: var(--muted); opacity: 0; transition: opacity .15s; cursor: pointer; font-size: 11px; }
.output-img-wrap:hover .output-del { opacity: 1; }
.output-del:hover { color: var(--danger); background: var(--danger-bg); border-color: var(--danger); }
.output-empty { font-size: 11px; color: var(--faint); text-align: center; padding: 20px; }

/* ========== Loop节点 ========== */
.loop-settings { display: flex; flex-direction: column; gap: 8px; }

/* ========== ComfyUI节点 ========== */
.comfy-body { display: flex; flex-direction: column; gap: 10px; }
.comfy-workflow-list { display: flex; flex-direction: column; gap: 4px; max-height: 160px; overflow-y: auto; }
.comfy-workflow-item { padding: 8px 12px; border-radius: 10px; border: 1px solid var(--line-2); font-size: 11px; font-weight: 600; cursor: pointer; transition: all .15s; }
.comfy-workflow-item:hover, .comfy-workflow-item.active { border-color: var(--strong); background: rgba(0,180,216,.06); color: var(--strong); }

/* ========== 选区框 ========== */
.selection-box { position: absolute; border: 1.5px solid var(--strong); background: rgba(0,180,216,.08); border-radius: 4px; pointer-events: none; z-index: 100; display: none; }
.selection-hub { position: absolute; z-index: 50; padding: 4px 12px; border-radius: 999px; background: var(--strong); color: var(--strong-text); font-size: 11px; font-weight: 800; pointer-events: none; display: none; white-space: nowrap; box-shadow: 0 4px 16px var(--shadow); }

/* ========== 右键菜单 ========== */
.context-menu { position: fixed; z-index: 200; min-width: 180px; padding: 6px; border-radius: 14px; background: var(--panel); border: 1px solid var(--line); box-shadow: 0 16px 44px var(--shadow); backdrop-filter: blur(16px); display: none; }
.context-menu.show { display: block; }
.ctx-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 10px; font-size: 12px; font-weight: 600; color: var(--text); cursor: pointer; transition: background .12s; }
.ctx-item:hover { background: var(--soft-2); }
/* 菜单项右侧的补充说明（连线创建时标注「会连到哪个端口」） */
/* 拖线到空白处的落点幽灵框 */
.drop-hint {
  position: absolute; top: 0; left: 0;
  border: 2px dashed var(--strong);
  border-radius: 14px;
  background: color-mix(in srgb, var(--strong) 8%, transparent);
  pointer-events: none;
  z-index: 1;
  animation: dropHintPulse 1.2s ease-in-out infinite;
}
@keyframes dropHintPulse {
  0%, 100% { opacity: .55; }
  50% { opacity: 1; }
}
/* 松手会弹菜单，给个「可以放手」的暗示 */
body.link-drop-hint .board { cursor: copy; }

.ctx-hint {
  margin-left: auto; padding-left: 10px;
  font-size: 10px; font-weight: 700; color: var(--faint);
  white-space: nowrap;
}
.ctx-item svg { width: 14px; height: 14px; color: var(--muted); }
.ctx-sep { height: 1px; background: var(--line); margin: 4px 8px; }
.ctx-item.danger { color: var(--danger); }
.ctx-item.danger svg { color: var(--danger); }

/* ========== 属性面板 ========== */
.prop-panel { position: absolute; top: 70px; right: 16px; width: 300px; max-height: calc(100vh - 90px); background: var(--panel); border: 1px solid var(--line); border-radius: 20px; box-shadow: 0 16px 44px var(--shadow); backdrop-filter: blur(16px); z-index: 25; display: none; flex-direction: column; overflow: hidden; }
.prop-panel.open { display: flex; }
/*
  助手抽屉里的生图参数区。
  ------------------------------------------------------------------
  之前是内联的 grid-template-columns:1fr 1fr，有两个真问题：

  1. 「1fr」等于 minmax(auto, 1fr)，列**可以被内容撑大**。
     模型下拉的选项文字长（「跟随默认（gpt-image-2-03）」），
     把第一列撑爆，比例和分辨率被挤到溢出。

  2. 「1fr 1fr」这种等分对不合过的字段：
     模型名自己就很长，而「张数」只是一个数字。
     强行平分就会有一列被压缩、另一列被撑宽 —— 就是「四个功能位不一样大」的来源。

  改成：模型独占一行（它确实需要宽度），张数 / 比例 / 分辨率 三个短参数等分一行。
  宽度指数的机制要写正：用 minmax(0, 1fr) 而不是 1fr。
*/
.agent-params-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
  padding: 10px 2px 4px;
}
.agent-param {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}
/* 模型独占一行：超过三个短参数的总宽，长模型名不会地被挤或截断 */
.agent-param-wide { grid-column: 1 / -1; }
/*
  宽度覆盖需要显式给。
  .llm-input 是 flex:1，在 grid 容器里样式不生效，所以它实际上还是自然宽度。
*/
.agent-param .llm-input { width: 100%; }

/*
  抽屉头部的图标按钮。
  单独一个类，而不是复用主工具栏的 .canvas-nav-btn：
    主工具栏的按钮在顶部长条区里，32px 的圆盖是合适的；
    抽屉只有 420px 宽，头部守着一面文字，32px 的圆按钮在那里显得夸张。
    共用一个类的结果就是主工具栏也变小，而那里本来就没问题。

  三个按钮（刷新 / 清空 / 关闭）同尺寸。
  之前重新用 canvas-nav-btn（32px）、关闭用 prop-close（28px），
  大小不一致就显得错落，尤其是关闭那个比前两个小一圈。
*/
.drawer-head-btn {
  width: 26px; height: 26px; min-width: 26px;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--muted); background: transparent;
  transition: background .15s var(--ease), color .15s var(--ease);
}
.drawer-head-btn svg { width: 14px; height: 14px; }
.drawer-head-btn:hover { background: var(--soft-2); color: var(--text); }
.drawer-head-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.prop-head { padding: 14px 16px; border-bottom: 1px solid var(--line); display: flex; align-items: center; justify-content: space-between; }
.prop-title { font-size: 12px; font-weight: 800; color: var(--text); text-transform: uppercase; letter-spacing: .08em; }
.prop-close { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: var(--muted); transition: all .15s; }
.prop-close:hover { background: var(--soft-2); color: var(--text); }
.prop-body { padding: 16px; overflow-y: auto; flex: 1; }

/* ========== 小地图 ========== */
.minimap {
  position: absolute; left: 80px; bottom: 102px; right: auto; z-index: 23;
  width: 180px; height: 120px; padding: 8px; border-radius: 16px;
  background: var(--panel); border: 1px solid var(--line);
  box-shadow: 0 12px 36px var(--shadow); backdrop-filter: blur(16px);
  overflow: hidden; cursor: crosshair; display: none;
}
.minimap.open { display: block; }
.minimap-content { position: relative; width: 100%; height: 100%; border-radius: 10px; background: rgba(148,163,184,.08); overflow: hidden; }
.minimap-node { position: absolute; border-radius: 2px; background: rgba(15,23,42,.5); min-width: 2px; min-height: 2px; }
.theme-dark .minimap-node { background: rgba(248,250,252,.6); }
.minimap-viewport { position: absolute; border: 1.5px solid var(--strong); background: rgba(0,180,216,.08); border-radius: 3px; pointer-events: none; }

.view-dock {
  position: absolute; left: 80px; bottom: 52px; z-index: 24;
  display: flex; align-items: center; gap: 2px; padding: 4px;
  border-radius: 999px; background: var(--panel); border: 1px solid var(--line);
  box-shadow: 0 8px 24px var(--shadow); backdrop-filter: blur(16px);
}
.view-dock-btn {
  width: 32px; height: 32px; padding: 0; border: none; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; color: var(--muted); cursor: pointer;
  transition: background .14s var(--ease), color .14s var(--ease);
}
.view-dock-btn:hover { background: var(--soft); color: var(--text); }
.view-dock-btn.active { background: var(--soft-2); color: var(--strong); }
.view-dock-btn svg { width: 16px; height: 16px; }
.view-dock-sep { width: 1px; height: 18px; background: var(--line); margin: 0 3px; }

/* ========== 提示文本 ========== */
.hint { position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%); z-index: 20; padding: 6px 16px; border-radius: 999px; background: var(--panel); border: 1px solid var(--line); box-shadow: 0 8px 24px var(--shadow); backdrop-filter: blur(16px); font-size: 11px; color: var(--muted); font-weight: 600; white-space: nowrap; pointer-events: none; }

/* ========== 拖放覆盖层 ========== */
/*
 * 这一层必须**很轻**：用户要在整个拖拽过程中看清画布，才知道该往哪放。
 *
 * 之前是 background: rgba(0,180,216,.12) + backdrop-filter: blur(4px)，
 * 拖到空白处时整块画布糊成一片 —— 而这恰恰是最需要看清的时刻：
 * 要数着节点之间的空隙，才知道卡片放哪不会压住连线。
 * 「能放」的提示不应该靠挡住画布来传达。
 *
 * 现在：去掉模糊、底色淡到几乎看不出、虚线描边压到六成。
 * 提示文字也从画布正中挪到顶部：正中是最常点、最需要看清的位置，
 * 一块实心色块停在那里，等于在用户瞄准时挡住他的视线。
 */
.drop-overlay {
  position: absolute; inset: 0; z-index: 100; display: none;
  align-items: flex-start; justify-content: center; padding-top: 76px;
  background: color-mix(in srgb, var(--strong) 6%, transparent);
  border: 2px dashed color-mix(in srgb, var(--strong) 60%, transparent);
  border-radius: 0; pointer-events: none;
}
.drop-overlay.active { display: flex; }
.drop-overlay-text {
  padding: 5px 12px; border-radius: 999px;
  background: color-mix(in srgb, var(--strong) 88%, transparent);
  color: var(--strong-text); font-size: 11px; font-weight: 800;
}

/* ========== 删除确认 ========== */
.delete-confirm { position: absolute; inset: 0; z-index: 4; display: flex; align-items: center; justify-content: center; background: rgba(15,23,42,.15); backdrop-filter: blur(3px); border-radius: var(--radius-card); }
.delete-box { padding: 14px; border-radius: 14px; background: var(--card-solid); border: 1px solid var(--line); box-shadow: 0 12px 32px var(--shadow); text-align: center; width: 90%; }
.delete-box-title { font-size: 12px; font-weight: 800; margin-bottom: 10px; }
.delete-box-actions { display: flex; gap: 6px; }
.delete-confirm-btn { flex: 1; height: 30px; border-radius: 999px; font-size: 11px; font-weight: 800; display: flex; align-items: center; justify-content: center; }
.delete-confirm-btn.primary { background: var(--text); color: var(--card-solid); }
.delete-confirm-btn.secondary { background: var(--soft); color: var(--text); border: 1px solid var(--line); }

/* ========== 滚动条 ========== */
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--line); border-radius: 999px; }
::-webkit-scrollbar-thumb:hover { background: var(--muted); }

/* ========== 动画 ========== */
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes fadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
.fade-in { animation: fadeIn .2s var(--ease); }

/* ========== Sidebar ==========
   固定 56px 图标栏，不再有「hover 展开成 210px」那套。
   原因：项多了以后展开也读不完，而且展开态会挡住紧贴它右侧的浮层面板。
   文字改由 .sidebar-item::after 的 tooltip 给出（原生 title 有约 1 秒延迟且外观不统一）。 */
.sidebar { position: fixed; left: 12px; top: 50%; transform: translateY(-50%); width: 56px; background: var(--panel); border: 1px solid var(--line); border-radius: 18px; z-index: 40; display: flex; flex-direction: column; align-items: center; padding: 12px 0; box-shadow: 0 8px 40px var(--shadow); backdrop-filter: blur(16px); }
.sidebar-nav { display: flex; flex-direction: column; align-items: center; gap: 2px; flex: 1; width: 100%; overflow-y: auto; overflow-x: visible; padding: 0 6px; scrollbar-width: none; }
.sidebar-nav::-webkit-scrollbar { display: none; }
.sidebar-item { width: 44px; height: 40px; border-radius: 12px; display: flex; align-items: center; justify-content: center; color: var(--muted); cursor: pointer; transition: all .18s var(--ease); flex-shrink: 0; border: 1px solid transparent; white-space: nowrap; position: relative; }
.sidebar-item:hover { background: var(--soft); color: var(--text); border-color: var(--line); }
.sidebar-item.active { background: var(--strong); color: var(--strong-text); border-color: var(--strong); }
.sidebar-item svg, .sidebar-item i { width: 18px; height: 18px; flex-shrink: 0; }
/* 文字只作为 tooltip 的数据源，视觉上由 ::after 负责 */
.sidebar-item-label { position: absolute; opacity: 0; pointer-events: none; width: 1px; height: 1px; overflow: hidden; }
.sidebar-item::after {
  content: attr(data-tip);
  position: absolute; left: calc(100% + 10px); top: 50%; transform: translateY(-50%);
  padding: 5px 9px; border-radius: 8px; white-space: nowrap;
  background: var(--strong); color: var(--strong-text);
  font-size: 11px; font-weight: 800; line-height: 1.2;
  opacity: 0; pointer-events: none; z-index: 60;
  transition: opacity .14s var(--ease);
  box-shadow: 0 6px 20px var(--shadow);
}
.sidebar-item:hover::after { opacity: 1; }
/* 面板开着的时候不要再冒 tooltip，否则和面板叠在一起 */
.sidebar-item.active::after { opacity: 0; }
.sidebar-sep { width: 28px; height: 1px; background: var(--line); margin: 6px auto; flex-shrink: 0; }
.sidebar-bottom { display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 0 6px; flex-shrink: 0; }
.sidebar-brand { font-size: 9px; font-weight: 800; color: var(--faint); text-transform: uppercase; letter-spacing: .1em; margin-top: 4px; text-align: center; }
.sidebar-user {
  width: 50px; height: 50px; padding: 0; border: 0; border-radius: 50%;
  background: transparent;
}
.sidebar-user:hover, .sidebar-user.active {
  background: transparent; border-color: transparent; color: inherit;
}
.sidebar-avatar {
  width: 40px; height: 40px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  color: #35c7d8; background: #0e1b27;
  border: 2px solid #233a4b;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.03);
  transition: border-color .18s var(--ease), box-shadow .18s var(--ease), transform .18s var(--ease);
}
.sidebar-avatar svg { width: 20px; height: 20px; }
.sidebar-user:hover .sidebar-avatar {
  border-color: #45d4e3;
  box-shadow: 0 0 0 3px rgba(53,199,216,.15), 0 0 18px rgba(53,199,216,.2);
  transform: translateY(-1px);
}

/* ========== 配置弹窗 ========== */
/* 用 display:none 而不是 opacity:0 + pointer-events:none。
   后者只挡鼠标，不挡键盘 —— 弹窗「关闭」时里面 7 个 tab 按钮 + 导入导出
   + 完成按钮全都还在 Tab 序列里，键盘用户连续按 Tab 会「消失」到看不见的
   弹窗里，以为页面卡死。其它弹层（.cmdk-overlay / .find-overlay /
   .modal-overlay）一直都是 display:none，这里是唯一的例外。 */
.config-overlay{position:fixed;inset:0;z-index:500;background:rgba(0,0,0,.45);backdrop-filter:blur(4px);display:none;align-items:center;justify-content:center;opacity:0}
.config-overlay.show{display:flex;opacity:1}
.config-modal{width:900px;max-width:95vw;max-height:85vh;background:var(--card-solid);border:1px solid var(--line);border-radius:20px;box-shadow:0 24px 64px rgba(0,0,0,.25);display:flex;flex-direction:column;overflow:hidden}
.config-head{padding:24px 28px 0;display:flex;flex-direction:column;gap:4px}
.config-head-top{display:flex;align-items:flex-start;justify-content:space-between}
.config-head h2{font-size:22px;font-weight:800;color:var(--text);letter-spacing:-.01em}
.config-head>p{color:var(--muted);font-size:12px;line-height:1.4}
.config-import-export{display:flex;gap:8px;margin-top:8px;flex-shrink:0}
.config-ie-btn{height:34px;padding:0 16px;border-radius:var(--radius-btn);border:1px solid var(--line);background:var(--card-solid);color:var(--text);font-size:12px;font-weight:700;display:flex;align-items:center;gap:6px;transition:all .15s var(--ease)}
.config-ie-btn:hover{background:var(--soft-2)}
.config-tabs{display:flex;gap:0;padding:0 28px;border-bottom:1px solid var(--line);margin-top:16px}
.config-tab-btn{padding:10px 0;margin-right:28px;font-size:13px;font-weight:700;color:var(--muted);border:none;border-bottom:2px solid transparent;background:none;transition:all .15s var(--ease);cursor:pointer}
.config-tab-btn:hover{color:var(--text)}
.config-tab-btn.active{color:var(--text);border-bottom-color:var(--text)}
.config-body{padding:24px 28px;overflow-y:auto;flex:1}
.config-tab-content{display:none}
.config-tab-content.active{display:block}
.config-section-title{font-size:14px;font-weight:800;color:var(--text);margin-bottom:14px}
.config-desc{color:var(--muted);font-size:12px;line-height:1.5;margin-bottom:12px}
.config-row-4{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:16px}
.config-row-4 label{display:flex;flex-direction:column;gap:4px}
.config-row-4 label span{font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em}
.config-row-4 select,.config-row-4 input:not([type="checkbox"]):not([type="radio"]){height:38px;border:1px solid var(--line-2);border-radius:var(--radius-input);background:var(--card-solid);color:var(--text);padding:0 10px;font-size:12px;font-weight:600;outline:none;width:100%}
.config-row-4 select{cursor:pointer;appearance:none}
.config-row-4 select:focus,.config-row-4 input:focus{border-color:var(--strong)}
.config-section-divider{border:none;border-top:1px solid var(--line);margin:16px 0}
.config-textarea{width:100%;min-height:80px;border:1px solid var(--line-2);border-radius:12px;padding:12px;background:var(--card-solid);font-size:12px;line-height:1.5;resize:vertical;color:var(--text);outline:none}
.config-textarea::placeholder{color:var(--faint)}
.config-textarea:focus{border-color:var(--strong)}
.config-add-btn{height:34px;padding:0 14px;border-radius:var(--radius-btn);border:1px solid var(--line);background:var(--card-solid);color:var(--text);font-size:12px;font-weight:700;display:inline-flex;align-items:center;gap:6px;cursor:pointer;transition:all .15s var(--ease)}
.config-add-btn:hover{background:var(--soft-2)}
.config-channel-list{display:flex;flex-direction:column;gap:8px;margin-top:12px}
.config-channel-card{padding:14px 16px;border:1px solid var(--line);border-radius:var(--radius-card);background:var(--card-solid);display:flex;align-items:center;justify-content:space-between}
.config-channel-info{min-width:0;flex:1}
.config-channel-name{font-size:14px;font-weight:800;color:var(--text);margin-bottom:2px}
.config-channel-meta{font-size:11px;color:var(--muted);font-weight:600}
.config-channel-actions{display:flex;gap:8px;flex-shrink:0;margin-left:12px}
.config-channel-actions button{height:30px;padding:0 12px;border-radius:var(--radius-btn);border:1px solid var(--line);background:var(--card-solid);font-size:11px;font-weight:700;display:flex;align-items:center;gap:5px;color:var(--muted);cursor:pointer;transition:all .15s var(--ease)}
.config-channel-actions button:hover{background:var(--soft-2);color:var(--text)}
.config-channel-actions .danger{color:var(--danger)}
.config-channel-actions .danger:hover{background:var(--danger-bg);border-color:var(--danger)}
.config-prompt-list{display:flex;flex-direction:column;gap:8px;margin-top:12px}
.config-prompt-card{padding:12px 16px;border:1px solid var(--line);border-radius:var(--radius-card);background:var(--card-solid);display:flex;align-items:center;gap:12px}
.config-prompt-toggle{width:36px;height:20px;border-radius:10px;background:var(--line-2);position:relative;cursor:pointer;flex-shrink:0;transition:background .2s}
.config-prompt-toggle.on{background:var(--strong)}
.config-prompt-toggle::after{content:"";position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:#fff;transition:transform .2s}
.config-prompt-toggle.on::after{transform:translateX(16px)}
.config-prompt-info{flex:1;min-width:0}
.config-prompt-title{font-size:13px;font-weight:700;color:var(--text);display:flex;align-items:center;gap:6px}
.config-prompt-title .tag{padding:1px 6px;border-radius:4px;background:var(--soft);color:var(--muted);font-size:10px;font-weight:700;border:1px solid var(--line)}
.config-prompt-url{font-size:11px;color:var(--muted);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.config-prompt-stats{display:flex;gap:10px;font-size:11px;color:var(--muted);margin-top:4px;align-items:center}
.config-prompt-stats .ok{color:var(--success);font-weight:700}
.config-prompt-actions{display:flex;gap:6px;flex-shrink:0;margin-left:12px}
.config-prompt-actions button{height:28px;padding:0 10px;border-radius:var(--radius-btn);border:1px solid var(--line);background:var(--card-solid);font-size:11px;font-weight:700;display:flex;align-items:center;gap:4px;color:var(--muted);cursor:pointer;transition:all .15s var(--ease)}
.config-prompt-actions button:hover{background:var(--soft-2);color:var(--text)}
.config-form-row{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px}
.config-form-group{display:flex;flex-direction:column;gap:4px}
.config-form-group label{font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.05em}
.config-form-group input:not([type="checkbox"]):not([type="radio"]){height:38px;border:1px solid var(--line-2);border-radius:var(--radius-input);background:var(--card-solid);color:var(--text);padding:0 12px;font-size:12px;font-weight:600;outline:none}
.config-form-group input:focus{border-color:var(--strong)}
.config-form-group input::placeholder{color:var(--faint)}
.config-form-hint{font-size:11px;color:var(--muted);margin-top:2px}
.config-form-actions{display:flex;gap:8px;margin-top:20px}
.config-form-actions button{height:34px;padding:0 14px;border-radius:var(--radius-btn);font-size:12px;font-weight:700;display:flex;align-items:center;gap:6px;cursor:pointer;transition:all .15s var(--ease);border:1px solid var(--line);background:var(--card-solid);color:var(--muted)}
.config-form-actions button:hover{background:var(--soft-2);color:var(--text)}
.config-form-actions button.primary{background:var(--strong);color:var(--strong-text);border-color:var(--strong)}
.config-sync-header{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:16px}
.config-sync-header .status{font-size:12px;color:var(--muted);font-weight:600}
.config-storage-card{padding:20px;border:1px solid var(--line);border-radius:var(--radius-card);background:var(--card-solid);margin-bottom:16px}
.config-storage-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}
.config-storage-header h3{font-size:14px;font-weight:800;color:var(--text);display:flex;align-items:center;gap:8px}
.config-storage-header .refresh-btn{height:30px;padding:0 12px;border-radius:var(--radius-btn);border:1px solid var(--line);background:var(--card-solid);font-size:11px;font-weight:700;display:flex;align-items:center;gap:5px;color:var(--muted);cursor:pointer;transition:all .15s var(--ease)}
.config-storage-header .refresh-btn:hover{background:var(--soft-2);color:var(--text)}
.config-storage-desc{font-size:12px;color:var(--muted);margin-bottom:14px;line-height:1.4}
.config-storage-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:16px}
.config-storage-stat{display:flex;flex-direction:column;gap:2px}
.config-storage-stat .label{font-size:11px;color:var(--muted);font-weight:600;display:flex;align-items:center;gap:4px}
.config-storage-stat .value{font-size:22px;font-weight:800;color:var(--text)}
.config-storage-stat .unit{font-size:12px;font-weight:600;color:var(--text)}
.config-storage-stat .note{font-size:10px;color:var(--muted)}
.config-progress-bar{height:6px;border-radius:3px;background:var(--soft);margin-top:12px;overflow:hidden}
.config-progress-fill{height:100%;border-radius:3px;background:var(--strong);width:0.01%}
.config-progress-label{display:flex;justify-content:space-between;font-size:11px;color:var(--muted);margin-top:4px}
.config-store-list{border-top:1px solid var(--line)}
.config-store-row{display:flex;align-items:center;justify-content:space-between;padding:12px 0;border-bottom:1px solid var(--line)}
.config-store-row:last-child{border-bottom:none}
.config-store-row .name{font-size:13px;font-weight:800;color:var(--text)}
.config-store-row .id{font-size:11px;color:var(--muted)}
.config-store-row .size{font-size:12px;font-weight:700;color:var(--text);text-align:right}
.config-store-row .count{font-size:11px;color:var(--muted);text-align:right}

/*
 * 手动清理行。
 *
 * 三段式：名字+条数 / 筛选+按钮 / 结果。
 * 结果单独占一行是刻意的 —— 它要把「将删除 300 条」和「连带 12 条运行记录」
 * 这类长文案完整显示出来，挤在右侧会被截断，而被截断的数字正是最不该
 * 看不见的数字。
 */
.config-clean-row{display:flex;flex-wrap:wrap;align-items:center;gap:10px;padding:11px 0;border-bottom:1px solid var(--line)}
.config-clean-row:last-child{border-bottom:none}
.config-clean-name{min-width:96px}
.config-clean-count{font-size:11px;color:var(--muted)}
.config-clean-ctrls{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.config-clean-sel{height:26px;font-size:11px;padding:0 6px;border-radius:7px;border:1px solid var(--line-2);background:var(--card-solid);color:var(--text)}
.config-clean-sel:disabled{opacity:.45}
.config-clean-num{height:26px;width:74px;font-size:11px;padding:0 7px;border-radius:7px;border:1px solid var(--line-2);background:var(--card-solid);color:var(--text)}
.config-clean-out{flex-basis:100%;font-size:11px;color:var(--muted);line-height:1.5}
.config-clean-out.hit{color:var(--text);font-weight:600}
.config-clean-out.bad{color:var(--danger,#d9534f);font-weight:600}

/*
 * 资产库的批量勾选。
 *
 * 勾选框固定在左上角且默认半透明 —— 不能让它盖住图片主体，
 * 但也不能让它消失，否则用户根本不知道「这张能勾」。
 *
 * 尺寸/配色都在 extra.css 的 .asset-act 里（勾选框现在也是图标按钮，
 * 和收藏/下载/删除共用一套规格）。这里只留「选中态」和「整卡高亮」——
 * 之前这里又写了一份 18px 白底的 .asset-pick，靠 extra.css 后加载
 * 把它盖回 22px，两条规则长期互相打架，改尺寸时必然只改到一处。
 */
.asset-item.picked{outline:2px solid var(--strong);outline-offset:-2px}
.asset-pick-hint{font-size:11px;color:var(--muted);align-self:center;padding:0 2px}
.config-footer{padding:16px 28px;border-top:1px solid var(--line);display:flex;justify-content:flex-end}
.config-footer button{height:38px;padding:0 24px;border-radius:var(--radius-btn);background:var(--strong);color:var(--strong-text);font-size:13px;font-weight:800;display:flex;align-items:center;gap:6px;transition:all .2s var(--ease);border:none;cursor:pointer}
.config-footer button:hover{opacity:.88}
.config-password-wrap{position:relative}
.config-password-wrap input{padding-right:36px}
.config-pw-toggle{position:absolute;right:8px;top:50%;transform:translateY(-50%);width:24px;height:24px;border-radius:6px;display:flex;align-items:center;justify-content:center;color:var(--muted);cursor:pointer;transition:all .15s}
.config-pw-toggle:hover{background:var(--soft-2);color:var(--text)}
.config-pw-toggle svg{width:14px;height:14px}
.config-row-2{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px}

/* ---------- LLM 对话历史条 ---------- */
.llm-clear-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; margin-top: 6px;
}
.llm-count { font-size: 10px; font-weight: 700; color: var(--faint); }
.llm-clear {
  display: inline-flex; align-items: center; gap: 4px;
  height: 22px; padding: 0 9px; border-radius: 8px;
  border: 1px solid var(--line); background: var(--soft);
  font-size: 10px; font-weight: 800; color: var(--muted);
  cursor: pointer; transition: background .12s, color .12s;
}
.llm-clear:hover { background: var(--soft-2); color: var(--danger); border-color: var(--danger); }
.llm-clear svg { width: 11px; height: 11px; }

/*
 * 画布冲突的节点级对比。
 *
 * 左右两栏是必要的：用户要能同时看到「我的版本」和「助手的版本」才能做决定。
 * 只给文字「保留本地版本」的按钮是不够的 —— 那就又成了一个盲选。
 *
 * 只有真正冲突的那几个节点需要黄度强调；双方互补的只列出来看到它存在就行。
 */
.merge-body{display:flex;flex-direction:column;gap:10px;max-height:62vh;overflow-y:auto}
.merge-hint{font-size:11px;color:var(--muted);line-height:1.6}
.merge-list{display:flex;flex-direction:column;gap:8px}
.merge-row{border:1px solid var(--line-2);border-radius:10px;padding:8px 10px;background:var(--soft)}
.merge-row.conflict{border-color:#d9a441;background:rgba(217,164,65,.07)}
.merge-row-title{font-size:11px;font-weight:800;color:var(--text);margin-bottom:6px}
.merge-cols{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.merge-col{text-align:left;padding:7px 9px;border-radius:8px;border:1px solid var(--line-2);background:var(--card-solid);cursor:pointer;transition:border-color .12s,box-shadow .12s}
.merge-col:hover{border-color:var(--line)}
.merge-col.picked{border-color:var(--strong);box-shadow:0 0 0 1px var(--strong) inset}
.merge-col-head{font-size:10px;font-weight:800;color:var(--muted);margin-bottom:3px}
.merge-col.picked .merge-col-head{color:var(--strong)}
.merge-col-body{font-size:11px;color:var(--text);line-height:1.5;word-break:break-word}
.merge-fields{margin-top:6px;font-size:10px;color:var(--muted)}

/* 「助手正在回复…」提示条
 *
 * 之前从发消息到模型答完这段时间里界面上毫无动静，用户以为没发出去就又发一遍，
 * 于是同一条消息进了两次历史、助手也答两次（两次都要花钱）。
 * 有了它，等待期是可见的；发送按钮同时被禁用，从根上断掉连点。
 */
.agent-busy {
  display: flex; align-items: center; gap: 7px;
  padding: 5px 9px; margin-bottom: 7px;
  border: 1px solid var(--line, rgba(255, 255, 255, .1));
  border-radius: 6px;
  font-size: 11px; color: var(--muted, #9aa0a6);
}
.agent-busy-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--accent, #4f9cff);
  animation: agent-busy-pulse 1.1s ease-in-out infinite;
}
@keyframes agent-busy-pulse {
  0%, 100% { opacity: .25; transform: scale(.75); }
  50%      { opacity: 1;   transform: scale(1); }
}
/* 等助手时不抢焦点：用户应该能接着打下一句 */
#agentSendBtn:disabled { opacity: .5; cursor: not-allowed; }

/* Agent 配置：工具三组分区 + 「需要先确认」独立成块 */
.tool-groups {
  display: flex; flex-direction: column; gap: 8px;
}
.tool-group {
  border: 1px solid var(--line, rgba(255, 255, 255, .09));
  border-radius: 8px; padding: 8px 9px;
}
.tool-group-head {
  display: flex; align-items: center; gap: 8px; margin-bottom: 4px;
}
.tool-group-label { font-size: 11px; font-weight: 800; }
.tool-group-list { display: flex; flex-direction: column; gap: 2px; }
.tool-row {
  padding: 4px 6px; border-radius: 6px;
}
.tool-row:hover { background: rgba(255, 255, 255, .04); }

/* 危险组用警示色描边：它是唯一不可逆的一组，不该和上面两行长得一样 */
.tool-group[data-group="danger"] {
  border-color: rgba(217, 164, 65, .45);
  background: rgba(217, 164, 65, .05);
}
.tool-group[data-group="danger"] .tool-group-label { color: #d9a441; }

/* 节点上的「打开助手」：替代原来那个聊天输入框 */
.llm-open-drawer {
  display: flex; align-items: center; justify-content: center; gap: 6px;
  width: 100%; padding: 7px 10px;
}
.llm-open-drawer i { width: 14px; height: 14px; }

/* TOOLBOX_PROBE */
