/* ============================================================
   且听风吟 · 设计系统 v2
   风格基调：简约、克制、纸感。以「一档阴影 + 一档描边」建立层次，
   交互只做 130~180ms 的位移与色彩反馈，不做夸张动效。
   正文 17px / 行高 1.9；全局单一强调色（赭红），涨跌沿用中文习惯。
   ============================================================ */

:root {
  --bg: #FFFFFF;
  --text: #2B2B29;
  --muted: #8A8880;
  --accent: #B4553F;
  --accent-ink: #FFFFFF;
  --accent-soft: rgba(180, 85, 63, .10);
  --accent-line: rgba(180, 85, 63, .32);

  --line: #E7E5E0;
  --line-strong: #D6D3CB;
  --panel: #FAF9F7;
  --panel-2: #F2F0EC;
  --hover: #F3F1ED;
  --ok: #4B7A5A;      --ok-soft: rgba(75, 122, 90, .12);
  --warn: #A8762F;    --warn-soft: rgba(168, 118, 47, .13);
  --danger: #A8453A;  --danger-soft: rgba(168, 69, 58, .10);

  --radius: 6px;
  --radius-lg: 10px;
  --radius-pill: 999px;

  /* 人物卡「基本信息」右列的形象图框：固定宽度 × 固定高度，**9:16 竖屏**（162 × 288）。
     尺寸放在变量里，是因为它同时被三处引用（有图框、无图占位、手机端居中），
     散成字面量迟早会出现「有图 288 / 无图 250」这种高度不一致。
     比例必须与 `crop.js` 的 `PORTRAIT_RATIO`（9/16）一致 —— 改一处必须改另一处。 */
  --portrait-w: 162px;
  --portrait-h: 288px;

  --shadow-xs: 0 1px 2px rgba(44, 40, 34, .05);
  --shadow-sm: 0 1px 3px rgba(44, 40, 34, .07), 0 1px 2px rgba(44, 40, 34, .04);
  --shadow: 0 6px 20px rgba(44, 40, 34, .09), 0 2px 5px rgba(44, 40, 34, .05);
  --shadow-lg: 0 22px 56px rgba(30, 26, 20, .20), 0 2px 8px rgba(30, 26, 20, .08);
  --ring: 0 0 0 3px var(--accent-soft);

  --t-fast: .13s ease;
  --t: .18s cubic-bezier(.4, 0, .2, 1);

  --fs-ui: 13px;
  --fs-body: 17px;
  --lh-body: 1.9;

  /* 界面缩放（个人偏好，见 store.js 的 uiScale）
     zoom 会把渲染等比放大，但**不会**改变 vh/vw 单位的取值。
     所以凡是要「撑满视口」的高度，都必须用 --vh100（缩放前的等效视口高），
     直接写 100vh 会让整个外壳溢出视口、多出一条外层滚动条。 */
  --ui-scale: 1;
  --vh100: calc(100vh / var(--ui-scale));
  --topbar-h: 48px;
  --left-w: 216px;
  --right-w: 340px;

  --serif: "Songti SC", "SimSun", "Source Han Serif SC", "Noto Serif CJK SC", Georgia, serif;
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", "PingFang SC",
    "Hiragino Sans GB", "Source Han Sans SC", sans-serif;
  --mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;

  color-scheme: light;
}

body.theme-night {
  --bg: #1E1E1D;
  --text: #D6D4CB;
  --muted: #8C8A82;
  --accent: #C97A5E;
  --accent-ink: #1B1613;
  --accent-soft: rgba(201, 122, 94, .16);
  --accent-line: rgba(201, 122, 94, .42);

  --line: #353532;
  --line-strong: #44443F;
  --panel: #262625;
  --panel-2: #2E2E2C;
  --hover: #2C2C2A;
  --ok: #7FA98C;      --ok-soft: rgba(127, 169, 140, .16);
  --warn: #C9A05E;    --warn-soft: rgba(201, 160, 94, .16);
  --danger: #D0847A;  --danger-soft: rgba(208, 132, 122, .14);

  --shadow-xs: 0 1px 2px rgba(0, 0, 0, .30);
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, .36);
  --shadow: 0 8px 24px rgba(0, 0, 0, .42);
  --shadow-lg: 0 26px 60px rgba(0, 0, 0, .60);

  color-scheme: dark;
}

body.theme-eye {
  --bg: #F5F2E3;
  --text: #463F31;
  --muted: #8A8069;
  --accent: #A85A3C;
  --accent-ink: #FFFFFF;
  --accent-soft: rgba(168, 90, 60, .11);
  --accent-line: rgba(168, 90, 60, .34);

  --line: #E0DAC5;
  --line-strong: #CFC7AC;
  --panel: #EFEBD9;
  --panel-2: #E8E3CE;
  --hover: #EAE5D1;
  --ok: #5E7A55;      --ok-soft: rgba(94, 122, 85, .13);
  --warn: #97722F;    --warn-soft: rgba(151, 114, 47, .13);
  --danger: #9C4B3C;  --danger-soft: rgba(156, 75, 60, .11);

  --shadow-xs: 0 1px 2px rgba(74, 66, 48, .07);
  --shadow-sm: 0 1px 3px rgba(74, 66, 48, .09), 0 1px 2px rgba(74, 66, 48, .05);
  --shadow: 0 6px 20px rgba(74, 66, 48, .11), 0 2px 5px rgba(74, 66, 48, .06);
  --shadow-lg: 0 22px 56px rgba(60, 52, 36, .22);
}

/* 护眼亮度分级：通过底色叠加深浅模拟 */
body.theme-eye.eye-1 { --bg: #FAF8F0; --panel: #F4F1E6; --panel-2: #EEEAE0; --line: #E7E2D3; --hover: #EFEBDE; }
body.theme-eye.eye-3 { --bg: #EDE9D8; --panel: #E6E1CE; --panel-2: #DFD9C4; --line: #D6D0B9; --hover: #E3DDC9; }

* { box-sizing: border-box; }

html {
  margin: 0;
  padding: 0;
  /* 高度走 --vh100：zoom 放大渲染后正好等于真实视口高 */
  height: var(--vh100);
  zoom: var(--ui-scale);
}

body {
  margin: 0;
  padding: 0;
  height: 100%;
}

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  font-size: var(--fs-ui);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

button, input, select, textarea {
  font-family: inherit;
  font-size: inherit;
  color: inherit;
}

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 6px; border: 2px solid transparent; background-clip: content-box; }
::-webkit-scrollbar-thumb:hover { background: var(--muted); background-clip: content-box; }

/* 滚动条占位：固定高度的内部滚动区（侧栏、正文区、控制台、弹层）
   始终预留滚动条宽度，避免内容变长时滚动条忽隐忽现、里面的内容跟着左右跳一下。
   注意：章节栏与控制台不再各自滚动（改为跟随正文，见 write.js syncCols），
   故此表里不再列它们。 */
.side,
.write .scroll,
.stream,
.modal .mb,
.imgpick { scrollbar-gutter: stable; }

:focus-visible { outline: none; box-shadow: var(--ring); border-radius: var(--radius); }

/* ---------------- 基础控件 ---------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 6px 13px;
  border: 1px solid var(--line);
  background: var(--bg);
  color: var(--text);
  border-radius: var(--radius);
  cursor: pointer;
  white-space: nowrap;
  line-height: 1.5;
  font-weight: 500;
  box-shadow: var(--shadow-xs);
  transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast),
    box-shadow var(--t-fast), transform var(--t-fast), opacity var(--t-fast);
}
.btn:hover { background: var(--hover); border-color: var(--line-strong); }
.btn:active { transform: translateY(1px); box-shadow: none; }
.btn:focus-visible { border-color: var(--accent); box-shadow: var(--ring); }
.btn:disabled, .btn.disabled { opacity: .45; cursor: not-allowed; box-shadow: none; }
.btn:disabled:hover { background: var(--bg); border-color: var(--line); }
.btn:disabled:active { transform: none; }
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
.btn.primary:hover { background: var(--accent); opacity: .92; border-color: var(--accent); }
.btn.primary:disabled:hover { opacity: .45; }
.btn.ghost { border-color: transparent; background: transparent; box-shadow: none; }
.btn.ghost:hover { background: var(--hover); border-color: transparent; }
.btn.danger { color: var(--danger); border-color: var(--line); }
.btn.danger:hover { background: var(--danger-soft); border-color: var(--danger); }
.btn.sm { padding: 3px 9px; font-size: 12px; }
.btn.sm.ghost { padding: 4px 7px; }
.btn.block { width: 100%; }
.btn.on { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.btn.icon { padding: 5px; }

.input, .select, .textarea {
  width: 100%;
  padding: 6px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg);
  outline: none;
  transition: border-color var(--t-fast), box-shadow var(--t-fast), background var(--t-fast);
}
.input:hover, .select:hover, .textarea:hover { border-color: var(--line-strong); }
.input:focus, .select:focus, .textarea:focus { border-color: var(--accent); box-shadow: var(--ring); }
.input::placeholder, .textarea::placeholder { color: var(--muted); }
.textarea { min-height: 76px; resize: vertical; line-height: 1.7; font-family: inherit; }
.select {
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%238A8880' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 8px center;
  padding-right: 24px;
  cursor: pointer;
}
body.theme-night .select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%238C8A82' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}

.field { margin-bottom: 14px; }
.field > label { display: block; margin-bottom: 5px; color: var(--muted); font-size: 12px; font-weight: 500; }
.field .hint { color: var(--muted); font-size: 12px; margin-top: 4px; line-height: 1.6; }
.field .err { color: var(--danger); font-size: 12px; margin-top: 4px; }

.row { display: flex; gap: 10px; }
.row.wrap { flex-wrap: wrap; }
.row > * { min-width: 0; }
.grow { flex: 1; }
.spacer { flex: 1; }

.chk { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
.chk input { accent-color: var(--accent); }

/* 开关 */
.switch {
  width: 36px; height: 20px; border-radius: 10px; background: var(--panel-2);
  border: 1px solid var(--line); position: relative; cursor: pointer; display: inline-block; flex: none;
  transition: background var(--t), border-color var(--t);
}
.switch::after {
  content: ''; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px;
  border-radius: 50%; background: var(--muted); transition: left var(--t), background var(--t);
}
.switch.on { background: var(--accent); border-color: var(--accent); }
.switch.on::after { left: 18px; background: #fff; }

/* ---------------- 标签 / 徽标 ---------------- */

.tag {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 1px 7px; border-radius: var(--radius-pill); font-size: 11px; line-height: 1.7;
  border: 1px solid var(--line); color: var(--muted); background: var(--bg);
  white-space: nowrap;
}
.tag.accent { color: var(--accent); border-color: var(--accent-line); background: var(--accent-soft); }
.tag.ok { color: var(--ok); border-color: transparent; background: var(--ok-soft); }
.tag.warn { color: var(--warn); border-color: transparent; background: var(--warn-soft); }
.tag.danger { color: var(--danger); border-color: transparent; background: var(--danger-soft); }
.tag.solid { background: var(--panel-2); border-color: transparent; }

.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--muted); display: inline-block; flex: none; }
.dot.ok { background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
.dot.warn { background: var(--warn); box-shadow: 0 0 0 3px var(--warn-soft); }
.dot.danger { background: var(--danger); box-shadow: 0 0 0 3px var(--danger-soft); }

/* ---------------- 布局外壳 ---------------- */

#app { height: 100%; }

.topbar {
  height: var(--topbar-h);
  border-bottom: 1px solid var(--line);
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 12px;
  background: var(--bg);
  position: sticky;
  top: 0;
  z-index: 30;
  box-shadow: var(--shadow-xs);
}
.topbar .brand {
  font-family: var(--serif);
  font-size: 17px;
  letter-spacing: .1em;
  cursor: pointer;
  flex: none;
  transition: color var(--t-fast);
}
.topbar .brand:hover { color: var(--accent); }
.topbar .sep { width: 1px; height: 18px; background: var(--line); flex: none; }
.topbar .meta { display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 12px; overflow: hidden; }
.topbar .meta b { font-weight: 600; color: var(--text); }

.layout { display: flex; min-height: calc(var(--vh100) - var(--topbar-h)); align-items: stretch; }
/* 沉浸式页面（写作页）：必须限高 + 裁切，让 .main → .write 的定高真正生效。
   用 height 而非 min-height：min-height 拦不住 flex 子项被内容顶高，
   .write 的 height 会形同虚设、.scroll 永不内部滚动，「两侧栏跟随正文滚动」随之失效。 */
.layout.full { height: calc(var(--vh100) - var(--topbar-h)); min-height: 0; overflow: hidden; }
.layout.full > .main { min-height: 0; overflow: hidden; }

.side {
  width: var(--left-w); flex: none;
  border-right: 1px solid var(--line);
  background: var(--panel);
  padding: 10px 0 30px;
  overflow-y: auto;
  height: calc(var(--vh100) - var(--topbar-h));
  position: sticky; top: var(--topbar-h);
}
.side.hide { display: none; }
/* 导航栏自己的收缩按钮（只有写作页会挂上来，别的页面没有「叫回来」的入口）。
   sticky 是必需的：导航项多起来要滚动，按钮跟着滚走就等于没有。 */
.side .side-head {
  position: sticky; top: 0; z-index: 2;
  display: flex; align-items: center; gap: 6px;
  padding: 2px 10px 6px 16px; background: var(--panel);
  color: var(--muted); font-size: 10.5px; letter-spacing: .12em;
}
.side .side-head .lab { flex: 1 1 0; text-transform: uppercase; font-weight: 600; }
.side .side-head .btn { padding: 3px 5px; line-height: 1; }
.side .grp { padding: 13px 16px 5px; color: var(--muted); font-size: 10.5px; letter-spacing: .12em; font-weight: 600; text-transform: uppercase; }
.side a.nav {
  display: flex; align-items: center; gap: 9px;
  padding: 7px 14px; color: var(--text); text-decoration: none;
  border-left: 2px solid transparent; font-size: 13px; margin: 1px 0;
  transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast);
}
.side a.nav:hover { background: var(--hover); text-decoration: none; }
.side a.nav.on { border-left-color: var(--accent); color: var(--accent); background: var(--accent-soft); font-weight: 600; }
.side a.nav .ic { width: 15px; height: 15px; flex: none; color: currentColor; }
.side a.nav .badge { margin-left: auto; color: var(--muted); font-size: 11px; }

