/* --- style.css --- */
/* ============================================================
   艾玛 · 全局样式 style.css
   扁平化设计体系：主题变量 / 卡片 / 悬浮 tab / 弹层组件
   ============================================================ */

/* ---------------- 主题变量 ---------------- */
:root {
  color-scheme: light;
  accent-color: var(--primary-text);

  /* ── 全局层级阶梯（**唯一出处**，别再散落字面量）──────────────────
     z-index 的**数值大小毫无意义**，只有**相对顺序**有意义。
     所以不要用 999 / 9999 这种「巨大数量级」：
       ① 看数字猜不出谁压谁 —— 9999 和 100 表达的是同一种关系；
       ② 它会**暗示「上面还能再插一层」** —— 后来的人随手写个更大的数
          （1000 → 1001 → 1500 → 2100 → 9999 全是这么来的），阶梯就碎了；
       ③ 撞车无处不在：1000 既是遮罩又是弹窗、1001 既是弹窗壳又是抽屉壳。
     这里固定成 **10 / 20 / … / 100** 的小阶梯（步长 10，留出插层空间），
     并且**每层一个 token**：插新层 = 改一个变量，不用全局搜数字。
     局部层级（只在父级 stacking context 内部比较的）一律用 1 / 2 / 3，
     永远小于全局最小值 10 —— 免得跟全局层级撞车
     （`.ms-card:hover` 原来写 10，正好跟顶栏打平；今天靠内容区的
       padding-top 恰好避开，一旦内容能贴到顶栏区域，便签就会盖住顶栏）。
     ⚠ 要加新层：**改 token**，不要写字面量。 */
  --z-topbar: 10;      /* 顶栏 */
  --z-popover: 20;     /* 铃铛浮层（今日提醒） */
  --z-tabbar: 30;      /* 底部悬浮 tab */
  --z-actionbar: 40;   /* 账本批量操作条（浮在 tab 之上） */
  --z-drawer-mask: 50; /* 抽屉遮罩 */
  --z-drawer: 60;      /* 聊天输入抽屉（对话页自己的浮层） */
  --z-fab: 70;         /* 生成中的「停止」胶囊 / 「回到底部」（页面级浮层） */
  /* ⚠ 2026-11-05（用户：「停止对话悬浮按钮的层级不应该在画像抽屉上方」）：
     通用抽屉（画像 / 记忆列表）必须**盖住**页面级浮层 ⇒ 单独一档，
     夹在 --z-fab 与 --z-modal 之间。
     ⚠ 聊天输入抽屉**不**提到这一档 —— 它是对话页自己的浮层，
       提到「停止」胶囊之上会让胶囊点不到（输出时得能按住停）。 */
  --z-drawer-panel: 75;/* 通用抽屉本体（画像 / 记忆列表） */
  --z-modal: 80;       /* 确认浮层 / 日程·备忘弹窗 */
  --z-image: 90;       /* 图片预览 */
  --z-toast: 100;      /* toast —— 永远最上层 */
  /* 横向滚动边缘提示的「边界色」—— 用来在渐隐起点画一道分界。
     极性必须跟内容的明暗相反，否则等于给文字加对比度、反而更显眼：
       浅色态内容是深字 → 用暗色边界（压暗）才看得出「到头了」
       夜间态内容是浅字 → 用亮色边界（提亮）才看得出
     所以夜间态这一行要反着来。 */
  --edge-hint: rgba(0, 0, 0, 0.34);
  /* 弹窗宽度（**唯一出处**）——
     2026-10-26 用户「文字排版优化，不要占满宽度，弹窗宽度也要缩小」：
     以前 .pk-confirm **没有** max-width ⇒ 长文案把它一路撑到整屏宽，
     一行字拉到天边（截图实测：清空对话那条几乎占满整个屏宽）。
     封在 82vw / 340px ⇒ 弹窗两边都留白，正文才有可读的行长。
     .pk-confirm（确认框）与 .pk-dialog（只读弹窗）共用这一个值。 */
  --pk-w: min(82vw, 340px);
  /* System safe area is counted once, outside the floating capsule. */

  --safe-b: max(env(safe-area-inset-bottom, 0px), var(--native-safe-b, 0px));
  --safe-t: max(env(safe-area-inset-top, 0px), var(--native-safe-t, 0px));
  --safe-l: max(env(safe-area-inset-left, 0px), var(--native-safe-l, 0px));
  --safe-r: max(env(safe-area-inset-right, 0px), var(--native-safe-r, 0px));
  /* Measured bottom edge of the topbar; maintained by app.js. */
  --topbar-h: 56px;

  /* 键盘占位：手机键盘盖住屏幕底部的高度，由 EMMA.ui.viewport 实时写入。
     固定在底部的输入条 / 抽屉、居中的弹窗都按它让位。 */
  --kb-inset: 0px;
  --bg-base: #f2f3f7;
  /* 页面底色 + 玻璃材质的 tint 源。
     页面底色只写这一处：纸系页面（待办/备忘/账本）用 body class 覆写
     --page-bg / --page-rgb / --veil-rgb 三件套，玻璃自动跟过去。
     口径见 docs/design/visual-unify-decisions.md §2.5 */
  --page-bg: #f2f3f7;
  --paper-bg: #faf7f0;
  --paper-rgb: 250 247 240;
  --page-rgb: 242 243 247;
  /* 玻璃材质的 tint 源（RGB 分量，方便用 rgb(... / 透明度) 写半透底色）。
     值与 --page-rgb 相同：顶栏玻璃条与底部渐变模糊带都吃它，
     纸系页面覆写这一档即可（口径见 §2.8 v8）。 */
  --veil-rgb: 242 243 247;

  --glass-opacity: .40;
  --glass-tint: rgb(var(--veil-rgb) / var(--glass-opacity));
  --glass-blur-radius: 28px;
  --glass-saturation: 195%;
  --glass-brightness: 1.03;
  /* 镜面高光：光从上方来 ⇒ 高光贴**上沿**；往内渐隐 ⇒ 与亮环同一个方向 */
  --glass-spec: linear-gradient(180deg, rgba(255, 255, 255, .26), rgba(255, 255, 255, 0) 20px);
  /* 细噪点：防渐变出色带，兼一点点「液体」颗粒（SVG feTurbulence，4% 不透明度） */
  --glass-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85 0.35' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23g)' opacity='0.04'/%3E%3C/svg%3E");

  --glass-ring: linear-gradient(150deg, rgba(255, 255, 255, .95), rgba(255, 255, 255, .10) 38%,
                                rgba(0, 0, 0, .09) 62%, rgba(255, 255, 255, .38));
  /* 厚度：只留**外投影**（悬空感），内侧高光交给 --glass-spec */
  --glass-depth: 0 12px 30px rgba(28, 42, 52, .22), 0 2px 6px rgba(28, 42, 52, .10);
  /* Dock capsule radius. */
  --glass-radius: 32px;
  /* Chrome geometry is adjustable without changing selector specificity. */
  --topbar-min-height: 56px;
  --topbar-inline-pad: 20px;
  --topbar-top-pad: 2px;
  --topbar-bottom-pad: 14px;
  --topbar-backdrop-extend: 28px;
  --dock-width: 308px;
  --dock-gutter: 12px;
  --dock-offset: 4px;
  --dock-ring-width: 1.4px;
  --dock-padding: 9px 10px;
  --dock-item-padding: 7px 4px;
  --dock-icon-size: 22px;
  --dock-color: var(--text-muted);
  --dock-hover-color: var(--text-primary);
  --console-radius: 13px;
  --console-border-width: 1px;
  --console-night-surface: #1b2024;
  --console-night-border: linear-gradient(125deg, #488b85 0%, #30444c 24%, #716384 52%, #283e46 76%, #538d92 100%);
  --console-night-shadow: 0 0 16px rgb(57 197 187 / .045);

  /* 卡片统一米白纸卡（= 账本 --ac-card #fffdf9），不是纯白 */
  --bg-card: rgba(255, 253, 249, 0.78);
  --bg-card-hover: rgba(255, 253, 249, 0.92);
  --bg-input: rgba(255, 253, 249, 0.86);
  --bg-modal: rgba(250, 247, 240, 0.88);
  --bg-card-solid: #fffdf9;
  /* 代码块底色：恒为深色。
     代码里的文字色（.md-content pre code 的 #e6edf3）与 highlight.js 的 github-dark
     配色都是按深底设计的，而浅色态的 --bg-card-solid 是 #ffffff ——
     两者一拼就变成「白底浅字」，实测对比度只有 1.18:1（AA 要 4.5），代码几乎看不见。
     仓库里只带了 github-dark 一套高亮主题，没有浅色版可换，所以代码块统一走深底。 */
  --code-bg: #0c0c0c;
  --bg-overlay: rgba(0, 0, 0, 0.15);
  /* 卡片实底：控制台卡片等需要「不透明底 + 正文色」的场景用它 */
  --card-bg: var(--bg-card);

  /* 墨色阶统一到暖墨（= 账本 --ac-ink / --ac-ink2），
     冷灰底上的中性黑跟纸底的暖褐墨不再各说各话。
     弱化色 alpha 不能低于 .66 —— .58 在米白卡上只有 4.0:1，12px 小字不够 AA */
  --text-primary: #23211c;
  --text-secondary: #55503f;
  --text-muted: rgba(35, 33, 28, 0.66);
  --text-placeholder: rgba(35, 33, 28, 0.64);

  /* 描边也收暖（= 账本 --ac-line / --ac-line-soft） */
  --border: rgba(70, 58, 38, 0.12);
  --border-light: rgba(70, 58, 38, 0.07);

  /* ── 初音绿色阶 ──
     三档各司其职，缺一不可：
       --primary      初音绿原色 #39c5bb，作**实心填充**（按钮、标签、气泡底）
       --primary-on   铺在 --primary 上的文字/图标色（深墨青）——
                      初音绿太亮，白字压上去只有 2.13:1，必须配深字
       --primary-text 当**字/图标色**用（在浅底上当文字要够深，4.67:1）
     拆开的原因见 docs/design/visual-unify-decisions.md §2.8 */
  --primary-rgb: 57 197 187;
  --primary: #39c5bb;
  --primary-on: #0d2b29;
  /* 当字色比原色深一档：#39c5bb 在冷灰底上只有 1.92:1，
     而且 --primary-light 的青色淡底比页面底更亮，
     压在淡底上只有 4.36:1 —— 这一档要同时满足「页面底 + 淡底」两种场景 */
  --primary-text: #116b64;
  --primary-light: rgba(57, 197, 187, 0.10);
  --primary-border: rgba(57, 197, 187, 0.32);

  /* 语义红统一走暖砖红（= 账本 --ac-expense 一族） */
  --danger: #a04a38;
  --danger-light: rgba(160, 74, 56, 0.10);
  --danger-border: rgba(160, 74, 56, 0.26);
  --danger-text: #a04a38;

  /* 语义琥珀 —— 「比常规重、但不是不可逆」的动作（2026-10-25 加）
     ⚠ 与 --danger 分开：危险红留给「不可逆 / 删数据」那批，
        琥珀留给「会改配置、但可退回」的操作（如「配置还原」）。 */
  --warning: #b7791f;
  --warning-light: rgba(183, 121, 31, 0.10);
  --warning-border: rgba(183, 121, 31, 0.30);
  --warning-text: #8a5a12;

  --shadow-sm: 0 1px 4px rgba(0, 0, 0, 0.06);
  --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.10);
}

/* ---------------- 夜间模式（跟随系统偏好，data-theme 控制） ---------------- */
html[data-theme="dark"] {
  color-scheme: dark;
  /* 与浅色态相反：夜间内容是浅字，边界要提亮才看得出 */
  --edge-hint: rgba(255, 255, 255, 0.26);
  /* 玻璃材质的 tint 源：夜间态跟 --page-bg 走 */
  --veil-rgb: 15 17 23;
  --page-bg: #0f1117;
  --paper-bg: #15150f;
  --paper-rgb: 21 21 15;
  --page-rgb: 15 17 23;
  /* 夜间保持遮罩可读性，减弱模糊、增色和高光，关闭深底上明显的颗粒。 */
  --glass-opacity: .50;
  --glass-blur-radius: 24px;
  --glass-saturation: 130%;
  --glass-brightness: 1;
  --glass-grain: none;
  --glass-spec: linear-gradient(180deg, rgba(255, 255, 255, .04), rgba(255, 255, 255, 0) 20px);
  /* 夜间档的「体」：亮环压得很淡（夜里的光很弱）、投影纯黑更实 */
  --glass-ring: linear-gradient(150deg, rgba(255, 255, 255, .45), rgba(255, 255, 255, .05) 38%,
                                rgba(0, 0, 0, .22) 62%, rgba(255, 255, 255, .16));
  --glass-depth: 0 12px 30px rgba(0, 0, 0, .50), 0 2px 6px rgba(0, 0, 0, .34);

  --bg-base: #0f1117;
  /* 卡片统一米白纸卡的夜间版（= 纸系两页的夜间卡 #1c1c16） */
  --bg-card: rgba(28, 28, 22, 0.72);
  --bg-card-hover: rgba(38, 38, 30, 0.90);
  --bg-input: rgba(28, 28, 22, 0.85);
  --bg-modal: rgba(21, 21, 15, 0.92);
  --bg-card-solid: #1c1c16;
  --bg-overlay: rgba(0, 0, 0, 0.5);
  --card-bg: #1c1c16;

  /* 墨色阶夜间：暖墨反相 */
  --text-primary: #f0ece0;
  --text-secondary: #c3bca9;
  --text-muted: rgba(240, 236, 224, 0.68);
  --text-placeholder: rgba(240, 236, 224, 0.60);

  --border: rgba(255, 255, 255, 0.11);
  --border-light: rgba(255, 255, 255, 0.07);

  /* 夜间：初音绿原色在深底上就够亮（8.95:1），当字色直接用原色；
     实心填充仍是原色，铺在上面的字用更深的墨青 */
  --primary-rgb: 57 197 187;
  --primary: #39c5bb;
  --primary-on: #05211f;
  --primary-text: #39c5bb;
  --primary-light: rgba(57, 197, 187, 0.16);
  --primary-border: rgba(57, 197, 187, 0.40);

  --danger: #e08c78;
  --danger-light: rgba(224, 140, 120, 0.15);
  --danger-border: rgba(224, 140, 120, 0.35);
  --danger-text: #f0a893;

  /* 夜间琥珀：字色要提亮才看得清（深底上的暖黄容易糊） */
  --warning: #e0b264;
  --warning-light: rgba(224, 178, 100, 0.14);
  --warning-border: rgba(224, 178, 100, 0.38);
  --warning-text: #e8bf76;

  --shadow-sm: 0 1px 4px rgba(0, 0, 0, 0.40);
  --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.55);
}

/* Resolve the tint at body scope so paper pages inherit their own veil RGB. */
body { --glass-tint: rgb(var(--veil-rgb) / var(--glass-opacity)); }

/* ---------------- 基础 ---------------- */
/* 滚动条统一隐藏（保留滚动功能，iOS 风格） */
* { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; scrollbar-width: none; -ms-overflow-style: none; }
html { min-height: 100%; overflow-y: auto; }
body {
  font-family: 'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'PingFang SC', 'HarmonyOS Sans', 'Microsoft YaHei', sans-serif;
  background: var(--bg-base);
  color: var(--text-primary);
  height: 100dvh;
  min-height: 100dvh;
  transition: color 0.35s ease;
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
}
html::-webkit-scrollbar, body::-webkit-scrollbar, *::-webkit-scrollbar { display: none; }
button, input, textarea { font-family: inherit; }
.icon { fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; width: 1em; height: 1em; }
a { color: inherit; text-decoration: none; }

/* ---------------- Topbar ---------------- */
/* Full-width glass fades inside the existing 56px box. Only the material is
   masked: text and hit targets remain crisp, with no extra band below it. */
.topbar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  width: 100%;
  margin: 0;
  z-index: var(--z-topbar);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: calc(var(--safe-t) + var(--topbar-top-pad)) var(--topbar-inline-pad) var(--topbar-bottom-pad);
  min-height: calc(var(--safe-t) + var(--topbar-min-height));
  border: 0;
  border-radius: 0;
  background: transparent;
}
.topbar::before {
  content: "";
  position: absolute;
  inset: 0 0 calc(-1 * var(--topbar-backdrop-extend));
  background: var(--glass-spec), var(--glass-grain), var(--glass-tint);
  background-size: auto, 140px 140px, auto;
  -webkit-backdrop-filter: var(--glass-filter, blur(var(--glass-blur-radius)) saturate(var(--glass-saturation)) brightness(var(--glass-brightness)));
  backdrop-filter: var(--glass-filter, blur(var(--glass-blur-radius)) saturate(var(--glass-saturation)) brightness(var(--glass-brightness)));
  /* Keep a soft fade below the topbar without washing over content borders. */
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 35%, #000c 56%, #0006 73%, #0001 90%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 0%, #000 35%, #000c 56%, #0006 73%, #0001 90%, transparent 100%);
  pointer-events: none;
  z-index: 0;
}
.topbar-brand, .topbar-actions { position: relative; z-index: 1; }
body.chat-drawer-open .topbar { z-index: calc(var(--z-drawer) + 1); }
.topbar-brand { display: flex; align-items: flex-end; gap: 10px; min-width: 0; flex: 1; }
/* 品牌头像已移除（2026-10-29，用户「不需要它了」）：
   顶栏只保留「艾玛 / Hermes」文字；favicon / PWA 图标用鸿蒙应用图标同一张画面（assets/brand/）。 */
/* 标题随模块切换，切换时滑出→换字→滑入；min-width 固定占位，避免徽章左右跳动 */
.topbar-title {
  font-size: 22px; font-weight: 800; letter-spacing: -0.03em; color: var(--text-primary);
  min-width: 68px; flex-shrink: 0;
  text-align: justify; text-align-last: justify;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  transition: opacity 120ms ease, transform 120ms ease;
  /* 点标题 = 刷新当前页（用户 2026-10-23：刷新入口从底部 tab 图标挪到顶栏标题，
     tab 图标只负责切页）。cursor 是唯一要加的可点击暗示 ——
     不额外加提示浮层，说明写在 title 属性里。 */
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.topbar-title.title-out { opacity: 0; transform: translateY(-6px); }
.topbar-title.title-in { animation: titleSlideIn 180ms ease forwards; }
/* 按下时的回弹反馈（只改 scale，不动颜色 —— 颜色对比度已按 AA 调过，不冒这个险） */
.topbar-title:active { transform: scale(.96); }
.topbar-title:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 3px;
  border-radius: 4px;
}
@keyframes titleSlideIn {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}
.topbar-sub {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.04em;
  padding: 2px 10px;
  border-radius: 999px;
  background: var(--primary-light);
  color: var(--primary-text);
  border: 0.5px solid var(--primary-border);
  flex-shrink: 0;
}
/* 夜间不再硬编码靛蓝：--primary-light / --primary-border / --primary-text
   在 html[data-theme="dark"] 里已经换成墨绿夜间版，这条覆盖已不需要。 */
.topbar-actions { flex-shrink: 0; display: flex; align-items: center; gap: 8px; }
.more-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border-radius: 999px;
  background: var(--bg-card);
  border: 0.5px solid var(--border);
  color: var(--text-secondary);
  cursor: pointer;
  transition: background 0.25s ease, color 0.25s ease, transform 0.15s ease;
}
.more-btn svg { width: 16px; height: 16px; }
.more-btn:hover { background: var(--bg-card-hover); color: var(--text-primary); }
.more-btn:active { transform: scale(0.94); }

/* ---------------- 今日提醒（顶栏铃铛）----------------
   顶栏右侧的全局入口：铃铛 + 角标 + 小浮层。
   口径见 docs/design/topbar-notice-decisions.md

   角标用「实心底 + 配套字色」两档 —— 跟全站语义色的分档口径一致：
     浅色 逾期 = #a04a38 底 + 白字（5.94:1）
     夜间 逾期 = #e08c78 底 + 深字（约 6.5:1）
   夜间那支浅砖红是给深底当**文字色**的，拿来当白字底只有 2.56:1 —— 老坑。 */
.nt-btn { position: relative; }
.nt-badge {
  position: absolute;
  top: -3px; right: -3px;
  min-width: 16px; height: 16px;
  padding: 0 4px;
  border-radius: 999px;
  background: var(--primary);
  color: var(--primary-on);
  font-size: 10px;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
  font-variant-numeric: tabular-nums;
  /* 描一圈页面底色：角标压在按钮上，不隔开会和按钮糊在一起 */
  box-shadow: 0 0 0 2px var(--page-bg);
  pointer-events: none;
}
.nt-badge.is-overdue { background: #a04a38; color: #fff; }
html[data-theme="dark"] .nt-badge.is-overdue { background: #e08c78; color: #241511; }

/* 小浮层：贴在铃铛下方。层级走全局阶梯：高于顶栏、低于抽屉遮罩 */
.nt-pop {
  position: fixed;
  z-index: var(--z-popover);
  width: 288px;
  background: var(--bg-card-solid);
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  animation: nt-pop-in .16s ease both;
}
@keyframes nt-pop-in {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}
.nt-pop-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding: 11px 13px 9px;
  border-bottom: 1px solid var(--border-light);
}
.nt-pop-title { font-size: 13px; font-weight: 700; color: var(--text-primary); }
.nt-pop-count { font-size: 11px; color: var(--text-muted); font-variant-numeric: tabular-nums; }

/* 「你还记得吗：…」—— 艾玛的 nudge，用主色底与顶栏 Hermes 胶囊同一套配色
   （--primary-light 底 + --primary-text 字，是全站既有的搭配，不是新配色） */
.nt-memo {
  display: block;
  width: 100%;
  padding: 9px 13px;
  border: 0;
  border-bottom: 1px solid var(--border-light);
  background: var(--primary-light);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}
.nt-memo-label {
  display: block;
  font-size: 12.5px;
  font-weight: 700;
  color: var(--primary-text);
  margin-bottom: 1px;
}
.nt-memo-text {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: 12.5px;
  color: var(--primary-text);
  /* 首行缩进 2 个字（用户 2026-10-28：「你还记得吗？」单独一行，
     正文行换，首字缩进 2 个字） */
  text-indent: 2em;
}

/* 待办三档：逾期 / 今天 / 7 天内。整块可滚，浮层高度才有上限 */
.nt-pop-body {
  max-height: 46vh;
  overflow-y: auto;
  border-top: 1px solid var(--border-light);
}
.nt-group-title {
  padding: 10px 13px 4px;
  font-size: 11px;
  color: var(--text-muted);
  letter-spacing: .04em;
}
/* 第一档贴着 body 顶（body 已有 border-top），其余各档之间加一条线分隔 */
.nt-group-title:not(:first-child) { border-top: 1px solid var(--border-light); }
.nt-item {
  width: 100%;
  display: flex;
  align-items: flex-start;
  gap: 9px;
  padding: 10px 13px;
  border: 0;
  border-bottom: 1px solid var(--border-light);
  background: transparent;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  transition: background .15s;
}
.nt-item:last-child { border-bottom: none; }
.nt-item:active { background: var(--bg-card-hover); }
.nt-item-dot {
  width: 6px; height: 6px; border-radius: 50%;
  margin-top: 6px; flex: none;
  background: var(--primary);
}
.nt-item.is-overdue .nt-item-dot { background: var(--danger); }
.nt-item-main { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.nt-item-title { font-size: 13px; color: var(--text-primary); overflow-wrap: anywhere; }
.nt-item-sub { font-size: 11px; color: var(--text-muted); }
.nt-item.is-overdue .nt-item-sub { color: var(--danger-text); }
.nt-pop-empty { padding: 22px 13px; text-align: center; font-size: 12.5px; color: var(--text-muted); }
.nt-pop-more {
  padding: 7px 13px;
  font-size: 11px;
  color: var(--text-muted);
  border-top: 1px solid var(--border-light);
}

/* 底部系统监测状态行：一行看完，点它跳控制台
   ⚠ 探不通时**不显示延迟** —— latency_ms 在失败时是「等超时花了多久」，不是服务延迟 */
.nt-status {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  padding: 9px 13px;
  border: 0;
  border-top: 1px solid var(--border-light);
  background: transparent;
  font-family: inherit;
  font-size: 11px;
  color: var(--text-muted);
  text-align: left;
  cursor: pointer;
  font-variant-numeric: tabular-nums;
}
.nt-status:hover, .nt-status:active { background: var(--bg-card-hover); }
.nt-status-left { color: var(--text-secondary); }
.nt-status-right { flex: none; }

/* ---------------- 页面骨架 ---------------- */
.page-wrap {
  max-width: 720px;
  margin: 0 auto;
  height: 100dvh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  /* --topbar-h 由 JS 同步为「**顶栏底沿**到视口顶的距离」（含安全区），
     内容紧贴顶栏，不目测留白 */
  padding-top: var(--topbar-h, calc(env(safe-area-inset-top) + 56px));
}
.page-body {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  /* 显式高度：部分 Android WebView 对 flex:1 的扩展计算不可靠 */
  height: calc(100dvh - var(--topbar-h, calc(env(safe-area-inset-top) + 56px)));
}
.page-enter { animation: pageIn 0.32s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; }
@keyframes pageIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }

/* ---------------- Dock ---------------- */
/* The dock shares the topbar material. The refraction ring stays inside the
   capsule; no rectangular gradient is painted above its rounded boundary. */
.tab-bar {
  position: fixed;
  bottom: calc(var(--safe-b) + var(--dock-offset));
  left: 0;
  right: 0;
  margin: 0 auto;
  width: min(calc(100vw - var(--dock-gutter) * 2), var(--dock-width));
  padding: 0;
  border-radius: var(--glass-radius);
  z-index: var(--z-tabbar);
  box-shadow: var(--glass-depth);
  background: var(--glass-spec), var(--glass-grain), var(--glass-tint);
  background-size: auto, 140px 140px, auto;
  -webkit-backdrop-filter: var(--glass-filter, blur(var(--glass-blur-radius)) saturate(var(--glass-saturation)) brightness(var(--glass-brightness)));
  backdrop-filter: var(--glass-filter, blur(var(--glass-blur-radius)) saturate(var(--glass-saturation)) brightness(var(--glass-brightness)));
  transition: opacity .2s ease, transform .2s ease;
}
.tab-bar::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: var(--dock-ring-width);
  background: var(--glass-ring);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
  z-index: 0;
}
.tab-glass {
  position: relative;
  z-index: 1;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 2px;
  padding: var(--dock-padding);
}
/* 抽屉打开时底部 tab 整体淡出：输入条接管底部，页面上不再同时存在两条底栏。
 *
 * 用 visibility:hidden 而不是 display:none —— 后者会让 syncNavSpace()
 * 量不到 tab 高度，聊天列表的下边距（--nav-space）就空了。
 * visibility 的切换延后到淡出结束之后（linear 0.2s），淡出过程中仍可被键盘聚焦。
 *
 * ⚠ 2026-10-31：胶囊化后**没有** translateX(-50%)（居中走的是 left/right + margin:auto），
 *   收起动画只保留 translateY(14px)（整条胶囊往下一沉）。
 */
body.drawer-open .tab-bar {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateY(14px);
  transition: opacity 0.2s ease, transform 0.2s ease, visibility 0s linear 0.2s;
}
.tab-item {
  flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; align-items: center;
  gap: 4px; padding: var(--dock-item-padding);
  border-radius: 18px; cursor: pointer;
  transition: color 0.25s ease;
  /* 默认：跟随主题色，半透明 */
  color: var(--dock-color);
  position: relative; overflow: hidden;
  border: none; background: transparent;
  z-index: 3;   /* 局部层级：压在玻璃装饰层(2)之上 */
}
.tab-item:not(.active):hover { color: var(--dock-hover-color); }
.tab-item.active { color: var(--primary-text); font-weight: 700; }
@keyframes tab-active-breathe {
  0%, 100% { filter: drop-shadow(0 0 2px var(--primary)); }
  50% { filter: drop-shadow(0 0 8px var(--primary)); }
}
.tab-item.active .icon { animation: tab-active-breathe 2.4s ease-in-out infinite; }
.tab-item .label { font-size: 10px; font-weight: 500; letter-spacing: .02em; }
.tab-item .icon { width: var(--dock-icon-size); height: var(--dock-icon-size); opacity: inherit; }
.tab-item .icon svg { width: 100%; height: 100%; }

/* ---------------- 遮罩 / 弹窗 / 确认框 ---------------- */
.overlay {
  position: fixed; inset: 0;
  background: var(--bg-overlay);
  -webkit-backdrop-filter: blur(18px) saturate(160%);
  backdrop-filter: blur(18px) saturate(160%);
  z-index: var(--z-drawer-mask);
  opacity: 0; visibility: hidden;
  transition: opacity 0.25s ease, visibility 0.25s;
}
.overlay.active { opacity: 1; visibility: visible; }

/* 移动端输入法弹起时，输入弹窗上移避让键盘；桌面端无键盘不触发 */