.main { flex: 1; min-width: 0; display: flex; flex-direction: column; }

/* 页面容器铺满主区（不做居中限宽）：
   右侧的页面级动作按钮（「导入备份 / 新建作品」「新建账号」等）靠 margin-left:auto 贴右，
   只有让容器右边缘 == 主区右边缘 == 窗口右边缘，开关侧栏时它们才不会跟着内容列左右移动。 */
.page { padding: 22px 26px 64px; width: 100%; }
.page.wide { max-width: none; }
.page-head { display: flex; align-items: baseline; gap: 12px; margin-bottom: 18px; flex-wrap: wrap; }
.page-head h1 { font-size: 20px; font-weight: 600; margin: 0; font-family: var(--serif); letter-spacing: .03em; }
.page-head .sub { color: var(--muted); font-size: 12px; }
.page-head .actions { margin-left: auto; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }

.card {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg);
  padding: 15px 17px;
  margin-bottom: 14px;
  box-shadow: var(--shadow-xs);
}
.card > h3 { margin: 0 0 11px; font-size: 13px; font-weight: 600; letter-spacing: .01em; }
.card > h3 .sub { font-weight: 400; color: var(--muted); margin-left: 6px; font-size: 12px; }
.card.tight { padding: 11px 13px; }
.card.hoverable { cursor: pointer; transition: border-color var(--t-fast), box-shadow var(--t-fast), transform var(--t-fast); }
.card.hoverable:hover { border-color: var(--accent-line); box-shadow: var(--shadow-sm); transform: translateY(-1px); }
.grid { display: grid; gap: 13px; }
.grid.c2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid.c3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid.c4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
/* 卡片列表：按可用宽度自动铺排，不留空列 */
.grid.cards { grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); }

/* 卡片底部操作条：用一条细线把「内容」和「操作」分开，避免按钮悬在空白里 */
.card .foot {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  margin-top: 13px; padding-top: 11px; border-top: 1px solid var(--line);
}

/* ---------------- 作品列表：一行一本（第 59 轮） ----------------
   作品卡上真正要看的只有「书名 / 类型 / 字数 / 上次写到哪」，
   摊成 300px 一格反而每样都折行；改成整宽一行一本，左侧固定一张 3:4 封面当识别锚点。
   网格用 area 命名而不是数格子：宽屏只是把「统计」从第二行挪到第三列，其余位置不动。 */

.book-list { display: flex; flex-direction: column; gap: 12px; }

.book-row {
  display: grid;
  grid-template-columns: 88px minmax(0, 1fr);
  grid-template-areas: "cover main" "cover stats" "foot foot";
  align-items: start;
  gap: 0 16px;
  margin-bottom: 0;          /* 行距交给 .book-list 的 gap，两套一起用会变成 26px */
  padding: 14px 16px 12px;
}

.book-cover {
  grid-area: cover;
  position: relative;
  width: 88px; height: 118px;     /* 近似 3:4，与常见书封比例一致 */
  border: 1px solid var(--line); border-radius: 5px;
  overflow: hidden; background: var(--panel-2); cursor: pointer;
}
.book-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* 没有封面时的占位：书名首字 + 提示。空框会被当成「加载失败」，也没人发现这里能点 */
.book-cover .ph {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 5px;
  background: linear-gradient(160deg, var(--accent-soft) 0%, var(--panel-2) 58%);
}
.book-cover .ph .ch { font-family: var(--serif); font-size: 30px; line-height: 1; color: var(--accent); opacity: .9; }
.book-cover .ph .hint { font-size: 10.5px; letter-spacing: .08em; color: var(--muted); }
/* 「换封面」只在悬停/聚焦时露出：常显会在每张封面上糊一条深色条 */
.book-cover .edit {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 4px 0;
  text-align: center; font-size: 11px; color: #fff; pointer-events: none;
  background: rgba(30, 26, 20, .58); opacity: 0; transition: opacity var(--t-fast);
}
.book-cover:hover .edit, .book-cover:focus-visible .edit { opacity: 1; }

.book-main { grid-area: main; min-width: 0; display: flex; flex-direction: column; }
.book-main .book-title { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
.book-main .book-title h3 {
  margin: 0; font-family: var(--serif); font-size: 16px; font-weight: 600;
  letter-spacing: .02em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.book-main .chips { margin-top: 7px; align-items: center; }
.book-main .book-time { margin-left: 4px; }
.book-main .book-desc {
  margin-top: 7px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; line-height: 1.65;
}
.book-stats { grid-area: stats; display: flex; flex-wrap: wrap; gap: 4px 22px; margin-top: 9px; }
.book-row .foot { grid-area: foot; margin-top: 12px; }

/* 宽屏：统计挪到右侧一列，中间那段不再留一大片空白 */
@media (min-width: 1120px) {
  .book-row { grid-template-columns: 88px minmax(0, 1fr) auto; grid-template-areas: "cover main stats" "foot foot foot"; }
  .book-stats { align-self: center; display: grid; grid-template-columns: repeat(2, minmax(74px, auto)); gap: 8px 28px; margin-top: 0; }
}

.muted { color: var(--muted); }
.small { font-size: 12px; }
.mono { font-family: var(--mono); font-size: 12px; }
.nowrap { white-space: nowrap; }
.pre { white-space: pre-wrap; word-break: break-word; }
.center { text-align: center; }
.hidden { display: none !important; }

/* ---------------- 表格 / 列表 ---------------- */

table.tbl { width: 100%; border-collapse: collapse; font-size: 13px; }
table.tbl th, table.tbl td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--line); vertical-align: top; }
table.tbl th { color: var(--muted); font-weight: 600; font-size: 12px; }
table.tbl tbody tr { transition: background var(--t-fast); }
table.tbl tbody tr:hover td { background: var(--panel); }
table.tbl tr:last-child td, table.tbl tr:last-child th { border-bottom: none; }
/* 「操作」列（以及所有放了多个行内控件的单元格）：
   整行排布、彼此留 6px，避免列宽不足时按钮被逐个挤到下一行、出现「重置密码 / 编辑 / （当 前账号）」那种断行。 */
table.tbl th.ops, table.tbl td.ops { white-space: nowrap; width: 1%; }
table.tbl td.ops > * { vertical-align: middle; }
table.tbl td.ops > * + * { margin-left: 6px; }

.list { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow-xs); }
.list .item {
  display: flex; align-items: center; gap: 10px; padding: 9px 12px;
  border-bottom: 1px solid var(--line); background: var(--bg);
  transition: background var(--t-fast);
}
.list .item:last-child { border-bottom: none; }
.list .item:hover { background: var(--panel); }
.list .item.click { cursor: pointer; }
.list .item .t { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.list .item .s { color: var(--muted); font-size: 12px; white-space: nowrap; }

.empty {
  padding: 42px 20px; text-align: center; color: var(--muted);
  border: 1px dashed var(--line-strong); border-radius: var(--radius-lg); font-size: 13px; line-height: 1.9;
  background: var(--panel);
}
.empty .t { color: var(--text); font-size: 14px; margin-bottom: 4px; font-weight: 600; }

/* ---------------- 标签页 / 分段 / 芯片 ---------------- */

.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--line); margin-bottom: 16px; overflow-x: auto; }
.tabs button {
  padding: 7px 13px; border: none; background: transparent; cursor: pointer;
  border-bottom: 2px solid transparent; color: var(--muted); white-space: nowrap;
  transition: color var(--t-fast), background var(--t-fast), border-color var(--t-fast);
  border-radius: var(--radius) var(--radius) 0 0;
}
.tabs button:hover { color: var(--text); background: var(--hover); }
.tabs button.on { color: var(--accent); border-bottom-color: var(--accent); }

.seg { display: inline-flex; border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow-xs); }
.seg button {
  padding: 4px 11px; border: none; background: var(--bg); cursor: pointer;
  border-right: 1px solid var(--line); color: var(--muted); font-size: 12px;
  transition: background var(--t-fast), color var(--t-fast);
}
.seg button:last-child { border-right: none; }
.seg button.on { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.seg button:hover:not(.on) { background: var(--hover); }

.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
  padding: 3px 11px; border: 1px solid var(--line); border-radius: var(--radius-pill);
  cursor: pointer; background: var(--bg); font-size: 12px;
  transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast);
}
.chip:hover { background: var(--hover); border-color: var(--line-strong); }
.chip.on { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }

/* ---------------- 写作页 ---------------- */

.write { display: flex; flex: 1; min-height: 0; align-items: stretch; height: calc(var(--vh100) - var(--topbar-h)); position: relative; }
/* 左栏统一 360px：目录和「设定库」的每一栏**必须是同一个框**。
   以前目录只占 224px（够排章节名），切成人物卡才放宽到 360 —— 于是每切一次栏目，
   正文的左边线就跟着抖一下，同一个位置上的字跳来跳去。宽度定死之后，
   换栏只是换内容，正文一个字都不动（作者 2026-10-27 提的第 1 条）。 */
.write .chapters {
  width: 360px; flex: none; border-right: 1px solid var(--line);
  background: var(--panel); overflow: hidden; padding: 0;
}
.write .chapters.hide { display: none; }
.write .chapters.panel-mode .col-inner {
  height: 100%; overflow-y: auto; overflow-x: hidden;
  scrollbar-gutter: stable; padding: 0 0 40px;
}
.write .chapters .colhead {
  display: flex; align-items: center; gap: 4px;
  padding: 8px 10px; border-bottom: 1px solid var(--line);
  font-size: 11px; letter-spacing: .06em; color: var(--muted);
}
.write .chapters .colhead .ttl { color: var(--fg); font-size: 12.5px; font-family: var(--serif); font-weight: 600; }
.write .chapters .colhead .spacer { flex: 1 1 0; }
/* 表头右侧的图标按钮（收起 / 展开）：**只有图标、没有文字** ——
   360px 的窄栏里，两个汉字宽的按钮等于从标题里抢走一半位置。
   24×24 是对齐后的最小可点尺寸，太高会把表头撑得比章节行还厚。 */
.write .chapters .colhead .headbtns { display: flex; align-items: center; gap: 2px; flex: none; }
.write .chapters .colhead .headbtns .btn { padding: 3px 5px; line-height: 1; }
/* 嵌入进来的整页：`.page` 的内边距在 `.embed-page` 上补回来 —— 那些页面
   本身是按「整页」写的，搬进窄栏后左内边距不补就会被裁掉一截。 */
.embed-page { padding: 14px 12px 40px; }
.write .chapters.expanded {
  position: absolute; left: 0; top: 0; bottom: 0; width: 100%;
  z-index: 30; border-right: none;
  box-shadow: 0 0 0 1px var(--line), 0 10px 30px rgba(0, 0, 0, .12);
}
.write .chapters.expanded .col-inner { padding-top: 0; }
.write .chapters.expanded .embed-page { padding: 18px 28px 60px; max-width: 980px; }

/* 窄栏里的整页（人物卡 / 世界观 / 大纲…）**一律单列**。
   360px 里再开三列，每列只剩一百来像素：姓名被压成一两个字宽、
   「年龄 / 性别 / 关系定位」三个输入框挤成三条缝（作者说的「挤在一起」）。
   一人一行也比三列小卡好扫 —— 这就是他要的「缩小时按列排布，每行只显示一张」。
   展开态（.expanded）不带这个类，各页面恢复自己原本的排布。 */
.write .chapters.panel-mode:not(.expanded) .embed-page .grid.c2,
.write .chapters.panel-mode:not(.expanded) .embed-page .grid.c3,
.write .chapters.panel-mode:not(.expanded) .embed-page .grid.c4,
.write .chapters.panel-mode:not(.expanded) .embed-page .grid.cards,
.write .chapters.panel-mode:not(.expanded) .embed-page .portrait-basic {
  grid-template-columns: minmax(0, 1fr);
}
/* 形象图在单列里居中：9:16 的观赏比例靠固定宽度保证（见 .portrait-pick 注释），
   不铺满整行，也不贴着左边线。 */
.write .chapters.panel-mode:not(.expanded) .embed-page .portrait-pick { margin: 0 auto; }
/* 两侧栏的内层：承接「跟随正文滚动」的位移（见 write.js 的 syncCols）。
   用 transform 而非改 scrollTop：位移在合成层完成，不动布局、不触发重排；
   外层 overflow:hidden 负责裁掉移出去的部分，形成「跟着纸往上走」的观感。 */