/* ---------------- 底部抽屉（可复用：高度可自定义、表单样式统一、遮罩模糊） ---------------- */
.drawer-body {
  flex: 1;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
/* 抽屉内统一表单控件 */
.drawer-panel input, .drawer-panel textarea, .drawer-panel select {
  width: 100%;
  background: var(--bg-input);
  border: 0.5px solid var(--border);
  border-radius: 10px;
  padding: 10px 12px;
  font-size: 14px;
  color: var(--text-primary);
  box-sizing: border-box;
  outline: none;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.drawer-panel input:focus, .drawer-panel textarea:focus, .drawer-panel select:focus {
  border-color: var(--primary-border);
  box-shadow: 0 0 0 3px var(--primary-light);
}
.drawer-panel textarea { resize: vertical; min-height: 120px; line-height: 1.6; font-family: inherit; }
/* Markdown 渲染（知识库/聊天共用）· GitHub 文档风：
   浅色代码块、全边框表格、粉红内联代码、蓝色链接、绿色引用、标题分隔线 */
.md-content {
  font-size: 13px;
  line-height: 1.7;
  color: var(--text-secondary);
}
.md-content h1,.md-content h2,.md-content h3,.md-content h4,.md-content h5,.md-content h6 { color: var(--text-primary); }
.md-content h1 { font-size: 20px; font-weight: 700; margin: 0 0 12px; padding-bottom: 8px; border-bottom: 1px solid var(--border); }
.md-content h2 { font-size: 17px; font-weight: 700; margin: 18px 0 10px; padding-bottom: 4px; border-bottom: 1px solid var(--border); }
.md-content h3 { font-size: 15px; font-weight: 600; margin: 14px 0 8px; }
.md-content h4 { font-size: 14px; font-weight: 600; margin: 12px 0 6px; }
.md-content h5,.md-content h6 { font-size: 13px; font-weight: 600; margin: 10px 0 4px; }
.md-content p { margin: 0; }
.md-content strong { font-weight: 700; color: var(--text-primary); }
.md-content em { font-style: italic; }
.md-content ul,.md-content ol { padding-left: 24px; margin: 0 0 10px; }
.md-content li { margin: 2px 0; }
.md-content li > p { margin: 0; }
/* 引用块 / 表格条纹的内凹底：跟随所在容器做一层深浅。
   不能用 --bg-base —— 那是「页面底色」，放进聊天气泡里会形成突兀的色块断层
   （浅色几乎看不出，夜间模式下是深色气泡里一块更深的黑）。 */
.md-content blockquote {
  border-left: 4px solid #39d5b8;
  padding: 6px 14px;
  color: var(--text-muted);
  margin: 10px 0;
  background: var(--bubble-inset, var(--bg-base));
  border-radius: 0 6px 6px 0;
}
.md-content blockquote p:last-child { margin-bottom: 0; }
/* 链接色：浅色用深蓝（气泡白底 5.72:1 / 页面底 5.16:1）；
   夜间必须另取亮蓝 —— 同一个 #0b64c8 落在夜间气泡底 #2a2a38 上只有 2.47:1，看不清 */
.md-content a { color: #0b64c8; text-decoration: none; border-bottom: 1px solid rgba(11, 100, 200, 0.30); }
.md-content a:hover { border-bottom-color: #0b64c8; }
html[data-theme="dark"] .md-content a { color: #7fb3ff; border-bottom-color: rgba(127, 179, 255, 0.30); }
html[data-theme="dark"] .md-content a:hover { border-bottom-color: #7fb3ff; }
.md-content hr { border: none; border-top: 1px solid var(--border); margin: 16px 0; }
/* 表格：外层 .md-table-wrap 负责横向滚动，表格本身回到正常表格布局。
   单元格不折行 —— 数字被拦腰折断（"128.5" / "0"）比表格横向滚动难看得多。
   于是：窄表撑满气泡宽度，宽表超出时在 wrapper 内横向滑动（滑动查看），
   与 .ac-chips 同一套约定（滚动条隐藏，靠滑动）。 */
.md-table-wrap {
  overflow-x: auto;
  margin: 10px 0;
  /* 超长表格显示横向滚动条（覆盖全局隐藏滚动条规则） */
  scrollbar-width: thin;
  scrollbar-color: rgba(110, 118, 129, 0.4) transparent;
}
/* 右侧渐隐提示：内容被裁断的一侧用气泡底色淡出，
   一眼能看出「右边还有」，而不是以为渲染坏了。
   两层叠加：
     上层 = 底色渐隐（内容溶进底色）—— 纯白/纯底色单靠它太弱，看不出边界
     下层 = 边缘暗影/亮影（var(--edge-hint)）—— 在渐隐起点画一道分界
   夜间态两者极性相反，见 :root / html[data-theme="dark"] 里的 --edge-hint。
   .has-more-right / .has-more-left 由 js/core/ui.js 的 bindScrollHint 挂上
   （滚到两端会自动摘掉）。 */
.md-table-box {
  position: relative;
}
.md-table-box::after {
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: 44px;
  pointer-events: none;
  opacity: 0;
  transition: opacity .2s ease;
  background:
    linear-gradient(to left, var(--bubble-fill, var(--bubble-bg, #ffffff)) 0%, var(--bubble-fill, var(--bubble-bg, #ffffff)) 20%, transparent 100%),
    linear-gradient(to left, var(--edge-hint, rgba(0,0,0,.34)) 0%, transparent 48%);
}
.md-table-box.has-more-right::after { opacity: 1; }
/* 左侧渐隐：往回滑时左端被裁掉的那截同样要有提示 */
.md-table-box::before {
  content: '';
  position: absolute;
  top: 0; left: 0; bottom: 0;
  width: 44px;
  pointer-events: none;
  opacity: 0;
  transition: opacity .2s ease;
  background:
    linear-gradient(to right, var(--bubble-fill, var(--bubble-bg, #ffffff)) 0%, var(--bubble-fill, var(--bubble-bg, #ffffff)) 20%, transparent 100%),
    linear-gradient(to right, var(--edge-hint, rgba(0,0,0,.34)) 0%, transparent 48%);
}
.md-table-box.has-more-left::before { opacity: 1; }
.md-content table {
  border-collapse: collapse;
  width: 100%;
  font-size: 12px;
}
.md-content th,.md-content td { border: 1px solid var(--border); padding: 6px 12px; text-align: left; white-space: nowrap; }
.md-content th { background: var(--bubble-inset, var(--bg-base)); font-weight: 600; }
.md-content tr:nth-child(even) { background: var(--bubble-inset, var(--bg-base)); }
.md-content code {
  background: rgba(175, 184, 193, 0.20);
  padding: 2px 6px;
  border-radius: 4px;
  font-size: 12px;
  font-family: monospace;
  color: #c2185b;
}
/* 代码块：cmd 命令窗口样式（深色底 + 标题栏 + 语言类型），保留 hljs 多彩语法高亮 */
.md-content .code-block {
  margin: 10px 0;
  border-radius: 10px;
  background: var(--code-bg, #0c0c0c);
  border: 0.5px solid rgba(255, 255, 255, 0.10);
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.28);
  overflow: hidden;
}
html[data-theme="dark"] .md-content .code-block {
  border-color: rgba(255,255,255,0.10);
  box-shadow: none;
}
.md-content .code-block-header {
  display: flex;
  align-items: center;
  height: 32px;
  padding: 0 12px;
  background: linear-gradient(180deg, #262626, #1a1a1a);
  border-bottom: 0.5px solid rgba(255, 255, 255, 0.08);
}
/* cmd 标题栏三键 */
.md-content .code-block-header::before {
  content: "";
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: #ff5f57;
  box-shadow: 15px 0 0 #febc2e, 30px 0 0 #28c840;
  flex-shrink: 0;
}
.md-content .code-block-lang {
  margin-left: 32px;
  font-size: 11px;
  font-weight: 600;
  color: rgba(255, 255, 255, 0.60);
  font-family: 'Consolas', 'Courier New', monospace;
  letter-spacing: 0.5px;
  text-transform: uppercase;
}
.md-content pre {
  background: #0c0c0c;
  padding: 14px 16px;
  border-radius: 0;
  overflow-x: auto;
  font-size: 12px;
  margin: 0;
  line-height: 1.55;
  /* 超长 pre 显示横向滚动条（覆盖全局隐藏滚动条规则） */
  scrollbar-width: thin;
  scrollbar-color: rgba(110, 118, 129, 0.4) transparent;
}
.md-content .code-block pre { background: transparent; }
.md-content pre code { background: transparent; padding: 0; color: #e6edf3; font-size: 12px; }
/* 兜底渲染（markdown-it 没加载时）：当纯文本显示，不套代码块那身深色底。
   否则整条回复会变成一整块近黑色 + 横向滚动条 —— 正是这次修的那个症状。
   white-space: pre-wrap 保留原始换行，但长行照样折行，不产生横向滚动。 */
.md-content pre.md-plain {
  background: var(--bubble-inset, var(--bg-base));
  color: inherit;
  white-space: pre-wrap;
  word-break: break-word;
  overflow-wrap: anywhere;
  overflow-x: auto;
}
.md-content pre.md-plain code { background: transparent; padding: 0; color: inherit; font-size: 13px; }
.md-content img { max-width: 100%; height: auto; border-radius: 6px; margin: 8px 0; }
/* 滚动条样式：pre 与表格滚动容器共用一套。
   注意 Chromium 121+ 起，元素上只要写了 scrollbar-width（哪怕是 thin），
   ::-webkit-scrollbar 伪元素就整组失效 —— 滚动条由标准属性接管，
   表现为「滚动/悬停时闪现一条细条」，不常驻。所以这套 webkit 规则
   主要照顾 iOS Safari 与旧内核，桌面 Chromium 走上面的 scrollbar-width。 */
.md-content pre::-webkit-scrollbar,
.md-table-wrap::-webkit-scrollbar { height: 8px; display: block; }
.md-content pre::-webkit-scrollbar-track,
.md-table-wrap::-webkit-scrollbar-track { background: transparent; }
.md-content pre::-webkit-scrollbar-thumb,
.md-table-wrap::-webkit-scrollbar-thumb { background: rgba(110, 118, 129, 0.4); border-radius: 4px; }
.md-content pre::-webkit-scrollbar-thumb:hover,
.md-table-wrap::-webkit-scrollbar-thumb:hover { background: rgba(110, 118, 129, 0.6); }
html[data-theme="dark"] .md-content pre::-webkit-scrollbar-thumb,
html[data-theme="dark"] .md-table-wrap::-webkit-scrollbar-thumb { background: rgba(139, 148, 158, 0.4); }
/* 夜间模式：inline code 配色（代码块已统一为 cmd 深色，无需覆盖） */
html[data-theme="dark"] .md-content code { background: rgba(255, 255, 255, 0.12); color: #f0a8c9; }
.drawer-wrap {
  /* ⚠ 2026-11-05：通用抽屉（画像 / 记忆列表）必须盖住「停止」胶囊 ——
     胶囊是**页面级**浮层（--z-fab），抽屉是**全局**浮层 ⇒ 抽屉更高一档。 */
  position: fixed; inset: 0; z-index: var(--z-drawer-panel);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.25s ease;
}
.drawer-wrap.active { opacity: 1; pointer-events: auto; }
.drawer-panel {
  position: fixed; bottom: var(--kb-inset, 0px); left: 0; right: 0;
  /* 键盘占位也要从高度里扣掉：否则抽屉按整屏高算，
     底部的输入框（金额 / 用途）会落在键盘底下点不到 */
  max-height: calc(100dvh - var(--topbar-h, 56px) - var(--kb-inset, 0px));
  height: auto;
  background: var(--bg-card-solid);
  color: var(--text-primary);
  border-radius: 20px 20px 0 0;
  padding: 12px 20px env(safe-area-inset-bottom);
  display: flex;
  flex-direction: column;
  transform: translateY(calc(100% + env(safe-area-inset-bottom)));
  pointer-events: auto;
}
.drawer-panel.active {
  transform: translateY(0);
  transition: transform 0.35s cubic-bezier(0.32, 0.72, 0, 1);
  pointer-events: auto;
}
/* ⚠ 2026-10-31（键盘起来时不再重复预留安全区）：
 *   键盘弹起后抽屉被 --kb-inset 顶到键盘上沿 —— 键盘区里已经包含了
 *   Home Indicator，面板自己的 padding-bottom 再留一份 env(safe-area-inset-bottom)
 *   就是**重复预留** ⇒ 面板凭空高出 32px，内容区被挤矮一截。
 *   ⇒ 键盘在的时候（html.kb-up）只留 12px 基础内边距；
 *     键盘不在时安全区必须留着（否则底部按钮压在系统导航条底下）。
 *   与 .mini-drawer 的同一条口径（见 css/pages.css 的 html.kb-up 规则）。 */
html.kb-up .drawer-panel {
  padding-bottom: 12px;
}
.drawer-handle {
  width: 40px; height: 4px;
  background: var(--drawer-accent, var(--border));
  border-radius: 2px; margin: 0 auto 10px;
  transition: background 0.25s ease;
}
/* 语义配色：panel 上的 drawer-color-* 只负责注入 --drawer-accent，
   具体染色点集中在 handle / 图标 / 强调文字，业务模块换色只需换 class。
   底色需与白色图标 ≥4.5:1（handle 4px 细条按图形组件 ≥3:1）。 */
/* 抽屉的收支主题色：与账本页同一套语义色（收入松绿 / 支出砖红），
   不再各用一支冷绿冷红 —— 全站语义色只此一套 */
.drawer-panel.drawer-color-in { --drawer-accent: #3f7a55; }    /* 收入 · 松绿 */
.drawer-panel.drawer-color-out { --drawer-accent: #a04a38; }   /* 支出 · 砖红 */
.drawer-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 15px;
}
.drawer-header.is-compact {
  margin-bottom: 8px;
  justify-content: flex-end;
}
/* 头部：图标 +（标题 / 副标题）+ 操作按钮组 */
.drawer-icon {
  width: 40px; height: 40px; border-radius: 12px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 20px; line-height: 1;
  background: var(--primary-light); color: var(--primary-text);
}
.drawer-icon svg { width: 22px; height: 22px; }
.drawer-panel[class*="drawer-color-"] .drawer-icon {
  background: var(--drawer-accent); color: #fff;
}
.drawer-heading { flex: 1; min-width: 0; }
.drawer-title { font-size: 18px; font-weight: 700; color: var(--text-primary); min-width: 0; white-space: nowrap; overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch; scrollbar-width: none; }
.drawer-title::-webkit-scrollbar { display: none; }
.drawer-sub { font-size: 13px; color: var(--text-muted); margin-top: 2px; }
.drawer-actions {
  display: flex;
  flex-shrink: 0;
  gap: 8px;
  /* 不加 align-items 时默认 stretch：矮按钮会被同组高按钮拉高，
     导致同一个抽屉在新增/编辑两态下按钮高度不一致。统一居中，尺寸交给按钮自身。 */
  align-items: center;
}
.drawer-action-btn {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  padding: 8px 15px;
  border-radius: 6px;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  border: 0.5px solid var(--border);
  background: var(--bg-card);
  color: var(--text-secondary);
  box-shadow: var(--shadow-sm);
  transition: all 0.2s ease;
  -webkit-tap-highlight-color: transparent;
}
.drawer-action-btn:hover { background: var(--bg-card-hover); color: var(--text-primary); transform: translateY(-1px); }
.drawer-action-btn:active { transform: scale(0.96); }
/* 纯图标按钮（不带文字标签）：正方形 + 图标居中 */
.drawer-action-btn.is-icon {
  width: 34px;
  height: 34px;
  padding: 0;
  gap: 0;
}
.drawer-action-btn.is-icon .icon { width: 16px; height: 16px; }
.drawer-action-btn.collapse {
  background: var(--primary-light);
  border-color: var(--primary-border);
  color: var(--primary-text);
}
.drawer-action-btn.collapse:hover { background: var(--primary); border-color: var(--primary); color: var(--primary-on); }
.drawer-action-btn.danger {
  background: var(--danger-light);
  border-color: var(--danger-border);
  color: var(--danger-text);
}
.drawer-action-btn.danger:hover { background: var(--danger); border-color: var(--danger); color: #fff; }
.drawer-action-btn:disabled { opacity: 0.5; cursor: not-allowed; pointer-events: none; box-shadow: none; }
.drawer-item {
  display: flex; align-items: center; gap: 14px;
  padding: 14px 0; border-bottom: 0.5px solid var(--border-light);
  cursor: pointer; transition: all 0.2s ease;
}
.drawer-item:hover { background: var(--primary-light); margin: 0 -28px; padding: 14px 28px; border-radius: 12px; }
.drawer-item-icon {
  width: 38px; height: 38px; border-radius: 10px;
  background: var(--primary-light); display: flex; align-items: center; justify-content: center;
  font-size: 18px;
}
/* 抽屉内统一表单骨架（对齐 gyx 的 form-section / form-card / form-row）：
   业务模块只负责组装内容，间距与描边只在样式表里维护 */
.form-section { margin-bottom: 14px; }
.form-card {
  background: var(--bg-card);
  border: 0.5px solid var(--border);
  border-radius: 14px;
  padding: 2px 14px;
}
.form-row {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 0;
  border-bottom: 0.5px solid var(--border-light);
}
.form-row:last-child { border-bottom: 0; }
.form-row > label {
  flex-shrink: 0; width: 68px;
  font-size: 14px; font-weight: 500; color: var(--text-secondary);
}

/* ---------------- Toast 轻提示 ---------------- */
.toast-container {
  position: fixed; z-index: var(--z-toast);
  pointer-events: none;

  bottom: calc(var(--nav-space, 76px) + 6px);
  left: 50%; transform: translateX(-50%);
}
.toast {
  display: inline-block;
  padding: 8px 18px;
  border-radius: 16px;
  font-size: 13px; font-weight: 500; line-height: 1.5;
  /* 轻提示是「一句话」的位置（用户 2026-10-25：不能溢出屏幕）：
     宽度封顶 + 允许换行 + 居中。原先是 nowrap —— 一句长点的话就顶出屏幕了。
     JS 那边还会把超长的正文截断到 30 字，大篇幅内容走只读弹窗。 */
  max-width: min(86vw, 420px);
  white-space: normal;
  overflow-wrap: anywhere;
  text-align: center;
  opacity: 0; transform: translateY(8px) scale(0.92);
  -webkit-backdrop-filter: blur(14px) saturate(1.5);
  backdrop-filter: blur(14px) saturate(1.5);
}
.toast.show { animation: capsuleIn 0.28s cubic-bezier(0.34, 1.40, 0.64, 1) forwards; }
.toast.hide { animation: capsuleOut 0.22s ease forwards; }
@keyframes capsuleIn { to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes capsuleOut { to { opacity: 0; transform: translateY(6px) scale(0.94); } }
.toast-success { background: var(--primary); color: var(--primary-on); border: 0.5px solid var(--primary); box-shadow: 0 4px 16px rgba(57, 197, 187, 0.30); }
.toast-error   { background: #a04a38; color: #fff; border: 0.5px solid #a04a38; box-shadow: 0 4px 16px rgba(160, 74, 56, 0.25); }
.toast-warning { background: #b45309; color: #fff; border: 0.5px solid #b45309; box-shadow: 0 4px 16px rgba(180, 83, 9, 0.25); }
.toast-info    { background: var(--primary); color: var(--primary-on); border: 0.5px solid var(--primary); box-shadow: 0 4px 16px rgba(57, 197, 187, 0.30); }
.toast-default { background: rgba(255, 253, 249, 0.72); color: var(--text-primary); border: 0.5px solid var(--border); box-shadow: 0 4px 16px rgba(0,0,0,0.12); }

/* ---------------- 通用图片预览（全屏毛玻璃遮罩） ----------------
 *
 * 结构：全屏遮罩（backdrop-filter 模糊 + 半透明底）+ 居中大图 + 右上角关闭 icon。
 * 遮罩走全局阶梯 `--z-image`：压在聊天抽屉和停止胶囊之上，
 * 低于 toast —— 提示永远在最上层。
 *
 * 关闭 icon **低透明度**（.5）：它是「退出」不是「主操作」，
 * 压在图上时不该比图还抢眼，hover 才变实。
 */
.img-preview {
  position: fixed;
  inset: 0;
  z-index: var(--z-image);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: rgba(18, 20, 26, 0.42);
  -webkit-backdrop-filter: blur(18px) saturate(1.08);
  backdrop-filter: blur(18px) saturate(1.08);
  opacity: 0;
  transition: opacity 0.2s ease;
}
.img-preview.open { opacity: 1; }
.img-preview-img {
  max-width: 92vw;
  max-height: 80vh;
  border-radius: 16px;
  background: var(--bg-card);
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45);
  transform: scale(0.97);
  transition: transform 0.22s cubic-bezier(0.2, 0.7, 0.3, 1);
}
.img-preview.open .img-preview-img { transform: scale(1); }
.img-preview-close {
  position: absolute;
  top: max(14px, env(safe-area-inset-top));
  right: 14px;
  width: 38px;
  height: 38px;
  border: 0;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.16);
  color: #fff;
  opacity: 0.5;
  cursor: pointer;
  transition: opacity 0.18s ease, background 0.18s ease;
}
.img-preview-close:hover { opacity: 1; background: rgba(255, 255, 255, 0.26); }
.img-preview-close .icon { width: 18px; height: 18px; }

/* ---------------- Loading ---------------- */
@keyframes spin { to { transform: rotate(360deg); } }

/* ---------------- 通用工具类（契约 §2：内联样式 → CSS 类） ---------------- */
.is-hidden { display: none !important; }
.drawer-item-text { flex: 1; }
.drawer-item-title { font-size: 15px; }
.drawer-item-desc { font-size: 12px; color: var(--text-muted); margin-top: 3px; }
/* 聊天错误文案：深浅底各取一档，浅底用深红、深底用浅红 */
.chat-error-text { color: var(--danger-text); }

/* ---------------- Responsive chrome ---------------- */
@media (max-width: 480px) {
  :root {
    --dock-icon-size: 24px;
    --dock-item-padding: 8px 4px;
    --dock-padding: 5px 8px;
  }
  .page-wrap { padding: var(--topbar-h, calc(env(safe-area-inset-top) + 56px)) 0 0; }
  .tab-item .label { display: none; }
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .topbar::before, .tab-bar { background: rgb(var(--veil-rgb) / .94); }
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0s !important;
    scroll-behavior: auto !important;
  }
}
@media (forced-colors: active) {
  .tab-bar { border: 1px solid CanvasText; }
  .topbar::before { background: Canvas; }
}

/* Full-width bottom glass fades into the page, including the home-indicator area. */
.dock-backdrop {
  position: fixed; inset: auto 0 0; height: calc(var(--nav-space, 88px) + 42px);
  z-index: calc(var(--z-tabbar) - 1); pointer-events: none;
  /* Ambient layer stays smooth; the capsule owns the surface highlights. */
  background: var(--glass-tint);
  -webkit-backdrop-filter: var(--glass-filter, blur(var(--glass-blur-radius)) saturate(var(--glass-saturation)) brightness(var(--glass-brightness)));
  backdrop-filter: var(--glass-filter, blur(var(--glass-blur-radius)) saturate(var(--glass-saturation)) brightness(var(--glass-brightness)));
  -webkit-mask-image: linear-gradient(to top, #000 0%, #000 20%, #000c 56%, #0006 73%, #0001 90%, transparent 100%);
  mask-image: linear-gradient(to top, #000 0%, #000 20%, #000c 56%, #0006 73%, #0001 90%, transparent 100%);
  transition: opacity .2s ease;
}
body.drawer-open .dock-backdrop, body.emma-opening .dock-backdrop, html.kb-up .dock-backdrop { opacity: 0; }
.topbar {
  padding-left: max(var(--topbar-inline-pad), var(--safe-l, 0px));
  padding-right: max(var(--topbar-inline-pad), var(--safe-r, 0px));
}
.emma-proposal {
  display: grid;
  gap: .65rem;
  margin-top: .7rem;
  padding: .85rem;
  border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
  border-radius: .85rem;
  background: color-mix(in srgb, currentColor 4%, transparent);
}
.emma-proposal label {
  display: grid;
  gap: .25rem;
  font-size: .85em;
}
.emma-proposal input,
.emma-proposal select {
  width: 100%;
  min-width: 0;
  box-sizing: border-box;
  padding: .45rem .55rem;
  border: 1px solid color-mix(in srgb, currentColor 24%, transparent);
  border-radius: .45rem;
  background: transparent;
  color: inherit;
  font: inherit;
}
.emma-proposal button {
  padding: .45rem .8rem;
  border: 1px solid color-mix(in srgb, currentColor 24%, transparent);
  border-radius: .5rem;
  background: transparent;
  color: inherit;
  cursor: pointer;
  font: inherit;
}
.emma-proposal button + button { margin-inline-start: .5rem; }
.emma-proposal button[type="submit"] {
  background: #386764;
  border-color: #386764;
  color: white;
}
.emma-proposal button:disabled { opacity: .55; cursor: wait; }

/* --- pages.css --- */
/* ============================================================
   艾玛 · 页面样式 pages.css
   各页面样式以 .pg-* 前缀隔离，互不影响
   ============================================================ */

/* ================ 沉浸式滚动（聊天/日程/备忘/账本，勿删） ================
   背景：.topbar 是 fixed 透明顶栏，但 .page-wrap 的
   padding-top: var(--topbar-h)（style.css）会把内容区整体推到
   顶栏正下方。若不去掉占位，内容滚不进顶栏区域，透明/渐变/毛玻璃
   全部不可见，顶栏区域露出的永远是同色 body 底色，看起来像
   "topbar 自带背景色"。规则顺序敏感，缺一不可：
   1) body.is-bleed-top 去掉底部 tab 占位（输入区已固定遮挡 tab）
   2) .page-wrap 去掉顶栏占位 → 内容可滚进透明 topbar 区域并透过
   3) 各页滚动容器按 --topbar-h 补首屏 padding，避免首条被顶栏/状态栏遮挡
      --topbar-h 由 JS 实测 topbar 渲染高度（含 env(safe-area-inset-top)），
      故刘海/挖孔/状态栏高度变化时自动跟随，不再有固定 56px 的漏算
   ============================================================ */
body.is-bleed-top { padding-bottom: 0; }
body.is-bleed-top .page-wrap { padding-top: 0; }
body.is-bleed-top .pk-content,
body.is-bleed-top .ac-content,
body.is-bleed-top .cx {
  padding-top: var(--topbar-h, calc(env(safe-area-inset-top) + 56px));
}

/* ================= 口袋页面 v2 ================= */
/* 布局：内容区全宽 */
.pk-layout {
  display: flex;
  height: 100%;
  width: 100%;
  overflow: hidden;
}

/* 内容区：全宽 */
.pk-content {
  flex: 1;
  overflow-y: auto;
  height: 100%;
  /* 顶部占位不在本类：非沉浸式页由 .page-wrap 承担，沉浸式页由 is-bleed-top 规则按实测 --topbar-h 补 */
  padding-top: 0;
  padding-bottom: var(--nav-space, 85px);
  padding-left: 16px;
  padding-right: 16px;
  box-sizing: border-box;
}

/* 加载/空状态 */
.pk-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 200px;
  color: var(--text-muted);
  font-size: 14px;
}
.pk-empty.pk-load-error {
  box-sizing:border-box;
  height:min(52vh, 360px);
  min-height:240px;
  margin:0 auto;
  display:flex;
  flex-direction:column;
  gap:12px;
  color:var(--text-secondary);
  font-size:14px;
}
.pk-load-error-icon {
  display:grid;
  place-items:center;
  width:36px;
  height:36px;
  border-radius:50%;
  background:var(--bg-input);
  color:var(--text-muted);
  font-size:18px;
  font-weight:600;
}
.pk-load-retry {
  margin-top:2px;
  padding:8px 14px;
  border:1px solid var(--border);
  border-radius:999px;
  background:var(--bg-card);
  color:var(--text-secondary);
  font:inherit;
  font-size:12px;
  cursor:pointer;
}
.pk-load-retry:active { background:var(--bg-input); }

/* ══════════════════════════════
   待办 · 工具条（列表顶部的新增按钮）
   随内容一起滚走 —— 吸顶会变成一条浮在卡片上的横条，滚动时压住下方的
   待办卡片（用户 2026-10-21 反馈：还不如不改）。
   工具条放在 .pk-schedule 之外，中轴线才不会从它身上穿过去。
══════════════════════════════ */
.st-toolbar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  height: 44px;
  margin: 0 0 4px;
  padding: 0 16px;
  box-sizing: border-box;
}

/* 端点用同色 rgba(...,0) 而不是 transparent，与中轴线保持同一套渐隐逻辑 */
.st-toolbar .st-line {
  flex: 1;
  height: 2px;
  background: linear-gradient(to right, rgba(120, 96, 60, 0), rgba(120, 96, 60, .5));
  border-radius: 2px;
}
.st-toolbar .st-line.right {
  background: linear-gradient(to left, rgba(120, 96, 60, 0), rgba(120, 96, 60, .5));
}

/* 新增按钮：方章（手账风），不再是蓝色圆钮 */
.st-add-btn {
  width: 38px;
  height: 38px;
  border-radius: 4px;
  /* 墨色方章：形状/渐变/投影是纸系工艺，**色相跟主题**。
     原先是暖棕 #6b5327→#4a3819 —— 全站换成初音绿之后，
     纸底上留一个深棕方块会变成第二个主题色。 */
  background: linear-gradient(140deg, #16544d 0%, #0d3b38 100%);
  color: #f6f1e6;
  border: none;
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 3px 9px -3px rgba(74, 56, 25, .75), 0 1px 2px rgba(0,0,0,.12);
  position: relative;
  z-index: 1;   /* 局部层级：压在工具栏的线之上 */
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.st-add-btn:active {
  transform: scale(0.94);
  box-shadow: 0 1px 4px rgba(5, 33, 31, .55);
}
html[data-theme="dark"] body.is-journal .st-add-btn {
  /* 夜间是反相章：亮青底 + 深墨字（跟纸系其他反色处理一致） */
  background: linear-gradient(140deg, #3fc9bf 0%, #33b8ae 100%);
  color: #05211f;
  box-shadow: 0 3px 9px -3px rgba(0, 0, 0, .8);
}

/* ══════════════════════════════
   手账页底色（待办 / 备忘共用）
   与账本的 is-paper 同一套机制：模块 render 时给 body 加 class，destroy 时移除。
   纸底比账本更暖（像牛皮纸内页），墨色阶随之反相。
   口径见 docs/design/schedule-memo-decisions.md
   ═══════════════════════════════ */
/* 纸变量放在全局作用域（:root / html[data-theme="dark"]），不挂在 body.is-journal 上：
   body class 是 JS 在 render 时加的，万一没加上，var(--jr-ink) 会静默失效回退成继承色。 */
:root {
  /* 纸片与账本统一（#faf7f0 / 夜 #15150f）：三个纸系页面同底色，切页不再换色调 */
  --jr-paper: var(--paper-bg);
  --jr-card: var(--bg-card-solid);
  --jr-card-muted: rgba(255, 253, 249, .58);
  /* 描边与账本同源（--ac-line / --ac-line-soft） */
  --jr-line: rgba(70, 58, 38, .16);
  --jr-line-soft: rgba(70, 58, 38, .08);
  --jr-hover-line: rgba(70, 58, 38, .34);
  /* 墨色阶与账本、与冷灰页的 --text-* 全部同源：
     同一张纸底上，待办/备忘与账本的正文深浅必须一样，
     否则切页时「同一本手账」的墨色会跳。
     --jr-ink3 压深到 #756d5e（纸片上 4.78:1）——
     旧值 #93826a 只有 3.48:1，12px 小字用它在 AA 之下。 */
  --jr-ink: var(--text-primary);
  --jr-ink2: var(--text-secondary);
  --jr-ink3: #756d5e;
  --jr-stamp: #b4442f;
  /* 逾期卡的呼吸微光（外扩的柔光圈）—— 浅色态用砖红的低透明度 */
  --jr-stamp-glow: rgba(180, 68, 47, .16);
  /* 即将到期卡的边框色：soon 纸调那个橙的满强度（--jr-soon-a 的同一族）
     浅底 3.5:1 / 夜底 6.83:1 —— 非文本图形 ≥ 3:1 达标 */
  --jr-soon-line: #c2761b;
  /* 琥珀：11px 印章文字要过 AA 4.5:1，#b06d0a 只有 4.1:1，压深到 #9a5c05（5.3:1） */
  --jr-amber: #9a5c05;
  --jr-blue: #2a6a94;
  --jr-serif: 'Noto Serif SC', 'Times New Roman', 'Songti SC', 'STSong', SimSun, serif;
  /* 中轴墨线：一条暖墨细线，两端渐隐 */
  --jr-axis: rgba(120, 96, 60, .30);
  --jr-axis-0: rgba(120, 96, 60, 0);
  /* 纸纹：两层错位网点，浅色用墨点 */
  --jr-grain:
    radial-gradient(rgba(90, 70, 40, .05) 0.5px, transparent 0.6px),
    radial-gradient(rgba(90, 70, 40, .032) 0.5px, transparent 0.6px);
  /* 状态纸调：每档一组「起 → 止」，止色与起色同色但 alpha 为 0 */
  --jr-past-a: rgba(180, 68, 47, .10);   --jr-past-b: rgba(180, 68, 47, 0);
  --jr-today-a: rgba(201, 140, 38, .12); --jr-today-b: rgba(201, 140, 38, 0);
  --jr-soon-a: rgba(199, 121, 31, .09);  --jr-soon-b: rgba(199, 121, 31, 0);
  --jr-near-a: rgba(42, 116, 168, .08);  --jr-near-b: rgba(42, 116, 168, 0);
  /* 各档描边色（纸感下手账风，边框要轻） */
  --jr-bd-past: rgba(180, 68, 47, .30);
  --jr-bd-today: rgba(201, 140, 38, .40);
  --jr-bd-soon: rgba(199, 121, 31, .28);
  --jr-bd-near: rgba(42, 116, 168, .24);
  /* 状态**顶部光条**的颜色：比印章色亮一档 ——
     顶部条要的是「发光」，印章要的是「读得清」，两套要求不一样。
     素纸底 #faf7f0 上，饱和度高的颜色看着才像一道光而不是一条灰线。 */
  --jr-bar-past: #c8523a;    /* 逾期 · 砖红（亮一档） */
  --jr-bar-today: #dfa032;   /* 到期 · 琥珀（亮一档） */
  --jr-bar-soon: #d98b22;    /* 3 天内 · 橙 */
  --jr-bar-near: #4b8fbe;    /* 7 天内 · 淡蓝 */
  /* 「今天」那档的静态光晕（非文本图形，≥3:1 即可，不需要 4.5:1） */
  --jr-glow-today: rgba(223, 160, 50, .30);
}
html[data-theme="dark"] {
  --jr-card-muted: rgba(28, 28, 22, .58);
  --jr-line: rgba(255, 255, 255, .13);
  --jr-line-soft: rgba(255, 255, 255, .07);
  --jr-hover-line: rgba(255, 255, 255, .24);
  /* 墨色阶夜间与账本、与冷灰页的 --text-* 同源 */
  --jr-ink3: var(--text-muted);
  --jr-stamp: #e07a63;
  --jr-stamp-glow: rgba(224, 122, 99, .22);
  --jr-soon-line: #d9963c;
  --jr-amber: #d9a441;
  --jr-blue: #6ea8d4;
  --jr-axis: rgba(255, 255, 255, .16);
  --jr-axis-0: rgba(255, 255, 255, 0);
  --jr-grain:
    radial-gradient(rgba(255, 255, 255, .05) 0.5px, transparent 0.6px),
    radial-gradient(rgba(255, 255, 255, .03) 0.5px, transparent 0.6px);
  --jr-past-a: rgba(224, 122, 99, .16);   --jr-past-b: rgba(224, 122, 99, 0);
  --jr-today-a: rgba(217, 164, 65, .16);  --jr-today-b: rgba(217, 164, 65, 0);
  --jr-soon-a: rgba(217, 150, 60, .13);   --jr-soon-b: rgba(217, 150, 60, 0);
  --jr-near-a: rgba(90, 150, 200, .13);   --jr-near-b: rgba(90, 150, 200, 0);
  --jr-bd-past: rgba(224, 122, 99, .34);
  --jr-bar-past: #e07a63;
  --jr-bar-today: #d9a441;
  --jr-bar-soon: #d9963c;
  --jr-bar-near: #6ea8d4;
  --jr-glow-today: rgba(217, 164, 65, .26);
  --jr-bd-today: rgba(217, 164, 65, .42);
  --jr-bd-soon: rgba(217, 150, 60, .30);
  --jr-bd-near: rgba(90, 150, 200, .28);
}
/* 页面底色：只有待办 / 备忘两页是手账纸底 */
body.is-journal {
  background: var(--jr-paper);
  color: var(--jr-ink);
}

/* ══════════════════════════════
   待办 · 时间轴 + 素纸卡片
   中轴墨线 + 纸色孔节点 + 左右交替，纸片仍是素纸（米白 + 纸纹 + 细描边）。
   口径见 docs/design/schedule-memo-decisions.md
══════════════════════════════ */
.pk-schedule {
  position: relative;
  padding: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
}

/* 中轴：一条素雅的墨线（沿用上一版装订线的取法），两端同色渐隐
   —— 彩虹渐变太闹，与素纸卡片不搭（用户 2026-10-21 反馈） */
.pk-schedule::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 0;
  bottom: 0;
  width: 2px;
  transform: translateX(-50%);
  /* 端点用「同色 + alpha 0」而不是 transparent，避免部分内核插值时发灰 */
  background: linear-gradient(to bottom,
    var(--jr-axis-0) 0%,
    var(--jr-axis)   4%,
    var(--jr-axis)  96%,
    var(--jr-axis-0) 100%);
}

/* 日期节点：线上打的一个孔（纸色圆 + 墨环），呼应上一版的装订孔 */
.s-date-dot {
  position: absolute;
  left: 50%;
  top: 3px;
  transform: translateX(-50%);
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--jr-paper);
  border: 2px solid var(--jr-hover-line);
  box-shadow: 0 0 0 3px var(--jr-line-soft);
  z-index: 2;   /* 局部层级：压在纸片（auto）之上 */
}

.s-date-group {
  /* 组与组之间错开的基准延时（2026-10-27）。
     卡片在组内还要再加一段（行内 --s-in-delay）⇒ 两段错峰叠起来。 */
  --s-group-delay: 0s;
  display: flex;
  flex-direction: column;
  width: 100%;
  position: relative;
  margin-bottom: 16px;
}

/* 入场动画挂在子节点上（不是组本身），圆点才能始终钉在线上不位移。
   ⚠ **动画名与延迟都走变量**（--s-in / --s-in-delay）：
   `animation-name` 是**单值**属性 —— 后面的规则会**整个替换**它。
   逾期卡的呼吸微光若直接写 `animation: s-past-breathe …`，
   入场动画就被**吞掉**（实测逾期卡 animation-name 只有 ['s-past-breathe']，
   不滑入、直接出现）。变量化之后，呼吸那条规则可以把入场动画
   **一起列进 animation 列表** —— 两个动画各跑各的
   （入场动 opacity/transform，呼吸动 filter），互不干扰。 */
.s-date-group > :not(.s-date-dot) {
  --s-in: slideIn;
  --s-in-delay: var(--s-group-delay);
  animation: var(--s-in) 0.5s ease var(--s-in-delay) both;
}
/* 内容贴住中轴向外生长：靠轴那侧固定 20px 间隙，外侧随内容长短留白。
   类名说的是「内容落在轴的哪一侧」：.right = 右半区，.left = 左半区。 */
.s-date-group.right {
  align-items: flex-start;
  padding-left: calc(50% + 20px);
  text-align: left;
}
.s-date-group.right > :not(.s-date-dot) { --s-in: slideInRight; }
.s-date-group.left {
  align-items: flex-end;
  padding-right: calc(50% + 20px);
  text-align: right;
}
.s-date-group.left > :not(.s-date-dot) { --s-in: slideInLeft; }
/* 组与组错开：延时挂在**组**上（--s-group-delay），子元素继承。
   卡片在组内再叠一段（行内 --s-in-delay = 组延时 + 组内序号 × 0.07s）
   ⇒ 组内卡片也是一张张进来的，不再齐刷刷一起出现。 */
.s-date-group:nth-child(2) { --s-group-delay: 0.1s; }
.s-date-group:nth-child(3) { --s-group-delay: 0.2s; }
.s-date-group:nth-child(4) { --s-group-delay: 0.3s; }
.s-date-group:nth-child(5) { --s-group-delay: 0.4s; }

@keyframes slideInRight {
  from { opacity: 0; transform: translateX(40px); }
  to { opacity: 1; transform: translateX(0); }
}
@keyframes slideInLeft {
  from { opacity: 0; transform: translateX(-40px); }
  to { opacity: 1; transform: translateX(0); }
}
@keyframes slideIn {
  from { opacity: 0; transform: translateY(20px); }
  to { opacity: 1; transform: translateY(0); }
}

/* 待办空状态：纸底上的居中提示（此前无任何样式，裸文字浮在页面上） */
.s-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 56px 0;
  color: var(--jr-ink3);
}
.s-empty-icon { font-size: 30px; line-height: 1; opacity: .75; }
.s-empty-text { font-size: 14px; color: var(--jr-ink2); letter-spacing: .02em; }

/* 纸片：米白纸底 + 极淡纸纹 + 细描边
   宽度自适应内容（贴住中轴向外生长），不撑满半区 ——
   待办内容普遍很短，撑满半区反而显空旷（用户 2026-10-21 反馈） */
.s-card {
  --jr-tint: linear-gradient(135deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 62%);
  /* 微光（状态光晕）走变量：常态一份、hover 一份。
     ⚠ 不能直接写死 box-shadow —— .s-card:hover 的抬升阴影会**整个盖掉**它，
     而 hover 的抬升是必须的（纸片要「能被拿起来」）。
     ⇒ 常态与 hover 各留一个变量，box-shadow 由它们拼出来。 */
  --glow: 0 0 0 rgba(0,0,0,0);
  --glow-hover: 0 0 0 rgba(0,0,0,0);
  position: relative;
  /* 宽度上限 = 半栏宽（100% 相对于所在半区）。
     必须有它：标题一旦 nowrap，它的 min-content 就是全文宽度，
     纸片会跟着涨到把半栏撑爆（实测 461px > 半栏 159px），冲出时间轴。
     加上限之后纸片停在半栏宽，标题在自己框里横向滑动。 */
  max-width: 100%;
  min-width: 0;
  background-color: var(--jr-card);
  /* 三层：状态纸调 + 两层纸纹。底色必须用 background-color，
     background 简写会把 background-image 整个归零（纸纹就没了） */
  background-image: var(--jr-tint), var(--jr-grain);
  background-size: 100% 100%, 4px 4px, 4px 4px;
  background-position: 0 0, 0 0, 2px 2px;
  border: 1px solid var(--jr-line);
  border-radius: 3px;
  padding: 10px 12px;
  box-shadow: var(--glow), 0 1px 1px rgba(90,70,40,.07), 0 10px 18px -14px rgba(90,70,40,.50);
  transition: transform .2s ease, box-shadow .2s ease, border-color .2s ease,
              scale .28s cubic-bezier(.34, 1.45, .5, 1);
  cursor: pointer;
  /* 点击回弹（2026-10-27 加的动效；样式一律不动）。
     ⚠ 用**独立的 scale 属性**，不用 transform ——
       入场动画是 `animation: … both`，动画值在层叠里压过普通声明，
       transform 一直被它占着（.s-card:hover 的抬升因此一直不生效）。
       scale 是独立的 transform 分量（CSS Transforms Level 2），
       与 transform 互不干扰 ⇒ 两条动效能同时在。 */
  scale: 1;
}
/* 按下时缩一下，松手回弹（回弹感来自 scale 那条带回弹的缓动曲线） */
.s-card:active { scale: .965; }
@media (prefers-reduced-motion: reduce) {
  .s-card { transition: transform .2s ease, box-shadow .2s ease, border-color .2s ease; }
  .s-card:active { scale: 1; }
}
/* 同日期的多张纸片之间留出间距（原来贴在一起，0 间距） */
.s-card + .s-card { margin-top: 8px; }

.s-card:hover {
  transform: translateY(-2px);
  box-shadow: var(--glow-hover), 0 2px 3px rgba(90,70,40,.10), 0 16px 24px -14px rgba(90,70,40,.55);
  border-color: var(--jr-hover-line);
}

/* 定点高亮：顶栏「今日提醒」跳进来，闪一下目标纸片
   颜色用初音绿（--primary）而不是纸系色 —— 纸系的琥珀/蓝/砖红
   已经各自占了一个状态含义，再加第四种颜色就是第四种状态；
   绿是全站交互主色，在这里读作「你在这儿」。

   选择器写成 .pk-schedule .s-card.is-flash 是**必需**的：
   时间轴入场动画挂在 .s-date-group > :not(.s-date-dot) 上（0-3-0），
   写短了动画名会被入场动画抢走 —— 表现是「高亮类加上了但没闪」。
   同为 0-3-0 时靠源码顺序决胜，本条在后面。 */
.pk-schedule .s-card.is-flash {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(57,197,187,.20), 0 10px 18px -14px rgba(90,70,40,.50);
  /* 整条 animation 覆盖入场动画：这张纸片直接出现 + 闪，不再横向滑入 */
  animation: s-flash-pulse 1.9s ease-out;
}
/* 100% 与 .pk-schedule .s-card.is-flash 的静态 box-shadow 完全一致 —— 动画结束不跳变 */
@keyframes s-flash-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(57,197,187,.55), 0 1px 1px rgba(90,70,40,.07); }
  30%  { box-shadow: 0 0 0 9px rgba(57,197,187,.18), 0 12px 20px -12px rgba(90,70,40,.50); }
  100% { box-shadow: 0 0 0 3px rgba(57,197,187,.20), 0 10px 18px -14px rgba(90,70,40,.50); }
}

/* ─── 状态表达：顶部色条 + 微光（三条通道同时分档） ───────────────────
   用户 2026-10-26：「在改版之前有一个样式我挺喜欢的 —— 卡片顶部变色和微光，
   程度还有不同」，同时保留现在的印章。

   那是 git 29e089a（待办时间轴华丽卡片）那版的写法：
     · 顶部 3px 光条   linear-gradient(90deg, transparent, 色, transparent)
     · 卡片底 135deg 淡彩渐变
     · 微光            逾期 = 红色脉冲光晕 / 今天 = 金色光晕
                       / 3 天内 = 只有条 / 7 天内 = 蓝条
   现在用**素纸色板**把它复刻回来，**整圈实色边框撤掉** ——
   那才是「像表单报错」的根源（红框 + 2px 加粗 = 纸片鼓出一块）。

   三条通道同时递进，就是用户说的「程度还有不同」：
     逾期   砖红条(满) + 呼吸红晕 + 浓纸调
     到期   琥珀条(满) + 静态琥珀光晕 + 浓纸调
     3 天内 橙条(满)  + 无光晕             + 淡纸调
     7 天内 蓝条(0.7)+ 无光晕             + 更淡纸调
     更远   无条                            + 压纸底亮度

   ⚠ 顶部条**不能**用 ::before —— .s-card::before/::after 已经被
     「横向滚动渐隐提示」占用了（js/core/ui.js 的 bindScrollHint 挂
     .has-more-left / .has-more-right）⇒ 只能加子元素 <i class="s-card-top">。
   ⚠ 顶部条盖在卡片上沿（top/left/right 各 -1px）—— 正好盖住那 1px 描边，
     免得「彩条 + 一道描边」叠成两条线。 */

/* 顶部光条：默认不显（普通卡 hover 才亮一条），状态卡常显。 */
.s-card-top {
  position: absolute;
  top: -1px; left: -1px; right: -1px;
  height: 3px;
  border-radius: 3px 3px 0 0;
  background: linear-gradient(90deg, transparent, var(--jr-ink3) 22%, var(--jr-ink3) 78%, transparent);
  opacity: 0;
  transition: opacity .25s ease;
  pointer-events: none;
}
.s-card:hover > .s-card-top { opacity: .45; }
/* ⚠ 状态条用 `.s-card.s-card-xxx`（两个类）—— 与上面那条 hover 规则同权重，
   靠**源码顺序**取胜：写在后面 ⇒ 状态卡 hover 时条不会反而变淡。 */
.s-card.s-card-past > .s-card-top {
  opacity: 1;
  background: linear-gradient(90deg, transparent, var(--jr-bar-past) 22%, var(--jr-bar-past) 78%, transparent);
}
.s-card.s-card-today > .s-card-top {
  opacity: 1;
  background: linear-gradient(90deg, transparent, var(--jr-bar-today) 22%, var(--jr-bar-today) 78%, transparent);
}
.s-card.s-card-soon > .s-card-top {
  opacity: 1;
  background: linear-gradient(90deg, transparent, var(--jr-bar-soon) 22%, var(--jr-bar-soon) 78%, transparent);
}
.s-card.s-card-near > .s-card-top {
  opacity: .7;
  background: linear-gradient(90deg, transparent, var(--jr-bar-near) 22%, var(--jr-bar-near) 78%, transparent);
}

/* 逾期 —— 砖红：顶部条 + 呼吸红晕 + 浓纸调。描边是**柔色**，不是实色。 */
.s-card-past {
  --jr-tint: linear-gradient(135deg, var(--jr-past-a) 0%, var(--jr-past-b) 62%);
  --glow: 0 0 12px var(--jr-stamp-glow);
  --glow-hover: 0 0 18px var(--jr-stamp-glow);
  border-color: var(--jr-bd-past);
}
/* 呼吸微光：外扩的柔光圈，越急越明显。周期 2.8s —— 比控制台状态点(2.4s)
   慢一点，两处同时在动时不会互相打架。

   ⚠ **必须把入场动画一起列进 animation 列表**（用变量 --s-in / --s-in-delay）：
   `animation` 简写会**重置整个动画列表** —— 只写呼吸的话，
   入场动画就被吞掉（表现：逾期卡片不滑入、直接出现）。
   这里写成「入场 + 呼吸」两条：入场动 opacity/transform，呼吸动 filter，互不干扰。

   ⚠ 微光用 **filter: drop-shadow** 而不是 box-shadow：
   box-shadow 会被 .s-card:hover 的抬升阴影顶掉（动画优先级高于普通声明），
   悬浮时卡片就不抬了；filter 是另一个属性，两者不打架。 */
.pk-schedule .s-card.s-card-past {
  animation: var(--s-in) 0.5s ease var(--s-in-delay) both,
             s-past-breathe 2.8s ease-in-out infinite;
}
/* 逾期 + 高亮：两个动画一起跑 ——
   高亮（通知里点条目跳过来）是一次性事件，呼吸必须**继续**：
   若让高亮独占 animation 列表，这条逾期卡被高亮过就再也不呼吸了（直到整页重渲染）。
   两个动画动的属性不同（高亮动 box-shadow，呼吸动 filter），互不干扰。
   ⚠ 这条要比 .pk-schedule .s-card.is-flash（0-3-0）与上面的（0-3-0）都更 specific。
   ⚠ **不列入场动画**：这张卡已经入场过了，再列一次会**重播**滑入
   （is-flash 是渲染完之后才加的类）。 */
.pk-schedule .s-card.s-card-past.is-flash {
  animation: s-flash-pulse 1.9s ease-out, s-past-breathe 2.8s ease-in-out infinite;
}
/* 到期（今天）—— 琥珀：顶部条 + **静态琥珀光晕** + 浓纸调。
   光晕是静态的（今天要动，但它不是「欠了」），逾期才有呼吸。 */
.s-card-today {
  --jr-tint: linear-gradient(135deg, var(--jr-today-a) 0%, var(--jr-today-b) 62%);
  --glow: 0 0 16px var(--jr-glow-today);
  --glow-hover: 0 0 24px var(--jr-glow-today);
  border-color: var(--jr-bd-today);
}
/* 3 天内（即将到期）—— 橙：顶部条 + 无光晕 + 淡纸调 */
.s-card-soon {
  --jr-tint: linear-gradient(135deg, var(--jr-soon-a) 0%, var(--jr-soon-b) 62%);
  border-color: var(--jr-bd-soon);
}
@keyframes s-past-breathe {
  0%, 100% { filter: drop-shadow(0 0 0 rgba(180, 68, 47, 0)); }
  50%      { filter: drop-shadow(0 0 6px var(--jr-stamp-glow)); }
}
/* hover 时别把状态描边冲掉 —— .s-card:hover 的 border-color 会盖掉它 */
.s-card-past:hover   { border-color: var(--jr-bd-past); }
.s-card-today:hover  { border-color: var(--jr-bd-today); }
.s-card-soon:hover   { border-color: var(--jr-bd-soon); }
/* 7 天内 - 淡蓝：顶部条弱一档，无光晕 */
.s-card-near {
  --jr-tint: linear-gradient(135deg, var(--jr-near-a) 0%, var(--jr-near-b) 62%);
  border-color: var(--jr-bd-near);
}
.s-card-near:hover { border-color: var(--jr-bd-near); }
/* 未来卡片 - 只压纸底亮度，文字始终满对比
   （原 opacity:.7 会把文字一起压暗，夜间态下几乎读不出来） */
.s-card-future {
  background-color: var(--jr-card-muted);
  border-color: var(--jr-line-soft);
}

.s-card-title {
  font-size: 15px;
  color: var(--jr-ink);
  line-height: 1.6;
  font-weight: 500;
  /* 单行显示：超长时纸片内横向滑动，不折行、不把纸片撑高。
     （折行会让纸片长成一摞，纸感时间轴会走形） */
  white-space: nowrap;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.s-card-title::-webkit-scrollbar { display: none; }

/* 右侧渐隐提示：内容被裁断的一侧用纸色淡出 + 边缘分界，一眼看出「右边还有」。
   与聊天表格同一套结构（底色渐隐 + --edge-hint 分界），
   底色换成纸色 --jr-card，夜间态 --edge-hint 自动反极（提亮）。
   .has-more-right / .has-more-left 由 js/core/ui.js 的 bindScrollHint 挂上
   （滚到两端会自动摘掉）。 */
.s-card::after {
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: 36px;
  pointer-events: none;
  opacity: 0;
  transition: opacity .2s ease;
  background:
    linear-gradient(to left, var(--jr-card) 0%, var(--jr-card) 20%, transparent 100%),
    linear-gradient(to left, var(--edge-hint, rgba(0,0,0,.34)) 0%, transparent 48%);
  border-radius: 0 2px 2px 0;
}
.s-card.has-more-right::after { opacity: 1; }
/* 左侧渐隐：往回滑时左端被裁掉的那截同样要有提示 ——
   只做右边的话，滑到中间时左边被裁掉的内容完全看不出来（用户 2026-10-22 反馈）。 */
.s-card::before {
  content: '';
  position: absolute;
  top: 0; left: 0; bottom: 0;
  width: 36px;
  pointer-events: none;
  opacity: 0;
  transition: opacity .2s ease;
  background:
    linear-gradient(to right, var(--jr-card) 0%, var(--jr-card) 20%, transparent 100%),
    linear-gradient(to right, var(--edge-hint, rgba(0,0,0,.34)) 0%, transparent 48%);
  border-radius: 2px 0 0 2px;
}
.s-card.has-more-left::before { opacity: 1; }

/* 日期行：衬线日期 + 星期小字 + 状态印章
   日期行与纸片之间的间距比同日期纸片之间（8px）更大 —— 它是「这组」的组头，
   要和同组的其它纸片区分开 */
.s-date {
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin-bottom: 12px;
}
.s-date-text {
  font-family: var(--jr-serif);
  font-size: 14px;
  font-weight: 700;
  color: var(--jr-ink);
  letter-spacing: .02em;
}
.s-date-week {
  font-size: 11px;
  /* 用 --jr-ink2 而不是 --jr-ink3：11px 小字要过 WCAG AA 的 4.5:1，
     --jr-ink3 在纸片上只有 3.31:1 */
  color: var(--jr-ink2);
  letter-spacing: .04em;
}

/* 状态印章：与日期同排（组级，一个日期一枚）
   不占纸片内部的高度 —— 印章行塞在纸片里会把纸片撑高，纸片里只剩一句话时显空
   不用 opacity 表达弱化 —— 那会把印章文字的对比度一起吃掉 */
.s-stamp {
  display: inline-block;
  align-self: center;
  font-size: 11px;
  font-weight: 800;
  line-height: 1.5;
  border: 1.5px solid currentColor;
  border-radius: 4px;
  padding: 0 5px;
  transform: rotate(-8deg);
  letter-spacing: .1em;
  white-space: nowrap;
  /* 盖章落下（2026-10-27 加的动效；样式一律不动）。
     ⚠ 只动 transform / opacity —— 不改尺寸、不改位置。
     ⚠ .s-stamp 在 .s-date 里面，**不是** .s-date-group 的直接子元素 ⇒
       不会跟组入场动画抢 animation-name（那条只挂在 > :not(.s-date-dot) 上）。
     ⚠ 延时跟着组走（--s-group-delay 继承下来）⇒ 后面的组不会先于前面的组盖章。 */
  animation: stampDrop .44s cubic-bezier(.22, .9, .3, 1.35) calc(var(--s-group-delay, 0s) + .25s) both;
}
@keyframes stampDrop {
  0%   { opacity: 0; transform: rotate(-8deg) scale(2.3); }
  42%  { opacity: 1; transform: rotate(-8deg) scale(.86); }
  68%  { transform: rotate(-8deg) scale(1.09); }
  86%  { transform: rotate(-8deg) scale(.97); }
  100% { opacity: 1; transform: rotate(-8deg) scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .s-stamp { animation: none; }
}
.s-stamp-past  { color: var(--jr-stamp); }
/* 「今天」是实心章：纸色字 + 琥珀底，比描边章更跳，
   也不用靠 opacity 去区分档位 */
.s-stamp-today { color: var(--jr-card); background: var(--jr-amber); border-color: var(--jr-amber); }
.s-stamp-soon  { color: var(--jr-amber); }
.s-stamp-near  { color: var(--jr-blue); }

/* ============================================================
   账本 · 素纸版（V2）
   纸底 #faf7f0 + 细线分隔 + 衬线数字；页面上唯一的实心色块是记账按钮。
   口径见 docs/design/ledger-v2-decisions.md
   字号沿用全站阶梯 10/11/12/13/14/15/16/18，与日程、备忘、聊天同一套档位，
   不再出现 11.5 / 12.5 / 17 这类全站别处没有的半像素档。
   ============================================================ */

/* ---------------- 纸底（仅账本页生效） ---------------- */
body.is-paper {
  --ac-paper: var(--paper-bg);
  --ac-card: var(--bg-card-solid);
  --ac-line: rgba(70, 58, 38, .16);
  --ac-line-soft: rgba(70, 58, 38, .08);
  /* 墨色阶与待办/备忘、与冷灰页的 --text-* 全部同源 */
  --ac-ink: var(--text-primary);
  --ac-ink2: var(--text-secondary);
  --ac-ink3: #756d5e;
  /* 墨绿那一支已经换成初音绿（--primary）：实心填充用原色，
     当字/图标色用 --primary-text（浅底上够深的一档） */
  --ac-accent: var(--primary);
  --ac-accent-ink: var(--primary-text);
  --ac-accent-soft: rgba(57, 197, 187, .10);
  --ac-expense: #a04a38;
  --ac-expense-soft: rgba(160, 74, 56, .10);
  --ac-expense-line: rgba(160, 74, 56, .38);
  --ac-income: #3f7a55;
  --ac-income-soft: rgba(63, 122, 85, .10);
  --ac-income-line: rgba(63, 122, 85, .38);
  /* 实心按钮底色（白字）：与 --ac-expense / --ac-income 分开。
     夜间那两支是「深底上的浅字」色，拿来当白字按钮底只有 2.56:1，
     所以按钮底另取一档，保证白字 ≥4.5:1。 */
  --ac-expense-solid: #a04a38;
  --ac-income-solid: #3f7a55;
  /* 琥珀与手记页 --jr-amber 同源。
     ⚠ 2026-10-27 对比度复核：旧值 #9a5c05 在**标签自己的浅底**
       （--ac-pending-soft 10% 琥珀叠纸底 = #f0e8d8）上只有 **4.41:1**
       —— 差一点点不到 AA 4.5:1（11px/500 的小字）。
       同色在纯卡片底上够（5.29:1），但标签永远在自己那块浅底上 ⇒ 按浅底判定。
     旧值 #a8730d 更差（4.03:1），已弃用。
     现在 #8a5404：浅底 5.15:1 / 卡片 6.17:1 / 纸页 5.86:1，三处都过 AA，
     观感仍是同一支琥珀（只压深一档）。 */
  --ac-pending: #8a5404;
  --ac-pending-soft: rgba(138, 84, 4, .10);
  /* 分类色（可视化用）：三档必须一眼分得开，且**不占用**已有的语义色
     （红=支出、绿=收入、琥珀=待报销）—— 那些是状态，不是分类。
     分类色只出现在占比环 / 图例 / 柱子上，不参与别处的语义表达。 */
  --ac-cat-life: var(--primary);   /* 初音绿 */
  --ac-cat-work: #4a6f96;          /* 靛蓝 */
  --ac-cat-other: #a5751f;         /* 琥珀褐 */
  --ac-serif: 'Noto Serif SC', 'Times New Roman', 'Songti SC', 'STSong', SimSun, serif;
  background: var(--ac-paper);
  color: var(--ac-ink);
}
html[data-theme="dark"] body.is-paper {
  --ac-line: rgba(255, 255, 255, .13);
  --ac-line-soft: rgba(255, 255, 255, .07);
  --ac-ink3: var(--text-muted);
  --ac-accent: var(--primary);
  --ac-accent-ink: var(--primary-text);
  --ac-accent-soft: rgba(57, 197, 187, .16);
  --ac-expense: #e08c78;
  --ac-expense-soft: rgba(224, 140, 120, .14);
  --ac-expense-line: rgba(224, 140, 120, .45);
  --ac-income: #7fc79a;
  --ac-income-soft: rgba(127, 199, 154, .14);
  --ac-income-line: rgba(127, 199, 154, .45);
  /* 实心按钮底：夜间不变浅 —— 浅色那两支是给深底当文字色的，
     当白字按钮底会掉到 2.56:1 */
  --ac-expense-solid: #a04a38;
  --ac-income-solid: #3f7a55;
  --ac-pending: #d9a441;
  --ac-pending-soft: rgba(217, 164, 65, .14);
  /* 分类色（可视化用）夜间档：底更深 ⇒ 同一组分类色整体提亮一档 */
  --ac-cat-work: #7fa8cf;
  --ac-cat-other: #c99a52;
}
/* ── 纸系页面的「页面底色」收口 ──
   三个纸系页面（待办 is-journal / 备忘 is-journal / 账本 is-paper）共用同一套底色，
   所以覆写只写一份，不再按模块各写一遍。
   --page-bg   页面底色（body 背景）
   --page-rgb  页面底色的 RGB 分量，给需要 rgb(... / a) 的地方用
   --veil-rgb  玻璃材质的 tint 源（顶栏玻璃条 + 底部渐变模糊带共用）
   覆写 --veil-rgb 之后，纸底上玻璃也跟着走纸色 ——
   以前这层硬编码渐变让纸系顶栏比冷灰顶栏更实，两套底色下顶栏行为不一致。
   口径见 docs/design/visual-unify-decisions.md §2.5 */
body.is-journal,
body.is-paper {
  --page-bg: var(--paper-bg);
  --page-rgb: var(--paper-rgb);
  --veil-rgb: var(--paper-rgb);
  /* --bg-base 一起跟过来：它是 body 背景的全局兜底，
     不跟着走的话纸系页面上任何回落到 --bg-base 的地方会露出冷灰 */
  --bg-base: var(--paper-bg);
}

/* 内容区：左右不留白（纸面行要通栏）。
   自身**滚**（与待办/备忘同一套）：概览卡 / 可视化 / 搜索 / 筛选 / 列表头 / 明细
   一起滚，顶部不再冻结 —— 用户 2026-10-27：「账本要改成跟日程一样自然滚动」。
   底部留白（--nav-space）由 .pk-content 提供，跟着内容一起滚。 */
.ac-content {
  padding-left: 0;
  padding-right: 0;
}

/* ---------------- 概览卡 ---------------- */
.ac-hero {
  margin: 2px 18px 12px;
  background: var(--ac-card);
  border: 1px solid var(--ac-line);
  border-radius: 18px;
  padding: 12px 16px 11px;
  position: relative;
  overflow: hidden;
}
.ac-hero::before {
  content: '';
  position: absolute; left: 0; right: 0; top: 0; height: 2px;
  background: linear-gradient(90deg, var(--ac-accent), rgba(57, 197, 187, .14));
}
/* 标题行两端对齐：标题在左、范围徽标在右，别把信息都堆在左侧 */
.ac-hero-label {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  font-size: 12px; color: var(--ac-ink3); letter-spacing: .08em;
}
.ac-hero-amount {
  font-family: var(--ac-serif);
  font-size: 30px; line-height: 1.12; margin-top: 4px; color: var(--ac-ink);
  font-variant-numeric: tabular-nums; letter-spacing: -.01em;
}
/* 底部两块统计左右分开，整体铺开而不是挤在左边 */
.ac-hero-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  /* 数字特别多时允许换行兜底（正常宽度下永远是一行） */
  flex-wrap: wrap;
  margin-top: 9px; padding-top: 8px;
  border-top: 1px solid var(--ac-line-soft);
  font-size: 12px; color: var(--ac-ink2);
}
.ac-hero-foot b { font-weight: 600; color: var(--ac-ink); }
.ac-hero-stat { white-space: nowrap; }
.ac-hero-hint { color: var(--ac-ink3); }
.ac-dot {
  display: inline-block; width: 5px; height: 5px; border-radius: 50%;
  background: var(--ac-pending); margin-right: 5px; vertical-align: middle;
}

/* ---------------- 搜索 ---------------- */
.ac-search { position: relative; margin: 0 18px 10px; }
.ac-search-input {
  width: 100%; height: 36px;
  border-radius: 999px;
  border: 1px solid var(--ac-line);
  background: transparent;
  padding: 0 34px 0 33px;
  font-family: inherit; font-size: 14px; color: var(--ac-ink);
  outline: none;
}
.ac-search-input::placeholder { color: var(--ac-ink3); }
.ac-search-input:focus { border-color: var(--ac-accent-ink); }
.ac-search-ico {
  position: absolute; left: 11px; top: 50%; transform: translateY(-50%);
  width: 14px; height: 14px; color: var(--ac-ink3); display: flex; pointer-events: none;
}
.ac-search-clear {
  position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
  width: 18px; height: 18px; border: 0; border-radius: 50%;
  background: var(--ac-line); color: var(--ac-card);
  font-size: 10px; line-height: 1; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.ac-search-clear.is-hidden { display: none; }

/* ---------------- 筛选 chip ---------------- */
.ac-chips {
  display: flex; gap: 7px; padding: 0 18px 8px;
  overflow-x: auto; scrollbar-width: none;
}
.ac-chips::-webkit-scrollbar { display: none; }
.ac-chip {
  font-family: inherit; font-size: 13px; line-height: 1.4;
  padding: 5px 12px; border-radius: 999px;
  border: 1px solid transparent; background: transparent;
  color: var(--ac-ink2); white-space: nowrap; cursor: pointer;
  display: inline-flex; align-items: center; gap: 4px;
}
.ac-chip.on { background: var(--ac-accent); color: var(--primary-on); font-weight: 500; }
.ac-chips-cat { padding-bottom: 11px; }
.ac-chip-cat { padding: 4px 9px; }
.ac-chip-cat.on {
  background: var(--ac-accent-soft); color: var(--ac-accent-ink);
  border-color: rgba(57, 197, 187, .38); font-weight: 600;
}
html[data-theme="dark"] .ac-chip-cat.on { border-color: rgba(57, 197, 187, .42); }
.ac-chip-cat i {
  width: 18px; height: 18px; border-radius: 6px;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(70, 58, 38, .06); color: var(--ac-ink2);
}
.ac-chip-cat.on i { background: transparent; color: var(--ac-accent-ink); }
.ac-chip-cat i svg { width: 11px; height: 11px; }

/* ---------------- 明细列表（内容区里的一段，不再自己滚） ---------------- */
/* 概览卡 / 搜索 / 筛选 / 列表头都是 .ac-content 的直接子元素，
   整页一起滚（滚动容器是 .ac-content 自己）。 */
.ac-list { padding-bottom: 0; }

/* ---------------- 列表头 ---------------- */
.ac-list-head {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 9px 18px 5px; font-size: 13px; color: var(--ac-ink3); letter-spacing: .04em;
}
/* 列表头左侧：静态区块标题。
   原先是动态笔数（筛选/搜索时会跳），改成静态词后列表头是「标题 + 操作」的结构，
   且与日期组头分工明确 —— 组头管时间维度，标题管这一段是什么。 */
.ac-list-title { font-weight: 600; color: var(--ac-ink2); letter-spacing: .06em; }
/* 列表头右侧两个入口：同款描边图标按钮 —— 语义一致（都是动作入口）、
   形态一致（同样大小/描边/图标），只有图标不同。左=选择，右=新增。
   尺寸 26px 是桌面鼠标的尺寸，手机上点不准 —— 放大到 36px，间隔同时拉开到 12px，
   两枚按钮之间不会误触。 */
.ac-head-actions { display: flex; align-items: center; gap: 12px; }
.ac-head-btn {
  width: 36px; height: 36px; border-radius: 10px; padding: 0;
  border: 1px solid var(--ac-line); background: transparent;
  color: var(--ac-accent);
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; flex-shrink: 0;
  transition: background .14s ease, transform .12s ease;
}
.ac-head-btn:hover { background: var(--ac-accent-soft); }
.ac-head-btn:active { transform: scale(.94); }
.ac-head-btn svg { width: 18px; height: 18px; stroke-width: 2; }

/* ---------------- 纸面行 ---------------- */
/* 日期组头只留时间维度（当天笔数已去掉） */
.ac-day {
  display: flex; align-items: baseline; justify-content: space-between;
  padding: 12px 18px 5px; font-size: 12px; color: var(--ac-ink3); letter-spacing: .05em;
}
.ac-row {
  display: flex; align-items: center; gap: 11px;
  padding: 10px 18px;
  border-bottom: 1px solid var(--ac-line-soft);
  cursor: pointer;
}
.ac-row.picked { background: var(--ac-accent-soft); }
/* 已报销 = 这笔账已结清：整行弱化，不再抢注意力（分元素降透明度，保留徽标可辨） */
.ac-row.is-done .ac-row-ico { opacity: .5; }
.ac-row.is-done .ac-row-main { opacity: .58; }
.ac-row.is-done .ac-row-amt { opacity: .5; }
.ac-chk {
  width: 19px; height: 19px; border-radius: 50%;
  border: 1.5px solid var(--ac-line); flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
}
.ac-chk.on { background: var(--ac-accent); border-color: var(--ac-accent); }
.ac-chk svg { width: 11px; height: 11px; color: var(--primary-on); opacity: 0; }
.ac-chk.on svg { opacity: 1; }
/* 已报销的项在选择模式下不可勾：勾了也不会参与报销关联 */
.ac-chk.off { opacity: .32; border-style: dashed; }
.ac-row-ico {
  width: 32px; height: 32px; border-radius: 10px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  background: rgba(70, 58, 38, .055); color: var(--ac-ink2);
}
html[data-theme="dark"] .ac-row-ico { background: rgba(255, 255, 255, .07); }
.ac-row-ico svg { width: 16px; height: 16px; }
.ac-row-main { flex: 1; min-width: 0; }
.ac-row-title {
  font-size: 15px; font-weight: 500; color: var(--ac-ink);
  display: flex; align-items: center; gap: 6px; min-width: 0;
}
.ac-row-sub {
  display: block; font-size: 12px; color: var(--ac-ink3); margin-top: 1px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ac-flag {
  font-style: normal; font-size: 11px; font-weight: 500;
  padding: 1.5px 6px; border-radius: 5px; white-space: nowrap;
}
.ac-flag-pending { background: var(--ac-pending-soft); color: var(--ac-pending); }
.ac-flag-done { background: var(--ac-accent-soft); color: var(--ac-accent-ink); }
.ac-book-tag {
  font-style: normal; font-size: 11px; color: var(--ac-ink3);
  border: .5px solid var(--ac-line); border-radius: 5px; padding: 1px 5px; white-space: nowrap;
}
.ac-row-expand {
  background: transparent; border: 0; padding: 4px 0; cursor: pointer;
  color: var(--ac-ink3); display: flex; flex-shrink: 0;
}
.ac-row-expand svg { width: 15px; height: 15px; transition: transform .18s ease; }
.ac-row-amt {
  font-family: var(--ac-serif);
  font-size: 15px; color: var(--ac-expense); white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.ac-row-amt.in { color: var(--ac-income); }

/* 关联明细 */
.ac-related {
  margin: 0 18px 0 61px; padding: 6px 10px;
  border-radius: 10px;
  background: var(--ac-accent-soft);
  border: .5px dashed rgba(57, 197, 187, .30);
}
html[data-theme="dark"] .ac-related { border-color: rgba(57, 197, 187, .34); }
.ac-related-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; padding: 3px 0; cursor: pointer;
}
.ac-related-name { font-size: 11px; color: var(--ac-ink2); }
.ac-related-amt {
  font-family: var(--ac-serif); font-size: 12px; color: var(--ac-expense); white-space: nowrap;
}
.ac-related-amt.in { color: var(--ac-income); }

/* 空态 */
.ac-empty { text-align: center; padding: 60px 20px; color: var(--ac-ink3); }
.ac-empty-text { font-size: 14px; margin-bottom: 8px; }
.ac-empty-hint { font-size: 12px; opacity: .75; }

/* 记账入口：现在在列表头右侧，是一枚与「选择」同款的图标按钮
   （.ac-head-btn.ac-add-btn，见列表头区）。原先是右下角 50px 悬浮按钮，
   会压住列表最后几行，已移除。 */

/* ---------------- 选择模式 ---------------- */
/* tab 整条让位：与聊天迷你抽屉同一套做法（opacity 0 + 下移 + visibility hidden），
   页面下缘只留一条底栏
   ⚠ 2026-10-30：tab 改贴底后不再水平居中 ⇒ 收起动画只有 translateY(14px)，
     保留 translateX(-50%) 会把整条 dock 往左平移半个屏宽。 */
body.ac-selecting .tab-bar {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateY(14px);
  transition: opacity 0.2s ease, transform 0.2s ease, visibility 0s linear 0.2s;
}
/* 选择模式：底部操作条接管页面下缘，页面多留一段底边距，最后一行不被压住。
   留白挂在 .ac-content 上（整页的滚动容器）—— .ac-list 不再自己滚。 */
body.ac-selecting .ac-content { padding-bottom: calc(var(--nav-space, 85px) + 66px); }

.ac-actionbar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  width: 100%;
  padding: 9px 10px max(9px, var(--safe-b));
  z-index: var(--z-actionbar);
  display: none; align-items: center; gap: 8px;
  background: var(--ac-card);
  border-top: 1px solid var(--ac-line);
  border-radius: 0;
  box-shadow: 0 -6px 18px -12px rgba(70, 58, 38, .28);
}
/* ⚠ 2026-10-30（tab 改贴底）：操作条跟着改成贴底 dock。
 *   以前是「离底 20px 的悬浮卡片」，与悬浮胶囊 tab 同一条基线；
 *   tab 贴底后，选择模式让位出来的这条也必须贴底，
 *   否则会浮在 tab 原本的位置（屏底上方一条孤零零的卡片）。
 *   ⚠ padding-bottom 用 max(9px, --safe-b) —— 安全区那一截必须让出来，
 *     但桌面 inset=0 时仍留 9px 基础内边距，按钮不会贴着纸面。 */
body.ac-selecting .ac-actionbar { display: flex; }
/* 计数与按钮同一行高，视觉上是一条齐平的底栏 */
.ac-action-count {
  font-size: 11px; color: var(--ac-ink3); padding: 0 2px;
  white-space: nowrap; line-height: 42px;
}
.ac-actionbar button {
  /* 等宽平分剩余空间：按内容长度分配会把「报销」挤成一条缝 */
  flex: 1 1 0; min-width: 0;
  height: 42px; border-radius: 11px;
  border: 1px solid var(--ac-line); background: transparent;
  font-family: inherit; font-size: 14px; color: var(--ac-ink);
  display: flex; align-items: center; justify-content: center; gap: 5px;
  cursor: pointer; white-space: nowrap;
}
.ac-actionbar button svg { width: 14px; height: 14px; flex-shrink: 0; }
/* 纯图标键固定宽度，不参与等宽分配 */
.ac-actionbar button.icon-only { flex: 0 0 44px; padding: 0; }
.ac-actionbar button.icon-only svg { width: 16px; height: 16px; }
.ac-actionbar button.primary { background: var(--ac-accent); color: var(--primary-on); border-color: transparent; font-weight: 500; }
.ac-actionbar button:disabled { opacity: .38; cursor: not-allowed; }

/* ---------------- 抽屉（纸面） ---------------- */
.drawer-panel.ac-drawer {
  /* 抽屉主题色跟收支走：支出 = 砖红，收入 = 松绿 —— 与列表金额、收支段控同一套语义色。
     单独开一组变量而不是改 --ac-accent：那是全站墨绿，页面 hero / chip / 列表都在用，改了会全页变色。 */
  --ac-drawer-accent: var(--ac-expense);
  --ac-drawer-accent-soft: var(--ac-expense-soft);
  --ac-drawer-accent-line: var(--ac-expense-line);
  /* 实心按钮单独一档：accent 是「深底上的文字色」，不能直接当白字按钮底 */
  --ac-drawer-accent-solid: var(--ac-expense-solid);
  --drawer-accent: var(--ac-drawer-accent);
  /* 操作按钮统一高度：以编辑态的删除按钮为基准（31px），
     新增/编辑两态完全一致，抽屉高度不再跳变 */
  --ac-act-h: 31px;
  background: var(--ac-card);
  color: var(--ac-ink);
}
/* 收入态：面板挂 is-income（由 account-ui.js 的 syncType 统一切换） */
.drawer-panel.ac-drawer.is-income {
  --ac-drawer-accent: var(--ac-income);
  --ac-drawer-accent-soft: var(--ac-income-soft);
  --ac-drawer-accent-line: var(--ac-income-line);
  --ac-drawer-accent-solid: var(--ac-income-solid);
}
/* 账本抽屉的所有操作按钮共用一个高度 */
.drawer-panel.ac-drawer .drawer-action-btn { height: var(--ac-act-h); }
.ac-drawer .drawer-title { color: var(--ac-ink); }
.ac-drawer .drawer-sub { color: var(--ac-ink3); }
.ac-drawer .drawer-icon { background: var(--ac-drawer-accent-soft); color: var(--ac-drawer-accent); }
/* 顶部拖拽条也跟着主题色走，但压到 35~45% 透明度 —— 素纸页面要的是「暗示」不是「色块」 */
.ac-drawer .drawer-handle { background: var(--ac-drawer-accent-line); }

/* 记账表单 */
.ac-form { padding-bottom: 2px; }
.ac-seg {
  display: flex; gap: 18px;
  border-bottom: 1px solid var(--ac-line);
  margin: 2px 0 0;
}
.ac-seg-btn {
  position: relative; background: transparent; border: 0;
  font-family: inherit; font-size: 15px; color: var(--ac-ink3);
  padding: 6px 16px 9px; cursor: pointer; border-radius: 8px;
}
.ac-seg-btn.on { color: var(--ac-ink); font-weight: 600; }
.ac-seg-btn.on::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: -1px;
  height: 2px; border-radius: 2px; background: var(--ac-drawer-accent);
}
.ac-amount-row {
  display: flex; align-items: baseline; justify-content: center; gap: 3px;
  padding: 14px 0 12px;
}
.ac-cur { font-size: 16px; color: var(--ac-ink3); }
/* 抽屉里的 input 会被 style.css 的 .drawer-panel input 规则盖住
   （选择器更具体），这里用同样的 .drawer-panel 前缀反压回去 */
.drawer-panel .ac-amount {
  width: auto; flex: 0 1 auto; min-width: 130px; max-width: none;
  border: 0; background: transparent; padding: 0;
  font-family: var(--ac-serif); font-size: 36px; color: var(--ac-drawer-accent);
  text-align: left; outline: none;
  font-variant-numeric: tabular-nums;
}
.drawer-panel .ac-amount:focus { border: 0; box-shadow: none; }
.ac-amount::placeholder { color: var(--ac-ink3); opacity: .5; }
.ac-lbl { font-size: 13px; color: var(--ac-ink3); letter-spacing: .06em; margin: 3px 0 9px; }
html[data-theme="dark"] .ac-cat i { background: rgba(255, 255, 255, .06); }
html[data-theme="dark"] .ac-cat.on i { border-color: rgba(57, 197, 187, .52); }
/* 内联新建分类（记账抽屉里点「新建」） */
.ac-cat-create { grid-column: 1 / -1; display: flex; align-items: center; gap: 8px; }

.ac-field {
  display: flex; align-items: center; gap: 10px;
  padding: 13px 0; border-top: 1px solid var(--ac-line-soft);
}
.ac-field label { font-size: 14px; color: var(--ac-ink2); width: 40px; flex-shrink: 0; }
/* 日期字段一行：标签 + 两个快捷键（昨天 / 前天）+ 原生日期框。
   原来还有第三个键「今天」——去掉它腾出 60px，整行才放得下且不互相压边
   （三个键 + 日期框固有宽度 > 手机屏宽，硬挤会让快捷键的透明内边距
   盖住日期框的左边框，点日期框左缘其实点在快捷键上）。 */
.ac-date-chips { display: flex; gap: 8px; flex-shrink: 0; }
/* 日期快捷键：13px（与筛选 chip 同档）、内边距 7px 12px → 实测高 35px，
   手指点得中；字号仍是全站阶梯里的 13px，没另开半像素档 */
.ac-date-chips button {
  font-family: inherit; font-size: 13px; padding: 7px 12px; border-radius: 10px;
  border: 1px solid transparent; background: transparent; color: var(--ac-ink2); cursor: pointer;
  white-space: nowrap; flex-shrink: 0;
}
.ac-date-chips button.on {
  background: var(--ac-drawer-accent-soft); color: var(--ac-drawer-accent);
  border-color: var(--ac-drawer-accent-line); font-weight: 600;
}
/* 原生日期框 = 这行里真正持值的那一个，所以它**始终是选中态**：
   快捷键只是它的快捷入口 —— 点「昨天」写进这个框，框里的值再点亮对应的快捷键。
   两处用同一套配色，整行读起来是一个控件（而不是「一个框 + 两个孤立按钮」）。
   字号留 14px（与用途输入同档），只放大盒子，不改字号。 */
.drawer-panel .ac-date {
  /* 吃掉行内剩余空间（去掉「今天」后右侧还余 39px）—— 日期框是这行里
     唯一持值的控件，铺到右边缘既是更整的版式，也是更大的点击区 */
  width: auto; flex: 1 1 auto; min-width: 0;
  border: 1px solid var(--ac-drawer-accent-line); border-radius: 10px;
  background: var(--ac-drawer-accent-soft);
  color: var(--ac-drawer-accent);
  font-family: inherit; font-size: 14px; padding: 7px 9px;
}
.drawer-panel .ac-date:focus { border-color: var(--ac-drawer-accent); box-shadow: none; }
/* 日历图标压到半透明，免得抢日期数字的注意力 */
.drawer-panel .ac-date::-webkit-calendar-picker-indicator { opacity: .5; }
.drawer-panel .ac-purpose {
  flex: 1; min-width: 0;
  border: 0; background: transparent; padding: 2px 6px; outline: none;
  font-family: inherit; font-size: 14px; color: var(--ac-ink);
}
/* padding 不能省：输入框默认 overflow:clip，padding 为 0 时
   插入符贴着内容框左边缘，手机上渲染成更粗的一竖 → 左半截被裁掉；
   上下留 2px 也是给光标的高度留位置。 */
.drawer-panel .ac-purpose:focus { border: 0; box-shadow: none; }
/* 账本 / 分类管理里的内联新建表单输入框 */
.drawer-panel .ac-input {
  width: 100%; flex: none;
  border: 1px solid var(--ac-line); border-radius: 10px; background: transparent;
  padding: 9px 11px; font-family: inherit; font-size: 14px; color: var(--ac-ink);
}
.drawer-panel .ac-input:focus { border-color: var(--ac-accent-ink); box-shadow: none; }

/* 「待报销」toggle：常驻抽屉 header 右侧，做成开关形态。
   位置固定 → 切换收支只改按钮状态，抽屉高度恒定，不再闪。
   不画外边框：开关轨道本身就是唯一的状态载体，再套一圈框是多余的一层。 */
.drawer-action-btn.ac-pending-toggle {
  gap: 7px; padding: 0 8px 0 11px;
  font-size: 13px;
  background: transparent;
  border: 0;
  box-shadow: none;
  color: var(--ac-ink2);
}
.drawer-action-btn.ac-pending-toggle:hover {
  background: transparent; color: var(--ac-ink); transform: none;
}
.ac-toggle {
  width: 34px; height: 19px; border-radius: 999px;
  background: rgba(70, 58, 38, .16);
  position: relative; flex-shrink: 0;
  transition: background .18s ease;
}
.ac-toggle i {
  position: absolute; top: 2px; left: 2px;
  width: 15px; height: 15px; border-radius: 999px; background: #fff;
  box-shadow: 0 1px 2px rgba(0, 0, 0, .22);
  transition: left .18s ease;
}
html[data-theme="dark"] .ac-toggle { background: rgba(255, 255, 255, .16); }
/* 开态不铺背景色：只靠「文字变琥珀」表达状态。
   背景块会让 header 右侧多出一块实心色，与「开关轨道是唯一状态载体」的取法打架。 */
.drawer-action-btn.ac-pending-toggle.on {
  color: var(--ac-pending);
}
.drawer-action-btn.ac-pending-toggle.on .ac-toggle { background: var(--ac-pending); }
.drawer-action-btn.ac-pending-toggle.on .ac-toggle i { left: 17px; }

.ac-form-foot {
  position: sticky; bottom: 0;
  margin-top: 14px; padding: 10px 0 20px;
  display: flex; gap: 10px;
  background: linear-gradient(to bottom, rgba(255, 253, 249, 0) 0%, var(--ac-card) 30%);
}
/* 表单里可聚焦的控件要避开底部的粘性按钮条（sticky footer）：
   浏览器把焦点元素滚进视野时默认滚到内容区最底部 —— 那里正好压着按钮条，
   聚焦的输入框会整个躲在按钮下面，看不见自己打在哪。
   scroll-margin 让浏览器的「聚焦滚入视野」直接留出按钮条的高度。 */
.drawer-body .ac-form :is(input, textarea, select) {
  scroll-margin-bottom: 92px;
}
html[data-theme="dark"] .ac-form-foot {
  background: linear-gradient(to bottom, rgba(28, 28, 22, 0) 0%, var(--ac-card) 30%);
}
.ac-btn-ghost, .ac-btn-primary {
  flex: 1; height: 42px; border-radius: 12px;
  font-family: inherit; font-size: 14px; cursor: pointer;
}
.ac-btn-ghost { border: 1px solid var(--ac-line); background: transparent; color: var(--ac-ink); }
.ac-btn-primary { border: 0; background: var(--ac-drawer-accent-solid); color: #fff; font-weight: 500; }


/* ══════════════════════════════
   备忘 · JS 瀑布流（双列 flex）
══════════════════════════════ */

.ms-wall {
  display: flex;
  gap: 12px;
  width: 100%;
  box-sizing: border-box;
}
.ms-col {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* 工具条：同样吸顶 —— 搜索框与新增按钮常驻，搜着滚也方便 */
.mt-toolbar {
  display: flex;
  gap: 10px;
  margin: 0 -16px 12px;
  padding: 8px 16px;
  align-items: center;
  /* 不吸顶：吸顶会变成一条浮在便签上的横条，滚动时压住内容（用户 2026-10-21 反馈） */
}
.mt-search-wrap {
  flex: 1;
  position: relative;
  display: flex;
  align-items: center;
}
.mt-search {
  width: 100%;
  height: 36px;
  padding: 0 36px 0 13px;
  border: 1px solid var(--jr-line);
  border-radius: 4px;
  background: var(--jr-card);
  color: var(--jr-ink);
  font-size: 14px;
  outline: none;
  box-sizing: border-box;
  box-shadow: inset 0 1px 2px rgba(90, 70, 40, .07);
}
.mt-search:focus {
  border-color: var(--jr-hover-line);
}
.mt-clear-btn {
  position: absolute;
  right: 10px;
  width: 20px;
  height: 20px;
  border: none;
  background: var(--jr-ink3);
  /* 同 .ac-search-clear：用不透明底色保证 ✕ 字形可读 */
  color: var(--jr-card);
  border-radius: 50%;
  cursor: pointer;
  font-size: 11px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* 新增按钮：方章（手账风），不再是蓝色圆钮 */
.mt-add-btn {
  width: 36px;
  height: 36px;
  border-radius: 4px;
  background: linear-gradient(140deg, #16544d 0%, #0d3b38 100%);
  color: #f6f1e6;
  border: none;
  font-size: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex-shrink: 0;
  box-shadow: 0 3px 9px -3px rgba(74, 56, 25, .75);
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.mt-add-btn:active {
  transform: scale(0.94);
  box-shadow: 0 1px 4px rgba(5, 33, 31, .55);
}
html[data-theme="dark"] body.is-journal .mt-add-btn {
  background: linear-gradient(140deg, #3fc9bf 0%, #33b8ae 100%);
  color: #05211f;
  box-shadow: 0 3px 9px -3px rgba(0, 0, 0, .8);
}

/* 便签配色：JS 只写 data-color-idx（0–6），底色由 CSS 变量驱动（契约 §2.1）
   底色用 background-color（不是 background 简写）：简写会重置 background-image，
   把下面叠的纸纹冲掉。 */
:root {
  --memo-c0: #faf3e8; --memo-c1: #e8f5e9; --memo-c2: #e3f5fd; --memo-c3: #fce4ec;
  --memo-c4: #fff8e1; --memo-c5: #f3e5f5; --memo-c6: #e0f7fa;
  /* 纸纹：两层错位网点，浅色用黑点 */
  --memo-grain:
    radial-gradient(rgba(0, 0, 0, 0.045) 0.5px, transparent 0.6px),
    radial-gradient(rgba(0, 0, 0, 0.028) 0.5px, transparent 0.6px);
  /* 胶带：半透明一条，浅色偏白 */
  --memo-tape: rgba(255, 255, 255, 0.50);
  --memo-tape-edge: rgba(0, 0, 0, 0.10);
}
html[data-theme="dark"] {
  --memo-c0: #3d3526; --memo-c1: #263328; --memo-c2: #263340; --memo-c3: #3d2633;
  --memo-c4: #3d3a26; --memo-c5: #352640; --memo-c6: #26363d;
  /* 夜间用白点，同样的密度 */
  --memo-grain:
    radial-gradient(rgba(255, 255, 255, 0.050) 0.5px, transparent 0.6px),
    radial-gradient(rgba(255, 255, 255, 0.030) 0.5px, transparent 0.6px);
  --memo-tape: rgba(255, 255, 255, 0.10);
  --memo-tape-edge: rgba(255, 255, 255, 0.12);
}
.ms-card[data-color-idx="0"] { background-color: var(--memo-c0); }
.ms-card[data-color-idx="1"] { background-color: var(--memo-c1); }
.ms-card[data-color-idx="2"] { background-color: var(--memo-c2); }
.ms-card[data-color-idx="3"] { background-color: var(--memo-c3); }
.ms-card[data-color-idx="4"] { background-color: var(--memo-c4); }
.ms-card[data-color-idx="5"] { background-color: var(--memo-c5); }
.ms-card[data-color-idx="6"] { background-color: var(--memo-c6); }

/* 便签墙空状态（此前无任何样式） */
.ms-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 56px 16px;
  text-align: center;
  color: var(--jr-ink3);
}
.ms-empty-icon { font-size: 30px; line-height: 1; opacity: .75; }
.ms-empty-text { font-size: 15px; color: var(--jr-ink2); }
.ms-empty-hint { font-size: 13px; }

/* 便签卡片 - 便利贴风格 */
.ms-card {
  border-radius: 3px;
  padding: 16px 14px 14px 16px;
  position: relative;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
  cursor: pointer;
  box-shadow: 2px 4px 10px -5px rgba(90, 70, 40, .55);
  animation: stickyIn 0.4s ease both;
  /* 纸纹：叠在底色之上（底色见 data-color-idx） */
  background-image: var(--memo-grain);
  background-size: 4px 4px, 4px 4px;
  background-position: 0 0, 2px 2px;
}
/* 深色模式便签：仅调整阴影，底色由 --memo-cN 接管 */
:root[data-theme="dark"] .ms-card { box-shadow: 2px 4px 12px -5px rgba(0,0,0,.75); }

/* 便签悬浮效果 */
.ms-card:hover {
  transform: rotate(-1deg) translateY(-3px);
  box-shadow: 4px 8px 16px -6px rgba(90, 70, 40, .62);
  z-index: 1;   /* 局部层级：抬起来压过同排便签；不能碰全局层级 */
}

/* 时间淡化：30天以上 —— 只降便签底色饱和度，文字保持满对比
   原 opacity:.55 会连文字一起压暗，夜间几乎读不出来。正文是灰色系，
   saturate 对文字几乎无影响，只压得动便签底色。 */
.ms-card.faded {
  filter: saturate(0.55);
}
.ms-card.faded:hover {
  filter: saturate(1);
}

/* 便签内容 */
.mc-text {
  font-size: 13px;
  line-height: 1.6;
  color: var(--jr-ink);
  margin-bottom: 10px;
  word-break: break-word;
  /* 正文最多 3 行，超出的部分用省略号收住 ——
     长备忘不再把整张便签撑得高低不齐（瀑布流看着参差）。
     完整内容在点开后的编辑窗里看（点便签 = 打开编辑窗）。
     ⚠ -webkit-line-clamp 必须配 display:-webkit-box + -webkit-box-orient:vertical，
     原来的 display:flex 会让它失效（flex 容器不是 line-clamp 盒子）。 */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
}
/* 顶部胶带：半透明一条，露出在卡片上沿之外（落在 12px 卡片间距里，不压到邻卡） */
.ms-card::after {
  content: '';
  position: absolute;
  top: -6px;
  left: 50%;
  transform: translateX(-50%) rotate(-1.6deg);
  width: 42px;
  height: 14px;
  background: var(--memo-tape);
  border-left: 1px dashed var(--memo-tape-edge);
  border-right: 1px dashed var(--memo-tape-edge);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07);
  pointer-events: none;
}

.mc-footer {
  display: flex;
  justify-content: flex-end;
  align-items: center;
}
/* 日期用 --jr-ink2 而不是 --jr-ink3：10px 小字要过 WCAG AA 的 4.5:1，
   实测 ink3 只有 3.31:1（浅）/ 3.63:1（夜），ink2 是 5.87:1 / 6.28:1 */
.mc-date {
  font-family: var(--jr-serif);
  font-size: 10px;
  color: var(--jr-ink2);
  letter-spacing: .04em;
}

/* 入场动画 */
@keyframes stickyIn {
  0% { opacity: 0; transform: rotate(0deg) scale(0.8) translateY(20px); }
  60% { transform: rotate(0deg) scale(1.02) translateY(-2px); }
  100% { opacity: 1; transform: rotate(0deg) scale(1) translateY(0); }
}
.ms-card:nth-child(1) { animation-delay: 0.05s; }
.ms-card:nth-child(2) { animation-delay: 0.1s; }
.ms-card:nth-child(3) { animation-delay: 0.15s; }
.ms-card:nth-child(4) { animation-delay: 0.2s; }
.ms-card:nth-child(5) { animation-delay: 0.25s; }
.ms-card:nth-child(6) { animation-delay: 0.3s; }
.ms-card:nth-child(7) { animation-delay: 0.35s; }
.ms-card:nth-child(8) { animation-delay: 0.4s; }

/* 便签完成消失动画 */
@keyframes stickyOut {
  to { opacity: 0; transform: scale(0.5) rotate(10deg); }
}

/* ══════════════════════════════
   备忘 · 新增/编辑弹窗（.mm-*）
══════════════════════════════ */

.mm-modal {
  width: min(90vw, 420px);
  /* 高度上限跟着可视区走：100% 是遮罩层的内容盒高度
     （已被 --kb-inset 扣掉键盘那一截），min() 保证不超出可视区 */
  max-height: min(86vh, calc(100% - 16px));
  display: flex;
  flex-direction: column;
  background: var(--bg-modal);
  border: 1px solid var(--border);
  border-radius: 20px;
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  padding-bottom: env(safe-area-inset-bottom);
  animation: pk-scalein 0.22s ease;
}

.mm-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 20px 12px;
  border-bottom: 1px solid var(--border-light);
}
.mm-title {
  font-size: 16px;
  font-weight: 600;
  color: var(--text-primary);
  /* 删除按钮在标题**右边**（2026-10-27 用户口径，与待办抽屉同款）——
     标题占满剩余宽度，保持左对齐，不被挤到中间。 */
  flex: 1;
  min-width: 0;
}
.mm-btn-delete {
  display: flex;
  align-items: center;
  padding: 5px;
  border: none;
  border-radius: 8px;
  background: transparent;
  color: var(--danger-text);
  cursor: pointer;
  transition: background 0.2s ease;
  flex: none;   /* 别被标题挤扁 */
}
.mm-btn-delete:hover { background: var(--danger-light); }

.mm-body {
  padding: 14px 20px 10px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-height: 0;
  /* 可视区被键盘压矮时，内容在弹窗内部滚动，
     而不是把输入框挤到键盘底下 */
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* 底部一行：字数在左、保存按钮在右（2026-10-27 用户口径）。
   ⚠ 这一行是 .mm-modal 的**直接子级**，不进 .mm-body ——
     .mm-body 是可滚动区（键盘弹起时它会变矮），按钮放进去会被滚走。 */
.mm-foot {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 0 20px 16px;
}

/* 便签纸输入区 */
.mm-paper {
  position: relative;
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: 12px;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
/* 原先输入区左上角有个「小耳朵」装饰（与便签卡片的折角呼应）。
   折角已取消，这个装饰在纸底上只剩一个突兀的灰块 —— 一起去掉。 */
.mm-paper:focus-within {
  border-color: var(--primary-text);
  box-shadow: 0 0 0 3px var(--primary-light);
}

.mm-input {
  display: block;
  width: 100%;
  box-sizing: border-box;
  padding: 16px 14px 12px;
  border: none;
  background: transparent;
  color: var(--text-primary);
  font-size: 14px;
  line-height: 1.7;
  font-family: inherit;
  resize: none;
  outline: none;
}
.mm-input::placeholder { color: var(--text-placeholder); }

.mm-count {
  font-size: 11px;
  color: var(--text-muted);
  /* 在底部一行里靠左（2026-10-27） */
  text-align: left;
}

.mm-btn-save {
  padding: 6px 14px;
  border-radius: 9px;
  border: 1px solid transparent;
  background: var(--primary);
  color: var(--primary-on);
  font-size: 13px;
  cursor: pointer;
  transition: transform 0.15s ease, background 0.2s ease;
}
.mm-btn-save:active { transform: scale(0.96); }

/* ══════════════════════════════
   手账页的浮层：待办抽屉 / 备忘弹窗 / 确认框
   面板改纸底 + 纸纹，标题改衬线，输入框改纸色，保存按钮改墨色方章。
   这三个浮层都挂在 body 上，用 body.is-journal 限定作用域。
   ═══════════════════════════════ */
body.is-journal .s-modal,
body.is-journal .mm-modal {
  background-color: var(--jr-card);
  background-image: var(--jr-grain);
  background-size: 4px 4px, 4px 4px;
  background-position: 0 0, 2px 2px;
  border: 1px solid var(--jr-line);
  border-radius: 4px;
  box-shadow: 0 18px 40px -18px rgba(60, 45, 20, .55);
}
body.is-journal .s-modal-header,
body.is-journal .mm-header {
  border-bottom-color: var(--jr-line-soft);
}
body.is-journal .s-modal-title,
body.is-journal .mm-title {
  font-family: var(--jr-serif);
  font-size: 16px;
  font-weight: 700;
  color: var(--jr-ink);
}
body.is-journal .s-form-group label { color: var(--jr-ink2); }
body.is-journal .s-form-group textarea,
body.is-journal .s-form-group input,
body.is-journal .s-form-group select,
body.is-journal .mm-paper {
  background: var(--jr-paper);
  border: 1px solid var(--jr-line);
  border-radius: 4px;
  color: var(--jr-ink);
  box-shadow: inset 0 1px 2px rgba(90, 70, 40, .07);
}
body.is-journal .mm-paper { border-radius: 3px; }
body.is-journal .mm-input { color: var(--jr-ink); }
/* 字数计数是 11px 小字，用 --jr-ink2（--jr-ink3 只有 3.31:1，低于 AA） */
body.is-journal .mm-count { color: var(--jr-ink2); }
body.is-journal .s-form-group textarea:focus,
body.is-journal .s-form-group input:focus,
body.is-journal .s-form-group select:focus,
body.is-journal .mm-paper:focus-within {
  outline: none;
  border-color: var(--jr-hover-line);
  box-shadow: inset 0 1px 2px rgba(90, 70, 40, .07), 0 0 0 3px rgba(120, 96, 60, .10);
}
body.is-journal .s-modal-footer { border-top-color: var(--jr-line-soft); }
body.is-journal .s-btn-done {
  background: transparent;
  border: 1px solid var(--jr-line);
  color: var(--jr-ink2);
  border-radius: 4px;
}
body.is-journal .s-btn-save,
body.is-journal .mm-btn-save {
  background: linear-gradient(140deg, #16544d 0%, #0d3b38 100%);
  color: #f6f1e6;
  border-radius: 4px;
  box-shadow: 0 3px 8px -3px rgba(5, 33, 31, .6);
}
/* 夜间反相章：亮青底 + 深墨字 */
html[data-theme="dark"] body.is-journal .s-btn-save,
html[data-theme="dark"] body.is-journal .mm-btn-save {
  background: linear-gradient(140deg, #3fc9bf 0%, #33b8ae 100%);
  color: #05211f;
  box-shadow: 0 3px 8px -3px rgba(0, 0, 0, .7);
}
/* 确认框：面板纸化；主按钮保留砖红 —— 它在两处都是「删除 / 完成」，
   改成墨色会让人把删除读成普通操作 */
body.is-journal .pk-confirm {
  background-color: var(--jr-card);
  background-image: var(--jr-grain);
  background-size: 4px 4px, 4px 4px;
  background-position: 0 0, 2px 2px;
  border-color: var(--jr-line);
  border-radius: 4px;
}
body.is-journal .pk-confirm-msg { color: var(--jr-ink); font-family: var(--jr-serif); }
body.is-journal .pk-confirm-cancel {
  background: rgba(120, 96, 60, .06);
  color: var(--jr-ink2);
  border-radius: 4px;
}
body.is-journal .pk-confirm-ok { border-radius: 4px; }

.pk-overlay {
  position: fixed;
  inset: 0;
  /* 层级走全局阶梯 `--z-modal`：**必须在抽屉之上**。
     抽屉遮罩是 --z-drawer-mask、抽屉本体(.drawer-wrap)是 --z-drawer ——
     原来的 999 落在它们**下面**，于是「在抽屉里弹出的二次确认框」
     被抽屉盖住（用户 2026-10-23 报「被遮挡」：记忆抽屉点
     「清空全部 / 删除所选」的确认框看不见）。
     仍低于图片预览(--z-image)与 toast(--z-toast)。 */
  z-index: var(--z-modal);
  background: var(--modal-mask, rgba(0,0,0,0.4));
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  display: flex;
  align-items: center;
  justify-content: center;
  /* 键盘占位：弹窗在「可视区」里居中，而不是在「整屏」里居中 ——
     否则键盘一弹起，弹窗下半截（含输入框）就落到键盘底下点不到 */
  padding-bottom: var(--kb-inset, 0px);
  animation: pk-fadein 0.2s ease;
}
.pk-confirm {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 20px;
  padding: 24px 26px;
  min-width: min(240px, var(--pk-w));
  /* ⚠ 宽度封顶（2026-10-26，用户「文字排版优化，不要占满宽度，弹窗宽度也要缩小」）：
     以前**没有** max-width ⇒ 长文案把弹窗一路撑到整屏宽，
     一行字拉到天边（截图实测：清空对话那条弹窗几乎占满 430pt 的屏宽）。
     宽度走全局 token --pk-w（一个事实只说一次），正文才有可读的行长。 */
  max-width: var(--pk-w);
  box-shadow: 0 8px 32px rgba(0,0,0,0.2);
  animation: pk-scalein 0.2s ease;
}
/* 问题：一句话，加粗居中。
   ⚠ white-space: pre-wrap —— 以前**不保留换行**，调用方把「问题 + 解释」写进同一段
     \n\n 时，解释被压成一行长句（截图里「要重启 Hermes 网关？ 重启期间…」中间那个空格
     就是被吃掉的换行）。 */
.pk-confirm-msg {
  font-size: 15px;
  font-weight: 600;
  line-height: 1.55;
  color: var(--text-primary);
  text-align: center;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  margin-bottom: 20px;
}
/* 解释正文：与问题分层 —— 左对齐（段落居中难读）、字更小更轻。
   有它时问题与它之间只留 10px（.has-detail 收窄问题的下边距）。 */
.pk-confirm-detail {
  font-size: 13px;
  line-height: 1.7;
  color: var(--text-secondary);
  text-align: left;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  margin-bottom: 20px;
}
.pk-confirm-emphasis { color:var(--danger-text); font-weight:700; }
.pk-confirm.has-detail .pk-confirm-msg { margin-bottom: 10px; }
.pk-confirm-btns {
  display: flex;
  gap: 12px;
  justify-content: center;
}
.pk-confirm-cancel, .pk-confirm-ok {
  padding: 8px 20px;
  border-radius: 10px;
  border: none;
  font-size: 14px;
  cursor: pointer;
}
.pk-confirm-cancel {
  background: var(--bg-input);
  color: var(--text-secondary);
}
.pk-confirm-ok {
  background: var(--danger);
  color: #fff;}

/* 账本页（纸底）上的确认浮层：主按钮改用墨绿，避免「确认报销」被误读成危险操作 */
body.is-paper .pk-confirm {
  background: var(--ac-card);
  border-color: var(--ac-line);
}
.pk-confirm-btns { flex-direction:row; }
.pk-confirm-ok { order:0; background:var(--primary); color:var(--primary-on); }
.pk-confirm-cancel { order:1; }
.pk-confirm-ok:disabled { opacity:.48; cursor:not-allowed; }
body.is-paper .pk-confirm-msg { color: var(--ac-ink); }
body.is-paper .pk-confirm-cancel { background: rgba(70, 58, 38, .06); color: var(--ac-ink2); }
body.is-paper .pk-confirm-ok { background: var(--ac-accent); color: var(--primary-on); }

@keyframes pk-fadein { from { opacity: 0; } to { opacity: 1; } }
@keyframes pk-scalein { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }

/* ─── 只读弹窗（大篇幅信息用）───
   轻提示只有一句话的位置、2 秒就消失；报错正文要说的是
   「哪一步卡住 + 怎么补救 + 原数据有没有动」—— 那种内容弹窗才装得下。
   与 .pk-confirm 同族（同一张纸、同一套层级），只是**只有一个「知道了」**：
   它不是确认框，没有「确定 / 取消」。 */
.pk-dialog {
  /* 与 .pk-confirm 同一个宽度口径（--pk-w，一个事实只说一次） */
  max-width: var(--pk-w);
  max-height: min(80vh, 560px);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.pk-dialog-title {
  flex: none;
  font-size: 16px;
  font-weight: 700;
  color: var(--text-primary);
  text-align: center;
  margin-bottom: 10px;
}
.pk-dialog-msg {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  font-size: 14px;
  line-height: 1.6;
  color: var(--text-secondary);
  text-align: left;
  /* 报错正文常是多行的 —— 保留换行，别压成一坨 */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  margin-bottom: 16px;
}
/* 自定义内容区（要放控件的弹窗：下拉框等）——
   控件由调用方自己拼，这里只管版式。 */
.pk-dialog-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  margin-bottom: 16px;
}
.pk-dialog-ok {
  align-self: center;
  flex: none;
  padding: 9px 22px;
  border-radius: 10px;
  border: none;
  font-family: inherit;
  font-size: 14px;
  cursor: pointer;
  /* 「知道了」不是危险操作 —— 不沿用确认框的砖红主按钮 */
  background: var(--primary);
  color: var(--primary-on);
}

/* 弹窗 */
/* 日程弹窗（新建 / 编辑待办）
   ⚠ 底部安全区**归并进 footer**，弹窗本体不再单独垫一层
     （用户 2026-10-28：「日程弹窗 footer 下边距过大」——
      原来是 footer 自己 16px + .s-modal 的 env(safe-area-inset-bottom)
      两层叠在按钮下面，按钮离屏幕底远得不像话）。
   ⚠ 安全区仍要留 —— 按钮不能被系统导航条压住；只是挪到 footer 内部，
     和 footer 的 padding 合成一处，不再是「footer 高度 + 一整块安全区」。 */
.s-modal {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 16px;
  width: 90%;
  max-width: 380px;
  /* 高度上限跟着可视区走（同 .mm-modal）：键盘弹起时弹窗不能高过可视区 */
  max-height: min(86vh, calc(100% - 16px));
  display: flex;
  flex-direction: column;
  animation: pk-scalein 0.2s ease;
}
.s-modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 20px 12px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.s-modal-title { font-size: 16px; font-weight: 600; color: var(--text-primary); }
.s-modal-delete {
  background: transparent; border: none; color: var(--danger-text); cursor: pointer; padding: 4px;
  display: flex; align-items: center;
}
.s-modal-body {
  padding: 16px 20px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  /* 键盘把可视区压矮时，内容在弹窗内部滚动 */
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.s-form-row { display: flex; gap: 10px; }
.s-form-group-grow { flex: 1; }
.s-modal-footer {
  /* 底部安全区**并进 footer 自己的 padding**（用户 2026-10-28：「日程弹窗 footer 下边距过大」）。
     原来是 footer 16px + .s-modal 的 env(safe-area-inset-bottom) 两层叠在按钮下面，
     按钮离屏幕底远得不像话。现在只有一层：12px + 安全区。
     ⚠ 安全区不能丢 —— 按钮不能被系统导航条压住。 */
  padding: 12px 20px calc(12px + env(safe-area-inset-bottom));
  display: flex;
  gap: 10px;
  justify-content: flex-end;
  border-top: 1px solid var(--border);
  flex-shrink: 0;
}
.s-btn-done {
  background: var(--bg-input); color: var(--text-primary); border: 1px solid var(--border);
  border-radius: 10px; padding: 10px 20px; font-size: 15px; cursor: pointer; flex: 0 0 auto;
}
.s-btn-save {
  background: var(--primary); color: var(--primary-on); border: none; border-radius: 10px;
  padding: 10px 24px; font-size: 15px; cursor: pointer; flex: 1;
}
.s-form-group { display: flex; flex-direction: column; gap: 6px; }
.s-form-group label { font-size: 13px; color: var(--text-secondary); font-weight: 500; }
.s-form-group textarea, .s-form-group input, .s-form-group select {
  background: var(--bg-input); border: 1px solid var(--border);
  border-radius: 10px; padding: 10px 12px; font-size: 15px;
  color: var(--text-primary); resize: none; width: 100%; box-sizing: border-box;
  font-family: inherit;
}
.s-form-group textarea:focus, .s-form-group input:focus, .s-form-group select:focus {
  outline: none; border-color: var(--primary-text);
}

/* ══════════════════════════════
   控制台 · 仪表盘（V6）
═══════════════════════════════ */
/* 设计口径：docs/design/console-capabilities-decisions.md

   V6 推翻了 V4 的「三组清单」—— 用户 2026-10-23 说「可读性太差了，
   还不如使用仪表盘布局」，于是回到卡片式排布（V3 的壳），但**内容精简到三块**：
     ① Hermes 网关（总览卡：状态 + 操作）
     ② 默认模型（读 ~/.hermes/config.yaml，只给模型名 + 厂商）
     ③ 额度（按厂商选渲染方案：周期百分比 / ¥ 余额 / 「—」+ 原因）
   删掉：服务器状态组（内存 / 磁盘 / 运行时长 / OS / Python / Node）
        与本地数据组（待办·备忘 / 对话·附件）。

   三条硬约束：
     1. 一个事实只说一次 —— 地址、延迟、原因各只出现一次
     2. 「坏了」与「探不到」分开 —— 坏 = 砖红，探不到 = 中性灰，且写明原因
     3. 探不到就写「—」+ 原因，**绝不写 0%**（「查不到」≠「用光了」）

   值一律等宽字体 + tabular-nums，定时刷新时数字不左右跳。
   颜色全部取自全站令牌（style.css），不新增第三套语言：
     · 唯一实色 = 初音绿（--primary），只用在「活着」的东西上
     · 异常 = 砖红（--danger-text）
     · 底色 = 冷灰（与「对话」同族），纸底留给待办/备忘/账本 */
.cx {
  --cx-serif: 'Noto Serif SC', 'Times New Roman', 'Songti SC', 'STSong', SimSun, serif;
  --cx-mono: ui-monospace, 'JetBrains Mono', 'SF Mono', Menlo, Consolas, 'Courier New', monospace;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  /* 滚动容器顶到屏幕最上沿（沉浸式）—— 内容滚起来从透明顶栏底下穿过，
     机制与待办完全相同：.page-wrap 的顶栏占位由 body.is-bleed-top 去掉，
     首屏占位改由本容器的 padding-top 补（见上方 is-bleed-top 规则）。
     底部留白 = 悬浮 tab 的高度（--nav-space），最后一行停在 tab 上沿。 */
  padding-top: 0;
  padding-bottom: var(--nav-space, 85px);
  padding-left: 14px;
  padding-right: 14px;
  box-sizing: border-box;
}

/* ─── 卡面 ─── */
.cx-panel {
  background: var(--bg-card);
  border: var(--console-border-width) solid var(--border);
  border-radius: var(--console-radius);
  padding: 13px 14px 14px;
  margin-top: 10px;
}
.cx-panel:first-child { margin-top: 0; }
/* Static iridescent edge: no animation or moving light, including at rest. */
html[data-theme="dark"] .cx-panel {
  border-color: transparent;
  background: linear-gradient(var(--console-night-surface), var(--console-night-surface)) padding-box,
              var(--console-night-border) border-box;
  box-shadow: var(--console-night-shadow);
}
@media (forced-colors: active) {
  html[data-theme="dark"] .cx-panel { border-color: CanvasText; background: Canvas; }
}


.cx-card-title {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: .14em;
  color: var(--text-muted);
}

/* ─── 总览卡：Hermes 网关 ─── */
.cx-hero { padding: 12px; }

/* 标题行：标题在左，动作按钮在右。
   ⚠ 2026-10-25：原来两个按钮在**状态行**右侧，那一行挤了
     「圆点 + 26px 状态词 + 版本徽标 + 两个按钮」—— 全卡最密的一行。
     现在按钮挪到标题行（那儿原本空着），状态行只留状态本身。
     标题行必须能撑住宽度：标题 flex:1 + min-width:0（长标题才不会被挤没）。 */
.cx-hero-head {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 30px;
}
.cx-hero-head .cx-card-title { flex: 1; min-width: 0; }

/* 状态行：圆点 + 状态词 + 版本徽标。
   gap 9 → 7：版本徽标可能很长（max-width 34vw），gap 大一点更挤。
   ⚠ margin-top 归零 —— 上下间距改由 .cx-hero-body 统一给。
   ⚠ 2026-10-26：两栏现在**底部对齐**（.cx-hero-body 的 align-items: flex-end）——
     顶边不再对齐是有意为之：信息栏（两行）比按钮列（两个按钮）矮，齐底才平衡。 */
.cx-hero-top { display: flex; align-items: center; gap: 7px; }
.cx-dot {
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--text-muted);
  flex: none; position: relative;
}
.cx-dot::after {
  content: ''; position: absolute; inset: -4px; border-radius: 50%;
  background: var(--primary); opacity: 0;
}
/* ⚠ 取值必须与 JS 的 dataset.state 一致：ok / bad / none / wait。
   V3 这里写的是 [data-state="on"]，而 JS 写的是 'ok' —— 正常态圆点一直灰的。 */
.cx-dot[data-state="ok"] { background: var(--primary); }
.cx-dot[data-state="ok"]::after { animation: cx-pulse 2.4s cubic-bezier(.2, .6, .3, 1) infinite; }
.cx-dot[data-state="bad"] { background: var(--danger); }
.cx-dot[data-state="bad"]::after {
  background: var(--danger);
  animation: cx-pulse 2.4s cubic-bezier(.2, .6, .3, 1) infinite;
}
/* wait = 「**还没探到**」（2026-10-26 分级加载）：死是活还不知道 ⇒ 灰色。
   ⚠ 不用红色 —— 还没测完就说「坏了」是在说谎。
   ⚠ 不加循环动效 —— 全站循环动效只留一处（ok / bad 已在用）。 */
.cx-dot[data-state="wait"] { background: var(--text-muted); }
@keyframes cx-pulse {
  0%   { transform: scale(.55); opacity: .45; }
  100% { transform: scale(1.9); opacity: 0; }
}
.cx-word {
  font-family: var(--cx-serif);
  font-size: 26px;
  line-height: 1.15;
  letter-spacing: -.02em;
  color: var(--text-primary);
  /* ⚠ 状态词**不许断行** —— 挤窄时「运行中」会被拆成「运行 / 中」两行，难读。 */
  white-space: nowrap;
  flex: none;
}
.cx-word-bad { color: var(--danger-text); }
/* ⚠ 2026-11-05：动作区（.cx-hero-acts / .cx-refresh / .cx-refresh-wrap）
   随「重启网关 / 配置还原」两个按钮整块撤掉了 ⇒ 样式一并撤。 */

/* 左右分栏（2026-10-25 用户定案，第二次；2026-11-05 第三次）：
   左 = 状态（状态词 + 一行小字说明），右 = **三行键值**（版本 / 地址 / 延迟）。
   ⚠ 2026-11-05：右栏原来放两个动作按钮（重启网关 / 配置还原）⇒ 整块撤掉后
     右侧空出一大片（用户：「右侧那么空，可以增加信息量重新排版」）
     ⇒ 右栏改成键值列表，卡片高度不变（还是两栏）。
   ⚠ align-items: flex-end → center：右栏三行、左栏两行，
     齐底会显得左栏飘在半空 ⇒ 居中才平衡。 */
.cx-hero-body {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 4px;
}
.cx-hero-info { flex: 1; min-width: 0; }

/* 右栏：版本 / 地址 / 延迟 三行键值 */
.cx-hero-side {
  flex: none;
  width: 46%;
  max-width: 210px;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.cx-fact { display: flex; align-items: baseline; justify-content:flex-end; gap: 7px; min-width: 0; text-align:right; }
.cx-fact .k { flex: none; font-size: 11px; color: var(--text-muted); }
.cx-fact .v {
  flex: 1 1 auto; min-width: 0;
  font-family: var(--cx-mono); font-size: 12px; line-height: 15px;
  color: var(--text-secondary);
  /* ⚠ 值可能很长（版本号、地址）⇒ 省略号 + title（JS 写 title）。
     辅助信息可省略（正文与错误原文不适用 —— 见 §7.53 的口径）。 */
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* 左栏第二行：一句话说明（DSH 每轮起独立进程 / Hermes 进程号） */
.cx-hero-note {
  margin-top: 5px;
  font-size: 11.5px; line-height: 1.45;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}

.cx-busy { opacity: .5; pointer-events: none; }

/* ⚠ 2026-11-05：网关版本**徽标**（.cx-hero-ver）撤掉了 ——
   版本进右栏的「版本」行（徽标和右栏各写一次就是重复）。
   ⚠「这是不是真 Hermes」的提醒没丢：搬进右栏版本行的 title
     （mock_hermes.py 也让 /health 返 200，只写「运行中」是不如实）。
   ⇒ 这套样式一并撤，不留没人用的规则。 */

/* ⚠ 2026-11-05（用户：「网关状态不需要额外 sub 值文本描述，保持简洁」）：
   原来这组是「地址 + 延迟」那行的样式（.cx-hero-meta / .cx-hero-addr /
   .cx-hero-sep / .cx-hero-lat）—— 那行元素已从控制台模板撤掉 ⇒ 样式一并撤，
   不留没人用的规则。 */

/* ─── 原因行 ───
   上面一句人话，下面一句原始异常（小字）。原始异常不删 —— 那是真相。 */
.cx-why {
  margin: 8px 0 0;
  font-size: 13px;
  line-height: 1.55;
  color: var(--danger-text);
  /* 路径、URL 这类没有断点的串必须能断行，否则会把卡片撑宽 */
  overflow-wrap: anywhere;
}
.cx-why.neutral { color: var(--text-secondary); }
.cx-why small {
  display: block; margin-top: 3px;
  font-family: var(--cx-mono); font-size: 11px;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}
/* ⚠ 2026-11-05（用户：「参考模型卡使用滚动动画」）：
   原始异常超长时**横向滚动**，不换行把整张卡撑高（异常信息一长，卡能撑掉半屏）。
   · .cx-measure —— 测量时的临时态（单行，用来量「全文到底多宽」）；
   · .cx-scroll  —— 确认超长后的常态（单行 + 横向滚动动画）。
   ⚠ 减动效时 .cx-scroll 改为**换行全显**（见下面的 media 段）——
     不滚就换行全显，**不省略**（省略 = 信息没了）。 */
.cx-why small.cx-measure,
.cx-why small.cx-scroll { white-space: nowrap; overflow: hidden; }
/* 横向滚动的内容层：宿主裁剪（overflow:hidden），内容层自己平移。 */
.cx-marquee { display: inline-block; white-space: nowrap; max-width: none; vertical-align: top; }
/* 额度卡小字行里的**外链**（点一下直达厂商控制台）。
   主色 + 下划线：不加下划线用户不知道这行能点；
   字色用 --primary-text（站里既有的「可点/强调」字色，浅底上 4.67:1），不自造新颜色。 */
.cx-why a {
  color: var(--primary-text);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* ─── 小卡（默认模型 / 额度）───
   ⚠ 轨道必须写 minmax(0, 1fr) 而不是 1fr：`1fr` 的最小值是 auto（= 内容最小宽度），
   一段没有断点的长路径（Windows 的 C:\Users\...\config.yaml）会把整条轨道撑宽，
   两张卡一起溢出屏幕。minmax(0, 1fr) 允许轨道缩到内容宽度以下，让文字自己换行。 */
.cx-cards { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 10px; margin-top: 10px; }
.cx-cards > .cx-card { order:1; }
.cx-cards > .cx-card[data-card="quota"] { order:2; }
.cx-card { margin-top: 0; padding: 12px; display: flex; flex-direction: column; min-width: 0; }
.cx-card-main {
  /* ⚠ 2026-11-05（用户定案：「保持现有额度形态，模型卡降级」）：
     模型名降到与额度卡**进度条行同规格**（13px / 行盒 15px / 等宽）——
     两张小卡的 slot1（主值行）才会同高同基线 ⇒ 同行两卡真正视觉对齐。
     ⚠ 行高**必须钉死**（额度卡的 .cx-quota-line 行盒也是 15px），
       否则两边基线差一档，盯着看就是「歪的」。 */
  margin-top: 8px;
  font-family: var(--cx-mono);
  font-size: 13px; font-weight: 600; line-height: 15px;
  color: var(--text-primary);
  overflow-wrap: anywhere;
}
.cx-card-main.ok { color: var(--primary-text); }
.cx-card-main.bad { color: var(--danger-text); }
/* 「没有 / 查不到」这一档：字略大一点让人看得见，
   但**行盒仍是 15px** ⇒ 不破坏 slot1 的对齐（字号大 ≠ 行高高）。 */
.cx-card-main.none { color: var(--danger-text); font-weight: 700; font-size: 15px; line-height: 15px; }
[data-v="m.main"] { overflow: hidden; white-space: nowrap; overflow-wrap: normal; min-width: 0; }
.cx-model-ticker { display: inline-block; width: max-content; max-width: none; vertical-align: top; }
[data-v="m.main"].ok .cx-model-ticker {
  /* ⚠ 13px 的字上，8px 的光晕会糊成一团 ⇒ 收到 5px / .16 */
  text-shadow: 0 0 5px rgb(var(--primary-rgb) / .16);
}
@media (prefers-reduced-motion: reduce) {
  /* ⚠ 2026-11-05（用户：「保留滚动动画 + 换行兜底」）：
     不滚动时**换行全显**，不是省略号 —— 省略号 = 信息没了，
     模型名看不清等于不知道在用什么模型。 */
  [data-v="m.main"] { white-space: normal; overflow-wrap: anywhere; }
  .cx-model-ticker { display: inline; }
}
.cx-card-sub {
  /* ⚠ 2026-11-05：margin-top 3px → 8px、line-height 17px → 15px ——
     与额度卡第 2 条 .cx-quota-line（margin-top 8px / 行盒 15px）同规格
     ⇒ 两卡的 slot2（副值行）也在同一水平线上。 */
  margin-top: 8px;
  font-family: var(--cx-mono); font-size: 12px;
  /* ⚠ 行高**必须钉死**（2026-10-30 Windows 实测）：
     12px 字体的 `line-height: normal` 在本机是 **14px/行**、Linux 上是 17px/行
     ⇒ 而骨架 `.cx-pending.cx-card-sub` 的 min-height 必须与它**同值**，
     ⇒ 两态恒等，跨平台恒等（骨架已同步改成 15px）。 */
  line-height: 15px;
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
  overflow-wrap: anywhere;
}
.cx-card-meta {
  margin-top: auto; padding-top: 8px;
  font-family: var(--cx-mono); font-size: 11px;
  /* ⚠ 同上：底行是**两行**固定文案 ⇒ 行高必须确定，
     否则 8 + 2×(平台行高) 会漂（11px 等宽：Linux 15px/行、Windows 也 15px/行，
     但这属于运气，不是保证）⇒ 钉死，两态恒等于 min-height 38px。 */
  line-height: 15px;
  color: var(--text-muted);
  overflow-wrap: anywhere;
  /* 额度底行要写两行（厂商 + 「查于 HH:MM:SS」）—— pre-line 才认 \n */
  white-space: pre-line;
}

/* ⚠ 2026-11-06：模型卡的「切换模型」入口**整套撤除**（改 Hermes 网关配置 + 重启）——
   下面这一组样式连同 JS 里的弹窗一起删掉：
   .cx-card-title-row / .cx-card-go / .cx-card-clickable /
   .cx-md-body / .cx-md-cur / .cx-md-detail / .cx-mf-* —— 不留死代码。 */
.cx-geometry-locked { min-height: var(--cx-reserved-height); }

/* ─── 整行大卡（记忆栈 / 运维）───
   记忆栈比「模型 / 额度」更值得一块有分量的地方：服务挂了，艾玛就记不住事了。
   ⚠ 最高度封顶 —— 原因行（不可达时的原始报错）可能很长，
     不封顶就会把整页顶出一屏，那就不是仪表盘了。 */
.cx-full { margin-top: 10px; padding: 12px; }
.cx-mem { max-height: 132px; overflow: hidden; cursor: pointer; }
.cx-mem:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
/* 标题行：左边「记忆栈」，右边入口提示（「查看记忆 ›」）。
   ⚠ 提示**不另起一行** —— 另起一行就得在加载态先占住那行的高度，
     否则数据到了才长出来 = 高度跳动（实测 +37px）。
     标题行加载前后都在 → 几何天然不变。 */
.cx-mem-title {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}
/* ⚠ 2026-10-26（用户要求）：插件名 + 版本从底行挪到**标题后** ——
   原来「Hy-Memory 2.0.4 · 地址 · 归属」四段挤在底行（版本号跟端口路径并排）。
   ⇒ 标题后是一个小号的淡色徽标，底行只留地址 + 归属。
   ⚠ 徽标空着的时候**不留高度**（行盒由标题文字撑着）⇒ 加载前后 Δ0px 仍成立。 */
.cx-mem-name {
  display: flex;
  align-items: baseline;
  gap: 7px;
  min-width: 0;
}
.cx-mem-ver {
  font-family: var(--cx-mono);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .01em;
  color: var(--text-muted);
  white-space: nowrap;
}
.cx-mem-go {
  flex: none;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .02em;
  color: var(--primary-text);
}
.cx-mem-go[hidden] { display: none; }
.cx-mem-body {
  display: flex;
  align-items: center;
  gap: 9px;
  margin-top: 7px;
}
.cx-mem-word {
  font-family: var(--cx-serif);
  font-size: 21px;
  line-height: 1.2;
  letter-spacing: -.01em;
  color: var(--text-primary);
}
.cx-mem-word.ok { color: var(--primary-text); }
.cx-mem-word.bad { color: var(--danger-text); }
.cx-mem-word.none { color: var(--text-muted); }
.cx-mem-count {
  margin-left: auto;
  /* ⚠ 2026-11-05（用户：「不可以溢出」）：
     原来 flex:none ⇒ 条数一多（十万级）就撑出卡外。
     ⇒ 改成可缩 + 数字可断行（断行是撑高一行，截断才是丢信息）。 */
  flex: 0 1 auto; min-width: 0;
  display: flex;
  align-items: baseline;
  gap: 3px;
}
.cx-mem-count b {
  font-family: var(--cx-mono);
  font-size: 21px;
  /* ⚠ 2026-10-25（实测出来的亚像素坑）：
     这里**必须写死行高** —— 加载态显示「—」、加载后显示「3」，
     破折号与数字的行框高度差 0.19px（字体字形不同）→
     记忆栈卡高度 96.00 → 96.19 → 它下面所有卡跟着位移 0.19px，
     探针取整后显示成「Δ1px」（其实人眼根本看不出来）。
     写死 line-height 后行框与字形无关 → 加载前后完全一致。
     取 1.2 是为了跟旁边的状态词（.cx-mem-word）同高。 */
  line-height: 1.2;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}
.cx-mem-count b.ok { color: var(--primary-text); }
.cx-mem-count b.none { color: var(--text-muted); }
.cx-mem-count i { font-style: normal; font-size: 11px; color: var(--text-muted); }
/* ⚠ 2026-11-05：.cx-mem-line（记忆栈卡底行「地址 · 归属」）已随那行元素撤掉。 */

/* ─── 额度卡：按「额度形态」渲染，样式不认识厂商 ───
   · 周期型 → N 条「标签 + 进度条 + 百分比」（N 与标签由后端给）
   · 金额型 → 一个金额大字
   两者都不是「活着的东西」，所以数字用中性墨色；颜色留给进度条。 */
.cx-quota { display: flex; flex-direction: column; flex: 1; min-height: 0; min-width: 0; }
.cx-quota .cx-card-meta { margin-top: auto; }
.cx-quota-line { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
/* 标签位 48px = 4 个全角字（11px 字号）——
   ⚠ 标签**约定 ≤4 字**，更长的说法写 caption（后端的活）。
     nowrap 保证它不换行（换行会把整行高度撑高，跳动）。
     不用 ellipsis：宁可让标签挤一点，也不砍字。 */
.cx-quota-label {
  flex: none; width: 48px; font-size: 11px; color: var(--text-secondary);
  white-space: nowrap;
  /* ⚠ 行高**必须钉死**（2026-10-30 Windows 实测）：
     真实标签是 11px 字体 + `line-height: normal` ⇒ 行盒高度**随平台字体漂**
     （Linux 15px/行、Windows 16px/行）⇒ 而骨架 `.cx-quota-skel .cx-quota-label`
     写死 `height: 15px` ⇒ 两态差 1px/行（两行就差 2px）⇒ 额度卡加载后长高。
     ⇒ 钉成 15px：Linux 上不变，Windows 上与 Linux 一致，两态恒等。 */
  line-height: 15px;
  /* ⚠ 2026-11-05（用户：「不可以溢出」）：
     标签超过 4 个全角字（后端偶尔给长标签）时**不能压到进度条上** ——
     固定 48px 宽 + nowrap ⇒ 文字直接画到进度条那一格。
     ⇒ 宽度不变（两行进度条起点要对齐），超出的部分**省略号**，
       完整文字挂 title（JS 写）⇒ 不溢出、不压进度条、信息还能悬停看全。 */
  overflow: hidden; text-overflow: ellipsis;
}
.cx-quota-bar {
  flex: 1; height: 6px; border-radius: 3px;
  background: var(--border); overflow: hidden;
}
.cx-quota-bar i { display: block; height: 100%; border-radius: 3px; background: var(--primary); }
.cx-quota-bar i.bad { background: var(--danger); }
.cx-quota-pct {
  flex: none; min-width: 38px; text-align: right;
  font-family: var(--cx-mono); font-size: 13px;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
  /* ⚠ 同上：钉死行高，别让平台字体决定行盒（骨架那边写死 15px）。 */
  line-height: 15px;
}
.cx-quota-pct.bad { color: var(--danger-text); }
.cx-quota-pct.none { color: var(--text-muted); }
.cx-quota-balance {
  margin-top: 5px;
  font-size: 24px; font-weight: 800; line-height: 1.15;
  letter-spacing: -.01em;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}

/* ─── 控制台加载态（骨架 + 数字落地滚动）──────────────────────────────
   用户 2026-10-24：「控制台卡片加载态需要设计，使用跳动数字或者骨架动画，
   数据加载前后不可以产生高度跳动」

   两条一起做，缺一不可：
   ① **几何先占住**（下面的 min-height + 骨架块）→ 加载前后卡片高度 0 变化；
      实测（tools/probe_console_loading.py）修之前：
      默认模型卡 90→120px、额度卡 90→120px、记忆栈卡 79→91px，
      下面的「我的数据」卡被顶下去 42px。
   ② **数据到了数字滚一下**（JS 的 countUp）→ 眼睛知道「数出来了」。
      滚的是**真值**，不是加载时那种假数字 —— 假数字会骗人。

   骨架只在**首屏**出现：轮询刷新时保留上一次的读数
   （控制台的老规矩：取不到就保持上一次读数，不清空）。
   轮询要是也清成骨架，界面会一直闪。 */
.cx-card { min-height: 120px; }   /* 两张小卡：额度卡内容最富（2 条 + 底行） */
.cx-persona { min-height:0; padding:12px; overflow:hidden; position:relative; isolation:isolate; }
.cx-persona::before {
  content:""; position:absolute; z-index:0; inset:0;
  background:url('/assets/persona-card/emma-sunglasses.webp') left center / auto 100% no-repeat;
  opacity:.16; pointer-events:none;
  mask-image:linear-gradient(90deg,#000 0%,#000 42%,transparent 100%);
}
.cx-persona > * { position:relative; z-index:1; }
@media (forced-colors:active) { .cx-persona::before { display:none; } }
.cx-cards > .cx-full { grid-column:1 / -1; }
.cx-persona-head { display:flex; align-items:center; justify-content:space-between; min-height:24px; }
.cx-persona-fields { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:10px; margin:12px 0; }
.cx-persona-field { display:grid; grid-template-columns:minmax(0,1fr); gap:6px; align-content:center; min-width:0; padding:4px 2px; border:0; border-radius:0; background:transparent; }
.cx-persona-label { color:var(--text-primary); font-size:13px; font-weight:600; }
.cx-persona-field select {
  appearance:none; -webkit-appearance:none;
  width:100%; min-width:0; padding:8px 22px 8px 9px;
  border:1px solid var(--border); border-radius:9px;
  background-color:var(--surface);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='m3 4.5 3 3 3-3' fill='none' stroke='%23777' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5'/%3E%3C/svg%3E");
  background-position:right 9px center; background-repeat:no-repeat; background-size:12px 12px;
  color:var(--text-primary); font:inherit; font-size:12px;
}
.cx-persona-foot button:focus-visible { outline:2px solid var(--primary); outline-offset:2px; }
.cx-persona-foot { display:flex; justify-content:flex-end; align-items:center; gap:10px; padding-top:10px; border-top:1px solid var(--border); }
.cx-persona-foot span { margin-right:auto; min-height:16px; color:var(--text-muted); font-size:11px; }
.cx-persona-foot span.is-error { color:var(--danger-text); }
.cx-persona-foot button { padding:8px 14px; border:0; border-radius:10px; background:var(--primary); color:var(--primary-on); font-size:12px; font-weight:600; cursor:pointer; }
.cx-persona-foot button:disabled { opacity:.55; cursor:wait; }
@media (max-width:560px) { .cx-persona-fields { grid-template-columns:repeat(2,minmax(0,1fr)); } }
.cx-why, [data-v="m.meta"] { display:none !important; }
.cx-mem  { min-height: 97px; }    /* 记忆栈：状态行 + 地址行（字号上调后按实测高度给）
   ⚠ 96 → 97：地址行那格的真值内容实测 96.19px，比 min-height 96 多 0.19px →
     加载态（内容更少，min-height 兜底）= 96，加载后 = 96.19 →
     它下面所有卡跟着位移 0.19px（探针取整显示成 Δ1px）。
     抬到 97 → 两态都锁在 97，整条竖列的位置不再漂。
     （顺带把地址行的占位符从「空」改成「—」：空元素没有行框，
      加载态就少占 13px —— 占位符必须和真值一样占位，这是项目的通例。） */
/* 总览卡：标题行 + 左右分栏（信息两行 / 按钮两枚）—— 2026-10-25 改成左右布局 */
.cx-hero { min-height: 128px; }
/* 健康卡：4 个指标格 + 探测条 / 最后检测 / 自动刷新（原页脚，移进来了）。 */
.cx-health-card { min-height: 266px; }

/* 骨架块：盖在数据位上。
   ⚠ 元素本身**不隐藏**（只把字色变透明）—— 高度还是它自己的文字撑的，
   所以骨架块的高度天然等于真实内容的高度 → 加载态与加载后**同高**。

   ⚠ 2026-10-26 从「整页一个 .cx-loading」改成**槽位级 .cx-pending**：
   整页骨架 = 整页在等最慢的那一个端点（实测网关不可达时 overview 要
   2077–3532ms，而 model-config 只要 13ms）。
   现在 pending 挂在**槽位自己**身上 ⇒ 谁的数据到了谁撤，其余继续占位。 */
.cx-pending {
  position: relative;
  color: transparent !important;
  -webkit-text-fill-color: transparent;
  text-shadow: none;
}
.cx-pending * { color: transparent !important; -webkit-text-fill-color: transparent; }
.cx-pending::before {
  content: '';
  position: absolute;
  left: 0; top: 0;
  width: var(--sk-w, 62%);
  height: 1em;
  border-radius: 4px;
  background: linear-gradient(90deg,
    rgba(70,58,38,.09) 0%, rgba(70,58,38,.16) 50%, rgba(70,58,38,.09) 100%);
  background-size: 220% 100%;
  animation: cxShimmer 1.35s linear infinite;
}
@keyframes cxShimmer {
  0%   { background-position: 120% 0; }
  100% { background-position: -20% 0; }
}
/* 各数据位的骨架宽度（按真实内容的大致长度给）
   ⚠ 2026-11-05：#cxAddr / #cxLat / [data-v="mem.meta"] 三处已随元素撤掉。 */
.cx-pending[data-v="m.main"]  { --sk-w: 58%; }
.cx-pending[data-v="m.sub"]   { --sk-w: 38%; }
.cx-pending[data-v="m.meta"]  { --sk-w: 70%; }
.cx-pending[data-v="mem.main"]{ --sk-w: 42px; }
.cx-pending[data-v="mem.num"] { --sk-w: 30px; }
/* 计数那格是行内元素 → 得给它一个自己的盒子，骨架块才有高度 */
.cx-pending[data-v="mem.num"] { display: inline-block; }
/* 额度卡的骨架：两条进度 + 一行底行，结构跟真实内容一致
   （首屏模板里就摆着这三条，数据到了 renderQuota 整块换掉） */
.cx-quota-loading .cx-quota-skel .cx-quota-label {
  background: rgba(70,58,38,.09);
  border-radius: 3px;
  /* ⚠️ 高度必须 = 真实行的行盒高度。
     ⚠⚠ 2026-10-30 实测：真实 .cx-quota-line 是 **15px**（13px 的 .cx-quota-pct
     撑起来的 line-height normal），之前写 16px ⇒ 骨架行比真实行高 1px，
     两行就差 2px ⇒ 数据一到额度卡缩 2px ⇒ 「两张小卡那一行」131 → 127
     （probe_console_loading 实测 Δ-4px，修完底行后剩下的那 2px 就是它）。 */
  height: 15px;
}
.cx-quota-loading .cx-quota-skel .cx-quota-bar {
  background: rgba(70,58,38,.09);
  position: relative;
  overflow: hidden;
}
.cx-quota-loading .cx-quota-skel .cx-quota-bar::after {
  content: '';
  position: absolute; inset: 0;
  background: linear-gradient(90deg,
    rgba(70,58,38,.05) 0%, rgba(70,58,38,.14) 50%, rgba(70,58,38,.05) 100%);
  background-size: 220% 100%;
  animation: cxShimmer 1.35s linear infinite;
}
.cx-quota-loading .cx-quota-skel .cx-quota-pct {
  background: rgba(70,58,38,.09);
  border-radius: 3px;
  /* ⚠️ 同上：13px 字体的 line-height normal = 15px（真实值，不是 12px 也不是 14px） */
  height: 15px;
}
/* ⚠ 额度卡的底行**没有** .cx-slot class（它的骨架是结构化的）⇒
   上面的 .cx-pending 规则管不到它 ⇒ 加载态少了 26px 占位
   （实测「两张小卡那一行」121 → 126，Δ5px）。
   ⇒ 额度卡自己占位：加载态的底行高度 = 真值。 */

/* ⚠⚠ 占位高度**必须按本机字体实测**，不能照抄别的机器的数（2026-10-26 修）
   ─────────────────────────────────────────────────────────────
   远程提交里写的是 34px / 14px —— 那是 **Windows 开发机**上 11px ui-monospace
   的行高（13px/行）算出来的。本机 Linux 上同一段 CSS 的真实行高不同：

     · 模型卡底行「读自 ~/.hermes/config.yaml」（换行成 2 行）真实 **36px**
     · 额度卡底行「厂商 + 查于时刻」（2 行）真实 **40px**
     · 模型卡副行「厂商 xxx」真实 **17px**

   照抄 34px ⇒ 加载态比真值矮 ⇒ 数据一到卡片**长高** 6px ⇒
   「两张小卡那一行」126 → 132，其下三张卡整体被推下 6px
   （probe_console_loading 实测 6 项不达标；远程声称 Δ0px，本机不成立）。

   判据：**占位高度 = 该元素真值高度**，不是「差不多够」。
   换机器 / 换字体后若探针又报 Δ，量一遍真实行盒再改这两个数。

   ⚠⚠ 2026-10-30（回归实测推翻上面这套做法）：
     就算把 min-height 量得**一模一样**（都写 40px），也还是会跳 ——
     因为**真值本身不是 40px**：模型卡底行实测 **35px**、额度卡底行实测 **38px**
     （11px 等宽字体 line-height:normal = 15px/行，不是 16px；
       padding-top 8px + 2 行 15px = 38px，比写死的 40px 少 2px）。
     于是「占位 40px vs 真值 35/38px」⇒ 数据一到卡片**缩 2~5px** ⇒
     「两张小卡那一行」131 → 127（Δ-4px），下面三张卡整体上移 4px。

   ⇒ 正确做法不是「把占位调成真值」，而是**让底行的高度由 CSS 决定**：
     给 `.cx-card-meta` 一个固定的 min-height（按真值取最大的那个），
     **加载态和真态都吃这一条** ⇒ 两边必然一致 ⇒ 几何天然不变。
     占位（min-height）只作为「内容比预期短时」的兜底，
     真值超出时由内容撑开 —— 但真值是**两行固定文案**，不会超出。 */
.cx-card-meta {
  /* 两行 11px 等宽（line-height normal ≈ 15px）+ padding-top 8px = 38px。
     取 38px：这是额度卡底行（两行）的真值，也是模型卡底行（两行）的真值上界。 */
  min-height: 38px;
}
/* 加载态的底行是空的 → 靠上面那条占住位置（内容空，min-height 生效）。
   额度卡的底行**没有** .cx-slot class（它的骨架是结构化的）⇒
   .cx-pending 那条管不到它 ⇒ 这里单独补一条，但高度**跟上面一致**，
   两边才不会一个 38 一个 40。 */
.cx-quota-loading .cx-card-meta:empty {
  min-height: 38px;
}
.cx-pending.cx-card-meta:empty {
  min-height: 38px;
}
/* 「—」占位撑出来的行高跟真实副行对齐（12px 字体、line-height 钉死 15px）。
   占位比真实**高** 2px 同样算跳动 —— 数据一到卡片会缩 2px。
   ⚠ 2026-11-05：真实副行降到 15px（与额度卡 slot2 同规格）⇒ 占位同步降到 15px。 */
.cx-pending.cx-card-sub { min-height: 15px; }
/* 底行有 padding-top:8px → 骨架块要往下挪，不然浮在字上面 */
.cx-pending.cx-card-meta::before { top: 8px; }
/* 系统开了「减少动态效果」→ 骨架不流光（静态灰块），数字也不滚动 */
@media (prefers-reduced-motion: reduce) {
  .cx-pending::before,
  .cx-quota-loading .cx-quota-skel .cx-quota-bar::after { animation: none; }
  /* ⚠ 2026-11-05：横向滚动改为**换行全显** —— 不滚的时候不能省略，
     原始异常是「真相」，省略了用户就查不出问题在哪。 */
  .cx-why small.cx-scroll { white-space: normal; overflow-wrap: anywhere; }
}

/* ── 落地微光（2026-10-26）──────────────────────────────────────────
   数据落到某张卡上时，给那张卡一次**极短的青色微光** ——
   「这一格刚刚落地」一眼可见，又不刺眼。
   ⚠ 只给**这一张卡**（整页闪一下等于没分级）。
   ⚠ 动画在**元素内**做（不位移、不改尺寸）—— 项目硬要求：状态切换不许改位置。
   ⚠ prefers-reduced-motion 下自动关（见上面那条 media 查询）。 */
.cx-landed {
  animation: cxLanded .65s ease-out;
}
@keyframes cxLanded {
  0%, 100% { border-color: var(--border); box-shadow: inset 0 0 0 1px transparent; }
  35% { border-color: var(--primary-border); box-shadow: inset 0 0 0 1px rgb(var(--primary-rgb) / .45); }
}
.cx-requesting .cx-slot { position: relative; color: transparent !important; }
.cx-requesting .cx-slot { -webkit-text-fill-color: transparent; text-shadow: none; }
.cx-requesting .cx-mem-count i { visibility: hidden; }
.cx-foot.cx-requesting #cxChecked,
.cx-foot.cx-requesting #cxCap,
.cx-foot.cx-requesting #cxBand { position: relative; color: transparent !important; -webkit-text-fill-color: transparent; }
.cx-foot.cx-requesting #cxChecked::before,
.cx-foot.cx-requesting #cxCap::before,
.cx-foot.cx-requesting #cxBand::before { content: ''; position: absolute; inset: 0; border-radius: 4px;
  background: linear-gradient(90deg, var(--border-light), var(--border), var(--border-light));
  background-size: 220% 100%; animation: cxShimmer 1.35s linear infinite; }
#cxChecked { min-width: 58px; min-height: 1em; }
#cxCap { min-width: 90px; min-height: 1em; }
.cx-hero-top { flex-wrap: wrap; }
.cx-hero-info { min-width: 0; }
.cx-hero .cx-why { overflow-wrap: anywhere; }
/* ⚠ 2026-11-05：.cx-hero-ver（版本徽标）已撤 ⇒ 这条 flex-shrink 规则一并撤。 */
.cx-requesting .cx-slot::before { content: ''; position: absolute; left: 0; top: 0;
  width: var(--sk-w, 62%); height: 1em; border-radius: 4px;
  background: linear-gradient(90deg, var(--border-light), var(--border), var(--border-light));
  background-size: 220% 100%; animation: cxShimmer 1.35s linear infinite; }
.cx-requesting #cxQuota { position: relative; }
.cx-requesting #cxQuota > * { visibility: hidden; }
.cx-requesting #cxQuota::after { content: ''; position: absolute; inset: 8px 0;
  border-radius: 6px; background: linear-gradient(90deg, var(--border-light), var(--border), var(--border-light));
  background-size: 220% 100%; animation: cxShimmer 1.35s linear infinite; pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
  .cx-landed { animation: none; }
}

/* 记忆栈 → 记忆列表的入口提示（在标题行里，不是独立一行 —— 见 .cx-mem-title）。
   可点声明走最少一套：cursor + 原生 title + role/tabindex，**不加提示浮层**。 */
.cx-mem-go:active { transform: scale(.97); }

/* ─── 我的数据：三排两列的卡片/按钮 ───
   ⚠ 2026-10-26（用户定案）：从「一列行式按钮」改成**三排两列的卡片**。
     第一行：对话数据、数据体积（只读格）
     第二行：导入备份、导出备份
     第三行：清空对话、重置记忆（危险）
   ⇒ 6 格正好排满 2×3 网格，顺序即 DOM 顺序（grid 自动按行填）。
   ⚠ 每格**等高**：只读格（标签 + 值）与按钮格（标题 + 尾部）用同一套内边距，
     值排一行 ⇒ 加载前后 Δ0px 仍成立。 */
.cx-ops-card { padding: 12px; }
/* ⚠ 2026-10-26（用户定案，第二次）：「我的数据」卡现在是
     **小方格在上（4 列 × 2 行）+ 四个按钮在下（2 列 × 2 行）**。
   「对话数据」「数据体积」两个只读小卡片**已撤掉** ——
   它们的数字并进了上面的小方格（向量体积 / 对话轮数），
   所以这里只剩四个**动作按钮**。 */
.cx-ops {
  margin-top: 9px;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 7px 9px;
}
/* 按钮格：只有一个动作名（右侧那句重复的「导入 ›」之类的 label 已移除 ——
   按钮本身就是动作，再挂一个说法是废话）。 */
.cx-op {
  width: 100%;
  min-width: 0;
  min-height: 42px;
  display: flex;
  align-items: center;
  padding: 8px 10px;
  border: 1px solid var(--border-light);
  border-radius: 9px;
  background: var(--bg-input);
  font-family: inherit;
  font-size: 13px;
  color: var(--text-primary);
  text-align: left;
  cursor: pointer;
  transition: opacity .16s;
  /* 值不许被截断（截断了用户不知道这个按钮干什么）—— 放不下就换行 */
  white-space: pre-line;
  overflow-wrap: anywhere;
}
.cx-op:active { opacity: .6; }
.cx-op-danger { color: var(--danger-text); }

/* ⚠ 2026-10-26（用户要求）：四个按钮**右侧**各配一个图标 ——
   icon 取自项目图标库（EMMA.icons，stroke 线性，svg 自带 class="icon"，
   全局 .icon 规则给了 fill:none / stroke:currentColor ⇒ 颜色随按钮走）。
   ⚠ 图标在**右侧**：margin-left:auto 把它推到行尾；
     按钮不因图标而变宽（图标 flex:none，文字那一项可压缩）。 */
.cx-op-ic {
  flex: none;
  margin-left: auto;
  padding-left: 6px;
  display: inline-flex;
  align-items: center;
  color: var(--text-muted);
  opacity: .8;
}
.cx-op-ic .icon { width: 14px; height: 14px; }
.cx-op:hover .cx-op-ic { opacity: 1; color: var(--text-secondary); }
.cx-op-danger .cx-op-ic { color: var(--danger-text); opacity: .75; }
.cx-op-danger:hover .cx-op-ic { opacity: 1; }

/* 导入结果：常驻在「我的数据」卡里 —— toast 只有 2 秒，装不下「什么没还原」，
   而那恰恰是用户必须知道的话。 */
.cx-op-result {
  margin-top: 10px;
  padding: 9px 10px;
  border: 1px solid var(--border-light);
  border-radius: 8px;
  background: var(--bg-input);
  font-size: 12px;
  line-height: 1.6;
  color: var(--text-secondary);
}
.cx-op-result[hidden] { display: none; }
.cx-op-result b { color: var(--text-primary); font-weight: 600; }
.cx-op-result ul { margin: 6px 0 0; padding-left: 16px; }
.cx-op-result li { margin-top: 3px; }
.cx-op-result .cx-op-result-warn { color: var(--danger-text); }

/* ⚠ 2026-10-26（用户定案）：「系统状态」卡**整张撤掉** ——
   原来这里的规则（.cx-health-card / .cx-hl-grid / .cx-hl-cell / .cx-hl-k /
   .cx-hl-v）连同那张卡一起删除 —— 不留死样式。
   它剩下的两个「用户自己的数据」格（对话数据 / 数据体积）搬进「我的数据」卡，
   2026-10-26 第二次定案又并进了下面的小方格（向量体积 / 对话轮数），
   ⇒ .cx-op-tile / .cx-op-k / .cx-op-v 这套只读格样式**也一并删除**（不留死样式）。 */

/* ─── 「我的数据」小方格（2026-10-26）──────────────────────────────────────
   「我的数据」卡里，**小方格在上、四个按钮在下**：
   8 格（4 列 × 2 行）说清你有哪些数据、各多少 —— 导出备份里装了什么、
   各多少条 + 哪些没装。
   ⚠ 4 列 × 2 行 = 8 格；每格「标签在上、值在下」，值**一行**
     （`white-space: pre-line` + `overflow-wrap`，绝不 ellipsis 截断）
     ⇒ 加载态与加载后同高（Δ0px）。
   ⚠ **自带一套类名**（.cx-x-cell / .cx-x-k / .cx-x-v）：原来复用健康指标那套
     .cx-hl-*，而那张卡在同一天被撤掉了 ⇒ 类名一起没了 ⇒ 这里自己带一套。 */
.cx-x-grid {
  margin-top: 10px;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 7px 8px;
}
@media (max-width:420px) { .cx-x-grid { grid-template-columns:repeat(2,minmax(0,1fr)); } }
.cx-x-cell {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 5px 8px;
  border-radius: 7px;
  background: var(--bg-input);
}
.cx-x-k {
  font-size: 10.5px;
  color: var(--text-muted);
  letter-spacing: .02em;
  white-space: nowrap;
}
.cx-x-v {
  font-size: 12.5px;
  line-height: 1.48;          /* ≈18.5px 一行 */
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
  white-space: pre-line;
  overflow-wrap: anywhere;
  min-height: 19px;           /* 一行 */
}
.cx-x-v.is-edited { color:var(--primary-text); font-weight:600; }
/* ⚠ 2026-10-26（用户定案，第三次）：原来下面那行**静态**说明
   「不装进备份：登录凭据（密钥）· 记忆向量索引（可从记忆文本重建）」
   **移除** —— 方格里已经说清有哪些数据、各多少，
   再挂一行「还差什么」是多余的字。 ⇒ .cx-x-gap 这套样式一并删除。 */

/* ─── 探测条 / 最后检测 / 自动刷新 ───
   ⚠ 2026-10-26（用户定案，第三次）：现在**独立在页面最底下、卡外**。
     之前它被塞进「系统状态」卡里，那张卡撤掉后它回到页面级 ——
     它说的是「这台机器刚才探得怎么样」，不是「我的数据」。
   ⚠ 左右留 14px 内边距 ⇒ 与卡内文字对齐（卡有 14px 内边距，容器有 14px）。
     分隔线用 --border-light：它是页面里的「分区线」，不是页面的界。 */
.cx-foot {
  margin-top: 16px;
  padding: 0 14px;
  border-top: 1px solid var(--border-light);
}
/* 探活条：24 格，每格一次探测 —— 通 = 绿，探不通 = 砖红，没探到 = 灰 */
.cx-band { display: flex; gap: 3px; height: 9px; }
.cx-band i {
  flex: 1 1 0; min-width: 2px; display: block;
  border-radius: 2px; background: var(--border-light);
}
.cx-band i.ok { background: var(--primary); }
.cx-band i.bad { background: var(--danger); }
.cx-band-cap {
  margin: 0;
  font-size: 11px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  /* ⚠ 2026-11-05：可缩 + 可断行 ⇒ 探测统计再长也不撑出页脚 */
  min-width: 0; overflow-wrap: anywhere;
}
.cx-frow {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 5px 0;
  font-size: 11.5px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  flex-wrap: wrap;
}
.cx-frow .k { flex: none; }
.cx-frow .v { font-family: var(--cx-mono); color: var(--text-muted); }
/* ⚠ 2026-10-26（用户要求）：「最后检测」与「最近 N 次探测 · 中位 xxxms」
   同一行、**左右分列** —— 原来一个在上一个在下，白占一行。
   左边 = 最后检测，右边 = 探测统计（margin-left:auto 把它推到行尾）。 */
.cx-frow-split { margin-top: 7px; }
.cx-frow-split .v { flex: none; }
.cx-frow-split .cx-band-cap {
  margin-left: auto;      /* 推到行尾 */
  text-align: right;
  min-width: 0;
}

/* 自动刷新段控：选中态用「淡底 + 字色 + 描边」三重提示，**不铺实心色块** ——
   实心块会跟页面上其它实色抢注意力（全站口径：初音绿只用在「活着」的地方）。 */
.cx-seg { display: flex; gap: 6px; margin-left: auto; flex-wrap: wrap; justify-content: flex-end; }
.cx-seg-btn {
  font-family: inherit;
  font-size: 11.5px;
  line-height: 1.2;
  color: var(--text-secondary);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 4px 10px;
  cursor: pointer;
  transition: border-color .18s, background .18s, color .18s;
}
.cx-seg-btn:active { transform: scale(0.96); }
.cx-seg-btn.on {
  color: var(--primary-text);
  background: var(--primary-light);
  border-color: var(--primary-border);
  font-weight: 600;
}

/* ─── 聊天页面 ─── */
.pg-chat {
  height: 100%;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  /* 立绘层与欢迎页都是 absolute，这里是它们的定位基准 */
  position: relative;
}

/* ─── 角色立绘 · 欢迎态 ↔ 对话态 ────────────────────────────────
 *
 * 同一张图两种形态（用户 2026-10-28）：
 *   .is-welcome  立绘站在页面里（完整、不透明、带柔光与投影）
 *   .is-chat     立绘**原地**融进平面（降透明 + 压亮度，尺寸位置一个字不改）
 *
 * ⚠ 判据是「**今天进过对话没有**」，不是「有没有消息」——
 *   后者会让聊过天之后欢迎页再也不出现，而用户要的是「每天固定展示」
 *   （2026-10-28 第二次改口径）。旧类名 .has-msgs 已删。
 * ⚠ 只动 opacity / filter，**不换元素** ——
 *   换元素就没有「旧状态」，CSS 过渡根本不触发。
 * ⚠ 状态切换**不带 scale**：用户要「尺寸和位置不变，看起来像从立绘
 *   融入平面内」—— 一旦缩放就不是「融入」而是「换了个东西」。
 * ⚠ 柔光用初音绿（主色同源），不另起第三套颜色语言。
 *
 * ── 三层结构（2026-10-28 第十一轮改）──────────────────────────────
 *   .chat-art         满页图层（底部让给 tab 栏）
 *     .chat-art-stage    **包裹层**：几何在挂载时量一次就写成固定 px
 *       .chat-art-figure   立绘盒（贴底，高度 = 约束的一段；**静止，不飘**）
 *         .chat-art-img    图：**透明底立绘**，按原比例自适应
 *
 * ⚠ 2026-10-28（第十一轮，用户定案）：
 *   「抠图效果很差！我已经不指望你抠图了，你直接使用我的白底素材压缩后，
 *     前端使用椭圆形外圈圈起立绘，做大边界羽化，
 *     立绘背面的发光块移除，立绘改为静止状态，不再飘动」
 *   ⇒ ① 素材**不再抠透明**（产物就是带白底的整幅图）；
 *     ② 矩形的白边由 **`.chat-art-oval` 的椭圆遮罩 + 大羽化**柔掉：
 *        mask 是一枚**内切于盒子的椭圆**（100% 100%）⇒ 盒角全淡出，
 *        中心 72% 实心、到边缘渐隐 ⇒ 看到的是一个**椭圆形的柔边白底区**；
 *     ③ **发光块（.chat-art-halo）整套删除**；
 *     ④ **飘动（artFloat）整套删除** —— 立绘静止。
 *
 * ⚠ 2026-10-28（第十一轮收尾，用户定案）：**回到透明底立绘**。
 *   用户原话：「你可以重绘为无底色的透明背景素材吗？这才是最优解」
 *   ⇒ 素材改成 **AI 重绘 + 本地色度键抠成真透明底**
 *     （tools/keyout_mascot.py：AI 画纯平品红底 → 已知背景色反解 → 透明 PNG/WebP）。
 *   ⇒ 于是「椭圆外圈 + 羽化」**整套删除**（.chat-art-oval 这一层不要了）：
 *     透明底素材没有矩形边，也不需要羽化；页面底色直接透上来。
 *   ⇒ 夜间档也不再需要「压暗白底」（透明底没有白底），
 *     只把角色本身略微压一档，避免在深色页面上太刺眼。
 *
 * ⚠ 2026-10-29（第十三轮）：立绘素材**整批换新**（用户自绘、已抠好透明底，9 张 → 11 张）。
 *   ⇒ 页面这边**什么都没改**（仍是透明底直出、无遮罩无羽化）；
 *     变的只是素材怎么进到 `assets/mascot/`：
 *     tools/build_mascots.py 现在按源图自动分流 —— 已透明底的走「预乘缩放 + alpha bleed」，
 *     品红底源图仍走色度键抠图（详见 docs/DEVELOPMENT.md §7.24）。
 *
 * 为什么几何要写成 px：
 *   Android / 鸿蒙 ArkWeb 的 resizes-content 模式下，键盘弹起会把
 *   **布局视口一起缩掉**。立绘若用 % / vh 高度，键盘一弹就跟着缩、
 *   位置也跳 —— 正是用户说的「不能随键盘弹起而位移或者变形」。
 *   挂载时量一次 → 之后视口怎么变都不影响它。
 */
.chat-art {
  position: absolute;
  /* ⚠ 2026-10-29（用户「立绘没有沉浸式，导致状态栏和安全区有底部元素渲染出来了」）：
     曾经改成**满屏**（底沿顶到屏幕底）⇒ 立绘的脚落进「悬浮 tab 栏 + 底部安全区」底下，
     **脚被遮住/看着像被截断** ⇒ 用户 2026-10-30：「我没让你做立绘沉浸式，不要截断立绘的脚，
     撤回这一部分修改」。
     ⇒ **撤回**：底沿回到 tab 栏上沿（脚完整可见），宽度也不许超页面宽（不裁两侧）。
     代价：屏幕底部那条带（tab 栏 + 系统导航条）露的是页面底色 —— 这是用户要的取舍。 */
  inset: 0 0 var(--nav-space, 88px) 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}
/* 包裹层：top / height 由 chat.js 的 layoutArt() 写成固定 px */
.chat-art-stage {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 0;
  z-index: 1;
  pointer-events: none;
}
/* 立绘盒：贴底，高度由 layoutArt() 写 px。
   ⚠ 第十一轮：**静止** —— 原来的 artFloat（±7px 上下飘）整套删掉，
     用户的原话是「立绘改为静止状态，不再飘动」。
   ⚠ 2026-10-30（用户「移除对话的 enter 按钮，改为点击立绘进入对话」）：
     欢迎态下立绘盒就是「进入对话」的按钮 ⇒ 打开 pointer-events + cursor。
     对话态（.is-chat）保持 pointer-events:none —— 那时它退成背景，
     不该再吃掉点击（否则会挡住消息气泡）。 */
.chat-art-figure {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 0;                     /* 由 chat.js 的 layoutArt() 写 px */
  pointer-events: none;
}
.pg-chat.is-welcome .chat-art-figure {
  pointer-events: auto;
  cursor: pointer;
}
/* 键盘聚焦：给一圈内描边（原生 outline 会画在盒外、贴到屏幕上沿） */
.pg-chat.is-welcome .chat-art-figure:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: -3px;
  border-radius: 10px;
}
/* 图：高度跟着立绘盒走、宽度按原比例自动 —— **不写死像素尺寸**
   ⚠ 第十一轮收尾：素材是**真透明底**（AI 重绘 + 本地色度键），
     所以**不需要** drop-shadow、也不需要任何遮罩/羽化来藏边；
     页面底色直接从透明处透上来。 */
.chat-art-img {
  position: absolute;
  left: 50%;
  bottom: 0;
  height: 100%;
  width: auto;
  transform-origin: 50% 100%;
  transform: translateX(-50%);
  transition: opacity .9s cubic-bezier(.22, .61, .36, 1),
              filter .9s ease;
}
/* 换图由 chat.js 的 Web Animations 管理；仅改变透明度，几何保持原位。 */
/* 对话模式：同一张图**原地**融进平面 —— 只降透明 + 降饱和，
   尺寸和位置保持不变（用户 2026-10-28：「看起来就像从立绘融入到平面内」）
   ⚠ 2026-10-28（第二次）：**不再虚化** —— 用户「融入背景不要使用模糊特效，
     改为降低透明度和亮度即可，要保持立绘细节清晰可见」。
   ⚠ 2026-10-28（第三次，用户定案 C 方案）：**不透明度 .22 → .40**。
     实测（tools/.shots/bg-variants.png，量角色区域亮度标准差）：
       .22 + brightness .9 → 细节 16.3
       .22 + brightness 1  → 细节 15.9（**亮度不是主因**）
       .40 + brightness 1  → 细节 27.7（+70%）
     ⇒ 「模糊」的真相是**对比度被压没了**，不是亮度太低。
     而且文字都落在**不透明气泡**上（口语化的 IM 壁纸做法），
     背景完全可以浓一倍而不伤可读性。
   ⚠ 判据从「有没有消息」换成「今天进过对话没有」：
     欢迎页是**每天一次**的落地页（用户 2026-10-28），进了对话就退成背景。 */
/* ⚠ 2026-10-29（用户第十四轮）：对话态再加一层**半透明遮罩**弱化立绘。
   原话：「在进入对话后将立绘增加一层半透明遮罩弱化，不使用模糊，
          也修改饱和度和亮度」。
   ⇒ ① 遮罩层 = .chat-art::after（一层页面底色的半透明膜，压在立绘**上面**）
        —— **不用 backdrop-filter、不用 blur**（用户明确不要模糊）；
     ② **不改饱和度、不改亮度**（用户随后更正：「是不使用模糊、
        不修改饱和度和亮度」）⇒ 弱化**只靠这层遮罩**；
     ③ 欢迎态（.is-welcome）**不加**遮罩 —— 那是落地页，立绘该是主角。
   ⚠ 遮罩是 .chat-art 的伪元素 ⇒ 盖住的是**整层立绘区域**，
     但素材是透明底 ⇒ 视觉上只作用在角色身上，页面底色不受影响。
   ⚠ 用 --veil-rgb（页面底色 tint）而不是写死颜色 ⇒ 纸系页面自动跟着走。 */
.pg-chat.is-chat .chat-art::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  /* 半透明膜：日间档 ~52% ⇒ 立绘明显退到背景里 */
  background: rgb(var(--veil-rgb) / .52);
  pointer-events: none;
  transition: background .9s ease;
}
/* 夜间档：底色本来就深，膜不用那么厚（否则角色直接消失），
   但要更「中性」—— 用纯黑压一层会把角色压成剪影（老口径踩过的坑），
   所以还是用页面底色，只是更薄。 */
html[data-theme="dark"] .pg-chat.is-chat .chat-art::after {
  background: rgb(var(--veil-rgb) / .42);
}
.pg-chat.is-chat .chat-art-img {
  opacity: .40;
  /* ⚠ 2026-10-29（用户更正）：**不改饱和度、不改亮度、不用模糊** ——
     弱化**只靠上面那层半透明遮罩**。原来的 saturate(.95) brightness(1)
     保持原样（等价于没改），避免把立绘调色调偏。 */
  filter: saturate(.95) brightness(1);
}
/* 夜间档：素材现在是**透明底**（没有白底块了），深色页面上角色本身会偏亮 ⇒
   只需把角色**略微压一档**（亮度 + 饱和度都收一点），
   不再需要「整图压暗」那种会把角色压成剪影的做法。
   ⚠ 之前的 `invert(1) hue-rotate(180deg)`（负片）与 `brightness(.34)`
     都是为「白底整幅图」设计的 —— 透明底立绘上这两种都会毁掉角色。 */
html[data-theme="dark"] .chat-art-img {
  filter: brightness(.92) saturate(.95);
}
html[data-theme="dark"] .pg-chat.is-chat .chat-art-img {
  opacity: .40;
  /* 同上：夜间也不改饱和度/亮度，只靠遮罩弱化 */
  filter: brightness(.88) saturate(.9);
}

/* 固定欢迎页：每天进对话模块的第一眼（.is-welcome）。
   对话模式（.is-chat）整块淡出 —— 用 opacity 而不是 display:none，
   因为立绘的可用高度是从**问候语的底沿**算的（layoutArt），
   display:none 会让它的高度变了 ⇒ 立绘的几何跟着变。 */
.chat-welcome {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(var(--topbar-h, 56px) + 10px);
  z-index: 1;
  pointer-events: none;
  transition: opacity .5s ease;
  /* 问候语的淡出比立绘快一点：先收话，再融进平面 */
  transform-origin: 50% 0;
}
.pg-chat:not(.is-welcome) .chat-welcome { opacity: 0; }

/* ── 艾玛的问候：**一行纯文本**（不再是气泡） ──────────────────────────
 *
 * 用户 2026-10-28（第九轮）：「**问候语气泡移除**，参照原先纯文本『我在。』的
 * 字体样式，**单行**显示问候语」。
 * ⇒ 盒子、描边、尖角、投影**全部拆掉** —— 就是一行衬线小字挂在立绘头顶。
 *   字体/字号/字距/颜色照抄原先那个纯文本占位（旧的 `.chat-reply-placeholder .poem`）：
 *     font-family: "Songti SC", "STSong", "SimSun", "Noto Serif CJK SC",
 *                  "Source Han Serif SC", Georgia, serif;
 *     font-size: 15px; line-height: 2.2; letter-spacing: .14em;
 *     color: var(--text-secondary);
 *
 * 三条硬约束：
 *   ① **单行**：`white-space: nowrap` ⇒ 文案池每句 ≤ 16 字（15px + .14em 字距
 *      约 17px/字 ⇒ 16 字 ≈ 275px，390 宽放得下，窄屏退让）。
 *   ② **居中**：`left: 50%` + `translateX(-50%)` + `width: max-content`
 *      ⇒ 盒子贴着文字收紧，整行正好在页面正中间。
 *   ③ **位置固定**：`top: topbar + 10px`（跟 `.chat-welcome` 一起），不随文案/键盘/主题变。
 */
.chat-greet {
  position: relative;
  left: 50%;
  transform: translateX(-50%);
  width: max-content;
  max-width: calc(100vw - 32px);
  text-align: center;
  pointer-events: none;
}
/* 文字：衬线、15px、.14em 字距 —— 与原先纯文本「我在。」一致。
   ⚠ `nowrap` ⇒ 永远只有一行；`.chat-greet-text` 是块级 ⇒ 高度恒定 = 15×2.2 = 33px。 */
.chat-greet-text {
  display: block;
  white-space: nowrap;
  overflow-wrap: normal;
  font-family: "Songti SC", "STSong", "SimSun", "Noto Serif CJK SC", "Source Han Serif SC", Georgia, serif;
  font-size: 15px;
  line-height: 2.2;
  letter-spacing: .14em;
  color: var(--text-secondary);
}
/* 窄屏（≤ 360px）：16 字放不下 ⇒ 缩一档字号与字距，仍然单行。 */
@media (max-width: 360px) {
  .chat-greet-text {
    font-size: 13.5px;
    letter-spacing: .08em;
  }
}
/* 整句交替淡出/淡入由 welcome.js 管理，可取消且不引起布局位移。 */
.chat-greet-words { display: inline-block; }
@media (prefers-reduced-motion: reduce) {
  .chat-art-img, .chat-welcome { transition: none; }
}

/* ⚠ 旧的两行欢迎文案（`.chat-welcome-poem` / `.chat-welcome-sub`）已删 ——
   2026-10-28（第四次）换成「艾玛的问候」；第九轮又从漫画气泡改成**一行纯文本**
   （见 .chat-greet）。同一个事实只说一次：问候语现在只有那一行。
   ⚠ 2026-10-30：「进入对话」按钮（.chat-enter-btn）**整套删除** ——
   入口改成**点立绘**（见 .chat-art-figure）。 */

/* 欢迎态下，消息列表容器不能吃掉点击（否则点不到立绘）。
   ⚠ .chat-reply 是 flex:1 的整屏容器、z-index:2 ⇒ 盖在立绘层（z-index:0）上面；
     欢迎态它里面是空的（renderList 不渲染），但**照样拦点击** ⇒ 必须关掉。
   对话态（.is-chat）保持 pointer-events:auto —— 消息气泡要能点。
   ⚠ 2026-10-31（用户「隐藏消息列表，恢复立绘能见度」）：
     立绘页 = **隐藏**消息列表（不是销毁）⇒ 列表容器整块 visibility:hidden。
     隐藏而不是清空 DOM 的三个好处：
       ① 重新进对话时列表原样回来（不用重画几百个气泡）；
       ② 正在流式的那条回复不会被摘掉（流式不中断）；
       ③ 「退回立绘页」这一步只改**形态**（.is-welcome），不改数据。
     ⚠ 用 visibility 而不是 display:none —— 后者会把容器从流里拿掉，
       高度变化会带动立绘几何（layoutArt 按「问候语底沿 → tab」算高度）。 */
.pg-chat.is-welcome .chat-reply {
  pointer-events: none;
  visibility: hidden;
}

/* 空对话的占位问候（对话态 + 一条消息都没有时的那条 bot 气泡）。
   ⚠ 它**不是真消息**：不进 msgs、不存档、不能重发（见 list.js 的同名注释）。
   ⚠ 特意**不带 `.chat-msg-bot`** 这个类 —— 那个类是「真的有一条助手回复」的语义，
     十几个探针脚本靠它找气泡（`document.querySelector('.chat-msg-bot')`）；
     占位气泡混进去会让它们量到一条假的"最后一条回复"。
     外观由 `.chat-msg-bot, .chat-msg-greeting` 共用的那条规则给（见下面）。 */

.chat-reply {
  flex: 1;
  overflow-y: auto;
  /* 底部留白 = 当前占着底部的那个东西（tab / 输入条 / 键盘遮挡），
     由 chat.js 的 syncBottomInset() 实时写进 --chat-bottom-inset。
     滚到底 = 最后一条消息正好贴在可见区下沿之上。 */
  padding: calc(var(--topbar-h, 56px) + 8px) 16px
           calc(var(--chat-bottom-inset, var(--nav-space, 85px)) + 12px);
  display: flex;
  flex-direction: column;
  gap: 12px;
  /* 压在立绘层之上：消息列表始终盖住背景 */
  position: relative;
  z-index: 2;
}

/* 消息入场（fadeup）：新消息淡入 + 上移一点。
 * ⚠ 只给**新出现**的气泡加（见 list.js 的 markEnter）——
 *   流式对账时整列会被重画，那时重播动画会让正在生成的那条闪一下。 */
@keyframes chatFadeUp {
  from { opacity: 0; transform: translateY(14px); }
  to { opacity: 1; transform: none; }
}
.chat-msg-enter {
  animation: chatFadeUp .34s cubic-bezier(.22, .61, .36, 1) both;
}
@media (prefers-reduced-motion: reduce) {
  .chat-msg-enter { animation: none; }
}

/* ─── 迷你抽屉（chat 专用） ───
 *
 * 结构分两层：
 *   1) 基础层 —— 只管布局与动效，视觉全部走 var(--dw-*)；
 *   2) 主题层 —— 玻璃拟态的视觉变量（见下方「主题 · 玻璃拟态」块）。
 *
 * 只保留这一套方案，主题层直接挂在 .mini-drawer 上，
 * 不再依赖任何 data-* 属性开关，也就不需要「属性缺失」的兜底分支。
 */
#chatMiniDrawer {
  position: fixed; inset: 0; z-index: var(--z-drawer);
  pointer-events: none; opacity: 0; visibility: hidden;
  transition: opacity .24s ease, visibility 0s linear .24s;
}
#chatMiniDrawer.open {
  opacity: 1; visibility: visible; pointer-events: none;
}
.mini-drawer {
  position: fixed;
  /* 键盘占位：输入条贴着「可视区下沿」而不是「屏幕下沿」，
     否则键盘一弹起就把输入框盖住，用户看不见自己打在哪 */
  bottom: var(--kb-inset, 0px);
  left: 0; right: 0; max-width: none; margin: 0;
  background: var(--dw-panel);
  -webkit-backdrop-filter: var(--dw-blur);
  backdrop-filter: var(--dw-blur);
  border: var(--dw-panel-border);
  border-radius: 18px 18px 0 0;
  /* 紧凑条：上边距给输入行，下边距是安全区。不需要「底座」——
   * 抽屉打开时底部悬浮 tab 会整体淡出（body.drawer-open），背后本来就没有
   * 需要遮挡的东西，面板也就不必垫高，整体读作一条浮在页面上的输入条。 */
  padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
  max-height: min(16vh, 112px);
  overflow-y: auto;
  transform: translate3d(0, 18px, 0) scale(.98);
  transform-origin: center bottom;
  opacity: 0;
  transition: transform .28s cubic-bezier(.2,.7,.3,1), opacity .24s ease, bottom .22s ease;
  box-shadow: var(--dw-panel-shadow);
  color: var(--dw-text);
}
#chatMiniDrawer.open .mini-drawer {
  transform: translate3d(0, 0, 0) scale(1); opacity:1; pointer-events: auto;
}
body.chat-entered .tab-bar { opacity: 0; transform: translateY(16px); pointer-events: none; }
body.chat-entered .dock-backdrop { opacity: 0; }
body.chat-entered #chatMiniDrawer { opacity: 1; visibility: visible; transition-delay: 0s; }
body.chat-entered #chatMiniDrawer .mini-drawer { pointer-events: auto; }
body.chat-entered .chat-stop-fab { display: none !important; }
/* ⚠ 2026-10-31（用户：「对话页键盘抬起后气泡底部多一截边距」）：
 *   键盘弹起时屏幕底部那一截**已经被键盘盖住**，而键盘区里就包含 Home Indicator
 *   —— 也就是 env(safe-area-inset-bottom) 描述的那一截。
 *   输入条再留一份安全区 = **重复预留** ⇒ 面板凭空高出 20px，
 *   底下的消息列表跟着多留一截 ⇒ 看着就是「气泡底下多一截边距」。
 *
 *   ⇒ 键盘在的时候（html.kb-up，由 EMMA.ui.viewport 挂），
 *     那一截让给键盘，输入条只留 12px 基础内边距。
 *   ⚠ 必须是独立 class：ArkWeb 的 resizes-content 模式下 --kb-inset 恒为 0，
 *     键盘起落跟 inset 无关 ⇒ 挂在 inset 上永远触发不了。
 *   ⚠ 键盘不在时**不要**动 padding —— 安全区必须留着，
 *     否则输入条会压在 Home Indicator 底下。 */
html.kb-up .mini-drawer {
  padding-bottom: 12px;
}
/* ⚠ 2026-10-30（用户第三项：「手机浏览下，对话抽屉被键盘抬高后，最底部为何有一个色块？」）
   —— 现象：手机键盘弹起后，抽屉被 --kb-inset 顶到键盘上沿，
      **键盘与抽屉之间那一截**露出来的是**页面底色**（--page-bg #f2f3f7 冷灰），
      而抽屉本身是米白纸色 #fdfcf6 ⇒ 底部出现一条**色块**（冷灰带）。

   ⇒ 解法：给抽屉加一条「**键盘填充条**」（::after），从抽屉下沿往下
      **铺满 --kb-inset 那一截**，颜色与抽屉一致 ⇒ 看起来就是抽屉一直延伸到键盘，
      中间没有断层。

   ⚠ 为什么不用「把页面底色改成纸色」：页面底色是全局 token，
      改了会让所有页面（非聊天）也跟着变 —— 影响面太大。
   ⚠ ::after 用 z-index:-1 + isolation 之外的写法？这里抽屉本身是 fixed，
      ::after 用 position:absolute + bottom:-Npx，父级需要 position:relative
      —— .mini-drawer 已是 position:fixed（本身就是包含块）✓
   ⚠ 不能给抽屉加 overflow:hidden（会裁掉这条填充）。 */
.mini-drawer::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  /* 从抽屉下沿往下铺到键盘上沿 */
  top: 100%;
  height: var(--kb-inset, 0px);
  background: var(--dw-panel);
  pointer-events: none;
}
/* 抽屉把手：视觉上不出现，只在面板顶部留一条 10px 透明拖拽热区
 *
 * 这个浮层其实是「底部输入条」而不是可拖拽的面板，把手属于多余视觉，
 * 但下拉关闭的手势（js/modules/chat.js 的 bindDragToClose）要留住 ——
 * 所以 DOM 与类名保持不变，用绝对定位 + 全透明把它变成纯热区。
 * 高度取 10px，正好等于面板的 padding-top，不会盖住输入框。
 */
.mini-drawer-handle {
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 10px;
  opacity: 0;
  pointer-events: auto;
  background: none;
  border-radius: 0;
  margin: 0;
}
.mini-drawer-row {
  display: grid;
  grid-template-columns: 40px minmax(0, 1fr) 40px;
  grid-template-rows: minmax(40px, auto);
  align-items: center;
  gap: 8px;
  /* 宽屏（桌面/平板）下不让输入区无限拉长，限宽居中更像成品 */
  max-width: 720px;
  margin: 0 auto;
}
.mini-drawer-row .mini-input { grid-column: 2; grid-row: 1; width: 100%; }
.mini-drawer-row .mini-icon-btn { grid-column: 1; grid-row: 1; }
.mini-drawer-row .mini-send-btn { grid-column: 3; grid-row: 1; }

/* 附件按钮
 *
 * 手机端没有 hover 语义：原先「悬停才上浮 + 旋转 + 变主色」在触屏上永远触发不到，
 * 按钮永远是最素的灰白方块。现在把那套主色外观固化为常态（--dw-btn-color / --dw-btn-border），
 * 动效只保留按压反馈 :active（触屏有真实语义）。 */
.mini-icon-btn {
  width: 40px; height: 40px;
  flex-shrink: 0;
  border: 1px solid var(--dw-btn-border);
  border-radius: var(--dw-btn-radius);
  background: var(--dw-btn-bg);
  color: var(--dw-btn-color);
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  box-shadow: var(--dw-btn-shadow);
  transition: transform 0.18s, box-shadow 0.18s;
}
.mini-icon-btn:active { transform: scale(0.92); }

/* 待发图片的悬浮缩略图：挂在抽屉面板的**右上方**（absolute）——
 * 刻意不做成占位元素：占位会把输入行往下挤，输入框跟着跑位。
 * absolute 定位相对 .mini-drawer（它是 fixed），所以 right/bottom 跟面板走。
 *
 * ⚠ bottom 的百分比基准是**定位祖先的 padding box 高度**，
 *   不是内容区：bottom:100% 的意思是「下沿落在 padding box 的上沿」，
 *   也就是面板顶边。往上再抬 10px 要**加** 10px（减 10px 是往下沉进面板里）。 */
.mini-drawer-thumb {
  position: absolute;
  right: 14px;
  bottom: calc(100% + 10px);
  width: 58px;
  height: 58px;
  border-radius: 14px;
  overflow: visible;
  background: var(--bg-card);
  border: 1px solid var(--border);
  box-shadow: 0 8px 24px rgba(15, 17, 23, 0.22);
}
.mini-drawer-thumb img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 13px;
  cursor: pointer;
}
.mini-drawer-thumb-del {
  position: absolute;
  top: -7px;
  right: -7px;
  width: 21px;
  height: 21px;
  border: 0;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-card);
  color: var(--text-secondary);
  box-shadow: 0 2px 8px rgba(15, 17, 23, 0.24);
  cursor: pointer;
}
.mini-drawer-thumb-del .icon { width: 12px; height: 12px; }

/* 输入框：内芯(--dw-input-bg) + 外圈描边(--dw-input-ring) 两层裁剪
   ⚠️ --dw-input-bg / --dw-input-ring 必须是渐变（image），不能写纯色：
   background 是列表属性，纯色 layer 会被整条丢弃。 */
.mini-input {
  flex: 1;
  min-width: 0;
  border: var(--dw-input-border-w) solid transparent;
  border-radius: var(--dw-input-radius);
  padding: var(--dw-input-pad);
  font-size: 15px;
  font-family: var(--dw-input-font);
  line-height: var(--dw-input-lh);
  resize: none; outline: none;
  color: var(--dw-input-text);
  /* 最多两行：单行 40px + 一行 22px */
  max-height: 62px;
  overflow-y: auto;
  background:
    var(--dw-input-bg) padding-box,
    var(--dw-input-ring) border-box;
  -webkit-background-clip: padding-box, border-box;
  background-clip: padding-box, border-box;
  box-shadow: var(--dw-input-shadow);
  transition: box-shadow 0.25s, transform 0.25s, border-color 0.25s;
}
.mini-input::placeholder { color: var(--dw-input-placeholder); }
.mini-input:focus {
  transform: translateY(-1px);
  box-shadow: var(--dw-focus-shadow);
}

/* 发送按钮 */
.mini-send-btn {
  width: 40px; height: 40px;
  flex-shrink: 0;
  border: none; border-radius: var(--dw-send-radius);
  background: var(--dw-send-bg);
  color: var(--dw-send-color);
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  opacity: var(--dw-send-off-opacity);
  box-shadow: var(--dw-send-shadow);
  transition: opacity 0.2s, transform 0.22s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.22s;
}
.mini-send-btn.on {
  opacity: 1;
  box-shadow: var(--dw-send-on-shadow);
}
.mini-send-btn:active { transform: scale(0.9); }
.mini-send-btn:disabled { background: var(--dw-send-disabled); }
.mini-send-btn.is-stop { background: #222; color: #fff; }
.chat-stop-fab { display: none !important; }

/* ═══════════════════════════════════════════════════════════════
   主题 · 玻璃拟态
   案例：iMessage / WhatsApp / Telegram — 毛玻璃输入条 + 三色渐变描边
   ═══════════════════════════════════════════════════════════════ */
.mini-drawer {
  /* 面板不透明：保证「背景不透出底部元素」这条约束始终成立。
   * #fbfcfd = 原 rgba(255,255,255,.72) 叠在页面底 #f2f3f7 上的等效实色，色调不变。
   * 面板不透明 → 没有可模糊对象，--dw-blur 直接关掉省一次合成。 */
  --dw-panel: #fdfcf6;
  --dw-blur: none;
  --dw-panel-border: 1px solid rgba(70, 58, 38, 0.08);
  --dw-radius: 24px 24px 0 0;
  /* ⚠ 2026-10-30（用户：「键盘弹起后为何迷你抽屉上方有异常色块和抬起？」）
     —— 原值是 `0 -14px 44px rgba(15,17,23,.16)`：**向上**投射的一大团柔影。
        抽屉平时贴在页面底部，投影落在 tab 上不显眼；
        键盘一弹起，抽屉被 --kb-inset 顶到屏幕中段，浮在**聊天内容**上 ——
        这团向上 58px 的投影就完整地压在内容区上，
        读作一条**横贯全屏的灰暗色块**，同时把抽屉「顶起来」的样子也带出来。

     ⇒ 改成「**纸边**」：极窄、极淡、贴边的一线（负 spread 收住扩散）。
        延伸量 = blur + |offset| - spread = 10 + 4 - 6 = 8px，
        而不是原来的 14 + 44 = 58px。
        纸片贴边本来就不该有厚投影 —— 厚投影是「浮空」的信号，
        而输入条是**贴着可视区下沿**的，不是浮在空中。 */
  --dw-panel-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.85),
    0 -4px 10px -6px rgba(15, 17, 23, 0.22);
  --dw-text: #23211c;
  /* 附件按钮：静态主色外观（原先这套只在 hover 时出现） */
  --dw-btn-bg: rgba(255, 253, 249, 0.60);
  --dw-btn-border: rgba(57, 197, 187, 0.34);
  --dw-btn-radius: 14px;
  --dw-btn-color: var(--primary-text);
  --dw-btn-shadow: 0 2px 8px rgba(15, 17, 23, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.9);
  --dw-input-bg: linear-gradient(rgba(255, 253, 249, 0.92), rgba(255, 253, 249, 0.92));
  --dw-input-ring: linear-gradient(120deg, rgba(57, 197, 187, 0.60), rgba(74, 158, 255, 0.45), rgba(192, 132, 252, 0.5));
  --dw-input-border-w: 2px;
  --dw-input-radius: 20px;
  --dw-input-pad: 7px 16px;
  --dw-input-lh: 22px;
  --dw-input-text: #23211c;
  --dw-input-placeholder: rgba(35, 33, 28, 0.62);
  --dw-input-font: inherit;
  --dw-input-shadow: 0 3px 14px rgba(15, 17, 23, 0.07), inset 0 1px 2px rgba(15, 17, 23, 0.05);
  --dw-focus-shadow: 0 8px 26px rgba(57, 197, 187, 0.22), 0 0 0 4px rgba(57, 197, 187, 0.20);
  --dw-send-bg: var(--primary);
  --dw-send-color: var(--primary-on);
  --dw-send-radius: 14px;
  --dw-send-off-opacity: 0.35;
  --dw-send-shadow: 0 3px 10px rgba(57, 197, 187, 0.38);
  --dw-send-on-shadow: 0 6px 18px rgba(57, 197, 187, 0.50), 0 0 0 4px rgba(57, 197, 187, 0.22);
  --dw-send-disabled: var(--primary);
}

/* ═══════════════════════════════════════════════════════════════
   夜间模式覆盖

   夜间块用 html[data-theme="dark"] .mini-drawer（特异性 0-2-1），
   压得住浅色块（0-1-0），且只作用于抽屉本身，不会外溢到别的元素。
   ═══════════════════════════════════════════════════════════════ */

/* 玻璃拟态 · 夜间 */
html[data-theme="dark"] .mini-drawer {
  /* 同浅色：不透明面板。#1a1a15 = 纸系页面夜间卡 #1c1c16 的邻近暖墨 */
  --dw-panel: #1a1a15;
  --dw-blur: none;
  --dw-panel-border: 1px solid rgba(255, 255, 255, 0.08);
  /* 同浅色：向上的厚投影压出一条色块 ⇒ 收成贴边纸线（见浅色处说明） */
  --dw-panel-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.06),
    0 -4px 10px -6px rgba(0, 0, 0, 0.5);
  --dw-text: #f0ece0;
  /* 附件按钮：静态主色外观（原先这套只在 hover 时出现） */
  --dw-btn-bg: rgba(255, 255, 255, 0.08);
  --dw-btn-border: rgba(57, 197, 187, 0.36);
  --dw-btn-color: var(--primary-text);
  --dw-btn-shadow: 0 2px 8px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.10);
  --dw-input-bg: linear-gradient(rgba(34, 34, 27, 0.92), rgba(34, 34, 27, 0.92));
  --dw-input-ring: linear-gradient(120deg, rgba(57, 197, 187, 0.50), rgba(74, 158, 255, 0.40), rgba(192, 132, 252, 0.45));
  --dw-input-text: #f0ece0;
  --dw-input-placeholder: rgba(240, 236, 224, 0.58);
  --dw-input-shadow: 0 3px 14px rgba(0, 0, 0, 0.35), inset 0 1px 2px rgba(0, 0, 0, 0.25);
  --dw-focus-shadow: 0 8px 26px rgba(57, 197, 187, 0.20), 0 0 0 4px rgba(57, 197, 187, 0.16);
  --dw-send-bg: var(--primary);
  --dw-send-off-opacity: 0.5;
  --dw-send-disabled: #4d4d5c;
}


/* ─── 气泡 ─── */
.chat-msg {
  /* 滚进视口时给顶栏让位：不然 scrollIntoView 会把消息停在顶栏底下，
     而顶栏是 fixed 的 → 那一块点不到（「点击气泡重发」就失效了） */
  scroll-margin-top: calc(var(--topbar-h, 56px) + 12px);
  max-width: 78%;
  padding: 8px 12px;
  font-size: 15px;
  line-height: 1.5;
  word-break: break-word;
  overflow-x: auto;
  /* flex-shrink 必须是 0：消息列表是「列方向 flex 容器 + 定高」，
     默认的 flex-shrink:1 会把每个气泡压扁到塞得下为止，
     气泡里的文字被 overflow 裁在半行上（看着像渲染坏了）。
     改成不收缩后，气泡保持自然高度，列表整体滚动 ——
     第一句贴着 topbar 下沿、最后一句贴着 tab 上沿，中间自然穿过两者。 */
  flex-shrink: 0;
  /* ⚠ 2026-10-25：**取消气泡内部滚动**（用户拍板）。
     以前是 `max-height: 60vh; overflow-y: auto` —— 超高回复时气泡自己滚。
     那样做的代价是：内容被关在气泡里的滚动区，**搜不到、也读不全**
     （用户要找一段内容时，它可能藏在气泡内部看不见的地方）。
     ⇒ 现在全文展开，长页面交给**列表的懒加载**扛。
     （列表滚动容器是 .chat-reply，不是气泡。） */
}
/* 气泡内滚动条：全局规则把滚动条藏了，这里显式放出来（超高时要看得出还有内容） */
.chat-msg::-webkit-scrollbar { width: 6px; height: 6px; display: block; }
.chat-msg::-webkit-scrollbar-track { background: transparent; }
.chat-msg::-webkit-scrollbar-thumb { background: rgba(110, 118, 129, 0.38); border-radius: 3px; }
.chat-msg::-webkit-scrollbar-thumb:hover { background: rgba(110, 118, 129, 0.6); }
html[data-theme="dark"] .chat-msg::-webkit-scrollbar-thumb { background: rgba(139, 148, 158, 0.4); }
.chat-msg-user {
  /* 用户消息的外层是「气泡 + 状态」的列容器。
     送达标志挂在**气泡外面**（用户 2026-10-24：从「气泡下方、右对齐」
     改成「气泡**左侧**」—— 标志是这一条消息的元信息，贴着气泡左边更顺眼，
     也不再占气泡下方的垂直空间）。
     定位基准：外层要 position:relative，标志才能贴着外层左缘摆出去。 */
  position: relative;
  align-self: flex-end;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 3px;
  padding: 0;
  max-height: none;
  overflow: visible;
}
/* 气泡本体：底色 / 圆角 / 内边距都在这一层 */
.chat-msg-user .chat-msg-bubble {
  max-width: 100%;
  min-width: 0;
  padding: 8px 12px;
  overflow-x: auto;
  /* ⚠ 同样取消内部滚动（2026-10-25）—— 与 .chat-msg 保持一致。 */
  overflow-y: visible;
  /* ⚠ 半透明（2026-10-29 用户：「双边气泡背景调节透明度为0.4，让背景透过来」；
     2026-10-30 第二项：日间再降到 **0.26** —— 让背景立绘更清晰）
     —— --primary 保持不透明（别处还要用），这里单独算 rgba。 */
  background: rgba(57, 197, 187, 0.26);
  color: var(--primary-on);
  /* 气泡内的引用块 / 表格条纹内凹层：跟着绿底做深浅，
     不能 fallback 到页面底色（不透明）—— 会盖出一块白。 */
  --bubble-inset: rgba(0, 0, 0, 0.045);
  border-radius: 16px 4px 16px 16px;
}
/* 用户气泡里的文字：
   ① display:block —— span 是行内盒，text-wrap: balance 对行内盒不生效，
      而 balance 是这一版排版的关键（见下）。
   ② text-wrap: balance —— 让各行长度尽量接近。
      起因：中文避头尾规则规定「？？」不能出现在行首，
      于是一句「…公园在哪吗？？？？」会把「？？」整体推到第二行，
      第一行空出一大截（实测 390 宽下气泡 279px、右侧空 207px）。
      balance 让浏览器重新分配断点，两行长度接近，
      再配合 JS 量出实际最宽行把气泡收窄（空白 207px → 5px）。 */
.chat-msg-user .chat-msg-text {
  min-width: 0;
  display: block;
  text-wrap: balance;
}
/* 消息里的图片：可点的缩略图，点开走通用预览组件。
 * ⚠ 刻意**不加图注**（文件名 / 尺寸 / 「[图片]」）——
 *   图片的元数据不该出现在聊天气泡里。
 * 放在气泡**上方**：自己发的图先看到，文字气泡的位置不受影响。 */
.chat-msg-images {
  display: flex;
  gap: 6px;
  margin-bottom: 5px;
  max-width: 100%;
}
.chat-msg-image {
  display: block;
  padding: 0;
  border: 0;
  border-radius: 12px;
  overflow: hidden;
  background: var(--bg-card);
  cursor: pointer;
  line-height: 0;
}
.chat-msg-image img {
  display: block;
  width: auto;
  height: auto;
  max-width: 62vw;
  max-height: 220px;
}
/* 送达状态：气泡**外侧左侧**（用户 2026-10-24：从「气泡下方一行、右对齐」
   改到「气泡左边」）。
   绝对定位在气泡左缘之外 → 不占气泡内的宽度，文字不会被图标挤开。
   图标语义：**✓ = 已送达**；**↻ = 失败，点一下重发**（不再写「未送达，点击重发」
   —— 刷新图标本身就是通用的重试符号，手机上又没有 hover 可依赖）。
   淡到刚好还能看清（≈3.1:1，非文字元素的 3:1 下限），
   但不跟正文抢注意力 —— 它只是「确认」，不是要用户操作的东西。
   ⚠ 纵向**对齐气泡底部**（用户 2026-10-24）：用 `bottom` 而不是 `top`。
     `top` 与 `bottom` 不能同时留着 —— 元素会被拉高（height:auto 的那个
     会被上下撑开，height:13px 的那个 top 会赢、bottom 被忽略）。 */
.chat-msg-state {
  position: absolute;
  right: 100%;          /* 贴在外层左缘之外 */
  bottom: 3px;          /* 纵向对齐气泡底部 */
  margin-right: 7px;
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  height: 13px;
  font-size: 10px;
  line-height: 1;
  color: var(--text-muted);
  /* ⚠ 这里**不再**整体降透明度 —— 状态标里现在还带着**对话时间**（文字），
     文字要按 ≥4.5:1 算，非文字元素才按 3:1。
     ⇒ 透明度下移到 .icon 上（2026-10-25）。 */
}
/* 状态图标：淡到刚好还能看清（≈3.1:1，非文字元素的 3:1 下限），
   但不跟正文抢注意力 —— 它只是「确认」，不是要用户操作的东西。 */
.chat-msg-state .icon { width: 11px; height: 11px; opacity: .78; }
/* 对话时间（用户 2026-10-25 要求加在气泡指示器上）：
   **不透明**（它跟状态图标不是一回事 —— 图标是装饰，时间是信息）。 */
.chat-msg-time {
  font-size: 10px;
  line-height: 1;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.chat-msg-state.is-sending .icon { opacity: .9; animation: chatStatePulse 1.4s ease-in-out infinite; }
.chat-msg-state.is-delivered .icon { opacity: .78; }
/* 未送达：刷新图标 + 红色 —— 点它重发（可点声明走最少一套：cursor + title
   + role=button/tabindex，不加提示浮层）。 */
.chat-msg-state.is-failed {
  opacity: 1;
  color: var(--danger-text);
  cursor: pointer;
}
.chat-msg-state.is-failed .icon { width: 13px; height: 13px; opacity: 1; color: var(--danger-text); }
.chat-msg-state.is-failed .icon svg { stroke-width: 2.2; }
@keyframes chatStatePulse {
  0%, 100% { opacity: .55; }
  50% { opacity: 1; }
}
/* 未送达的气泡可点重发（整条消息都是热区，状态行是提示） */
.chat-msg-failed { cursor: pointer; }
.chat-msg-user.chat-msg-failed .chat-msg-bubble {
  box-shadow: 0 0 0 1px rgba(255,255,255,.45), 0 2px 12px rgba(0,0,0,.3);
}
/* 生成中的「停止」入口：悬浮在 tab 栏上方的胶囊。
   放气泡里会压住流式文字（气泡宽度随内容变），放输入区又要点两下 —— 悬浮胶囊最省事。

   ⚠ 2026-10-30（用户第一项：「对话停止按钮改为红色醒目样式并增加涟漪波纹动画」）
   —— 停止是**破坏性操作**，原先是中性卡片样式（米白底 + 灰字），
      用户很难一眼把它和「普通胶囊」区分开。
   改法：
     ① 红色实底 + 白字 + 红色外发光（醒目）；
     ② ::after = **涟漪波纹**：一圈从按钮中心扩散又淡出的红环，
        常驻循环 —— 提示「这里正在发生什么、可以打断」。
   ⚠ 涟漪用 **transform + opacity** 动画（compositor 属性），
     不动 width/height ⇒ 不触发布局、不抖按钮。 */
.chat-stop-fab {
  position: fixed;
  left: 50%;
  /* ⚠ 2026-11-04（用户：「停止按钮在手机键盘弹起后会产生额外的边距，导致位置偏高」）：
     底部占位要跟**当前真正占着底部的东西**走 ——
       · 输入抽屉开着（键盘弹起）→ 占位是它（抽屉被 --kb-inset 顶到键盘上沿）
       · 抽屉关着 → 占位才是底部 tab 栏
     之前这里写死 --nav-space（= tab 栏高度 + 安全区 + 间隙）：
     键盘弹起时那个值**一动不动**（dock 只是淡出，尺寸没变）⇒
     胶囊停在「键盘还没弹起时的老位置」，离输入条上沿空出一大截 ⇒ 看着就是多一截边距、位置偏高。
     --chat-bottom-inset 就是这个值（list.js 的 syncBottomInset 写的，
     「回到底部」按钮 .chat-jump-btn 用的也是它）⇒ 两处口径统一。 */
  bottom: calc(var(--chat-bottom-inset, var(--nav-space, 85px)) + 14px);
  transform: translateX(-50%);
  z-index: var(--z-fab);
  display: none;
  align-items: center;
  gap: 6px;
  padding: 7px 14px;
  font-size: 13px;
  font-weight: 600;
  color: #fff;
  background: linear-gradient(180deg, #f0563f, #d63a24);
  border: 1px solid rgba(255, 255, 255, .22);
  border-radius: 20px;
  box-shadow:
    0 6px 20px rgba(214, 58, 36, .38),
    0 0 0 1px rgba(214, 58, 36, .18);
  cursor: pointer;
  overflow: hidden;        /* 涟漪不越出按钮圆角 */
  isolation: isolate;      /* 涟漪 z-index 不与外部元素打架 */
}
/* 涟漪波纹：两圈错开相位扩散 */
.chat-stop-fab::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 12px;
  height: 12px;
  margin: -6px 0 0 -6px;
  border-radius: 50%;
  background: rgba(255, 255, 255, .55);
  transform: scale(0);
  opacity: .55;
  pointer-events: none;
  z-index: -1;
  animation: stopFabRipple 1.9s cubic-bezier(.22, .61, .36, 1) infinite;
}
@keyframes stopFabRipple {
  0%   { transform: scale(0);   opacity: .50; }
  70%  { opacity: .16; }
  100% { transform: scale(11); opacity: 0; }
}
/* 夜间档：红底压暗一档，涟漪亮度也收一档（深色页面上太亮会刺眼） */
html[data-theme="dark"] .chat-stop-fab {
  background: linear-gradient(180deg, #d9482f, #a92c1a);
  box-shadow:
    0 6px 22px rgba(0, 0, 0, .55),
    0 0 0 1px rgba(217, 72, 47, .30);
}
html[data-theme="dark"] .chat-stop-fab::after {
  background: rgba(255, 255, 255, .38);
}
/* 尊重「减少动态效果」的系统偏好：关掉涟漪，保留红色（醒目性不受影响） */
@media (prefers-reduced-motion: reduce) {
  .chat-stop-fab::after { animation: none; opacity: 0; }
}

/* 「回到底部」：用户往上翻历史时出现在右下角。
   贴着底部时**不出现** —— 那时它没用，还会挡住最后一条消息。
   位置跟着 --chat-bottom-inset（输入条/键盘占的那一截）走，
   和「停止」胶囊不打架（那个在底部居中，这个在右下角）。 */
.chat-jump-btn {
  position: fixed;
  right: 14px;
  bottom: calc(var(--chat-bottom-inset, 85px) + 10px);
  z-index: var(--z-fab);
  display: none;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  padding: 0;
  color: var(--text-secondary);
  background: var(--bg-card, rgba(255,255,255,.92));
  border: 1px solid var(--border);
  border-radius: 50%;
  box-shadow: 0 4px 16px rgba(0,0,0,.14);
  cursor: pointer;
}
.chat-jump-btn:active { transform: scale(.94); }
.chat-jump-btn svg { width: 18px; height: 18px; }
html[data-theme="dark"] .chat-msg-user .chat-msg-bubble {
  /* 夜间是「深底浅字」的反相气泡：底色走初音绿色阶的深端，文字走浅端 */
  background: rgba(13, 59, 56, 0.4);
  color: #7fe6dc;
  --bubble-inset: rgba(0, 0, 0, 0.16);
  box-shadow: 0 0 0 1px rgba(57,197,187,0.34), 0 2px 12px rgba(0,0,0,0.3);
}
/* 夜间未送达：环改成砖红（白环在深青气泡上看不出来） */
html[data-theme="dark"] .chat-msg-user.chat-msg-failed .chat-msg-bubble {
  box-shadow: 0 0 0 1px rgba(224,140,120,.65), 0 2px 12px rgba(0,0,0,0.3);
}
/* 助手气泡的**外层**（.chat-msg-bot-wrap）：只干两件事 ——
   ① 给「回复耗时」当定位基准（.chat-msg-dur 用 left:100% 贴在气泡右缘之外）；
   ② 承接气泡的宽度上限（原来那条 78% 写在 .chat-msg 上，挪到外层，比例不变）。
   ⚠ 耗时**不能**直接放进 .chat-msg-bot：气泡自己是 overflow:auto 的滚动容器，
      绝对定位在它里面的东西会被**裁掉**（实测 elementFromPoint 命中的是 BODY，
      不是那个元素 —— 元素在，屏幕上却看不见）。 */
.chat-msg-bot-wrap {
  position: relative;
  align-self: flex-start;    /* 与气泡原来的 align-self 一致：贴左 */
  max-width: 78%;
  display: block;
  margin-left: 46px;          /* 给艾玛表情留出左侧位置，不改变气泡内部排版 */
  /* ⚠ 宽度上限**只留这一处**（外层）。以前内层 .chat-msg 也有一条 max-width:78%，
     两处相乘 ⇒ 气泡比外层窄一大截（实测 219 → 170.8px），
     思考态那句「已耗时 Ns」被挤成「已...」。
     ⚠ width 写死成 fit-content（而不是 auto）：
        auto 时这是 flex 项的隐式 shrink-to-fit，**百分比宽度在它里面会变成循环依赖**
        （父宽正在算、子宽又按父宽算）⇒ `width: min(248px, 78%)` 整条失效、
        气泡塌成一条窄缝。写死 fit-content 后百分比才有确定的参照。 */
  width: fit-content;
}
.chat-msg-expression {
  position: absolute;
  left: -47px;
  bottom: 0;
  z-index: 1;
  display: block;
  width: 40px;
  height: 40px;
  object-fit: contain;
  pointer-events: none;
  user-select: none;
}
@media (max-width: 360px) {
  .chat-msg-bot-wrap { margin-left: 40px; }
  .chat-msg-expression { left: -40px; width: 34px; height: 34px; }
}
/* 气泡宽度**跟着外层走**，不再二次相乘。 */
.chat-msg-bot-wrap > .chat-msg-bot,
.chat-msg-bot-wrap > .chat-msg-greeting {
  max-width: 100%;
}
/* 思考态：**宽度跟着文字撑开**（用户 2026-10-28 第八轮）。
   ⚠ 上一轮（第七轮）把思考态的气泡**钉在 78% 最大宽度**：
     「在思考过程里，气泡保持最大宽度也不好看」⇒ 本轮撤销。
     ⇒ 删掉 .is-thinking 的两条规则，外层 .chat-msg-bot-wrap 的
       `width: fit-content` 就是唯一的宽度口径 ⇒ 思考态与正文态行为一致。 */
.chat-msg-bot, .chat-msg-greeting {
  /* 气泡底色提成变量：markdown 表格的右侧渐隐提示要用它做渐变终点，
     否则渐隐层颜色对不上气泡底，边缘会露出一条色差 */
  --bubble-bg: #fffdf9;
  /* ⚠ 2026-10-30（用户第二项：「气泡日间模式下透明度增加，让背景立绘更清晰」）
     ⇒ 日间 0.4 → **0.26**（背景透过来的更多，立绘更清楚）。
     ⚠ 但 --bubble-bg 必须**保持不透明** —— 它是表格右侧渐隐层的终点色，
        改成半透明会让溢出表格的内容半透可见（等于没隐藏）。 */
  --bubble-fill: rgba(255, 253, 249, 0.26);
  background: var(--bubble-fill);
  color: #23211c;
  align-self: flex-start;
  border-radius: 16px 16px 16px 4px;
  box-shadow: 0 1px 4px rgba(0,0,0,0.08);
  /* 气泡内引用块 / 表格条纹的内凹层：跟随气泡底做深浅，不回到页面底色 */
  --bubble-inset: rgba(0, 0, 0, 0.035);
}

/* 回复耗时：气泡**外侧右侧**（用户 2026-10-24）。
   绝对定位在**外层**（.chat-msg-bot-wrap）的右缘之外 → 不占气泡宽度，
   气泡里的正文不受影响；也不会被气泡的 overflow 裁掉（见上面那条注释）。
   纵向**对齐气泡底部**（同 .chat-msg-state：用 `bottom`，别和 `top` 同时留）。
   是**文字**（11px）→ 颜色用 --text-muted 且不加 opacity，
   保证 ≥4.5:1（对比度下限对文字更严）。 */
.chat-msg-dur {
  position: absolute;
  left: 100%;           /* 贴气泡右缘之外 */
  bottom: 3px;          /* 纵向对齐气泡底部 */
  margin-left: 8px;
  font-size: 11px;
  line-height: 1.6;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  pointer-events: none;   /* 别挡住旁边的内容 */
}
/* 「时间 · 耗时」两段之间的分隔点：左右各留 4px ——
   不留的话拼成「23:45·3.1s」，两个数黏在一起读不出来。 */
.chat-msg-sep { margin: 0 4px; }
/* 夜间档助手气泡。
   ⚠ 2026-10-29（用户：「消息列表首条占位文本缺少夜间模式适配」）：
     原来这条只写 `.chat-msg-bot` ⇒ **占位气泡**（`.chat-msg-greeting`，
     故意不带 `chat-msg-bot` 那个类）拿不到夜间样式 ⇒ 夜间下仍是米白纸卡底 + 深墨字。
     ⇒ 占位气泡必须一起进这条规则。 */
html[data-theme="dark"] .chat-msg-bot,
html[data-theme="dark"] .chat-msg-greeting {
  --bubble-bg: #232320;
  /* 夜间同样半透明 0.4（背景透过来）—— --bubble-bg 保持不透明给表格渐隐用 */
  --bubble-fill: rgba(35, 35, 32, 0.4);
  background: var(--bubble-fill);
  color: #f0ece0;
  box-shadow: 0 0 0 1px rgba(57,197,187,0.28), 0 2px 12px rgba(0,0,0,0.4);
  --bubble-inset: rgba(0, 0, 0, 0.12);
}
/* 出错的气泡（bot 异常）：红底也**要半透明**。
   ⚠ 2026-10-30（用户：「需要将bot的异常气泡的红色背景也同样设置透明度」）
   —— 口径跟普通气泡一致：日间 0.26、夜间 0.4 ⇒ 背景立绘照样透过来，
      不会因为「这一条是红色」就突然变成一块死板的实色。
   ⚠ 红色不透明度低 ⇒ 文字对比度靠**文字颜色**保证（#c00 / #f88），
     不是靠底色深度。 */
.chat-msg-error {
  background: rgba(255, 240, 240, 0.26);
  color: #c00;
  border: 1px solid rgba(255, 153, 153, 0.45);
}
html[data-theme="dark"] .chat-msg-error {
  background: rgba(58, 32, 32, 0.4);
  color: #f88;
  border-color: rgba(122, 64, 64, 0.55);
}
/* 截断提示：回复**提前结束**时在气泡末尾的那句轻提示。
   ⚠ **不是红色** —— 红色是「出错」（这条回复作废）；这里是「话说了一半」，
     已经说出来的内容仍然有效，只是没说完 ⇒ 用次要信息那一档灰。
   ⚠ 不加图标、不加按钮：它只说明情况，不要求用户做任何事。 */
.chat-stop-note {
  display: block;
  margin-top: 6px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-muted);
}
/* 状态提示（2026-10-30）：网关的旁白（压缩上下文…），显示在气泡**上方**。
   ⚠ 用户原话：「在气泡内以弱化的提示文本呈现，这样不会被用户当做bot的回复内容」
     ⇒ 必须**明显弱于正文**：灰、小、斜体，且没有背景块/边框，
       不会被误读成 bot 说的一句话。 */
.chat-status-note {
  display: block;
  margin: 0 0 6px;
  font-size: 11.5px;
  line-height: 1.5;
  font-style: italic;
  color: var(--text-muted);
  opacity: 0.75;
}
/* 生成中的气泡：只留「弱化 + 斜体」的字色。
   圆点不再用 ::after 画 —— 提示条改由 JS 渲染（带已等待秒数 + 思考尾巴），
   两处同时画会出现两排圆点。 */
.chat-msg-loading {
  color: var(--text-muted);
  font-style: italic;
}
@keyframes loadPulse {
  0%, 80%, 100% { opacity: 0; transform: scale(0.6); }
  40% { opacity: 1; transform: scale(1); }
}

/* ── 流式期间的正文 ────────────────────────────────────────────────
   2026-10-26：流式阶段改成**直接渲染 markdown**（不再先写纯文本、done 才换）。
   ⇒ 这里不再是纯文本，`white-space: pre-wrap` 得撤掉 ——
   pre-wrap 会把 markdown 段落里的换行也变成真实换行，
   而 markdown-it 的 `breaks: true` 已经处理过换行（渲染成 <br>），
   两者叠加会多出空行。
   兜底路径（markdown-it 没加载）走 `<pre class="md-plain">`，它自己带 pre-wrap。 */
.chat-msg-bot .md-stream {
  word-break: break-word;
  overflow-wrap: anywhere;
}

/* ── 思考 header（气泡顶部的一行）────────────────────────────────────────
   用户 2026-10-26：「你可以在bot气泡header显示单行的省略思考文本，
   用户根本不关心思考内容，只需要让用户知道任务正在进行」。

   为什么需要（现象）：强推理时上游先想很久才吐正文（实测直连 MiniMax：
   响应头本身要 10.174s，思考 95 帧 / 正文 276 帧）。那段时间界面只有三个
   跳动点 —— **看不出这轮还在不在动**，用户会以为卡死了。

   ⚠ 与 2026-10-24 那次的差别（别又踩回去）：
     那天撤掉的是「**思考中 ●●● · Ns**」这个**状态标签** + **思考尾巴全文**
     （都塞在气泡**里面**、占地方、还把模型原话摊给用户）。
     现在这个是：位置在气泡**顶部**（header）、**单行 + 省略**、
     内容只是「还在动」的证据、且恒定显示**已耗时秒数**。

   ⚠ 2026-10-28（用户第八轮）：思考内容**彻底不显示**了 ——
     「还是直接隐藏思考内容吧，在前端让用户等待的反馈就只需要保留
     『已耗时 Ns』和 typing dots 即可」。
     ⇒ 这一行**永远只有秒数**，不含任何思考原文（后端也不下发原文）。
     ⇒ 「省略号」因此其实用不上了（秒数不会很长），但留着无害：
       万一以后又要放别的东西，它至少不会折行、不会把气泡撑成两行。

   为什么敢用省略号（项目里「值不许被截断」是硬要求）：
     那是针对**卡片上的值**（版本号 / 路径 / 域名）—— 截断了就成了假信息。
     这里是**流式过程提示**，内容长度事先不可知，省略号本身就在说「还有更多」，
     不存在「假装完整」的问题。 */
.chat-thinking-head {
  letter-spacing: .02em;               /* 沿用 2026-10-22 旧版思考提示的质感 */
  font-variant-numeric: tabular-nums;  /* 秒数等宽，数字不跳字 */
  display: block;
  max-width: 100%;
  font-size: 12px;
  line-height: 16px;
  color: var(--text-muted);   /* 次要信息：与 .chat-msg-dur 同一档灰 */
  /* 单行 + 溢出省略 ⇒ 内容再长也只占一行，不把气泡撑成两行 */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin: 2px 0 3px;
}
/* ⚠ 2026-10-30（用户第六项：「恢复气泡内的思考文本显示，但是不落库，
   思考文本输出时单行显示、弱化显示。」）
   —— 思考原文单独一段，比「已耗时 Ns」**更淡一档**：
      · .chat-thinking-text = 思考原文（最弱，像脚注）
      · .chat-thinking-sec  = 秒数（次弱）
   —— 单行由父级 .chat-thinking-head 的 nowrap + ellipsis 保证；
      这里额外把思考原文的 alpha 压到 .58 上下（--text-muted 是 .66，
      再淡一档 ⇒ 明显是「过程提示」而不是正式回复）。
   ⚠ 颜色用 var(--text-muted) 的 rgba 覆盖，别写死灰度：
     夜间档 --text-muted 本身是浅色，直接乘 alpha 会变得不可读。 */
.chat-thinking-text {
  color: var(--text-muted);
  opacity: .72;
  font-style: italic;   /* 斜体 ⇒ 一眼看出是「内心独白」不是回复 */
}
.chat-thinking-sec {
  color: var(--text-muted);
  opacity: .9;
}
/* 思考态的宽度**不在这里设**（外层 .chat-msg-bot-wrap 的 width: fit-content 就是全部口径）。
   ⚠ 2026-10-28（用户第八轮）：上一轮把它钉在 78% 最大宽度，
     「在思考过程里，气泡保持最大宽度也不好看」⇒ 本轮撤销，宽度跟着文字撑开。
   ⚠ 更早（2026-10-28 第五轮）之前这里是 `width: min(248px, 78%)` ——
     百分比对着「正在算自身宽度」的父级 ⇒ 循环依赖 ⇒ 整条声明失效（宽度退回 auto）
     ⇒ 气泡宽度跟着思考文本一路抖，用户看到的就是「宽度频繁变化」。
   这个类名继续留着：探针靠它判断「这一轮还在思考」。 */

/* ── 打字点（首 token 前）─────────────────────────────────────────────
   用户 2026-10-24：「bot气泡接收首token前使用彩色流光循环跳动的typing dots，
   不额外显示思考中」——
   所以原来的「思考中 ●●● · Ns」文字**和**上游思考尾巴**都撤掉**，
   气泡里只剩这三个点：
     · 彩色   —— 三点各一个色相（青 → 蓝 → 紫），跟品牌色同一族；
     · 流光   —— 每个点持续 hue-rotate，三点错峰 → 颜色在点之间流转；
     · 跳动   —— 上下弹 + 缩放，错峰延迟 → 一波一波地跳。
   点数固定三个（不是「…」的省略号，是三个独立的球）。 */

.chat-typing {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 20px;              /* 与一行正文齐平，不让气泡高出一截 */
}
.chat-typing-label {
  margin-inline-start: 4px;
  font-size: 0.85em;
  opacity: 0.8;
}
.chat-typing i {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--dot-c);
  box-shadow: 0 0 6px var(--dot-c);
  animation: chatDotBounce 1.15s ease-in-out infinite,
             chatDotHue 3.4s linear infinite;
  animation-delay: calc(var(--dot-i) * 0.13s), calc(var(--dot-i) * 0.13s);
}
.chat-typing i:nth-child(1) { --dot-i: 0; --dot-c: #39c5bb; }
.chat-typing i:nth-child(2) { --dot-i: 1; --dot-c: #56a8ff; }
.chat-typing i:nth-child(3) { --dot-i: 2; --dot-c: #9b8cff; }
@keyframes chatDotBounce {
  0%, 60%, 100% { transform: translateY(0) scale(1); opacity: .45; }
  30% { transform: translateY(-4px) scale(1.18); opacity: 1; }
}
@keyframes chatDotHue {
  from { filter: hue-rotate(0deg); }
  to { filter: hue-rotate(360deg); }
}

/* ---------------- 精简版新增：概览范围标记 + 分类选择 ---------------- */
.ac-hero-scope {
  font-size: 10px; letter-spacing: .04em; color: var(--ac-ink3);
  border: .5px solid var(--ac-line); border-radius: 5px; padding: 1px 6px;
  white-space: nowrap;
}
.ac-cat-pick { display: flex; gap: 10px; margin-bottom: 14px; }
.ac-cat-opt {
  flex: 1 1 0; min-width: 0;
  display: flex; align-items: center; justify-content: center; gap: 7px;
  height: 44px; border-radius: 12px;
  border: 1px solid var(--ac-line); background: transparent;
  font-family: inherit; font-size: 13px; color: var(--ac-ink2); cursor: pointer;
}
.ac-cat-opt i { display: flex; }
.ac-cat-opt i svg { width: 16px; height: 16px; }
.ac-cat-opt.on {
  background: var(--ac-drawer-accent-soft); color: var(--ac-drawer-accent);
  border-color: var(--ac-drawer-accent-line); font-weight: 600;
}

/* ── 顶栏记忆按钮 ── */
.memory-btn { color: var(--text-secondary); }
.memory-btn .icon { width: 16px; height: 16px; }

/* ── 顶栏「输入」按钮（手动开启输入抽屉）──────────────────────────
   ⚠ 2026-10-31（用户：「进入对话页后不再自动触发对话抽屉，
     由用户手动开启抽屉」）⇒ 抽屉的唯一入口是这个顶栏按钮。
   样式沿用 .more-btn（和画像按钮同一套），只调颜色/尺寸。 */
.chat-input-btn { color: var(--text-secondary); }
.chat-input-btn svg { width: 16px; height: 16px; }

/* ── 记忆抽屉 ── */
/* 记忆抽屉骨架：**头尾冻结、内容滚动**（用户 2026-10-23：
   「抽屉改为固定使用最大高度，内容滚动，冻结头部和底部」）。
   抽屉本体固定占满可用高度（不再按内容撑开 —— 见 memory-ui.js 里去掉 fitContent），
   .drawer-body 是抽屉给的滚动容器；这里把它排成三段：
     .mem-tabs   → 顶部页签，**冻结**（flex:none）
     .mem-panel  → 中间内容，**唯一的滚动区**（flex:1 + overflow-y:auto）
     .mem-foot   → 底部操作条，**冻结**（flex:none）
   height:100% 必须写 —— .drawer-body 的高度由 layoutBody() 写成确定的 px，
   子元素才拿得到确定高度，中间的滚动区才滚得动。
   （全局 box-sizing:border-box，所以 padding 算在 100% 里面，不会多出一截。） */
.mem-body {
  display: flex;
  flex-direction: column;
  gap: 10px;
  height: 100%;
  min-height: 0;
  /* 底部留一点边距 —— 贴着屏幕下沿看着像被切掉 */
  padding-bottom: 14px;
}

.mem-search-row {
  display: flex;
  align-items: center;
  gap: 8px;
  /* 搜索框**吸附在顶部**：列表滚下去时它还在（抽屉现在是固定高度的长列表，
     搜索框滚走就找不回来了）。底色要盖住下面滚过去的条目。 */
  position: sticky;
  top: 0;
  z-index: 1;   /* 局部层级：压在记忆列表（auto）之上 */
  background: var(--bg-card-solid);
  padding-bottom: 2px;
}
/* 「＋ 记一条」那套样式已删（2026-10-22）：入口整体移除，记忆改由对话管理。
   后端 POST /api/memory 仍在（程序化写入 / 测试灌数据）。 */
.mem-search {
  flex: 1 1 0;
  min-width: 0;
  height: 38px;
  padding: 0 12px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg-input);
  color: var(--text-primary);
  font-family: inherit;
  font-size: 14px;
}
.mem-search:focus { outline: none; border-color: var(--primary-border); }
.mem-search-btn {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg-input);
  color: var(--text-secondary);
  cursor: pointer;
}
.mem-search-btn .icon { width: 15px; height: 15px; }

/* 列表区：**空白台** —— 至少这么高，内容少时下方留白（不是抽屉缩成一条缝）。
   高度上限由 .drawer-body 的 max-height 兜住（内容多时列表自己滚）。 */
.mem-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  /* flex:none（不压缩）—— 列表是滚动区的**内容**：
     压缩它，内容就溢在列表框外，滚动区的高度也就不会跟着长（滚不动）。
     保持自然高度，超出的部分由 .mem-panel 滚动。 */
  flex: none;
  min-height: 190px;
}
/* 记忆条目：**单行 + 手风琴展开**（2026-10-22 改版）。
   列表态一行：层级徽标 + 正文（超长省略号）—— 不显示时间、不显示删除钮。
   点一下展开看全文 + 时间；一次只开一条（互斥）。 */
.mem-item {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  border-radius: 9px;
  background: var(--bg-card);
  border: 1px solid var(--border-light);
  /* 展开时圆角不跟着内容走 —— 裁一下 */
  overflow: hidden;
}
.mem-item.is-focus {
  border-color: var(--primary-border);
  background: var(--primary-light);
}
/* 勾选框：只在编辑态出现（平时整列宽 0，不占地方） */
.mem-pick { display: none; align-items: center; padding: 8px 0 0 9px; }
.mem-body.is-edit .mem-pick { display: flex; }
/* 自绘勾选框：原生那个在深底上发灰、边角太方，看着像系统控件而不是这张卡的一部分。
   对齐：上内边距 8px 让框心与正文首行行心同高（正文行盒 18.75px，头部上内边距 7px）。 */
.mem-pick-box {
  appearance: none;
  -webkit-appearance: none;
  width: 17px;
  height: 17px;
  margin: 0;
  border: 1.5px solid var(--border);
  border-radius: 6px;
  background: var(--bg-input);
  cursor: pointer;
  display: grid;
  place-content: center;
  transition: background .16s ease, border-color .16s ease, transform .12s ease;
}
.mem-pick-box::after {
  content: '';
  width: 8px;
  height: 4px;
  border-left: 1.8px solid var(--primary-on);
  border-bottom: 1.8px solid var(--primary-on);
  transform: rotate(-45deg) scale(.4);
  opacity: 0;
  transition: transform .16s ease, opacity .12s ease;
}
.mem-pick-box:hover { border-color: var(--primary-border); }
.mem-pick-box:checked { background: var(--primary); border-color: var(--primary); }
.mem-pick-box:checked::after { transform: rotate(-45deg) scale(1); opacity: 1; }
.mem-pick-box:active { transform: scale(.92); }
/* 条目体：展开/折叠的过渡做在这一层的 max-height 上 */
.mem-item-body {
  grid-column: 2;
  overflow: hidden;
  transition: max-height 0.26s ease;
}
/* 条目头：**三列网格**（徽标 | 正文 | 箭头），时间放第二行的正文列。
   时间放「正文那一列」而不是徽标列 —— 左边缘就和正文对齐了，
   不用靠 padding/margin 去猜徽标有多宽（换字体、换标签都会跑偏）。
   正文列必须写 minmax(0, 1fr)：默认 1fr 的最小值是 auto，
   正文列会被内容撑开，超长了也不截断（省略号失效）。 */
.mem-item-head {
  grid-column: 2;
  grid-row: 1;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-rows: auto auto;
  align-items: start;
  column-gap: 6px;
  width: 100%;
  min-width: 0;
  padding: 7px 9px;
  background: transparent;
  border: 0;
  font: inherit;
  text-align: left;
  color: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
/* 层级徽标：事实 / 意图 / 原话。
   徽标文字色都取自已有的墨色阶（--primary-text / --text-secondary / --text-muted），
   不自造新颜色 —— 自造的颜色在米白卡和夜间底上都得单独验对比度。 */
.mem-layer {
  display: inline-block;
  /* 徽标不许被挤窄换行 —— 「事实」两字折成两行，整条就多出一行高 */
  white-space: nowrap;
  grid-column: 1;
  grid-row: 1;
  /* 顶对齐第一行（正文换行变高后徽标不会掉到中间去）；
     上移 1px 补一点：徽标 17.5px 高、正文行盒 18.75px，顶对齐时差 0.6px。 */
  align-self: start;
  margin-top: 2px;
  padding: 0 6px;
  border-radius: 5px;
  font-size: 11px;
  font-weight: 600;
  line-height: 1.55;
  letter-spacing: .02em;
  border: 1px solid transparent;
  vertical-align: 1px;
}
.mem-layer-l2_fact {
  color: var(--primary-text);
  background: var(--primary-light);
  border-color: var(--primary-border);
}
.mem-layer-l7_intention {
  color: var(--text-secondary);
  background: rgba(120, 104, 74, 0.10);
  border-color: var(--border);
}
.mem-layer-l1_raw {
  color: var(--text-muted);
  background: rgba(120, 104, 74, 0.06);
  border-color: var(--border-light);
}
/* 列表态正文：**单行**，装不下就在**右端**省略号；
   展开时改成自然换行 —— 正文接着往下排，不另开一段重复内容。 */
.mem-item-text {
  grid-column: 2;
  grid-row: 1;
  min-width: 0;
  font-size: 14px;
  line-height: 1.5;
  color: var(--text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.mem-item.is-open .mem-item-text { white-space: normal; overflow: visible; }
/* 展开箭头：跟溯源标签一个约定（chevron 转 90°） */
.mem-item-caret {
  grid-column: 3;
  grid-row: 1;
  align-self: start;
  margin-top: 4px;          /* 14px 箭头对 21px 正文行盒（14 × 1.5），差的那点补上 */
  display: inline-flex;
  color: var(--text-muted);
  transition: transform 0.26s ease;
}
.mem-item-caret .icon { width: 14px; height: 14px; }
.mem-item.is-open .mem-item-caret { transform: rotate(90deg); }
/* 时间：只在展开时出现，**落在正文那一列** → 左边缘和正文对齐。
   不写左边距 —— 列的位置已经保证了。 */
.mem-item-time {
  grid-column: 2;
  grid-row: 2;
  display: none;
  padding: 3px 0 0;
  font-size: 12px;
  line-height: 1.55;
  color: var(--text-muted);
}
.mem-item.is-open .mem-item-time { display: block; }

.mem-state { padding: 22px 12px; text-align: center; }
.mem-state-title { font-size: 15px; color: var(--text-secondary); }
.mem-state-desc {
  margin-top: 6px;
  font-size: 13px;
  line-height: 1.7;
  color: var(--text-muted);
}
.mem-state-desc code {
  padding: 1px 5px;
  border-radius: 5px;
  background: var(--bg-input);
  font-size: 12px;
  color: var(--text-secondary);
}

.mem-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--border-light);
  /* 底部操作条**冻结**（不跟着内容滚走） */
  flex: none;
}
.mem-foot-note { font-size: 12px; color: var(--text-muted); }
.mem-clear,
.mem-del-sel {
  flex: none;
  padding: 6px 13px;
  border-radius: 8px;
  border: 1px solid var(--danger-border);
  background: transparent;
  font-family: inherit;
  font-size: 13px;
  color: var(--danger-text);
  cursor: pointer;
}
.mem-clear:hover,
.mem-del-sel:hover { background: var(--danger-light); }
.mem-del-sel:disabled { opacity: .45; cursor: default; }
.mem-del-sel:disabled:hover { background: transparent; }

.mem-panel {
  display: flex;
  flex-direction: column;
  gap: 10px;
  /* 中间这段是抽屉里**唯一**的滚动区（头尾已冻结） */
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
/* ---------------- 画像（2026-11-06 只读重做）----------------
 * 画像 = 记忆按固定六类的结构化视图（真相源仍是记忆）。
 *
 * ⚠ 2026-11-06 用户定案：**这个抽屉只读** —— 原话「我不需要在这里做写入，
 *   这个抽屉只读」。⇒ 「＋记一条」表单、顶栏铅笔（编辑 → 勾选删除）、
 *   条目里的红线三态动作、底部「删除所选」条**全部撤掉**，
 *   对应的 CSS 也一并删掉（不留死样式）。写入与删除在控制台 → 记忆栈里做。
 *
 * 视觉语言：
 *   · 印象 = 主角 ⇒ 一张带**顶部渐变光条**的卡（用户认可过的「顶部变色 + 微光」）；
 *   · 记忆档案 = 支撑材料 ⇒ 六张**手风琴卡片**，互斥（一次只展开一类）；
 *   · 「约束」用**琥珀**而不是红 —— 全站红色是「危险 / 不可逆 / 删数据」的语义，
 *     拿它表示「硬约束」会误导（2026-11-06 改）。
 *
 * 字号只用全站阶梯（10/11/12/13/14/15/16/18），不新增半像素档。
 */

/* 顶部状态条（分类没跑起来 / 红线表读不出来）：
   收成一条细条 —— 原来是一张卡，插在印象与六类之间，把主结构打断了。 */
.pf-warn {
  padding: 8px 11px;
  margin-bottom: 10px;
  border-radius: 10px;
  border: 1px solid var(--danger-border);
  background: var(--danger-light);
  font-size: 12px;
  line-height: 1.6;
  color: var(--danger-text);
}
/* 「红线表读不出来」是**部分失效**，用琥珀（软）不用红（硬失败） */
.pf-warn.is-soft {
  border-color: var(--warning-border);
  background: var(--warning-light);
  color: var(--warning-text);
}
.pf-warn-title { font-weight: 700; }
.pf-warn-desc { margin-top: 2px; opacity: .92; }

/* 六个分组：**手风琴卡片**（2026-11-06）。
   标题做成 button（可点开收起）—— 视觉上跟原来的 div 一样，
   按钮默认样式（边框/背景/内边距）全部抹掉后自己画一张卡。 */
.pf-groups {
  display: flex;
  flex-direction: column;
  gap: 8px;
  /* flex:none —— 跟 .mem-list 一样：分组是滚动区的**内容**，
     压缩它内容会溢在框外，滚动区高度也不跟着长（滚不动）。 */
  flex: none;
}
.pf-group { display: flex; flex-direction: column; }
.pf-group-head {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 10px 11px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--bg-card);
  text-align: left;
  font-family: inherit;
  cursor: pointer;
}
.pf-group-head:hover { border-color: var(--primary-border); }
/* 序号：圆形徽章 —— 原来是一行纯数字，现在才看得出「这是第几类」 */
.pf-group-no {
  flex: none;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
  color: var(--text-muted);
  background: rgba(70, 58, 38, .06);
  font-variant-numeric: tabular-nums;
}
.pf-group-name {
  font-size: 14px;
  font-weight: 700;
  color: var(--text-primary);
}
/* 条数：药丸 —— 原来是一行灰字「3 条」，混在标题里读不出来 */
.pf-group-count {
  margin-left: auto;
  font-size: 11px;
  color: var(--text-muted);
  background: rgba(70, 58, 38, .06);
  border-radius: 20px;
  padding: 1px 8px;
  font-variant-numeric: tabular-nums;
}
/* 箭头：收起时朝右（chevron 图标本身就是朝右），展开时转 90° 朝下。 */
.pf-group-caret {
  display: inline-flex;
  flex: none;
  color: var(--text-muted);
  transition: transform .18s ease;
}
.pf-group-head[aria-expanded="true"] .pf-group-caret { transform: rotate(90deg); }
.pf-group-caret .icon { width: 13px; height: 13px; }
/* 展开区：过渡做在 max-height 上（跟条目手风琴同一手法，见 .mem-item-body）。
   overflow:hidden 是必须的 —— 不裁的话内容会从卡片里溢出来。 */
.pf-group-items {
  display: flex;
  flex-direction: column;
  gap: 4px;
  overflow: hidden;
  transition: max-height .28s ease;
  padding: 8px 2px 2px;
}
.pf-group-items[hidden] { display: none; }
.pf-group-empty {
  font-size: 13px;
  color: var(--text-muted);
  padding: 2px 2px 4px 27px;
}

/* 「约束」那组：**琥珀** —— 不用靠位置也能一眼认出「不能碰」的那一栏。
   ⚠ 原来这里是**红色竖线 + margin-left:-11px**：
     ① 全站红色是「危险 / 不可逆 / 删数据」的语义（--danger 的注释写死了），
        拿它表示「硬约束」会误导；
     ② 负 margin 把整行推得跟另外五行对不齐，看着歪。
   ⇒ 改成整张卡染琥珀（描边 + 淡底 + 字色），不挪位置。 */
.pf-group.is-constraint .pf-group-head {
  border-color: var(--warning-border);
  background: var(--warning-light);
}
.pf-group.is-constraint .pf-group-name { color: var(--warning-text); }
.pf-group.is-constraint .pf-group-no {
  background: var(--warning-light);
  color: var(--warning-text);
}
.pf-group.is-constraint .pf-group-count {
  background: var(--warning-light);
  color: var(--warning-text);
}

/* 红线**状态**徽标（只读）：画像抽屉不再给改的动作，但状态要看得见。
   琥珀 —— 红线是「硬约束」，不是「危险」。 */
.mem-pin-badge {
  display: inline-block;
  align-self: start;
  margin-top: 2px;
  padding: 0 6px;
  border-radius: 5px;
  font-size: 10px;
  font-weight: 600;
  line-height: 1.7;
  color: var(--warning-text);
  background: var(--warning-light);
  border: 1px solid var(--warning-border);
  white-space: nowrap;
}

/* 徽标槽：层级徽标 + 红线徽标并排，占正文左边那一列。
   （.mem-item-head 的列：徽标 | 正文 | 箭头） */
.mem-item-tags {
  grid-column: 1;
  grid-row: 1;
  display: inline-flex;
  align-items: flex-start;
  gap: 4px;
  min-width: 0;
}

/* 印象区（2026-11-06 重做）
   —— 一段 3~5 句的叙述 + 「依据 N 条记忆」整段展开。
   印象是**主角**，六类退成支撑材料 ⇒ 这块自己占一整张卡，
   顶部一条渐变光条（用户认可过的「顶部变色 + 微光」那种语言），
   每句前面一个小圆点，读起来是一段叙述而不是一列条目。 */
.pf-imp {
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 13px 13px 11px;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: var(--bg-card);
}
.pf-imp::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: linear-gradient(90deg, var(--primary), #8ad9d2 68%, #c6ece8);
}
html[data-theme="dark"] .pf-imp::before {
  background: linear-gradient(90deg, var(--primary), #2f7d76 68%, #23564f);
}
.pf-imp-head { display: flex; align-items: baseline; gap: 8px; }
.pf-imp-title {
  font-size: 14px;
  font-weight: 700;
  color: var(--text-primary);
}
/* 句数：药丸 —— 原来是一行灰字，跟标题平级，看不出是「计数」 */
.pf-imp-count {
  margin-left: auto;
  font-size: 11px;
  color: var(--text-muted);
  background: rgba(70, 58, 38, .06);
  border-radius: 20px;
  padding: 1px 8px;
  font-variant-numeric: tabular-nums;
}
html[data-theme="dark"] .pf-imp-count { background: rgba(255, 255, 255, .07); }
.pf-imp-text { display: flex; flex-direction: column; gap: 4px; }
/* 每句一行：前面一个小圆点，读起来是叙述不是清单 */
.pf-imp-line {
  margin: 0;
  display: flex;
  gap: 7px;
  font-size: 13px;
  line-height: 1.8;
  color: var(--text-secondary);
}
.pf-imp-line::before {
  content: '';
  flex: none;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--primary);
  margin-top: 8px;
}
.pf-imp-ev {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  align-self: flex-start;
  padding: 0;
  border: 0;
  background: none;
  font-size: 12px;
  color: var(--text-muted);
  cursor: pointer;
}
.pf-imp-ev:hover { color: var(--text-secondary); }
.pf-imp-ev .icon { width: 12px; height: 12px; }
.pf-imp-caret {
  display: inline-flex;
  flex: none;
  transition: transform .18s ease;
}
.pf-imp-ev[aria-expanded="true"] .pf-imp-caret { transform: rotate(90deg); }
.pf-imp-ev-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-top: 2px;
}
.pf-imp-ev-list[hidden] { display: none; }

/* 印象没算出来 —— 如实说，不假装有印象（左边一条竖线，跟「空白占位」区分开）。 */
.pf-imp.is-fail {
  border-left: 2px solid var(--danger-text);
}
.pf-imp.is-fail::before { background: var(--danger-border); }
.pf-imp.is-fail .pf-imp-title { color: var(--danger-text); }
.pf-imp-reason {
  font-size: 12px;
  line-height: 1.6;
  color: var(--text-muted);
}

/* 「未进画像」块（2026-10-24 加 · 2026-11-06 微调）
   —— 判为「在未来的对话里没有用」的条目：默认收起，只报个数。
   整块压得很轻：它是**旁注**，不是画像的一组。
   ⚠ 只读之后这里**没有删除按钮**了 ⇒ 说明里补了一句「去哪儿管」，
     不然用户点开发现改不了，会以为功能坏了。 */
.pf-skipped {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--border-light);
}
.pf-skipped-head {
  display: flex;
  align-items: baseline;
  gap: 7px;
  width: 100%;
  padding: 0;
  background: none;
  border: none;
  text-align: left;
  cursor: pointer;
}
.pf-skipped-name {
  font-size: 13px;
  color: var(--text-muted);
}
.pf-skipped-count {
  margin-left: auto;
  font-size: 11px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.pf-skipped-desc {
  font-size: 12px;
  color: var(--text-muted);
  line-height: 1.5;
}
.pf-skipped-items { display: flex; flex-direction: column; gap: 4px; }
.pf-skipped-items[hidden] { display: none; }

/* Romantic portrait motion. Animate the wrapper so image swaps retain their alignment. */
.chat-art {
  background: radial-gradient(ellipse at 18% 22%, rgba(255,225,220,.26), transparent 70%),
    radial-gradient(ellipse at 85% 48%, rgba(234,230,255,.25), transparent 75%),
    linear-gradient(#fdf6f8, #f6f1f9);
}
html[data-theme="dark"] .chat-art {
  background: radial-gradient(ellipse at 18% 22%, rgba(115,65,86,.15), transparent 70%), #181c27;
}
.chat-art-figure { animation: emmaPortraitFloat 6s ease-in-out infinite; }
@keyframes emmaPortraitFloat {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-4px); }
}
.chat-particles {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  transition: opacity .55s ease, visibility 0s linear .55s;
}
.pg-chat.is-welcome .chat-particles {
  opacity: 1;
  visibility: visible;
  transition-delay: 0s;
}
@starting-style {
  .pg-chat.is-welcome .chat-particles { opacity: 0; }
}
.chat-particles i {
  position: absolute; left: var(--x); top: 0;
  width: var(--size); height: calc(var(--size) * .68);
  border-radius: 85% 15% 80% 20%;
  background: linear-gradient(135deg, #fff0f4 0%, #ffb6ca 48%, #ed789d 100%);
  box-shadow: 0 1px 8px rgba(234,111,151,.52), 0 0 14px rgba(255,199,214,.3);
  opacity: 0;
  animation: emmaPetalDrift var(--d) linear var(--delay) infinite backwards;
}
.chat-particles .petal-1 { border-radius: 15% 85% 20% 80%; background:linear-gradient(135deg,#fff5f7,#f49bb5 65%,#df6f94); }
.chat-particles .petal-2 { border-radius: 80% 20% 85% 15%; background:linear-gradient(135deg,#ffe8ef,#f7a9bf 55%,#e981a2); }
.chat-particles .petal-3 { border-radius: 20% 80% 15% 85%; background:linear-gradient(135deg,#fff1f4,#f4a0b8 58%,#d9688d); }
@keyframes emmaPetalDrift {
  0% { transform: translate3d(0, -8vh, 0) rotate(var(--turn)) scale(.78); opacity:0; }
  12% { opacity:.95; }
  50% { transform: translate3d(var(--sway), 45vh, 0) rotate(calc(var(--turn) + 220deg)) scale(1.08); }
  88% { opacity:.9; }
  100% { transform: translate3d(calc(var(--sway) * -1), 105vh, 0) rotate(calc(var(--turn) + 480deg)) scale(.82); opacity:0; }
}
body.chat-drawer-open .chat-art-figure,
html.kb-up .chat-art-figure { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) {
  .chat-art-figure, .chat-particles i { animation: none; }
  .chat-particles { display: none; }
}

/* The story ends in-place; only application chrome appears afterwards. */
body.emma-opening .topbar, body.emma-opening .tab-bar {
  opacity: 0; pointer-events: none;
}
/* Cold-start guard: if persisted chat mode arrives before the welcome reset,
   never expose chat placeholders or its dimmed portrait under the opening. */
body.emma-opening .chat-reply,
body.emma-opening #chatMiniDrawer,
body.emma-native-opening .chat-reply,
body.emma-native-opening #chatMiniDrawer { display: none !important; }
body.emma-opening .pg-chat.is-chat .chat-art-img { opacity: 1 !important; }
body.emma-opening .chat-art-figure { pointer-events: none; }
.topbar { transition: opacity .3s ease; }

/* The scene belongs to the body, outside transformed/clipped page containers. */
body.is-chat-page { isolation: isolate; background: transparent; }
body.is-chat-page::before {
  content: ''; position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background: radial-gradient(ellipse at 18% 22%, rgba(255,225,220,.26), transparent 70%),
    radial-gradient(ellipse at 85% 48%, rgba(234,230,255,.25), transparent 75%),
    linear-gradient(#fdf6f8, #f6f1f9);
}
@media (prefers-reduced-motion: no-preference) {
  body.is-chat-page::before { animation: emmaChatBackdropIn .7s ease-out both; }
}
@keyframes emmaChatBackdropIn {
  from { opacity: 0; }
  to { opacity: 1; }
}
html[data-theme="dark"] body.is-chat-page::before {
  background: radial-gradient(ellipse at 18% 22%, rgba(115,65,86,.15), transparent 70%), #181c27;
}
.pg-chat .chat-art { background: transparent; }
.pg-chat.is-chat .chat-art::after { background: transparent; }
.chat-art-stage { left: var(--safe-l, 0px); right: var(--safe-r, 0px); }
.chat-welcome { left: var(--safe-l, 0px); right: var(--safe-r, 0px); }

/* Emma dialogue theme START: shared verbatim with the packaged opening. */
@font-face {
  font-family: "EmmaDialogue";
  src: url("/assets/fonts/EmmaDialogue.ttf") format("truetype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
.chat-greet-text {
  font-family: "EmmaDialogue", "Noto Serif CJK SC", "Songti SC", serif;
  font-size: 17px;
  font-weight: 400;
  line-height: 40px;
  letter-spacing: 1.7px;
  color: #715863;
}
html[data-theme="dark"] .chat-greet-text { color: #e2d4df; }
.chat-greet { height: 56px; }
.chat-greet::after {
  content: "";
  display: block;
  width: 156px;
  height: 18px;
  /* Artwork occupies the existing gap; it must not resize the portrait's layout band. */
  position: absolute;
  top: 44px;
  left: 50%;
  transform: translateX(-50%);
  margin: 0;
  background: url("/assets/brand/dialogue-ornament.png") center / contain no-repeat;
}
body.emma-native-opening .chat-art-figure { animation: none; }
body.emma-native-opening .topbar,
body.emma-native-opening .tab-bar { opacity: 0 !important; pointer-events: none; }
body.emma-native-opening .chat-art-figure { pointer-events: none; }
@media (max-width: 360px) {
  .chat-greet-text { font-size: 15.5px; letter-spacing: 1.2px; }
}
/* Emma dialogue theme END */

html[data-theme="dark"] .pg-chat .chat-art,
html[data-theme="dark"] .pg-chat.is-chat .chat-art::after { background: transparent; }

/* Profile drawer (2026-11-06): 顶部营销文案那一块撤掉了 ——
   「让艾玛更了解你」+「长期记忆」eyebrow + 更新状态小字改成抽屉副标题里的
   一行数据摘要（多少条记忆 · 什么时候更新）。这里只留「记忆档案」段标题。 */

/* 顶部：状态条 + 印象卡。
   flex:none —— .mem-panel 是滚动区（flex 列），不写的话
   内容一多，印象卡会被 flex 压扁（而不是把滚动条顶长）。 */
.pf-hero {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.pf-section-title {
  flex: none;
  padding: 16px 2px 8px;
  font-size: 12px;
  letter-spacing: .08em;
  color: var(--text-secondary);
}
.chat-process { margin-top: 14px; border-top: 1px solid var(--border-light); font-size: 13px; }
.chat-process summary { position: sticky; top: 0; z-index: 1; padding: 10px 4px; cursor: pointer;
  color: var(--text-secondary); background: var(--page-bg); }
.chat-process summary::after { content: '展开'; float: right; margin-left: 14px; color: var(--primary-text); }
.chat-process[open] summary::after { content: '收起'; }
.chat-process-body { max-height: min(40dvh, 320px); overflow-y: auto; overscroll-behavior: contain;
  padding: 4px 4px 10px; overflow-wrap: anywhere; }

/* Reasoning is opt-in to read; tool activity never creates a transcript panel. */
.chat-reasoning summary { background: transparent; position: static; }
.chat-reasoning-body { white-space: pre-wrap; font-size: 13px; line-height: 1.65;
  color: var(--text-secondary); max-height: min(32dvh, 240px); }

/* Error explanations determine card height; reserve only a minimum during loading. */
.cx .cx-panel { flex-shrink: 0; min-width: 0; max-width: 100%; box-sizing: border-box; }
.cx .cx-hero { height: auto; max-height: none; }
.cx .cx-why, .cx .cx-why [data-why-main], .cx .cx-why [data-why-raw] {
  height: auto; max-height: none; white-space: normal; overflow: visible;
  overflow-wrap: anywhere; word-break: normal; text-overflow: clip;
  -webkit-line-clamp: unset;
}
.cx [data-v="m.main"] { display: block; min-height: 1.5em; line-height: 1.5;
  white-space: nowrap; overflow: hidden; }
.cx .cx-model-ticker { white-space: nowrap; }
.cx-memory-storage { border: 0; font: inherit; text-align: left; color: inherit; cursor: pointer; }
.cx-memory-storage:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.cx-storage-detail { padding: 4px 16px 16px; line-height: 1.65; overflow-wrap: anywhere; }
.cx-storage-detail dl { display: grid; grid-template-columns: minmax(0,1fr) auto; gap: 10px 16px; }
.cx-storage-detail dt, .cx-storage-detail dd { margin: 0; }

/* Global select styling: a steady border while focused, with one shared control shape. */
select,
.drawer-panel select,
.s-form-group select,
.cx-persona-field select,
.emma-proposal select {
  appearance:none;
  -webkit-appearance:none;
  box-sizing:border-box;
  min-height:40px;
  padding:9px 36px 9px 12px;
  border:1px solid var(--border) !important;
  border-radius:10px;
  background-color:var(--bg-input);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%237b817f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat:no-repeat;
  background-position:right 12px center;
  color:var(--text-primary);
  font:inherit;
  box-shadow:none !important;
  outline:none !important;
  transition:background-color .16s ease;
}
select:focus,
select:focus-visible,
select:active,
.drawer-panel select:focus,
.s-form-group select:focus,
.cx-persona-field select:focus-visible,
.emma-proposal select:focus {
  border-color:var(--border) !important;
  outline:none !important;
  box-shadow:none !important;
}
select option { background:var(--bg-card); color:var(--text-primary); }

/* --- auth.css --- */
/* ═══════════════════════════════════════════════════════════════════
   艾玛 · 登录页样式
   ═══════════════════════════════════════════════════════════════════
   独立成一个文件（css/auth.css），不混进 style.css ——
   登录页是唯一需要在未登录状态也能加载的界面，单独拆开
   便于以后整体替换/下线。
   ═══════════════════════════════════════════════════════════════════ */

.auth-overlay {
  position: fixed;
  inset: 0;
  /* 走全局层级 token（css/style.css 的 :root 唯一出处，1–100 阶梯）。
     登录页出现在主界面挂载之前，此时还没有抽屉 / toast / 确认浮层，
     所以用 --z-modal 这一档就够，不必破阶梯写 9999。 */
  z-index: var(--z-modal);
  display: none;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: var(--bg-base, #f2f3f7);
  /* 移动端键盘弹出时，允许卡片滚动而不是被截断 */
  overflow-y: auto;
}

.auth-card {
  width: 100%;
  max-width: 340px;
  padding: 32px 24px 26px;
  border-radius: 20px;
  background: var(--bg-card-solid, #fffdf9);
  border: 1px solid var(--border, rgba(70, 58, 38, 0.12));
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.10);
  text-align: center;
}

/* 字号说明（docs/DEVELOPMENT.md §7.3 的字号阶梯）：
   本文件**不引入阶梯外的新值**。
   · 18px 是阶梯内的档（正文级）
   · 22px 与顶栏品牌字（.topbar-title）同档 —— 登录页的「Emma」要和
     进去以后看到的品牌字一样大，不能一个 22 一个 18
   · 13 / 15px 是阶梯内的档
   阶梯本身是「正文档位表」，显示级字号（20/22/24/26）在 pages.css 里
   已有既有用法，这里沿用既有值，不另造新的。 */
.auth-logo {
  width: 52px;
  height: 52px;
  margin: 0 auto 14px;
  border-radius: 15px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 24px;
  font-weight: 600;
  color: var(--primary-on, #0d2b29);
  background: var(--primary, #39c5bb);
}

.auth-title {
  margin: 0 0 4px;
  font-size: 22px;
  font-weight: 600;
  color: var(--text-primary, #23211c);
}

.auth-sub {
  margin: 0 0 20px;
  font-size: 13px;
  color: var(--text-muted, rgba(35, 33, 28, 0.66));
}

.auth-input {
  width: 100%;
  box-sizing: border-box;
  height: 44px;
  padding: 0 14px;
  margin-bottom: 16px;
  font-size: 15px;
  color: var(--text-primary, #23211c);
  background: var(--bg-input, rgba(255, 253, 249, 0.86));
  border: 1px solid var(--border, rgba(70, 58, 38, 0.12));
  border-radius: 11px;
  outline: none;
  transition: border-color 0.16s ease;
}

.auth-input:focus {
  border-color: var(--primary, #39c5bb);
}

/* 错误行：出现时把登录按钮往下推，不出现时不占位 */
.auth-err {
  margin: 0 0 16px;
  font-size: 13px;
  color: var(--danger-text, #a04a38);
  text-align: left;
}

.auth-btn {
  width: 100%;
  height: 44px;
  font-size: 15px;
  font-weight: 500;
  letter-spacing: 2px;
  color: var(--primary-on, #0d2b29);
  background: var(--primary, #39c5bb);
  border: none;
  border-radius: 11px;
  cursor: pointer;
  transition: opacity 0.16s ease;
}

.auth-btn:hover:not(:disabled) { opacity: 0.88; }
.auth-btn:disabled { opacity: 0.55; cursor: default; }

/* 深色主题 */
@media (prefers-color-scheme: dark) {
  .auth-card {
    background: var(--bg-card-solid, #1c1c16);
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);
  }
}