.write .col-inner {
  will-change: transform;
  min-height: 100%;
}
.write .chapters .col-inner { padding: 8px 0 40px; }
.write .chapters .hd {
  display: flex; align-items: center; gap: 6px; padding: 4px 10px 8px;
  color: var(--muted); font-size: 11px; letter-spacing: .06em;
}
.write .chapters .ch {
  padding: 8px 10px; border-left: 2px solid transparent; cursor: pointer;
  display: block; transition: background var(--t-fast), border-color var(--t-fast);
}
.write .chapters .ch:hover { background: var(--hover); }
.write .chapters .ch.on { border-left-color: var(--accent); background: var(--accent-soft); }
.write .chapters .ch .n { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
.write .chapters .ch .n .cno { flex: none; color: var(--muted); font-size: 11px; font-family: var(--serif); }
.write .chapters .ch.inter .n .cno { color: var(--warn); }
.write .chapters .ch .n .ct { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.write .chapters .ch .n .ct.unnamed { color: var(--muted); font-style: italic; }
.write .chapters .ch .meta {
  display: flex; align-items: center; gap: 7px; margin-top: 4px;
  color: var(--muted); font-size: 11px; line-height: 1.4;
}
.write .chapters .ch .meta .w { flex: none; }
.write .chapters .ch .meta .st {
  flex: none; padding: 0 4px; border: 1px solid var(--line); border-radius: 3px;
}
.write .chapters .ch .meta .st.revising { color: var(--warn); border-color: currentColor; }
.write .chapters .ch .meta .st.final { color: var(--ok); border-color: currentColor; }
.write .chapters .ch.drag-over { border-top: 2px solid var(--accent); }

.write .canvas { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
/* 顶栏三栏栅格（1fr / auto / 1fr）：两侧列等宽 → 中间列必然落在正中间，
   因此「第 N 章 + 章节名」是严格居中的，不随右侧保存状态文字宽窄而偏移 */
.write .canvas .bar {
  display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center; gap: 8px; padding: 6px 12px;
  border-bottom: 1px solid var(--line); background: var(--bg);
}
.write .canvas .bar .bartitle {
  display: flex; align-items: center; justify-content: center; min-width: 0;
}
/* 章序号：由排列顺序推导、不可编辑，紧贴章节名左侧。
   右侧放一个同宽不可见的「镜像」占位，左右配重相等 → 章节名输入框始终严格居中 */
.write .canvas .bar .chno {
  flex: none; white-space: nowrap; color: var(--muted); font-size: 12px;
  letter-spacing: .04em; font-family: var(--serif); margin-right: 10px; pointer-events: none;
}
.write .canvas .bar .chno.mirror { visibility: hidden; margin-right: 0; margin-left: 10px; }
.write .canvas .bar .title-input {
  border: none; background: transparent; font-family: var(--serif);
  font-size: 15px; padding: 3px 6px; width: clamp(150px, calc(22vw / var(--ui-scale)), 300px); outline: none;
  text-align: center; border-radius: var(--radius);
  transition: background var(--t-fast), box-shadow var(--t-fast);
}
.write .canvas .bar .title-input:focus { background: var(--panel); box-shadow: var(--ring); }
.write .canvas .bar .title-input::placeholder { color: var(--muted); font-size: 13px; }
.write .canvas .bar .barend {
  display: flex; align-items: center; gap: 8px; justify-content: flex-end; min-width: 0;
}
.write .canvas .bar .barend .small { overflow: hidden; text-overflow: ellipsis; }
/* 顶栏两端的对称开关：「章节」在最左、「控制台」在最右，等宽，形成左右对称 */
.write .canvas .bar > .bar-toggle { justify-self: start; }
.write .canvas .bar > .barend { justify-self: end; }
.write .canvas .bar .bar-toggle { min-width: 86px; }
.write .canvas .bar .bar-toggle .ic,
.write .canvas .bar .bar-toggle svg { width: 14px; height: 14px; flex: none; }
.write .scroll { flex: 1; overflow-y: auto; padding: 26px 0 120px; }

.editor {
  max-width: var(--editor-max, 680px); margin: 0 auto; padding: 0 24px;
  font-family: var(--editor-font, var(--serif));
  font-size: var(--fs-body); line-height: var(--lh-body);
  outline: none; min-height: 300px; word-wrap: break-word;
}
body.indent-first .editor p { text-indent: 2em; }
.editor p { margin: 0 0 .9em; }
.editor:empty::before { content: attr(data-placeholder); color: var(--muted); }
.editor img { display: block; margin: 16px auto; max-width: 100%; height: auto; border-radius: var(--radius); }
.editor figure { margin: 18px 0; }
.editor figure img { margin: 0 auto; }
.editor figcaption { text-align: center; color: var(--muted); font-size: 12px; font-family: var(--sans); margin-top: 6px; }
.editor h1, .editor h2, .editor h3 { font-size: 1.15em; margin: 1.2em 0 .6em; }
.editor blockquote { margin: .8em 0; padding-left: 12px; border-left: 2px solid var(--line-strong); color: var(--muted); }
.editor .hl-ai { background: var(--accent-soft); border-bottom: 1px solid var(--accent); }

.aimark { border-bottom: 1px solid var(--accent); cursor: help; }

/* 插图定位闪烁 */
@keyframes figflash {
  0%   { background: var(--accent-soft); box-shadow: 0 0 0 0 var(--accent-line); }
  55%  { background: var(--accent-soft); box-shadow: 0 0 0 8px transparent; }
  100% { background: transparent; box-shadow: 0 0 0 0 transparent; }
}
.editor figure.flash { border-radius: var(--radius); animation: figflash 1.6s ease; }

/* 悬浮选区工具条 */
.seltool {
  position: fixed; z-index: 60; display: flex; gap: 2px; padding: 3px;
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.seltool button {
  border: none; background: transparent; padding: 4px 9px; cursor: pointer; border-radius: 4px; font-size: 12px;
  transition: background var(--t-fast), color var(--t-fast);
}
.seltool button:hover { background: var(--hover); color: var(--accent); }

/* ---------------- AI 面板 ---------------- */

/* ---------------- 右侧控制台 ---------------- */

.aipanel {
  width: var(--right-w); flex: none; border-left: 1px solid var(--line);
  background: var(--panel); display: flex; flex-direction: column;
  height: calc(var(--vh100) - var(--topbar-h)); overflow: hidden;
}
.aipanel.hide { display: none; }
.aipanel .hd {
  display: flex; align-items: center; gap: 6px; padding: 7px 8px;
  border-bottom: 1px solid var(--line); flex: none; background: var(--bg);
}
/* 控制台页签条固定在顶部（它是操作入口，滚走就找不到了）；只有下面 .bd 的内容跟随正文滚动。 */
.aipanel > .col-inner { display: flex; flex-direction: column; flex: 1; min-height: 0; }
/* 三个区块页签：图标 + 短标签，等分宽度，窄面板（340px）下也排得下一行、不被挤成竖排 */
.ctabs { display: flex; gap: 1px; flex: 1; min-width: 0; }
.ctab {
  flex: 1; min-width: 0; display: inline-flex; align-items: center; justify-content: center; gap: 3px;
  padding: 5px 2px; border: 1px solid transparent; border-radius: var(--radius);
  background: transparent; color: var(--muted); cursor: pointer; font-size: 12px; white-space: nowrap;
  transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast);
}
.ctab .ic, .ctab svg { flex: none; }
.ctab:hover { background: var(--hover); color: var(--text); }
.ctab.on { background: var(--accent-soft); color: var(--accent); border-color: var(--accent-line); font-weight: 600; }
/* .bd 的内容跟随正文滚动（由 write.js 的 syncCols 施加 transform），
   因此自身不再滚动：平移量由正文的滚动进度决定。 */
.aipanel .bd { flex: 1; min-height: 0; overflow: hidden; padding: 12px; will-change: transform; }
/* 「设置」页例外：模型·文风·提示词 / 正文排版 / 本章 / 全书四块合起来比面板高得多。
   它必须自己滚 —— base 的 overflow:hidden 会把「本章」「全书」静默裁在下面，
   既看不见也滚不到（连滚动条都没有），而界面上完全看不出少了东西。 */
.aipanel .bd.setup-mode { overflow-y: auto; overflow-x: hidden; scrollbar-gutter: stable; }
.setup { display: flex; flex-direction: column; }
.setup .blk { margin-bottom: 22px; }
.setup .blk:last-child { margin-bottom: 4px; }

/* 「设置」页最上面一行：撤回 / 前进。
   `margin-left: auto` 把快捷键提示顶到最右，两个按钮留在左手边 ——
   按钮是动作、提示是说明，两者贴着读反而分不清哪个能点。 */
.setup .uhist {
  display: flex; align-items: center; gap: 8px;
  margin-bottom: 16px; padding-bottom: 14px;
  border-bottom: 1px solid var(--line);
}
.setup .uhist .btn { gap: 5px; }
.setup .uhist .small.muted { margin-left: auto; }

/* AI 页签顶部那一行「当前模型 · 文风」：只读状态，点它跳到「设置」去改。
   模型/文风选择器搬进设置页之后，这里是唯一还看得见「谁在答」的地方。 */
.airun {
  display: flex; align-items: center; gap: 6px; width: 100%;
  padding: 6px 9px; margin-bottom: 10px;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg);
  color: var(--muted); font-size: 11px; text-align: left; cursor: pointer;
  transition: color var(--t-fast), border-color var(--t-fast), background var(--t-fast);
}
.airun .txt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.airun .ic, .airun svg { flex: none; }
.airun:hover { color: var(--accent); border-color: var(--accent-line); background: var(--accent-soft); }

/* ---- 控制台「总览」区块：全书体量 + 写作进度 + 作品资料 ---- */
.ov .statrow { margin-bottom: 0; }
.ov .statrow .stat { padding: 9px 11px; }
.ov .statrow .stat .n { font-size: 19px; }
.ov .statrow .stat .l { font-size: 11px; line-height: 1.5; }
.ov-bars { display: flex; flex-direction: column; gap: 7px; }
.ov-bar { display: grid; grid-template-columns: 46px 1fr 30px; align-items: center; gap: 8px; font-size: 12px; }
.ov-bar .k { color: var(--muted); }
.ov-bar .track { height: 6px; border-radius: var(--radius-pill); background: var(--panel-2); overflow: hidden; }
.ov-bar .fill { height: 100%; border-radius: var(--radius-pill); background: var(--accent); transition: width var(--t-fast); }
.ov-bar .v { text-align: right; color: var(--muted); font-variant-numeric: tabular-nums; }
.ov-bar.draft .fill { background: var(--muted); }
.ov-bar.revising .fill { background: var(--warn); }
.ov-bar.final .fill { background: var(--ok); }

/* AI 区块：模型/文风选项条 + 四个任务 */
.aiopts {
  display: flex; gap: 8px; padding: 8px 10px; margin-bottom: 10px; flex-wrap: wrap;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg);
  box-shadow: var(--shadow-xs);
}
.aiopts .opt { display: flex; flex-direction: column; gap: 3px; flex: 1; min-width: 118px; }
.aiopts .opt label { font-size: 11px; color: var(--muted); letter-spacing: .02em; font-weight: 500; }
.aiopts .opt .select { padding: 4px 24px 4px 8px; font-size: 12px; background-position: right 7px center; }

/* 预输入提示词：默认只占一行，点击才展开 textarea。
   收起态用整行按钮承载，展开后按钮变成这一块的「标题条」。 */
.preprompt { margin-bottom: 10px; }
.pp-toggle {
  display: flex; align-items: center; gap: 6px; width: 100%;
  padding: 7px 10px; border: 1px dashed var(--line-strong); border-radius: var(--radius);
  background: transparent; color: var(--muted); cursor: pointer; font-size: 12px;
  transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast);
}
.pp-toggle:hover { background: var(--hover); color: var(--text); border-color: var(--accent-line); }
.pp-toggle .ic, .pp-toggle svg { flex: none; }
.preprompt.open .pp-toggle {
  border-style: solid; border-color: var(--accent-line); color: var(--accent);
  background: var(--accent-soft); border-radius: var(--radius) var(--radius) 0 0;
}
.preprompt.open .pp-toggle:hover { color: var(--accent); }
/* 有内容且收起时给一个字数徽标：否则用户会忘了它还在生效 */
.pp-badge {
  flex: none; padding: 0 5px; border-radius: var(--radius-pill);
  background: var(--accent-soft); color: var(--accent); font-size: 11px; line-height: 1.6;
}
.preprompt .textarea {
  border-radius: 0 0 var(--radius) var(--radius); border-top-color: var(--accent-line);
}
.pp-hint {
  display: flex; align-items: center; gap: 8px;
  padding: 5px 2px 0; font-size: 11px; color: var(--muted);
}
.pp-clear {
  flex: none; border: none; background: none; color: var(--muted);
  cursor: pointer; font-size: 11px; padding: 2px 4px; border-radius: var(--radius);
}
.pp-clear:hover { color: var(--danger); background: var(--hover); }

.aiseg { margin-bottom: 10px; }
.aiseg .seg { display: flex; width: 100%; }
.aiseg .seg button { flex: 1; text-align: center; padding: 4px 2px; }

/* 本章区块 */
.chinfo {
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg);
  padding: 3px 10px; margin-bottom: 14px; box-shadow: var(--shadow-xs);
}
.chinfo-row { display: flex; gap: 10px; padding: 6px 0; border-bottom: 1px solid var(--line); font-size: 12px; }
.chinfo-row:last-child { border-bottom: none; }
.chinfo-row .l { color: var(--muted); flex: none; min-width: 76px; }
.chinfo-row .v { flex: 1; min-width: 0; }

.csec-label { color: var(--muted); font-size: 11px; font-weight: 600; letter-spacing: .05em; margin-bottom: 6px; }
.crow { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.crow.wrap { row-gap: 6px; }

.vercard { border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg); margin-bottom: 10px; box-shadow: var(--shadow-xs); }
.vercard .h { display: flex; align-items: center; gap: 6px; padding: 7px 10px; border-bottom: 1px solid var(--line); }
.vercard .c { padding: 10px; font-family: var(--serif); font-size: 14px; line-height: 1.85; white-space: pre-wrap; }
.vercard .f { display: flex; gap: 6px; padding: 7px 10px; border-top: 1px solid var(--line); flex-wrap: wrap; }

.stream {
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg);
  padding: 10px; font-size: 13px; line-height: 1.8; white-space: pre-wrap;
  font-family: var(--serif); min-height: 60px; max-height: 340px; overflow-y: auto;
}

/* ---------------- 对话助手 ---------------- */

.agent { display: flex; flex-direction: column; gap: 10px; min-height: 0; }

/* 对话模式要「消息区自己滚、输入区钉在底部」。
   `.bd` 本体是 overflow:hidden（内容超出直接裁掉，不出滚动条），
   所以必须在这里把 bd 变成 flex 列，让 .agent 吃掉剩余高度 ——
   否则输入框和发送按钮会被裁到视口外，而且没有任何滚动条提示它们还在。 */
.aipanel .bd.agent-mode { display: flex; flex-direction: column; }
.aipanel .bd.agent-mode > * { flex: none; }
.aipanel .bd.agent-mode > .agent { flex: 1 1 0; min-height: 0; }

/* 校对模式：跟对话同构 —— 「范围 + 开始校对」钉在顶上，只有结果区滚。
   `.bd` 本体是 overflow:hidden（超出直接裁掉，连滚动条都没有），
   一次校对几十条、比面板高一倍是常态，不把 bd 变成 flex 列的话，
   第 8 条以后的结果永远看不见，还看不出少了东西。 */
.aipanel .bd.proof-mode { display: flex; flex-direction: column; overflow: hidden; }
.aipanel .bd.proof-mode > .aibody {
  flex: 1 1 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden;
}
.aipanel .bd.proof-mode .proof-fixed { flex: none; }

/* 结果区是**独立滚动容器**：它不参与 `syncCols()` 的跟随平移（见 write.js）。
   两者叠加的时候，正文滚一下列表就整体上移 —— 点了一条跳过去，
   回来想点第二条，它已经不在刚才的位置（作者说的「显示位置会错」）。
   `scrollbar-gutter: stable` 不能省：条目从「不足一屏」涨到「超过一屏」时，
   不预留沟槽会让整块宽度抖一下。负外边距把滚动条顶到面板内边缘。 */
.proof-scroll {
  flex: 1 1 0; min-height: 0; margin-top: 12px;
  overflow-y: auto; overflow-x: hidden;
  scrollbar-gutter: stable;
  margin-right: -12px; padding-right: 12px;
}

.agent > .agent-head,
.agent > .agent-scope,
.agent > .agent-input,
.agent > .agent-actions { flex: none; }

.agent-head { display: flex; align-items: center; gap: 6px; }
.agent-head > .select { flex: 1 1 0; min-width: 0; }

/* 消息区自己滚。`scrollbar-gutter: stable` 不能省：不预留沟槽的话，
   消息从「不够长」涨到「够长」时整个面板会横向抖一下。 */
.agent-msgs {
  display: flex; flex-direction: column; gap: 10px;
  flex: 1 1 0; min-height: 120px; overflow-y: auto;
  scrollbar-gutter: stable; padding-right: 2px;
}

.agent-empty { padding: 14px 4px; }
.agent-empty .t { font-size: 13px; font-weight: 600; margin-bottom: 4px; }
.agent-empty .s { font-size: 12px; color: var(--muted); line-height: 1.7; }

.agent-msg { display: flex; flex-direction: column; gap: 3px; }
.agent-msg .who { font-size: 11px; color: var(--muted); }
.agent-msg .body {
  padding: 7px 10px; border-radius: var(--radius); font-size: 13px;
  line-height: 1.75; white-space: pre-wrap; word-break: break-word;
}
.agent-msg.user .body { background: var(--accent-soft); border: 1px solid var(--accent-line); }
.agent-msg.assistant .body { background: var(--panel); border: 1px solid var(--line); font-family: var(--serif); font-size: 13.5px; }
.agent-msg.errored .body { border-color: var(--danger); color: var(--danger); background: var(--danger-soft); }
.agent-msg .noted { font-size: 11px; color: var(--ok); }
/* 「这次没生成完」的统一提示条：对话气泡、润色/扩写/剧情的结果区都用它。
   颜色用 warn 而不是 danger —— 内容往往还是可用的，只是不完整。 */
.incomplete {
  font-size: 11.5px;
  line-height: 1.6;
  color: var(--warn);
  background: var(--warn-soft);
  border: 1px solid color-mix(in srgb, var(--warn) 38%, transparent);
  border-radius: 6px;
  padding: 6px 9px;
  margin-top: 2px;
}
.agent-msg .ctx { font-size: 11px; color: var(--muted); }
.agent-msg .acts { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 1px; }

/* 「模型现在在干什么」：只在流式期间存在，正文一到就整行消失。
   左侧是不闪瞎眼的小圆点脉冲（表示还有人在干活），右侧是思考的尾巴。
   为什么不给个可展开的完整思考：作者要的只是「它没卡死」，全文对他写小说没有用，
   而且推理模型的思考常常几千字，摊开会把对话区挤满。 */
.agent-msg .agent-think {
  display: flex; align-items: baseline; gap: 6px; min-width: 0;
  font-size: 11px; color: var(--muted); opacity: .85;
  /* 这里**绝不能**用 `animation ... .45s both` 来做「晚一点出现」：
     对话区每来一批思考就整块重绘（paintAgent），节点是新建的，
     动画也就从头重播 —— 结果是这一行永远停在 opacity 0：
     DOM 有、文本有、断言全绿、屏幕上没有。
     「什么时候出现」交给数据层（m.think.showAt），样式只管长得对不对。
     同理，`.st::before` 的脉冲动画即便被重播也不影响可读性（它只是相位跳变）。 */
}
.agent-msg .agent-think .st { flex: none; }
.agent-msg .agent-think .st::before {
  content: ''; display: inline-block; width: 5px; height: 5px;
  margin-right: 5px; border-radius: 50%; background: currentColor;
  vertical-align: 1px; animation: thinkPulse 1.1s ease-in-out infinite;
}
/* 思考的尾巴：一行放不下就裁掉左边（最新的字在右端），不做换行，
   否则正文一到之前这块高度会一直跳。 */
.agent-msg .agent-think .tx {
  flex: 1 1 0; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  font-family: var(--serif);
}
@keyframes thinkPulse {
  0%, 100% { opacity: .2; }
  50% { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .agent-msg .agent-think .st::before { animation: none; opacity: .6; }
}

/* 助手提出的「准备写入」清单。
   这是整个对话功能里唯一会动作者数据的地方，所以它必须长得**不像**一句聊天：
   实心边框 + 逐条列出来 + 明确的按钮，跟旁边的气泡区分开。 */
.agent-ops {
  display: flex; flex-direction: column; gap: 6px;
  padding: 8px 10px; border: 1px solid var(--accent-line); border-radius: var(--radius);
  background: var(--accent-soft);
}
.agent-ops .ops-head { font-size: 12px; font-weight: 600; }
.agent-ops .ops-head .n { color: var(--accent); }
.agent-ops .ops-list {
  display: flex; flex-direction: column; gap: 5px;
  margin: 0; padding: 0; list-style: none;
  max-height: 190px; overflow-y: auto; scrollbar-gutter: stable;
}
.agent-ops li { display: flex; flex-direction: column; gap: 1px; }
.agent-ops li .lbl { font-size: 12.5px; }
.agent-ops li .det { font-size: 11px; color: var(--muted); line-height: 1.5; }
.agent-ops li.conflict .lbl { color: var(--danger); }
.agent-ops li.conflict .det { color: var(--danger); }
.agent-ops .ops-acts { display: flex; align-items: center; gap: 6px; }
.agent-ops .done { font-size: 11.5px; color: var(--ok); }
.agent-ops .gone { font-size: 11.5px; color: var(--muted); }

/* 「收录」的目标菜单：四个选项各自带一句说明。
   只写「收录成大纲」的话，作者得先知道这套系统里「大纲」和「灵感」的区别才敢点。 */
.extract-menu { display: flex; flex-direction: column; gap: 6px; }
.extract-item {
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  padding: 10px 12px; text-align: left; cursor: pointer;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel);
}
.extract-item:hover { border-color: var(--accent-line); background: var(--accent-soft); }
.extract-item .t { font-size: 13.5px; }
.extract-item .s { font-size: 11.5px; color: var(--muted); line-height: 1.5; }

/* 差异预览：旧值划掉、新值用左边线挑出来。
   不要把两个值并排放在一行 —— 长段落并排会挤成两列细条，反而读不出差异。 */
.ops-dev-item {
  padding: 10px 12px; margin-bottom: 10px;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel);
}
.ops-dev-item:last-child { margin-bottom: 0; }
.ops-dev-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.ops-dev-head .lbl { font-size: 13px; }
.ops-dev-head .kind {
  font-size: 11px; color: var(--muted);
  border: 1px solid var(--line); border-radius: 999px; padding: 0 7px;
}
.ops-dev-rows { display: flex; flex-direction: column; gap: 9px; }
.ops-dev-row { display: grid; grid-template-columns: 76px 1fr; gap: 10px; align-items: start; }
.ops-dev-row .k { font-size: 11.5px; color: var(--muted); padding-top: 2px; }
.ops-dev-row .vbox { min-width: 0; }
.ops-dev-row .v { font-size: 13px; line-height: 1.65; white-space: pre-wrap; word-break: break-word; }
.ops-dev-row .v.old { color: var(--muted); text-decoration: line-through; opacity: .75; margin-bottom: 3px; }
.ops-dev-row .v.new { border-left: 2px solid var(--accent); padding-left: 8px; }
.ops-dev-row .v.new:empty { display: none; }

.agent-scope { display: flex; align-items: center; gap: 8px; }
/* 读取区间：`从[第N章] 到[第N章]` + 「前章读全文/摘要」开关。
   这一行**独占**一行 —— 两个下拉加一个开关刚好排满 360px 的栏，
   挤进下面那行会把「当前读的是哪几章」压成「当…」（作者贴过这张图）。
   下拉要能收（flex:1 1 0 + min-width:0），不然原生 select 的固有宽度会把开关顶出去。 */
.agent-scope.agent-range-row { gap: 6px; }
.agent-range { display: flex; align-items: center; gap: 4px; flex: 1 1 0; min-width: 0; }
.agent-range > .select { flex: 1 1 0; min-width: 0; max-width: 110px; }
.agent-range .range-lab { flex: none; font-size: 12px; color: var(--muted); }
.agent-prior { flex: none; white-space: nowrap; }
.agent-scope .small { flex: 1 1 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.agent-input {
  /* 2026-10-20 作者反馈「输入框有点小」：58 → 92px。
     输入时还会随内容长高（封顶 30% 视口高，见 renderAgentSection 里的 growInput），
     所以这里只需要给一个足够写两句的起点。 */
  width: 100%; min-height: 92px; resize: vertical;
  font-family: var(--serif); font-size: 14px; line-height: 1.75;
}
.agent-actions { display: flex; align-items: center; gap: 6px; }

.memlist { display: flex; flex-direction: column; gap: 6px; max-height: 46vh; overflow-y: auto; scrollbar-gutter: stable; }
.memrow {
  display: flex; align-items: flex-start; gap: 8px;
  padding: 7px 9px; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--panel); font-size: 12.5px; line-height: 1.65;
}
.memrow .txt { flex: 1 1 0; min-width: 0; }
.memrow .tag { flex: none; }
.memrow .btn { flex: none; }

/* ---------------- 弹层 ---------------- */

.mask {
  position: fixed; inset: 0; background: rgba(20, 20, 18, .46);
  backdrop-filter: blur(2px);
  display: flex; align-items: center; justify-content: center; z-index: 100; padding: 20px;
}
.modal {
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-lg);
  width: 100%; max-width: 520px; max-height: calc(var(--vh100) * 0.86); display: flex; flex-direction: column;
  box-shadow: var(--shadow-lg);
}
.modal.wide { max-width: 780px; }
.modal .mh { padding: 14px 17px; border-bottom: 1px solid var(--line); font-weight: 600; flex: none; font-size: 14px; }
.modal .mb { padding: 16px 17px; overflow-y: auto; flex: 1; }
.modal .mf { padding: 12px 17px; border-top: 1px solid var(--line); display: flex; gap: 8px; justify-content: flex-end; flex: none; }

.toasts { position: fixed; right: 16px; bottom: 16px; z-index: 200; display: flex; flex-direction: column; gap: 8px; }
.toast {
  background: var(--bg); border: 1px solid var(--line); border-left: 3px solid var(--accent);
  border-radius: var(--radius); padding: 10px 14px; max-width: 380px; font-size: 13px; line-height: 1.7;
  box-shadow: var(--shadow);
  animation: toast-in var(--t);
}
@keyframes toast-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.toast.err { border-left-color: var(--danger); }
.toast.ok { border-left-color: var(--ok); }

.menu {
  position: absolute; z-index: 120; background: var(--bg); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 4px; min-width: 140px;
  box-shadow: var(--shadow);
}
.menu button {
  display: block; width: 100%; text-align: left; padding: 6px 10px; border: none;
  background: transparent; cursor: pointer; border-radius: 4px; font-size: 13px;
  transition: background var(--t-fast), color var(--t-fast);
}
.menu button:hover { background: var(--hover); }
.menu button.danger { color: var(--danger); }
.menu button.danger:hover { background: var(--danger-soft); }

/* ---------------- 图片 ---------------- */

.thumbs { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 10px; }
/* `position: relative` 是给图片库那两个浮层（勾选框、使用状态徽标）当定位上下文用的。
   别的页面复用 .thumb 但不放浮层，多这一条没有任何副作用。 */
.thumb { position: relative; border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: var(--bg); cursor: pointer; box-shadow: var(--shadow-xs); transition: border-color var(--t-fast), box-shadow var(--t-fast), transform var(--t-fast); }
.thumb:hover { box-shadow: var(--shadow-sm); transform: translateY(-1px); }
.thumb.on { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
.thumb .im { width: 100%; height: 104px; object-fit: cover; display: block; background: var(--panel-2); }
.thumb .cap { padding: 6px 8px; font-size: 12px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 图片库里卡片下的第二、三行（使用情况 / 用途标记）。行距压紧，
   三行加起来只比原来的两行多一点点，不至于把网格撑得又高又空。 */
.thumb .sub { padding: 0 8px 5px; font-size: 11px; line-height: 1.5; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.thumb .sub.use { color: var(--accent); }

/* ---------------- 图片库：使用中标记 + 多选批量操作 ---------------- */

/* 勾选框浮在缩略图左上角。做成 20px 的方块而不是原生 checkbox：
   原生控件在这里只有 13px，戴着手套点不中，而且点击会冒泡出去顺手打开详情弹层。
   必须带阴影：缩略图底色是白的（或很浅）时，一个白方块压在图上等于隐形。 */
.thumb .pick {
  position: absolute; top: 6px; left: 6px; z-index: 2;
  width: 20px; height: 20px; border-radius: 5px;
  display: flex; align-items: center; justify-content: center;
  background: #fff; border: 1px solid var(--line-strong);
  box-shadow: 0 1px 4px rgba(30, 26, 20, .28);
  color: transparent; cursor: pointer; user-select: none;
  transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast);
}
.thumb .pick:hover { border-color: var(--accent); }
.thumb .pick:focus-visible { box-shadow: var(--ring); }
.thumb .pick[aria-checked="true"] { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }

/* 使用状态徽标浮在右上角。「使用中」用主题强调色（= 需要注意、别随手删），
   「未使用」用白底灰字（= 可以放心清理）。两种都要带阴影：
   缩略图什么底色都有，纯浅色徽标压在浅图上就等于没标。 */
.thumb .usebadge {
  position: absolute; top: 6px; right: 6px; z-index: 2;
  padding: 1px 6px; border-radius: var(--radius-pill);
  font-size: 11px; line-height: 1.6; white-space: nowrap;
  background: var(--accent); color: var(--accent-ink); border: 1px solid transparent;
  box-shadow: 0 1px 4px rgba(30, 26, 20, .28);
}
.thumb .usebadge.off { background: #fff; color: var(--muted); border-color: var(--line-strong); }

/* 选中卡片：与 .thumb.on 同一套视觉语言，作者不用重新学一遍「选中的样子」 */
.thumb.picked { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }

/* 批量操作条：只在有勾选时出现，出现时应当显眼到不可能被忽略 */
.batchbar {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 8px 12px; margin-bottom: 12px;
  border: 1px solid var(--accent-line); background: var(--accent-soft);
  border-radius: var(--radius);
}
.batchbar > .batchcount { font-weight: 600; color: var(--accent); font-size: 13px; }

.imgpick { max-height: 320px; overflow-y: auto; }

/* ---------------- 人物形象图 ---------------- */

/* 列表卡片左侧的圆形头像位。无图时用 --panel-2 底 + 首字占位，
   所以即使一个人物都没设形象图，卡片高度与有图时也一致（网格不跳）。 */
.portrait {
  width: 48px; height: 48px; flex: 0 0 48px;
  border-radius: 50%; overflow: hidden;
  background: var(--panel-2); border: 1px solid var(--line);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--serif); font-size: 19px; color: var(--muted);
  user-select: none;
}
.portrait img { width: 100%; height: 100%; object-fit: cover; display: block; }
.portrait.lg { width: 100%; height: auto; aspect-ratio: 3 / 4; border-radius: var(--radius); flex: none; font-size: 40px; }
.icon-btn .portrait, .portrait.sm { width: 28px; height: 28px; flex: 0 0 28px; font-size: 12px; }

/* 基本信息里那一格形象图：**图本身就是唯一入口**（左键选图、右键出菜单），
   不再摆一排按钮 —— 它要和左列的姓名/别名/年龄同列，按钮会把两列挤歪。 */
/* 基本信息：左列＝姓名/别名/年龄等字段，右列＝形象图。
   右列固定宽度、**固定高度**（9:16 竖屏框，与 `crop.js` 的 `PORTRAIT_RATIO` 同一契约）——
   预览图的尺寸是契约的一部分，换一张横图/方图不会让右列忽高忽低、把整个版式顶歪。
   框里用 `object-fit: cover` 铺满：上游已经被作者在「截取形象图」里按同一比例框过一次，
   这里再 cover 只是把框外的边角切掉，绝不等于「二次裁切」作者选的内容。 */
.portrait-basic {
  grid-template-columns: minmax(0, 1fr) var(--portrait-w, 162px);
  align-items: start;
}
.portrait-basic .pb-fields { min-width: 0; }
.portrait-basic .pb-portrait { min-width: 0; }
.portrait-basic .pb-portrait .field { margin-bottom: 0; }
.portrait-basic .portrait-cell { min-width: 0; }

/* 「主要设定」是这一页真正的正文：它要占满**输入框以上之外剩下的高度**，
   不是一个 220px 的小窗（作者 2026-10-27 提的第 2 条）。
   实际高度由 lore.js 量「框顶到滚动容器底」写进 style.height ——
   窄栏（360px）、展开态、手机端三种容器高度都不一样，写死像素必然有一边不对。
   这里只给一个下限兜底：脚本没跑到（或容器高度未知）时也不至于退回 220px。 */
.char-main-setting { min-height: 320px; resize: vertical; }

.portrait-pick {
  /* 宽度取 `--portrait-w` 而**不是 `100%`**：手机端退化为单列时网格列宽是整屏，
     `100%` 会把框拉成 360×288 的横条 —— 9:16 的观赏比例必须由**固定尺寸**保证。 */
  width: var(--portrait-w, 162px); border-radius: var(--radius); overflow: hidden;
  background: var(--panel-2); border: 1px solid var(--line);
  cursor: pointer; position: relative; user-select: none;
  transition: border-color .15s, box-shadow .15s;
}
/* 有图：固定 9:16 竖框，图铺满并裁掉超出的边（见上方注释，cover 在此是收边不是重裁） */
.portrait-pick.has-img { height: var(--portrait-h, 288px); }
.portrait-pick.has-img img { width: 100%; height: 100%; object-fit: cover; object-position: center; display: block; }
/* 无图：占位高度和「有图」的框一样高，这样右列高度不因有没有图上蹿下跳 */
.portrait-pick.empty { height: var(--portrait-h, 288px); }
.portrait-pick:hover { border-color: var(--accent); }
.portrait-pick:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.portrait-pick.empty { border-style: dashed; }
.portrait-pick .ph-body {
  width: 100%; height: 100%; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 6px;
  color: var(--muted); font-size: 12px;
}
.portrait-pick .ph-icon { display: block; opacity: .55; }
.portrait-pick .ph-icon svg { display: block; }
/* 右键是唯一操作入口，给个常驻角标做提示，否则没人会想到去右键 */
.portrait-pick::after {
  content: '右键编辑'; position: absolute; right: 6px; bottom: 6px;
  padding: 1px 6px; border-radius: 999px; font-size: 11px; line-height: 16px;
  background: rgba(0, 0, 0, .55); color: #fff; pointer-events: none;
  opacity: 0; transition: opacity .15s;
}
.portrait-pick:hover::after, .portrait-pick:focus-visible::after { opacity: 1; }

/* --- 截取形象图 ---
   左边＝原图 + 取景框，右边＝**落库后预览图的样子**（同比例、同 cover）。
   预览是必需的：没有它，作者是在盲框一块自己看不见最终效果的区域。 */
.crop-2col { display: flex; gap: 18px; align-items: flex-start; flex-wrap: wrap; }
.crop-wrap { display: flex; justify-content: center; background: var(--panel-2); border-radius: var(--radius); padding: 10px; }
.crop-side { display: flex; flex-direction: column; align-items: center; flex: none; }
/* 预览框：和详情页的 .portrait-pick 同构 —— 同比例、同 `cover`。
   作者在这里看到的，就是落库后详情页显示的样子。
   比例**写死 9 / 16**（不是 `var()`）：`aspect-ratio` 里用 `var(--a) / var(--b)`
   在部分浏览器里解析失败（整个声明被丢弃 → 框塌成 0 高，预览变成一条线）。
   这两个数字与 `--portrait-w/-h`（162 × 288）是同一个契约，改一处必须改另一处。 */
.crop-preview {
  width: 132px; aspect-ratio: 9 / 16;
  position: relative; overflow: hidden; border-radius: var(--radius);
  border: 1px solid var(--line); background: var(--panel-2); flex: none;
}
.crop-preview > img {
  display: block; position: absolute; max-width: none; -webkit-user-drag: none;
  /* 与详情页 .portrait-pick img 同一种铺法：cover（收边，不拉伸、不留白）。
     尺寸由 JS 按「框 → 预览」的缩放系数算好，这里只声明收边语义。 */
  object-fit: cover;
}
.crop-stage { position: relative; overflow: hidden; border-radius: 4px; touch-action: none; user-select: none; }
.crop-stage > img { display: block; width: 100%; height: 100%; pointer-events: none; -webkit-user-drag: none; }
.crop-box { position: absolute; cursor: move; box-shadow: 0 0 0 9999px rgba(0, 0, 0, .55); outline: 1px solid rgba(255, 255, 255, .9); }
/* 锁定尺寸：框只能挪，四角不出现把手，光标也不再暗示「可以拉」 */
.crop-box.locked { cursor: move; }
.crop-box .crop-grid {
  position: absolute; inset: 0; pointer-events: none;
  background:
    linear-gradient(to right, transparent 33.33%, rgba(255,255,255,.35) 33.33%, rgba(255,255,255,.35) calc(33.33% + 1px), transparent calc(33.33% + 1px)),
    linear-gradient(to right, transparent 66.66%, rgba(255,255,255,.35) 66.66%, rgba(255,255,255,.35) calc(66.66% + 1px), transparent calc(66.66% + 1px)),
    linear-gradient(to bottom, transparent 33.33%, rgba(255,255,255,.35) 33.33%, rgba(255,255,255,.35) calc(33.33% + 1px), transparent calc(33.33% + 1px)),
    linear-gradient(to bottom, transparent 66.66%, rgba(255,255,255,.35) 66.66%, rgba(255,255,255,.35) calc(66.66% + 1px), transparent calc(66.66% + 1px));
}
.crop-box .crop-handle { position: absolute; width: 12px; height: 12px; background: #fff; border: 1px solid rgba(0,0,0,.35); border-radius: 2px; }
.crop-box .crop-handle.nw { left: -6px; top: -6px; cursor: nwse-resize; }
.crop-box .crop-handle.ne { right: -6px; top: -6px; cursor: nesw-resize; }
.crop-box .crop-handle.sw { left: -6px; bottom: -6px; cursor: nesw-resize; }
.crop-box .crop-handle.se { right: -6px; bottom: -6px; cursor: nwse-resize; }
.crop-zoom { flex: 1 1 auto; min-width: 120px; max-width: 260px; }

/* 调整列表头像位置：左边 1:1 取景框，右边实时圆形头像预览。
   取景框恒定正方形（列表头像就是圆的），所以不需要拖角改比例，只拖右下角缩放。 */
.focus-wrap { display: flex; gap: 16px; align-items: flex-start; flex-wrap: wrap; }
.focus-main { background: var(--panel-2); border-radius: var(--radius); padding: 10px; }
.focus-stage { position: relative; overflow: hidden; border-radius: 4px; touch-action: none; user-select: none; }
.focus-stage > img { display: block; width: 100%; height: 100%; pointer-events: none; -webkit-user-drag: none; }
.focus-box {
  position: absolute; cursor: move; outline: 1px solid rgba(255, 255, 255, .9);
  box-shadow: 0 0 0 9999px rgba(0, 0, 0, .55);
}
.focus-box .focus-handle {
  position: absolute; right: -7px; bottom: -7px; width: 14px; height: 14px;
  background: #fff; border: 1px solid rgba(0, 0, 0, .35); border-radius: 2px; cursor: nwse-resize;
}
.focus-side { display: flex; flex-direction: column; align-items: center; }
/* 圆形头像预览：和列表里的 .portrait 同构（圆形 + cover + object-position），只是放大了 */
.focus-avatar {
  width: 80px; height: 80px; border-radius: 50%; overflow: hidden; position: relative;
  background: var(--panel-2); border: 1px solid var(--line); flex: none;
}
.focus-avatar img { display: block; }

/* 手机端：基本信息那一格退化为单列（形象图落到字段下方）。
   框**居中**而不是铺满整行 —— 9:16 的观赏比例靠固定尺寸保证，
   宽度若交给列宽就成了横条（见 `.portrait-pick` 的注释）。 */
@media (max-width: 860px) {
  .portrait-basic { grid-template-columns: minmax(0, 1fr); }
  .portrait-pick { margin: 0 auto; }
  .crop-stage { transform: scale(.92); transform-origin: top center; }
}

/* 本章插图汇总面板 */
.imgpanel-bar { margin-bottom: 10px; align-items: center; gap: 8px; flex-wrap: wrap; }
.imgrows { display: flex; flex-direction: column; gap: 8px; margin-top: 2px; }
.imgrow {
  display: flex; align-items: center; gap: 12px; padding: 8px 10px;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg);
  box-shadow: var(--shadow-xs);
  transition: border-color var(--t-fast), box-shadow var(--t-fast);
}
.imgrow:hover { border-color: var(--line-strong); box-shadow: var(--shadow-sm); }
.imgrow.idle { background: var(--panel); }
.imgrow .th { width: 64px; height: 64px; object-fit: cover; border-radius: 5px; flex: none; background: var(--panel-2); }
.imgrow .mid { flex: 1; min-width: 0; }
.imgrow .nm { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.imgrow .mid .small { margin-top: 2px; }
.imgrow .pv { margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-style: italic; opacity: .9; }
.imgrow .acts { display: flex; gap: 6px; flex: none; flex-wrap: wrap; justify-content: flex-end; }

/* 控制台比弹层窄得多：插图行改为两段式，按钮整行落在下方 */
.aipanel .imgrow { flex-wrap: wrap; }
.aipanel .imgrow .th { width: 52px; height: 52px; }
.aipanel .imgrow .acts { width: 100%; justify-content: flex-start; margin-top: 2px; }

/* ---------------- 排版控件（外观主题页 / 写作控制台共用） ---------------- */

.typo-label { color: var(--muted); font-size: 11px; font-weight: 600; letter-spacing: .05em; margin-bottom: 5px; }
.typo-row { display: flex; gap: 6px; align-items: center; }
.typo-row .typo-font { flex: 1; min-width: 0; }
.typo-row .select { width: 100%; }
.typo-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin-top: 10px; }
.typo-grid .cell { display: flex; align-items: center; gap: 5px; min-width: 0; }
.typo-grid .cell .l { color: var(--muted); font-size: 11px; flex: none; }
.typo-grid .cell .v { display: flex; align-items: center; gap: 3px; min-width: 0; flex: 1; }
.typo-grid .cell .input { padding: 4px 6px; font-size: 12px; width: 100%; min-width: 0; }
.typo-grid .cell .u { color: var(--muted); font-size: 11px; line-height: 1; flex: none; }
.typo-preview {
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg);
  padding: 12px 14px; color: var(--text); box-shadow: var(--shadow-xs);
  overflow-wrap: break-word;
}
.fontlist { display: flex; flex-direction: column; }
.fontrow { display: flex; align-items: center; gap: 6px; padding: 8px 0; border-bottom: 1px solid var(--line); }
.fontrow:last-child { border-bottom: none; }
.fontrow .mid { flex: 1; min-width: 0; }
.fontrow .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---------------- 登录页 ---------------- */

.auth {
  min-height: var(--vh100); display: flex; align-items: center; justify-content: center; padding: 24px;
  background: var(--panel);
}
.auth .box { width: 100%; max-width: 350px; }
.auth .brand { font-family: var(--serif); font-size: 27px; letter-spacing: .2em; text-align: center; }
.auth .tagline { text-align: center; color: var(--muted); font-size: 12px; margin: 8px 0 28px; letter-spacing: .14em; }
/* 注册页字段比登录多（用户名/显示名/密码/确认/邀请码），容器略放宽 */
.auth.reg .box { max-width: 420px; }
/* 注册页的「邀请码注册 / 提交申请」开关居中 */
.auth.reg .seg { margin: 0 auto; }

/* ---------------- 统计 ---------------- */

.statrow { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin-bottom: 14px; }
.stat { border: 1px solid var(--line); border-radius: var(--radius); padding: 11px 13px; background: var(--bg); box-shadow: var(--shadow-xs); }
.stat .n { font-size: 21px; font-family: var(--serif); letter-spacing: .01em; line-height: 1.35; }
.stat .l { color: var(--muted); font-size: 12px; margin-top: 2px; }

/* ---------------- 柱状图（AI 用量，纯 SVG） ---------------- */

.barchart { width: 100%; height: 168px; display: block; overflow: visible; }
.barchart .axis { stroke: var(--line-strong); stroke-width: 1; }
.barchart .bar { fill: var(--accent); opacity: .82; transition: opacity var(--t-fast); }
.barchart .bar:hover { opacity: 1; }
.barchart .axis-label { fill: var(--muted); font-size: 11px; font-family: var(--sans); }

/* ---------------- 依据 / 差异高亮 ---------------- */

.evidence { color: var(--muted); font-size: 11px; margin-top: 3px; }
.evidence a { color: var(--accent); }
.filled { border-color: var(--accent) !important; background: var(--accent-soft) !important; }
.diff { background: var(--panel-2); padding: 0 2px; }

/* ---------------- 移动端 ---------------- */

.bottomnav { display: none; }
/* 手机端抽屉遮罩：桌面端恒不显示（见媒体查询内的覆盖） */
.side-scrim { display: none; }

@media (max-width: 860px) {
  :root { --left-w: 0px; }
  .desktop-only { display: none !important; }
  .layout { display: block; }

  .topbar { gap: 8px; padding: 0 10px; }
  .topbar .meta { font-size: 11px; min-width: 0; }
  /* 书名右边的题材 tag 会把书名挤成逐字竖排 —— 窄屏先撤掉 tag 给书名腾位置 */
  .topbar .meta .tag { display: none; }
  .topbar .meta b { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

  /* 手机端侧栏是「抽屉」：**默认收起**，只有 show-mobile 才是展开态。
     旧写法是「带 show-mobile 才显示、兜底跟随桌面端的可见态」，于是首页一进来
     就被固定定位的侧栏盖住正文；而顶栏按钮第一次点只是补上 show-mobile，
     从"盖着"变成"还是盖着"，看起来像点了没反应。 */
  .side {
    position: fixed; top: var(--topbar-h); left: 0; bottom: 52px; width: 250px;
    height: auto; z-index: 50; border-right: 1px solid var(--line);
    box-shadow: var(--shadow-lg); display: none;
  }
  .side.show-mobile { display: block; }

  /* 遮罩压在侧栏之下（50）、底栏之上（70），因此点空白处能收起、底栏的「更多」仍可点 */
  .side-scrim {
    display: block; position: fixed; left: 0; right: 0;
    top: var(--topbar-h); bottom: 52px;
    background: rgba(20, 16, 12, .28); z-index: 45;
    opacity: 0; pointer-events: none; transition: opacity var(--t-fast);
  }
  .side-scrim.show { opacity: 1; pointer-events: auto; }

  .page { padding: 14px 12px 84px; }
  .grid.c2, .grid.c3, .grid.c4 { grid-template-columns: minmax(0, 1fr); }

  .write { display: block; height: auto; }
  .write .chapters {
    width: auto; height: auto; max-height: calc(var(--vh100) * 0.46); border-right: none;
    border-bottom: 1px solid var(--line); position: static; overflow-y: auto;
  }
  .write .chapters.hide { display: none; }
  .write .canvas .bar { position: sticky; top: 0; z-index: 10; grid-template-columns: minmax(0, 1fr) auto; gap: 6px; }
  /* 底栏已有「章节」与「控制台」，写作页顶栏这两个开关在手机上是重复入口，撤掉 */
  .write .canvas .bar .bar-toggle { display: none; }
  /* 窄屏让章节名占满剩余宽度，居中留白会挤没，改为左对齐并隐藏序号镜像 */
  .write .canvas .bar .chno { font-size: 11px; margin-right: 6px; }
  .write .canvas .bar .chno.mirror { display: none; }
  .write .canvas .bar .title-input { width: auto; flex: 1; min-width: 0; text-align: left; }
  .write .scroll { overflow: visible; padding: 18px 0 96px; }
  .editor { padding: 0 16px; font-size: 16px; }

  /* 手机端控制台自底部弹出，只占半屏 —— 上半屏仍可看到正文，便于边写边看 */
  .aipanel {
    position: fixed; left: 0; right: 0; bottom: 0; top: auto; width: auto;
    height: calc(var(--vh100) * 0.5); z-index: 80; border-left: none; border-top: 1px solid var(--line);
  }
  .aipanel.hide { display: none; }

  /* 手机端整页滚动（.write display:block、.scroll overflow:visible），
     两侧栏本就没有独立容器，跟随位移无意义 —— 撤销为静态，避免错位。 */
  .write .col-inner, .aipanel .bd { transform: none !important; }
  .write .chapters .col-inner { padding: 8px 0 40px; }

  .imgrow { flex-wrap: wrap; }
  .imgrow .acts { width: 100%; justify-content: flex-start; }

  .bottomnav {
    display: flex; position: fixed; left: 0; right: 0; bottom: 0; height: 52px;
    border-top: 1px solid var(--line); background: var(--bg); z-index: 70;
    box-shadow: 0 -2px 12px rgba(30, 26, 20, .07);
  }
  .bottomnav button {
    flex: 1; border: none; background: transparent; display: flex; flex-direction: column;
    align-items: center; justify-content: center; gap: 3px; cursor: pointer;
    color: var(--muted); font-size: 11px; padding: 0;
  }
  .bottomnav button.on { color: var(--accent); }
  .bottomnav button .ic { width: 17px; height: 17px; }

  .modal { max-width: none; }
  .toasts { left: 12px; right: 12px; bottom: 64px; }
  .toast { max-width: none; }
}

/* ---------------- 分隔线 / 手动分章 ---------------- */

.divider { height: 1px; background: var(--line); margin: 14px 0; }

.manual-lines {
  max-height: calc(var(--vh100) * 0.52);
  overflow: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.manual-line {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 3px 10px;
  cursor: pointer;
  border-bottom: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 12px;
}
.manual-line:last-child { border-bottom: none; }
.manual-line:hover { background: var(--hover); }
.manual-line.on { background: var(--accent); color: var(--accent-ink); }
.manual-line .ln { min-width: 46px; text-align: right; color: var(--muted); }
.manual-line.on .ln { color: rgba(255, 255, 255, .78); }
.manual-line .lb {
  font-size: 11px;
  color: var(--accent);
  border: 1px solid currentColor;
  border-radius: 3px;
  padding: 0 4px;
  visibility: hidden;
}
.manual-line:hover .lb, .manual-line.on .lb { visibility: visible; }
.manual-line.on .lb { color: #FFFFFF; }
.manual-line .lt { flex: 1; min-width: 0; white-space: pre-wrap; word-break: break-all; font-family: var(--sans); }

/* ---------------- 校对（控制台第三个页签） ----------------

   结果列表要同时满足两件事：① 一眼看出「哪类各几条」；② 点一条，正文里立刻
   看见它在哪儿。第 ② 件靠 `::highlight()`（CSS Custom Highlight API）——
   它只登记一个 Range，DOM 一个字节都不动。往 contenteditable 里插 `<mark>`
   虽然 `serialize()` 读 `innerText` 吃不到它，但它会进撤回栈快照，
   作者撤一次就多出一个莫名其妙的标签。
   ------------------------------------------- */

::highlight(qtwy-proof) {
  background: rgba(180, 85, 63, .22);
  text-decoration: underline solid var(--accent);
  text-underline-offset: 3px;
}

@keyframes proofflash {
  0% { background: var(--accent-soft); }
  100% { background: transparent; }
}
/* 不支持 Highlight API 时的退路：整段闪一下，至少能看出「跳到了」 */
.editor .proof-flash { animation: proofflash 1.6s ease; border-radius: 3px; }

.proof-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
.proof-head .sum { font-size: 13px; font-weight: 600; }
.proof-head .chips { margin-left: auto; }

.proof-group {
  display: flex; align-items: center; gap: 6px;
  margin: 12px 0 6px; font-size: 11.5px; font-weight: 600;
  letter-spacing: .06em; color: var(--muted);
}

.proof-item {
  /* 四列网格：勾选框 / 内容 / 动作（替换 + 忽略）。
     勾选框必须与内容**同一行顶端对齐** —— 单独一行会把卡片撑高一倍，
     而列表本来就长，一屏能看到的条数直接减半。 */
  display: grid;
  grid-template-columns: 16px minmax(0, 1fr) auto;
  align-items: start;
  column-gap: 8px;
  padding: 8px 10px; margin-bottom: 6px;
  border: 1px solid var(--line); border-left: 3px solid var(--line-strong);
  border-radius: var(--radius); background: var(--bg); cursor: pointer;
  transition: border-color var(--t-fast), background var(--t-fast);
}
.proof-item .pb { min-width: 0; }
.proof-item .proof-chk {
  width: 14px; height: 14px; margin: 3px 0 0;
  accent-color: var(--accent); cursor: pointer;
}
/* 「替换 / 忽略」竖着摞在右列：控制台只 320~360px 宽，
   两个按钮横排会从正文内容里再抢走 90px，引文只剩一两个字就折行。 */
.proof-item .proof-acts { display: flex; flex-direction: column; gap: 4px; flex: none; }
.proof-item .proof-fix-btn,
.proof-item .proof-skip-btn { padding: 2px 8px; font-size: 12px; line-height: 1.5; white-space: nowrap; }
.proof-item:hover { border-color: var(--accent-line); background: var(--accent-soft); }
/* 点过的那一条要留痕。跟 hover 不同 —— hover 走过就走，这个留着：
   作者切回正文改完两个字、再回面板，得一眼认出「我刚改的是这条」。
   用 border-color 整圈上色，被 .hi 的 border-left-color 覆盖左侧也正好（仍是危险色）。 */
.proof-item.on { border-color: var(--accent); background: var(--accent-soft); }
/* 只有「读者会明显察觉」的才上重色，不参与排序 —— 作者是顺着正文往下改的 */
.proof-item.hi { border-left-color: var(--danger); }
.proof-item .q {
  font-family: var(--serif); font-size: 13.5px; line-height: 1.6;
  word-break: break-word;
}
.proof-item .f {
  margin-top: 4px; font-family: var(--serif); font-size: 13.5px;
  line-height: 1.6; color: var(--accent); word-break: break-word;
}
.proof-item .f .ar { margin-right: 6px; color: var(--muted); font-family: var(--sans); font-size: 12px; }
.proof-item .n { margin-top: 3px; font-size: 11.5px; color: var(--muted); line-height: 1.5; }

/* 「对照」一行：这一条依据的是哪个人物卡 / 世界观词条 / 伏笔，或本章哪一句。
   它比 note 更要紧 —— 作者是先判断「这条依据成不成立」，才轮到看结论，
   所以放在说明之上、紧贴引文。淡底 + 竖线，是为了让它和不参与替换的引文区分开。
   只有情节 / 线索 / 伏笔三类会有这一行；前三类没有 `ref` 就不渲染。 */
.proof-item .ref {
  margin-top: 5px; padding: 3px 8px; font-size: 11.5px; line-height: 1.5;
  color: var(--text); background: var(--panel-2);
  border-left: 2px solid var(--accent-line); border-radius: 4px;
  word-break: break-word;
}
.proof-item .ref .lab {
  margin-right: 6px; color: var(--muted); font-family: var(--sans); font-size: 11px;
}

/* 「它不做什么」的说明：单独一行，不再和按钮抢同一行（名单拆成两个按钮后，
   320px 面板里三个东西挤一行会全部折行，反而更高）。 */
.proof-foot { margin-top: 8px; }
.proof-foot .small { min-width: 0; }

/* 黑名单 / 白名单两个入口：并排在「本章·选中片段」和「开始校对」之间。
   放这个位置是因为它们决定**查什么**（范围）和**跳过什么**（名单），
   都属于「开跑之前」的设置；扔进底部说明行里，作者根本不会去看。 */
.proof-lists { display: flex; gap: 6px; margin-top: 8px; }
.proof-lists .btn { flex: 1 1 0; min-width: 0; justify-content: center; }
.proof-lists .n {
  margin-left: 5px; color: var(--muted); font-size: 11px;
  font-variant-numeric: tabular-nums;
}

/* 批量替换条：**勾了才出现**（`display:none` 默认）。
   它固定在结果区上方，不随列表滚 —— 勾完最后一条不用滚回顶部。 */
.proof-batch {
  display: none; align-items: center; gap: 8px;
  margin-top: 8px; padding: 6px 8px;
  border: 1px solid var(--accent-line); border-radius: var(--radius);
  background: var(--accent-soft);
}
.proof-batch.show { display: flex; }
.proof-batch .spacer { flex: 1 1 0; }

/* 校对名单弹层：一个弹层只管一种名单，所以不再需要「先选类型」那一列 */
.proof-rules .prform { display: grid; gap: 6px; align-items: center; }
.proof-rules .prform.b { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto; }
.proof-rules .prform.w { grid-template-columns: minmax(0, 1fr) auto; }
.proof-rules .prblock { margin-top: 14px; }
.proof-rules .prtitle { font-size: 12px; font-weight: 600; color: var(--muted); margin-bottom: 4px; }
.proof-rules .prrow {
  display: flex; align-items: center; gap: 8px;
  padding: 5px 0; border-bottom: 1px solid var(--line); font-size: 13px;
}
.proof-rules .prrow .p { font-family: var(--serif); }
.proof-rules .prrow .r { color: var(--accent); font-family: var(--serif); min-width: 0; overflow-wrap: anywhere; }
.proof-rules .prrow .p { min-width: 0; overflow-wrap: anywhere; }
.proof-rules .prrow .ar { color: var(--muted); margin-right: 4px; font-family: var(--sans); }
.proof-rules .prrow > button { margin-left: auto; flex: none; }

@media print {
  .topbar, .side, .side-scrim, .aipanel, .bottomnav, .write .chapters { display: none !important; }
}

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}

