/* ============================================
 * 既心 - 智能对话伴侣 UI 样式
 * 设计灵感：Slack + Discord + Linear
 * 主色调：柔和紫 (#8b5cf6) / 靛蓝 (#4f46e5)
 * 基准网格：8px
 * ============================================ */

/* -------------------------- */
/* 设计 Token - 全局变量      */
/* -------------------------- */
:root {
  /* 主色调 */
  --primary-50: #f5f3ff;
  --primary-100: #ede9fe;
  --primary-200: #ddd6fe;
  --primary-300: #c4b5fd;
  --primary-400: #a78bfa;
  /* 白字压这一族要从 --primary-600 起：压 --primary-500 只有 4.23:1（正文判据 4.5），压 600 是 5.70:1。
     渐变同理——首档是较浅那一截，界面上最差的分数按它算，所以渐变起点也写 600（第 57 轮，scripts/contrast.js 量）。 */
  --primary-500: #8b5cf6;
  --primary-600: #7c3aed;
  --primary-700: #6d28d9;
  --primary-800: #5b21b6;
  --primary-900: #4c1d95;
  /* 紫字（不是白字压紫底，是紫底上的那行紫字）单独一档：从前全站 7 处直接写 --primary-500，
     压在最浅的白底上 4.23:1、压在 --bg-chat 上 4.05:1——正文判据 4.5:1，从来没读够。
     浅色往深取一档（--primary-600 压白底 5.70:1），暗色方向相反、要往亮走三档
     （--primary-300 压 --bg-card 5.59:1，而 --primary-400 只有 3.79:1）。
     第 61 轮级联感知量出来的，见 scripts/contrast.js。 */
  --accent-text: var(--primary-600);
  
  /* 中性色 */
  --neutral-50: #f8fafc;
  --neutral-100: #f1f5f9;
  --neutral-200: #e2e8f0;
  --neutral-300: #cbd5e1;
  --neutral-400: #94a3b8;
  --neutral-500: #64748b;
  --neutral-600: #475569;
  --neutral-700: #334155;
  --neutral-800: #1e293b;
  --neutral-900: #0f172a;
  
  /* 背景色 */
  --bg-page: #f0f2f5;
  --bg-sidebar: #ffffff;
  --bg-chat: #f8fafc;
  --bg-card: #ffffff;
  --bg-input: #ffffff;
  /* 悬停那一层与次级按钮那一层。这几处从前写的是 --bg-hover / --bg-secondary，
     而全站从来没定义过这两个变量——浏览器把整条 background 丢掉：
     头像按钮和数据按钮看着没有底，悬停也点不出任何反馈。 */
  --bg-hover: var(--neutral-100);
  --bg-secondary: var(--neutral-100);
  
  /* 文字色 = neutral 刻度的一档一级：正文 slate-800、次级 slate-700、次要 slate-600。
     从前是 800/500/400，次要那档 #94a3b8 压在最深的页面底 #f0f2f5 上只有 2.29:1——提示语、
     时间戳、输入框占位符这些"次要文字"（全站 37 + 48 处）从来没读清过。WCAG 正文 4.5:1
     不看这行字重不重要，只看得清看不清，所以整条刻度往下挪一档而不是挑几处改。 */
  --text-primary: #1e293b;
  --text-secondary: #334155;
  --text-muted: #475569;
  --text-white: #ffffff;
  
  /* 边框与分隔 */
  --border-color: #eef2f6;
  --border-focus: #8b5cf6;
  
  /* 危险色分两档：--danger 给人读的字（正文 4.5:1），--danger-solid 给压白字的实心。
     暗色下这两个方向相反（白字要底够暗、红字要够亮），一个色号扛不住——分档实测见 scripts/contrast.js。 */
  --danger: #b91c1c;
  --danger-solid: #dc2626;
  --danger-soft: rgba(185, 28, 28, 0.1);
  --danger-outline: rgba(185, 28, 28, 0.35);
  /* 只服务导入完成那一句；从前这里引用的是全站没有定义的 var(--success-600)，绿字从没画出来过。
     #15803d 压在最深的页面底 --bg-page 上只有 4.47:1，差一点点，往下取一档。 */
  --success: #166534;
  
  /* 阴影 - 更柔和的微阴影 */
  --shadow-sm: 0 2px 8px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.02);
  --shadow-md: 0 4px 16px rgba(0,0,0,0.05), 0 8px 24px rgba(0,0,0,0.03);
  --shadow-lg: 0 8px 32px rgba(0,0,0,0.06), 0 12px 40px rgba(0,0,0,0.04);
  --shadow-input-focus: 0 0 0 3px rgba(139, 92, 246, 0.1);
  
  /* 圆角 - 标准值 */
  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 16px;          /* 消息气泡 */
  --radius-xl: 20px;          /* 按钮 */
  --radius-2xl: 24px;         /* 大卡片 */
  --radius-full: 9999px;
  
  /* 间距 */
  --spacing-xs: 4px;
  --spacing-sm: 8px;
  --spacing-md: 16px;
  --spacing-lg: 24px;
  --spacing-xl: 32px;
  --spacing-2xl: 48px;
  
  /* 字体 */
  --font-family: 'Inter', 'Noto Sans SC', system-ui, -apple-system, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
  --font-brand: 'Noto Serif SC', 'Noto Sans SC', serif;
  --font-size-xs: 11px;
  --font-size-sm: 13px;
  --font-size-base: 14px;
  --font-size-lg: 15px;
  --font-size-xl: 18px;
  --font-size-2xl: 24px;
  
  /* 行高 */
  --line-height-tight: 1.2;
  --line-height-normal: 1.5;
  --line-height-relaxed: 1.6;
  
  /* 过渡 */
  --transition-fast: 0.15s cubic-bezier(0.4, 0, 0.2, 1);
  --transition-normal: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
  --transition-slow: 0.3s cubic-bezier(0.4, 0, 0.2, 1);

  /* 宽屏下正文列的限宽，超出部分补成左右边距 */
  --chat-measure: 900px;
}

/* 暗色模式 */
[data-theme="dark"] {
  --bg-page: #0f172a;
  --bg-sidebar: #1e293b;
  --bg-chat: #1e293b;
  --bg-card: #334155;
  --bg-input: #334155;
  /* 暗色下跟着本文件既有的写法往亮走一档（[data-theme="dark"] .menu-item:hover 那一族用的就是它）：
     浅色档的 --neutral-100 在暗色是近白，悬停等于在深底上闪一块白。 */
  --bg-hover: var(--neutral-700);
  --bg-secondary: var(--neutral-700);
  
  /* 暗色方向相反：文字要往亮走才读得清，所以整条刻度同样挪一档——正文 slate-50、
     次级 slate-200、次要 slate-300。从前次要是 #64748b，压在 --bg-card / --bg-input（#334155）
     上只有 2.18:1，比浅色系那一档还读不清；这一处和浅色档是同一个 85 处的账。 */
  --text-primary: #f8fafc;
  --text-secondary: #e2e8f0;
  --text-muted: #cbd5e1;
  /* 紫字在暗色要往亮走（压 --bg-card #334155：400 只有 3.79:1，300 有 5.59:1） */
  --accent-text: var(--primary-300);
  
  --border-color: #475569;
  --border-focus: #a78bfa;
  
  /* 暗色读档往亮走、实心底不动：#dc2626 压白字在两套主题都是 4.83:1，够，所以 --danger-solid 不覆盖 */
  --danger: #ffb3b3;
  --danger-soft: rgba(255, 179, 179, 0.12);
  --danger-outline: rgba(255, 179, 179, 0.4);
  --success: #4ade80;
  
  --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.2), 0 2px 4px -2px rgba(0, 0, 0, 0.15);
  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.25), 0 4px 6px -4px rgba(0, 0, 0, 0.15);
}

/* -------------------------- */
/* 基础样式重置               */
/* -------------------------- */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html {
  font-family: var(--font-family);
  font-size: var(--font-size-base);
  line-height: var(--line-height-normal);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body {
  min-height: 100vh;
  overflow: hidden;
  /* color/background 放在 body 上求值：暗色变量定义在 body[data-theme="dark"]，
     此处 var() 才能取到对应主题的变量值并正确继承给未显式声明颜色的子元素 */
  color: var(--text-primary);
  background-color: var(--bg-page);
}

/* Lucide 图标尺寸 */
.icon-xs {
  width: 14px;
  height: 14px;
  stroke-width: 2;
  vertical-align: -2px;
  display: inline-block;
  flex-shrink: 0;
}
.icon-sm {
  width: 18px;
  height: 18px;
  stroke-width: 2;
  vertical-align: -3px;
  display: inline-block;
  flex-shrink: 0;
}
.icon-md {
  width: 22px;
  height: 22px;
  stroke-width: 2;
  display: inline-block;
  flex-shrink: 0;
}

/* API 状态指示器 */
.api-status {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 12px;
  border-radius: var(--radius-full);
  background: var(--neutral-100);
  font-size: var(--font-size-xs);
  color: var(--text-secondary);
  transition: all var(--transition-normal);
  margin-right: 8px;
}
[data-theme="dark"] .api-status {
  background: var(--neutral-700);
  color: var(--text-muted);
}
.api-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #22c55e;
  flex-shrink: 0;
  transition: background var(--transition-normal);
}
.api-status.bailian .api-dot {
  background: #f59e0b;
}
.api-status.offline .api-dot {
  background: var(--danger);
}
.api-label {
  font-weight: 500;
  letter-spacing: 0.02em;
}
.api-status:hover {
  background: var(--neutral-200);
  cursor: default;
}
[data-theme="dark"] .api-status:hover {
  background: var(--neutral-600);
}

/* 数据操作按钮图标 */
.data-action-btn svg, .data-action-btn .icon-xs {
  vertical-align: -2px;
  margin-right: 2px;
}

button {
  font-family: inherit;
  font-size: inherit;
  cursor: pointer;
  border: none;
  background: none;
  outline: none;
}

input, textarea {
  font-family: inherit;
  font-size: inherit;
  outline: none;
}

/* -------------------------- */
/* 滚动条美化                 */
/* -------------------------- */
::-webkit-scrollbar {
  width: 6px;
  height: 6px;
}

::-webkit-scrollbar-track {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background: var(--neutral-300);
  border-radius: 3px;
  transition: background var(--transition-fast);
}

::-webkit-scrollbar-thumb:hover {
  background: var(--neutral-400);
}

[data-theme="dark"] ::-webkit-scrollbar-thumb {
  background: var(--neutral-600);
}

[data-theme="dark"] ::-webkit-scrollbar-thumb:hover {
  background: var(--neutral-500);
}

/* -------------------------- */
/* 动画定义                   */
/* -------------------------- */
@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes scaleIn {
  from {
    opacity: 0;
    transform: scale(0.95);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

@keyframes pulse {
  0%, 100% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
}

@keyframes slideInRight {
  from {
    opacity: 0;
    transform: translateX(20px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

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

@keyframes skeletonPulse {
  0%, 100% {
    background-color: var(--neutral-200);
  }
  50% {
    background-color: var(--neutral-300);
  }
}

/* -------------------------- */
/* 登录页面                   */
/* -------------------------- */
/* 原先这里压了一张 Unsplash 雪山图（实测 316,907 B），上面盖着 55% 不透明的渐变，
   几乎看不出来，却要让每个用户的首屏去等一个外国 CDN。改成纯 CSS 分层：
   上面两个渐变负责色调，下面这层径向光负责"不平"，配合 ::before/::after 的漂浮光斑。 */
.auth-panel {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--spacing-lg);
  position: relative;
  overflow: hidden;
  background:
    linear-gradient(135deg, rgba(139, 92, 246, 0.55) 0%, rgba(79, 70, 229, 0.45) 100%),
    radial-gradient(120% 90% at 18% 8%, rgba(199, 210, 254, 0.55) 0%, transparent 55%),
    linear-gradient(135deg, var(--primary-50) 0%, var(--bg-page) 100%);
}

/* 漂浮渐变光斑，增加层次 */
.auth-panel::before,
.auth-panel::after {
  content: '';
  position: absolute;
  border-radius: 50%;
  filter: blur(80px);
  opacity: 0.5;
  pointer-events: none;
  z-index: 0;
}
.auth-panel::before {
  width: 420px;
  height: 420px;
  top: -120px;
  left: -80px;
  background: radial-gradient(circle, rgba(167, 139, 250, 0.9), transparent 70%);
  animation: blobFloat 14s ease-in-out infinite;
}
.auth-panel::after {
  width: 380px;
  height: 380px;
  bottom: -100px;
  right: -60px;
  background: radial-gradient(circle, rgba(56, 189, 248, 0.75), transparent 70%);
  animation: blobFloat 18s ease-in-out infinite reverse;
}

@keyframes blobFloat {
  0%, 100% { transform: translate(0, 0) scale(1); }
  50% { transform: translate(40px, 30px) scale(1.1); }
}

@media (prefers-reduced-motion: reduce) {
  .auth-panel::before,
  .auth-panel::after {
    animation: none;
  }
}

/* 双栏布局：左侧品牌展示 + 右侧表单 */
.auth-layout {
  display: flex;
  width: 100%;
  max-width: 880px;
  min-height: 560px;
  border-radius: var(--radius-2xl);
  overflow: hidden;
  box-shadow: 0 24px 60px rgba(30, 27, 75, 0.35), 0 8px 24px rgba(0, 0, 0, 0.12);
  position: relative;
  z-index: 1;
}

/* 左侧品牌区：夜空渐变 + 标语。
   原先是 Unsplash 夜景图（实测 169,292 B）加一层 78% 的深色叠加，
   图基本被压成一片底色，却要为首屏多等一个外站。改成 CSS 分层渐变：
   底下两层径向光当"星野与远山"，上面保留原来的深色叠加保证白字对比度。 */
.auth-brand {
  flex: 1.1;
  display: flex;
  align-items: flex-end;
  padding: var(--spacing-2xl);
  background:
    linear-gradient(160deg, rgba(139, 92, 246, 0.35) 0%, rgba(30, 27, 75, 0.78) 100%),
    radial-gradient(90% 60% at 78% 14%, rgba(129, 140, 248, 0.55) 0%, transparent 60%),
    radial-gradient(70% 50% at 20% 92%, rgba(56, 189, 248, 0.32) 0%, transparent 65%),
    linear-gradient(180deg, #1e1b4b 0%, #312e81 55%, #4c1d95 100%);
  color: #fff;
}

.brand-logo {
  font-family: var(--font-brand);
  font-size: 44px;
  font-weight: 700;
  letter-spacing: 2px;
  margin-bottom: var(--spacing-sm);
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
}

.brand-slogan {
  font-size: var(--font-size-lg);
  opacity: 0.92;
  margin-bottom: var(--spacing-xl);
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.3);
}

.brand-features {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--spacing-md);
}

.brand-features li {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
  font-size: var(--font-size-base);
  opacity: 0.95;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
}

.feature-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  background: rgba(255, 255, 255, 0.18);
  border-radius: var(--radius-md);
  backdrop-filter: blur(4px);
}

/* 右侧表单区 */
.auth-container {
  width: 100%;
  max-width: 420px;
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(20px);
  padding: var(--spacing-2xl);
  display: flex;
  flex-direction: column;
  justify-content: center;
}

[data-theme="dark"] .auth-container {
  background: rgba(30, 41, 59, 0.95);
}

/* 移动端：隐藏品牌区，单栏居中 */
@media (max-width: 768px) {
  .auth-layout {
    max-width: 420px;
    min-height: auto;
  }
  .auth-brand {
    display: none;
  }
}

/* 320~375px 的手机上，48px 的卡片内边距会把输入框压到 176px，
   连一个手机号都显示不全；这里收紧外圈留白，把宽度还给输入框。
   必须排在 .auth-container 基础规则之后，同权重靠后者优先。 */
@media (max-width: 480px) {
  .auth-panel {
    padding: var(--spacing-md);
  }
  .auth-container {
    padding: var(--spacing-xl) var(--spacing-lg);
  }
}

.auth-header {
  text-align: center;
  margin-bottom: var(--spacing-xl);
}

.auth-header h1 {
  font-family: var(--font-brand);
  font-size: var(--font-size-2xl);
  font-weight: 700;
  background: linear-gradient(135deg, var(--primary-600), var(--primary-400));
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  margin-bottom: var(--spacing-sm);
}

.auth-header p {
  color: var(--text-secondary);
  font-size: var(--font-size-sm);
}

.auth-tabs {
  display: flex;
  background: var(--neutral-100);
  border-radius: var(--radius-lg);
  padding: 4px;
  margin-bottom: var(--spacing-xl);
}

[data-theme="dark"] .auth-tabs {
  background: var(--neutral-700);
}

.auth-tab {
  flex: 1;
  padding: var(--spacing-sm) var(--spacing-md);
  border-radius: var(--radius-md);
  font-size: var(--font-size-sm);
  font-weight: 500;
  color: var(--text-secondary);
  transition: all var(--transition-normal);
}

.auth-tab.active {
  /* 白字压 --primary-500 只有 4.23:1（正文判据 4.5），压 --primary-600 是 5.70:1：
     和第 56 轮 --danger / --danger-solid 那一刀同一个理由，这里直接落到已有的深一档 */
  background: var(--primary-600);
  color: var(--text-white);
}

.auth-form {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-md);
}

.form-group {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-xs);
}

.form-group label {
  font-size: var(--font-size-sm);
  font-weight: 500;
  color: var(--text-secondary);
}

.form-group input {
  padding: var(--spacing-md);
  border: 1.5px solid var(--border-color);
  border-radius: var(--radius-lg);
  background: var(--bg-input);
  color: var(--text-primary);
  transition: all var(--transition-fast);
}

.form-group input:focus {
  border-color: var(--border-focus);
  box-shadow: var(--shadow-input-focus);
}

.form-group input::placeholder {
  color: var(--text-muted);
}

.auth-btn {
  margin-top: var(--spacing-sm);
  padding: var(--spacing-md);
  /* 登录那颗大按钮从前是 500→600 的渐变压白字，最浅那一端只有 4.23:1；整条降一档
     （600→700，最差端 5.70:1）。第 56 轮给红做过同一刀，本轮把蓝紫这一族补齐。 */
  background: linear-gradient(135deg, var(--primary-600), var(--primary-700));
  color: var(--text-white);
  border-radius: var(--radius-lg);
  font-weight: 500;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--spacing-sm);
  transition: all var(--transition-normal);
  box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
}

.auth-btn:hover:not(:disabled) {
  transform: translateY(-2px);
  box-shadow: 0 6px 16px rgba(139, 92, 246, 0.4);
}

.auth-btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

/* 登录/注册失败那句（#authError）。auth.js 的 showAuthError/hideAuthError 一直在切 .show，
   但这份表里 .show 只跟 .modal / .toast / .reaction-picker 配过对，.auth-error 一条规则都没有：
   提示于是常年用正文墨色显示在表单下面，看不出是错误，空着的时候还占一行。 */
.auth-error {
  display: none;
}

.auth-error.show {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
  margin-top: var(--spacing-lg);
  padding: var(--spacing-sm) var(--spacing-md);
  border: 1px solid var(--danger-outline);
  border-radius: var(--radius-md);
  background: var(--danger-soft);
  color: var(--danger);
  font-size: var(--font-size-sm);
  line-height: var(--line-height-normal);
}

/* 站点关闭注册时注册表单顶上那行常驻说明。和 .mention-status 同一条规矩：
   规则里不写 display，收起靠元素自身的 hidden——写了就把那条默认样式顶掉，说明会常驻成背景噪音。 */
.register-notice {
  margin: 0 0 var(--spacing-sm);
  padding: var(--spacing-sm) var(--spacing-md);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md);
  background: var(--bg-secondary);
  color: var(--text-secondary);
  font-size: var(--font-size-sm);
  line-height: var(--line-height-normal);
}

/* -------------------------- */
/* 主应用容器                 */
/* -------------------------- */
.app-container {
  display: flex;
  height: 100vh;
  /* dvh 会随手机地址栏收起/软键盘弹出实时变化，vh 不会：
     用 vh 时输入框常被地址栏压掉半截，或被键盘顶到屏幕外。 */
  height: 100dvh;
  overflow: hidden;
}

/* -------------------------- */
/* 左侧侧边栏                 */
/* -------------------------- */
.sidebar {
  width: 33.333%;
  min-width: 280px;
  max-width: 400px;
  background: var(--bg-sidebar);
  border-right: 1px solid var(--border-color);
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
}

.sidebar-header {
  padding: var(--spacing-lg) var(--spacing-md);
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* 右边这排现在有两颗（管理＋抽屉关闭），标题让出走道、自己吃掉剩下的宽度 */
  gap: var(--spacing-sm);
  border-bottom: 1px solid var(--border-color);
}

.sidebar-header h1 {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--font-size-xl);
  font-weight: 700;
  background: linear-gradient(135deg, var(--primary-600), var(--primary-400));
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

.manage-btn {
  width: 36px;
  height: 36px;
  background: linear-gradient(135deg, var(--primary-600), var(--primary-700));
  color: var(--text-white);
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all var(--transition-normal);
  box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
}

.manage-btn:hover {
  transform: rotate(90deg) scale(1.05);
}

.manage-btn svg, .manage-btn .icon-xs {
  color: white;
}

/* Tab 切换 */
.sidebar-tabs {
  display: flex;
  padding: var(--spacing-sm);
  gap: var(--spacing-xs);
}

.tab-btn {
  flex: 1;
  padding: var(--spacing-sm) var(--spacing-md);
  border-radius: var(--radius-md);
  font-size: var(--font-size-sm);
  font-weight: 500;
  color: var(--text-secondary);
  transition: all var(--transition-fast);
}

.tab-btn.active {
  background: var(--primary-50);
  color: var(--primary-600);
}

/* 这层淡底压在侧栏 #1e293b 上，字色留 --primary-600 只有 2.06:1（第 63 轮浏览器按真底量的，
     尺子原先按最差表面估成 1.54:1——两个数都低，但只有前一个是屏幕上那一对）。换浅一档的 300 到 6.35:1。
     亮色那边压 --primary-50 是 5.20:1，本来就该过，不动。 */
[data-theme="dark"] .tab-btn.active {
  background: rgba(139, 92, 246, 0.2);
  color: var(--primary-300);
}

/* tab 标签上的未读数：和列表行里那个红点是同一个数，只是换了个不会被侧栏收起挡住的位置 */
.tab-unread {
  margin-left: 5px;
  min-width: 16px;
  height: 16px;
  padding: 0 5px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  background: #dc2626;
  color: #fff;
  font-size: 10px;
  line-height: 1;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  vertical-align: middle;
}

/* 侧栏那张列表没读到时常驻的一行。规则里不许写 display：这一行靠 [hidden] 收起，
   一写 display 就把 UA 样式表的 display:none 顶掉，它会常驻成背景噪音（第 52 轮的账）。 */
.sidebar-status {
  flex-shrink: 0;
  margin: 0 var(--spacing-sm) var(--spacing-xs);
  padding: var(--spacing-sm) var(--spacing-md);
  border: 1px solid var(--danger-outline);
  border-radius: var(--radius-md);
  background: var(--danger-soft);
  color: var(--danger);
  font-size: var(--font-size-sm);
  line-height: var(--line-height-normal);
}

/* 列表容器 */
.character-list,
.group-list {
  flex: 1;
  overflow-y: auto;
  padding: var(--spacing-xs);
}

/* 列表项 */
.character-item,
.group-item {
  display: flex;
  align-items: center;
  padding: var(--spacing-md);
  height: 72px;
  border-radius: var(--radius-lg);
  cursor: pointer;
  /* 只淡这两样，不写 all。抽屉靠 .sidebar 的 visibility 退出 Tab 序（第 88 轮），拉开那一下
     继承来的 hidden→visible 会被 all 重新淡成 0.15s，第一帧里这一行还是 hidden、压根 focus 不上去，
     sidebar.js 那句 focusDialog 于是白跑。691 宽实测：抽屉里 15 颗可聚焦控件，6 行 + 那颗 × 共 7 颗接得住，
     另外 8 颗（管理键、两张标签、页脚那五颗）自己还写着 all、本轮没动；hover 换底色、按下闪的透明度照旧淡。 */
  transition: background-color 0.15s ease, opacity 0.15s ease;
  position: relative;
}

/* 侧栏这两行现在是真按钮，不然大列表 Tab 键根本进不来。
   浏览器给 button 的那套（边框、灰底、居中、自带字体）全抹掉，
   外观必须和原来的 div 分行一模一样。
   width:100% 是必须的：button 天生按内容算宽，实测 279px 的侧栏里
   行宽排到 374~418px，长签名的角色会把侧栏横向撑出去。 */
button.character-item,
button.group-open {
  width: 100%;
  border: none;
  background: transparent;
  font: inherit;
  color: inherit;
  text-align: left;
  -webkit-appearance: none;
  appearance: none;
}

/* 群组那行的编辑键只能做兄弟节点（按钮不许套按钮），所以开聊的部分缩在里面这一层 */
.group-open {
  display: flex;
  align-items: center;
  flex: 1;
  min-width: 0;
  padding: 0;
  margin: 0;
  border-radius: var(--radius-lg);
}

/* 焦点环画在整行上：群组行里那个按钮只占行内一块，环套按钮会缩成一条窄框。
   :focus-within 连编辑键一起罩住，鼠标点进来也认得出停在哪一行。 */
.character-item:focus-visible,
.group-item:focus-within {
  outline: 2px solid var(--border-focus);
  outline-offset: -2px;
}

.character-item:hover,
.group-item:hover {
  background: #f8fafc;
}

[data-theme="dark"] .character-item:hover,
[data-theme="dark"] .group-item:hover {
  background: var(--neutral-700);
}

.character-item:active,
.group-item:active {
  background: #f1f3f5;
}

[data-theme="dark"] .character-item:active,
[data-theme="dark"] .group-item:active {
  background: var(--neutral-600);
}

.character-item.active,
.group-item.active {
  background: #f5f3ff;
}

[data-theme="dark"] .character-item.active,
[data-theme="dark"] .group-item.active {
  background: rgba(139, 92, 246, 0.15);
}

.character-item.active::before,
.group-item.active::before {
  content: '';
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 3px;
  height: 24px;
  background: var(--primary-500);
  border-radius: 0 2px 2px 0;
}

/* 头像 */
.avatar {
  width: 40px;
  height: 40px;
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 18px;
  font-weight: 600;
  flex-shrink: 0;
  margin-right: var(--spacing-md);
  position: relative;
  background: linear-gradient(135deg, var(--primary-600), var(--primary-800));
  color: var(--text-white);
  overflow: hidden;
  object-fit: cover;
}

.avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* JS 实际渲染结构：.info > .name + .preview。
   这两行是 button 的内容，h3/p 属于流式内容不能塞进按钮，所以换成 span——
   它们是 .info 这个 flex 列的子项，天生就是块级，省略号照旧生效。 */
.character-item .info,
.group-item .info {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.character-item .info .name,
.group-item .info .name {
  font-size: var(--font-size-base);
  font-weight: 500;
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.character-item .info .preview,
.group-item .info .preview {
  font-size: var(--font-size-sm);
  color: var(--text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 未读徽章：数字来自 /api/characters 与 /api/groups，0 条时前端根本不渲染这个节点。
   放在行尾而不是头像角上，因为 .avatar 有 overflow:hidden（图片要裁圆角），
   压在角上会被裁掉一半。 */
.unread-badge {
  flex-shrink: 0;
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  margin-left: var(--spacing-sm);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 10px;
  /* 实心红压白字，取 --danger-solid 而不是 --danger：后者暗色档是给人读的红字，白字压上去掉到 2.8:1 */
  background: var(--danger-solid);
  color: #fff;
  font-size: 11px;
  font-weight: 600;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

/* 群列表最后一条消息的时间。渲染里一直有这个 span，此前没有任何规则，
   于是不管多长都用正文默认字号把行高顶开 */
.group-item .time {
  flex-shrink: 0;
  margin-left: var(--spacing-sm);
  font-size: var(--font-size-xs);
  color: var(--text-secondary);
  white-space: nowrap;
}

/* 群聊条目的编辑入口：平时透明，悬停/选中/键盘聚焦才显出来。
   触屏没有 hover，所以保持半透明常驻，否则永远改不了群名 */
.group-edit-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  flex-shrink: 0;
  margin-left: var(--spacing-sm);
  padding: 0;
  border: none;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--text-secondary);
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.15s ease, background 0.15s ease, color 0.15s ease;
}

/* 键盘走进这一行就得看得见铅笔：以前只有 hover/active/focus-visible 三种时机，
   Tab 到「开聊」按钮时编辑入口还是全透明的。 */
.group-item:hover .group-edit-btn,
.group-item.active .group-edit-btn,
.group-item:focus-within .group-edit-btn,
.group-edit-btn:focus-visible {
  opacity: 1;
}

/* 悬停这支铅笔时，父级 .group-item:hover 必然同时命中，所以地板是"悬停那一行的底 + 这层 0.12 淡底"，
   不是平时那块——按平时的底估会估高。第 63 轮浏览器按真底量：亮色 #e5e1f5 上 --primary-600 只有 4.45:1
   （尺子先前给的 4.42 其实是对的，我先前那句"真底是 4.91"才是估错了那一档）。换 --primary-700 到 5.55:1。 */
.group-edit-btn:hover {
  background: rgba(139, 92, 246, 0.12);
  color: var(--primary-700);
}

/* 暗色那边同一层淡底压在悬停行的 #475569 上：--primary-600 只有 1.25:1，换 300 是 3.86:1 还差一档，
   换 200 才到 5.14:1（同一批实测）。 */
[data-theme="dark"] .group-edit-btn:hover {
  color: var(--primary-200);
}

@media (hover: none) {
  .group-edit-btn {
    opacity: 0.65;
  }
}

/* 新建群组按钮 */
.create-group-btn {
  width: calc(100% - 16px);
  margin: var(--spacing-sm);
  padding: var(--spacing-md);
  background: transparent;
  border: 2px dashed var(--border-color);
  border-radius: var(--radius-lg);
  color: var(--primary-600);
  font-size: var(--font-size-sm);
  font-weight: 500;
  transition: all var(--transition-fast);
}

.create-group-btn:hover {
  background: var(--primary-50);
  border-color: var(--primary-300);
}

/* 这个按钮不画底，底是侧栏那块 #1e293b——静态问不到（选择器里没写明祖先），尺子只能按最差表面估。
   浏览器按真底量：--primary-600 是 2.57:1，换 300 到 7.92:1。
   顺带把 hover 那层也钉住：--primary-50 是个近乎白的紫（亮度 0.9073），在暗色侧栏上画出一块亮斑，
   而且刚换上去的 --primary-300 压着它只有 1.68:1——不一起改，上面那笔颜色账就成了新的漏。
   换成同一族 0.2 淡底之后实测 6.35:1。 */
[data-theme="dark"] .create-group-btn {
  color: var(--primary-300);
}

[data-theme="dark"] .create-group-btn:hover {
  background: rgba(139, 92, 246, 0.2);
  border-color: var(--primary-500);
}

/* 底部操作按钮 */
.footer-btn {
  flex: 1;
  padding: var(--spacing-sm);
  border-radius: var(--radius-md);
  font-size: var(--font-size-base);
  transition: all var(--transition-fast);
  color: var(--text-secondary);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
}

.footer-btn:hover {
  background: var(--neutral-100);
  color: var(--text-primary);
}

[data-theme="dark"] .footer-btn:hover {
  background: var(--neutral-700);
}

.logout-btn {
  flex: 1;
  padding: var(--spacing-sm);
  border-radius: var(--radius-md);
  font-size: var(--font-size-sm);
  color: var(--text-secondary);
  transition: all var(--transition-fast);
  display: flex;
  align-items: center;
  justify-content: center;
}

.logout-btn:hover {
  background: var(--danger-soft);
  color: var(--danger);
}

/* -------------------------- */
/* 右侧聊天区域               */
/* -------------------------- */
.chat-area {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* 聊天头部 */
.chat-header {
  height: 60px;
  background: var(--bg-card);
  border-bottom: 1px solid var(--border-color);
  padding: 0 var(--spacing-lg);
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-shrink: 0;
}

.chat-info {
  display: flex;
  align-items: center;
  gap: var(--spacing-md);
  min-width: 0;
}

.chat-info .avatar {
  width: 36px;
  height: 36px;
  font-size: 16px;
  /* 头部被挤窄时先是标题缩没，再往后头像也会被压扁；头像定死不让缩 */
  flex-shrink: 0;
}

.user-info {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.user-info h2 {
  font-size: var(--font-size-base);
  font-weight: 600;
  color: var(--text-primary);
  /* 长角色名不换行撑高头部，超出部分省略 */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.user-info p {
  font-size: var(--font-size-xs);
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.chat-actions {
  display: flex;
  gap: var(--spacing-sm);
}

.action-btn {
  width: 36px;
  height: 36px;
  border-radius: var(--radius-md);
  font-size: var(--font-size-base);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-secondary);
  transition: all var(--transition-fast);
}

.action-btn:hover {
  /* 写死浅色档的 --neutral-100，暗色下就是"深底上闪一块近白"，而字色跟着换成 --text-primary
     （暗色是近白）——图标 1.05:1，等于悬停那一下按钮里的东西消失。第 56 轮定义的 --bg-hover
     才是"悬停那一层"：浅色档它就是 --neutral-100，样子一点没变，暗色档换成 --neutral-700。 */
  background: var(--bg-hover);
  color: var(--text-primary);
}

.action-btn.danger:hover {
  background: var(--danger-soft);
  color: var(--danger);
}

/* 更多操作下拉菜单 */
.more-actions {
  position: relative;
}

.more-actions-menu {
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  background: var(--bg-card);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  min-width: 180px;
  /* 手机上「更多」菜单会多出一组会话工具，横屏时视口只剩三百来像素高：
     不给上限它会一路长到屏幕外，排在最后的「重置所有数据」永远点不到。
     菜单挂在 top: 头部(60) + 8 的位置，所以可用高度是视口减去这一段。 */
  max-height: calc(100dvh - 76px);
  overflow-y: auto;
  padding: var(--spacing-xs);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-8px);
  transition: all var(--transition-fast);
  z-index: 100;
  border: 1px solid var(--border-color);
}

.more-actions-menu.open {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

.menu-item {
  width: 100%;
  padding: var(--spacing-sm) var(--spacing-md);
  border-radius: var(--radius-md);
  text-align: left;
  font-size: var(--font-size-sm);
  color: var(--text-primary);
  transition: all var(--transition-fast);
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
}

.menu-item svg, .menu-item .icon-xs {
  color: var(--text-secondary);
  flex-shrink: 0;
}

.menu-item:hover {
  background: var(--neutral-100);
}

[data-theme="dark"] .menu-item:hover {
  background: var(--neutral-700);
}

.menu-item.danger:hover {
  background: var(--danger-soft);
  color: var(--danger);
}

.menu-item.danger:hover svg, .menu-item.danger:hover .icon-xs {
  color: var(--danger);
}

.menu-divider {
  border: none;
  height: 1px;
  background: var(--border-color);
  margin: var(--spacing-xs) 0;
}

/* 聊天消息区域 */
.chat-messages {
  flex: 1;
  background: var(--bg-chat);
  /* 细腻的几何纹理（SVG data URI，气泡/星点图案），亮色模式极淡紫 */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140' viewBox='0 0 140 140'%3E%3Cg fill='none' stroke='%238b5cf6' stroke-opacity='0.05' stroke-width='1.5'%3E%3Cpath d='M24 34h26a7 7 0 0 1 7 7v11a7 7 0 0 1-7 7H38l-9 9v-9h-5a7 7 0 0 1-7-7V41a7 7 0 0 1 7-7z'/%3E%3Ccircle cx='104' cy='28' r='3.5'/%3E%3Ccircle cx='116' cy='104' r='4.5'/%3E%3Cpath d='M82 116h22a6 6 0 0 0 6-6v-9'/%3E%3Ccircle cx='28' cy='110' r='3.5'/%3E%3Cpath d='M100 62l3 6 6 3-6 3-3 6-3-6-6-3 6-3z'/%3E%3C/g%3E%3C/svg%3E");
  padding: var(--spacing-lg);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
}

/* 暗色模式：纹理换浅色描边 */
[data-theme="dark"] .chat-messages {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140' viewBox='0 0 140 140'%3E%3Cg fill='none' stroke='%23cbd5e1' stroke-opacity='0.05' stroke-width='1.5'%3E%3Cpath d='M24 34h26a7 7 0 0 1 7 7v11a7 7 0 0 1-7 7H38l-9 9v-9h-5a7 7 0 0 1-7-7V41a7 7 0 0 1 7-7z'/%3E%3Ccircle cx='104' cy='28' r='3.5'/%3E%3Ccircle cx='116' cy='104' r='4.5'/%3E%3Cpath d='M82 116h22a6 6 0 0 0 6-6v-9'/%3E%3Ccircle cx='28' cy='110' r='3.5'/%3E%3Cpath d='M100 62l3 6 6 3-6 3-3 6-3-6-6-3 6-3z'/%3E%3C/g%3E%3C/svg%3E");
}

/* 消息 */
.message {
  /* .reaction-btn 是绝对定位的，没有这一行它的包含块一路找到 body——
     实测一个群里 11 条消息的 😊 全叠在窗口右下角同一个坐标 (676,787)。 */
  position: relative;
  display: flex;
  margin-bottom: var(--spacing-md);
  animation: fadeInUp 0.2s ease-out;
}

.message.user {
  justify-content: flex-end;
  animation: slideInRight 0.2s ease-out;
}

.message.assistant {
  justify-content: flex-start;
  animation: slideInLeft 0.2s ease-out;
}

/* 第 78 轮：消息行第一次成为能用键盘走到的行（Alt+↑↓ 进列表、↑↓/Home/End 走、Enter 开菜单）。
   环本身交给全站那条 :focus-visible，这里只补圆角：这一行是整宽的，外侧 2px 的环正好落在
   .chat-messages 的 --spacing-lg 内边距里，压在 --bg-chat 上量得亮色 4.05:1、暗色 5.38:1
   （非文本对比判据 3:1，WCAG 1.4.11），要是往内收就会切进气泡那层紫色渐变上，反倒读不清停在哪一行。 */
.message:focus-visible {
  border-radius: var(--radius-lg);
}

.message-body {
  display: flex;
  gap: var(--spacing-sm);
  max-width: 80%;
  /* flex 子项默认 min-width:auto，宁可撑破父级也不缩；一行长代码就会把
     max-width: 80% 顶穿——实测 320px 手机上气泡涨到 722px，整块聊天区横向滚。
     写 0 之后超长内容由 .code-block pre 自己滚。 */
  min-width: 0;
}

.message.user .message-body {
  flex-direction: row-reverse;
}

.message .avatar {
  width: 32px;
  height: 32px;
  font-size: 14px;
  flex-shrink: 0;
  border-radius: 12px;
  object-fit: cover;
}

.message-content-wrapper {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.message-content {
  padding: var(--spacing-md) var(--spacing-lg);
  border-radius: 18px;
  line-height: var(--line-height-normal);
  font-size: var(--font-size-lg);
  word-wrap: break-word;
  white-space: pre-wrap;
  max-width: 100%;
}

/* 气泡里那个内联编辑框。以前只有 JS 里两条 inline 样式（宽度、内边距），
   字号和底色全吃浏览器默认：13px Arial 落在 --font-size-lg 的气泡里像贴了块别处的东西，
   白底黑字压在紫色渐变上，改一句话得先眯眼找边界。 */
.message-edit-input {
  width: 100%;
  box-sizing: border-box;
  padding: var(--spacing-sm);
  /* 编辑框要和它正在改的那句话长在同一副嗓子里 */
  font: inherit;
  line-height: var(--line-height-normal);
  color: var(--text-primary);
  background: var(--bg-input);
  border: 1px solid var(--border-focus);
  border-radius: 10px;
  /* 手一碰就拖出个歪框子，改一行字不需要这个能力 */
  resize: none;
  overflow: auto;
}

/* 编辑框下面那两个按钮。触屏上长按菜单进得来编辑框，出去却只有 Esc，
   而触屏根本没有 Esc——没有可见按钮，这条路就是单向的。 */
.message-edit-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: var(--spacing-sm);
}

.message-edit-btn {
  border-radius: var(--radius-full);
  padding: 6px 16px;
  /* 按钮字要跟着正在改的那句话，别在气泡里贴一块别处的字号 */
  font: inherit;
  font-weight: 500;
}

/* 两个都写死底色和字色：按钮压在紫渐变（自己说的话）和浅卡片（对方说的话）两种
   背景上，靠继承来的颜色总有一边看不清 */
.message-edit-save {
  background: var(--primary-600);
  color: var(--text-white);
}

.message-edit-save:hover {
  background: var(--primary-700);
}

.message-edit-cancel {
  background: var(--bg-card);
  color: var(--text-primary);
  border: 1px solid var(--border-color);
}

.message-edit-cancel:hover {
  border-color: var(--border-focus);
}

/* 保存被拒之后常驻在框子下面那一行：toast 一闪就走，而这行字要在人继续打字时
   一直说清"这一版还没进库"。底色写死实色而不是半透明——它压在紫渐变气泡和浅卡片
   气泡两种背景上，套一层红纱到自己说的话上面就看不清了。默认 hidden，被拒才现身。 */
.message-edit-unsaved {
  margin: var(--spacing-sm) 0 0;
  padding: 6px 8px;
  font-size: var(--font-size-sm);
  line-height: var(--line-height-normal);
  color: var(--danger);
  background: var(--bg-card);
  border: 1px solid var(--danger-outline);
  border-radius: var(--radius-md);
}

/* 用户气泡 - iMessage 风格 */
.message.user .message-content {
  background: linear-gradient(135deg, var(--primary-600), var(--primary-700));
  color: var(--text-white);
  border-radius: 20px 20px 6px 20px;
  box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
  transition: transform var(--transition-fast), box-shadow var(--transition-fast);
}

.message.user:hover .message-content {
  transform: scale(1.01);
  box-shadow: 0 6px 16px rgba(139, 92, 246, 0.4);
}

/* AI 气泡 - 右下角稍平 */
.message.assistant .message-content {
  background: var(--bg-card);
  color: var(--text-primary);
  border-radius: 20px 20px 20px 6px;
  box-shadow: var(--shadow-sm);
  transition: transform var(--transition-fast), box-shadow var(--transition-fast);
}

.message.assistant:hover .message-content {
  transform: scale(1.01);
  box-shadow: var(--shadow-md);
}

/* 撤回过的消息：外观由下面「撤回消息样式」那段管，这里只把气泡的 hover 缩放关掉，
   一条已经撤回的提示不该还在鼠标上跳 */
.message.recalled:hover .message-content {
  transform: none;
  box-shadow: none;
}

/* 连续消息优化 - 相邻消息间距减小 */
.message.user:last-of-type .message-content {
  border-radius: 18px 18px 4px 18px;
  margin-bottom: 2px;
}

.message.assistant:last-of-type .message-content {
  border-radius: 18px 18px 18px 4px;
  margin-bottom: 2px;
}

/* -------------------------- */
/* 群聊消息                    */
/* -------------------------- */
/* 一条群聊消息 = 行内「头像 + 气泡」，气泡里竖排「谁说的 / 正文 / 时间」。
   以前群聊直接复用 1:1 的那套 class，而 .message-body 在 1:1 里是"头像 + 正文"的横排——
   于是作者名和时间被当成正文旁边的两列，实测压成 16~31px 宽、逐字竖排。 */
.message.group {
  gap: var(--spacing-sm);
}

/* 32px 头像是从 80% 里 real 占走的，不扣掉的话群聊气泡就比 1:1 的宽出一截，
   两种会话切来切去时右边缘对不齐。 */
.message.group .message-content {
  max-width: calc(80% - 32px - var(--spacing-sm));
}

/* .avatar 那套（32px 方块、.avatar img 的 object-fit:cover、overflow:hidden 裁圆）
   是消息行里头像的既有样式，群聊借过来用；但它自带 margin-right:16px 是给侧栏列表留的，
   在消息行里会叠在这段 gap 之上，把上面那条 calc 的算术搞错。 */
.message.group .sender-avatar {
  margin-right: 0;
}

.message.group .message-body {
  align-items: flex-start;
  flex-direction: column;
  gap: 2px;
  max-width: 100%;
}

/* 群聊的正文 <p class="message-text"> 从前一条样式也没写过。上面那排是纵向 flex，
   align-items:flex-start 让每一项按内容取宽，而宽度是交叉轴——一行不换行的代码就把正文顶到
   556px，气泡只有 172px，代码块不是横向滚而是被裁掉（实测 pre 的 clientWidth 等于 scrollWidth，
   滚不起来）。收一条 max-width:100% 之后 pre 自己滚（170 可视 / 555 内容）。
   min-width:0 在这里不顶事：那一套只管主轴，交叉轴加上去实测仍是 556。 */
.message-body > .message-text {
  max-width: 100%;
}

.message.group.user .message-body {
  align-items: flex-end;
}

/* 作者名是群里唯一说明"这句是谁说的"的地方：比正文小一档，但要有分量、有颜色。
   基础规则不作用域到 .message.group，"正在输入"那条也用同一个 class。 */
.sender-name {
  color: var(--primary-600);
  font-size: var(--font-size-sm);
  font-weight: 600;
}

/* 这句不画底，底是它待的那颗气泡——第 63 轮浏览器按真底量：亮色压在白气泡上 5.70:1 本来就过，
   暗色压在 --bg-card #334155 上 --primary-600 只有 1.82:1，等于把"这句是谁说的"写没了。
   换 300 是 5.61:1；打字指示器那条用同一个 class，它的底是更深的聊天区，只会更高。 */
[data-theme="dark"] .sender-name {
  color: var(--primary-300);
}

/* 系统消息样式 */
.message.system {
  justify-content: center;
  margin: var(--spacing-sm) 0;
}

.message.system .message-content {
  background: rgba(139, 92, 246, 0.08);
  color: var(--text-secondary);
  padding: 6px 16px;
  border-radius: 12px;
  font-size: 12px;
  text-align: center;
  border: 1px solid rgba(139, 92, 246, 0.15);
  box-shadow: none;
}

.message.system .message-content::before {
  content: 'ℹ️ ';
}

/* 撤回消息样式 */
.message.recalled .message-content {
  background: rgba(148, 163, 184, 0.1);
  color: var(--text-muted);
  font-style: italic;
  padding: 8px 16px;
  border-radius: 12px;
  border: 1px dashed var(--border-color);
}

.message.recalled .message-content::before {
  content: '🗑️ ';
}

/* 编辑消息样式 */
.message.edited .message-content {
  position: relative;
}

.message-edited {
  font-size: var(--font-size-xs);
  color: var(--text-muted);
}

/* 图片缩略图：外面这颗是真按钮，尺寸上限交给按钮，图只按自己的比例缩。
   上限写成 min(300px, 100%)：只写 300px 的话，手机上一条横图能把 188px 的气泡撑到
   374px，整个聊天区跟着横向滚。
   以前 img 直接做 .message-content-wrapper（纵向 flex）的子项，被 align-items:stretch
   横向拉满、max-height 掐住，再配 object-fit:cover——实测 600x1500 的竖图和 120x80 的
   小图都被裁成同一个 300x200，比例全丢了。 */
button.image-open-btn {
  display: block;
  width: fit-content;
  max-width: min(300px, 100%);
  margin-top: 6px;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
}

button.image-open-btn:focus-visible {
  outline: 2px solid var(--border-focus);
  outline-offset: 2px;
}

.message-image {
  display: block;
  max-width: 100%;
  max-height: 200px;
  border-radius: var(--radius-md);
  border: 2px solid rgba(139, 92, 246, 0.2);
  transition: transform var(--transition-fast);
}

.message-image:hover {
  transform: scale(1.02);
}

/* 图字节到齐前它本征是 0×0，加上本条那 2px 描边只剩 4×4（实测），手机上这条图片消息
   等于不存在：看不见、点不到，图一到又凭空顶下近 200px。加载期先按缩略图的量级占一块，
   真图到了由 media.js 摘掉 is-pending、照原宽高收。
   底色走 --bg-hover 而不是直接写 --neutral-100：后者暗色档没换过值，还是近白，
   压在深底上就是闪一块白（暗色变量那一块开头那条注释说的正是这件事）。 */
.message-image.is-pending {
  width: 160px;
  height: 120px;
  background: var(--bg-hover);
}

/* --bg-hover 在暗色档就是 --neutral-700，而气泡那层 --bg-card 也是 #334155：同一个数画上去
   等于占了一块看不见的地方。暗色往亮走一档（和上面 .api-status:hover、滚动条那一族同一个写法），
   浅色档不动，仍旧是白气泡上一片浅灰。 */
[data-theme="dark"] .message-image.is-pending {
  background: var(--neutral-600);
}

.message.user .message-image {
  border-color: rgba(255, 255, 255, 0.3);
}

.image-error {
  color: var(--danger);
  font-size: var(--font-size-sm);
  padding: var(--spacing-sm);
  background: var(--danger-soft);
  border-radius: var(--radius-sm);
}

/* 消息功能菜单（右键/长按） */
.context-menu {
  display: none;
  position: fixed;
  background: var(--bg-card);
  border-radius: 8px;
  box-shadow: var(--shadow-lg);
  padding: 4px;
  z-index: 1000;
  min-width: 148px;
  /* 菜单项按归属动态增减，横屏手机上能长过视口；限高后自己滚 */
  max-height: calc(100vh - 16px);
  max-height: calc(100dvh - 16px);
  overflow-y: auto;
  border: 1px solid var(--border-color);
}

.context-menu button {
  display: block;
  width: 100%;
  padding: 8px 12px;
  background: none;
  border: none;
  text-align: left;
  cursor: pointer;
  border-radius: 6px;
  font-size: 13px;
  color: var(--text-primary);
  transition: background var(--transition-fast);
}

.context-menu button:hover {
  background: var(--primary-50);
}

/* 键盘方向键走到哪一项，得看得见 */
.context-menu button:focus-visible {
  outline: 2px solid var(--border-focus);
  outline-offset: -2px;
  background: var(--primary-50);
}

.ctx-group + .ctx-group {
  margin-top: 4px;
  padding-top: 4px;
  border-top: 1px solid var(--border-color);
}

.ctx-group-label {
  margin: 4px 12px 2px;
  font-size: var(--font-size-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

.ctx-group-label.danger {
  color: var(--danger);
}

.context-menu button.danger:hover {
  background: var(--danger-soft);
  color: var(--danger);
}

/* 提醒菜单：标题带消息正文、待提醒项带时间和摘要，都是不定长，换行会把菜单撑成一坨 */
.reminder-menu {
  min-width: 200px;
  max-width: min(320px, calc(100vw - 16px));
}

.reminder-menu .ctx-group-label,
.reminder-menu button {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* 引用块 */
.quote-block {
  /* .chat-input-area 是 flex row + wrap，引用栏不占满一整行的话就会和输入框挤在同一行、
     底部对齐——正文一长就横着压在 textarea 上（实测差 36px）。给它整行，引用永远浮在输入框上方。 */
  flex: 0 0 100%;
  display: flex;
  align-items: flex-start;
  gap: 8px;
  background: rgba(0, 0, 0, 0.03);
  border-left: 3px solid var(--primary-500);
  border-radius: 8px;
  padding: 8px 12px;
  font-size: 13px;
  color: var(--text-secondary);
  margin-bottom: 8px;
  /* 引用一整段长回答不该把输入区顶出屏幕：四行之内看完，多出来的自己滚 */
  max-height: 96px;
  overflow-y: auto;
}

/* 引用栏里那句正文要和原文一个样子。第37轮起这里装的是带换行的正文，
   以前那个 span 连 class 都没有，继承下来的 white-space: normal 把三段折成一行，
   实测三段共 76 字的引用只占 39px——引用了什么根本看不出。 */
.quote-text {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* 「谁说的」要压过正文一等，否则引用栏开头就是半句正文 */
.quote-author {
  color: var(--text-primary);
  font-weight: 600;
}

/* 正文吃满一行、× 待在最右。不给 min-width: 0，一个长词就能把整块撑破 */
.quote-content {
  flex: 1 1 auto;
  min-width: 0;
}

.quote-remove {
  flex: none;
  width: 24px;
  height: 24px;
  line-height: 1;
  border-radius: 6px;
  color: var(--text-secondary);
}

.quote-remove:hover {
  background: var(--bg-card);
  color: var(--danger);
}

[data-theme="dark"] .quote-block {
  background: rgba(255, 255, 255, 0.05);
}

.message-meta {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
  padding: 2px var(--spacing-sm);
}

.message.user .message-meta {
  justify-content: flex-end;
}

.message-time {
  font-size: var(--font-size-xs);
  color: var(--text-muted);
}

.message-status {
  font-size: var(--font-size-xs);
  color: var(--text-muted);
  transition: color var(--transition-fast);
}

.message-status.read {
  color: var(--accent-text);
}

@keyframes statusChange {
  0% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.2);
  }
  100% {
    transform: scale(1);
  }
}

.message-status.changing {
  animation: statusChange 0.3s ease-in-out;
}

/* 发送状态：pending 是"还没到服务端"，failed 要一眼看出来 */
.message-status.pending {
  animation: sendPulse 1.1s ease-in-out infinite;
}

.message-status.failed {
  color: var(--danger);
  font-weight: 700;
}

/* 图片上传失败后，状态位就是重试入口 */
.message-status.retryable {
  cursor: pointer;
}

.message-status.retryable:hover {
  text-decoration: underline;
}

.message.user.send-failed .message-content {
  border: 1px solid var(--danger-outline);
}

@keyframes sendPulse {
  0%, 100% { opacity: 0.3; }
  50% { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .message-status.pending {
    animation: none;
    opacity: 0.7;
  }
}

/* 加载状态 */
.message.loading .message-content {
  background: var(--neutral-100);
  padding: var(--spacing-md) var(--spacing-lg);
}

[data-theme="dark"] .message.loading .message-content {
  background: var(--neutral-700);
}

.typing-indicator {
  display: flex;
  gap: 4px;
}

/* 以前这条写作「.typing-indicator 后代里所有裸 span」，把三个 span 当弹跳点。
   结构后来换成 .typing-dots > .typing-dot（样式见下面「群聊正在输入」那段），
   选择器却没跟着收窄，变成"这条消息行里所有 span 都是 6px 灰点"——
   作者名 .sender-name 和 emoji 头像 .avatar-emoji 都被画成点。 */

/* 空状态 */
.empty-state {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
}

.empty-illustration {
  width: 220px;
  height: 220px;
  flex-shrink: 0;
  border-radius: 50%;
  margin-bottom: var(--spacing-lg);
  box-shadow: 0 12px 40px rgba(139, 92, 246, 0.25), 0 4px 12px rgba(0, 0, 0, 0.08);
  border: 4px solid var(--bg-card);
  animation: floatY 3s ease-in-out infinite;
}

@keyframes floatY {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-10px); }
}

.empty-state h3 {
  font-size: var(--font-size-xl);
  font-weight: 600;
  color: var(--text-secondary);
  margin-bottom: var(--spacing-sm);
}

.empty-state p {
  font-size: var(--font-size-sm);
}

/* 输入区域 */
.chat-input-area {
  /* 向上弹的浮层按这一块的真实高度限高，所以高度得先有个落点再让 JS 覆盖：
     chat-input.js 在载入时、每次打字后、以及 ResizeObserver 的回调里量完写进这里的 style。
     第 87 轮在触屏档现量（无 CSP 镜像 + 同源 iframe，先清掉草稿）：375×812 单聊待机 125、
     打满七行 241；320×640 群聊被 @选择器把工具栏顶成两行，于是 177 → 293；1280 桌面只有 81。
     这一格取窄屏待机那一档，当脚本跑起来之前那一帧的落点——不是第 87 轮以前那种抄在面板上的
     160，那一个谁也没量过，而真数从 81 到 293 都轮不到它。 */
  --composer-h: 125px;
  background: var(--bg-card);
  border-top: 1px solid var(--border-color);
  padding: var(--spacing-md) var(--spacing-lg);
  display: flex;
  gap: var(--spacing-md);
  align-items: flex-end;
  flex-shrink: 0;
}

.input-toolbar {
  display: flex;
  gap: var(--spacing-sm);
  padding-bottom: var(--spacing-sm);
  /* 群聊里这一排最后挂的是 @选择器，一个 <select> 按最宽的那条 option 定宽（实测 175px，
     成员昵称长就到 373px），nowrap 之下整排顶到 415px，右边挂出屏幕外。允许换行，
     @选择器自己折到第二行去用整行宽度。 */
  flex-wrap: wrap;
  /* 表情/快捷回复浮层的定位父级：贴着这一排触发按钮向上弹，
     宽屏正文列限宽后也不会和按钮错开 */
  position: relative;
}

.toolbar-btn {
  width: 32px;
  height: 32px;
  border-radius: var(--radius-md);
  font-size: var(--font-size-base);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-secondary);
  background: var(--neutral-100);
  transition: all var(--transition-fast);
}

[data-theme="dark"] .toolbar-btn {
  background: var(--neutral-700);
}

.toolbar-btn:hover {
  background: var(--primary-50);
  color: var(--primary-600);
}

[data-theme="dark"] .toolbar-btn:hover {
  background: rgba(139, 92, 246, 0.2);
}

/* 录音中的麦克风按钮。以前这里是把按钮文字换成 ⏹️/🎤，
   一并抹掉了里面的 lucide svg，用过一次语音输入图标就再也回不来了（实测）。
   状态只该用样式表达。 */
#voiceBtn.recording {
  background: #fee2e2;
  /* 写死的 #dc2626 压 #fee2e2 只有 3.95:1（第 63 轮实测，尺子算的也是这个数）；
     --danger 在亮色是 #b91c1c，同一块底上 5.30:1。暗色那档另有一条规则，本来就够。 */
  color: var(--danger);
  animation: pulse 1.2s ease-in-out infinite;
}

[data-theme="dark"] #voiceBtn.recording {
  background: rgba(220, 38, 38, 0.24);
  color: #fca5a5;
}

/* 输入框 */
.chat-input-area textarea {
  flex: 1;
  padding: 10px 16px;
  border: 1px solid var(--border-color);
  border-radius: 22px;
  background: var(--neutral-100);
  color: var(--text-primary);
  font-size: var(--font-size-base);
  font-family: var(--font-family);
  resize: none;
  overflow-y: hidden;
  min-height: 44px;
  max-height: 160px;
  transition: all 0.15s ease;
  line-height: var(--line-height-normal);
  box-shadow: none;
}

.chat-input-area textarea:focus {
  outline: none;
  border-color: var(--border-focus);
  box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.2);
  background: var(--bg-input);
}

.chat-input-area textarea::placeholder {
  color: var(--text-muted);
}

.chat-input-area textarea:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* 暗色下输入区底色要往深走一档（base 用的是浅色档的 --neutral-100，它不跟主题变）。
   边框色不再在这里重复——重复过一次，就把下面 .chat-input-area textarea:focus 的焦点边框压死了。 */
[data-theme="dark"] .chat-input-area textarea {
  background: var(--neutral-700);
}

/* 只有光晕按主题调深浅；焦点边框的颜色交给 base 的 :focus 规则（--border-focus），别再抄一份 --primary-400 */
[data-theme="dark"] .chat-input-area textarea:focus {
  box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.3);
}

/* 单行输入框 */
input[type="text"],
input[type="password"],
input[type="email"],
input[type="search"] {
  height: 44px;
  padding: 0 16px;
  border: 1px solid var(--border-color);
  border-radius: 16px;
  background: var(--bg-input);
  color: var(--text-primary);
  font-size: var(--font-size-base);
  font-family: var(--font-family);
  transition: all 0.15s ease;
}

input[type="text"]:focus,
input[type="password"]:focus,
input[type="email"]:focus,
input[type="search"]:focus {
  outline: none;
  border-color: var(--border-focus);
  box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.2);
}

input[type="text"]::placeholder,
input[type="password"]::placeholder,
input[type="email"]::placeholder,
input[type="search"]::placeholder {
  color: var(--text-muted);
}

/* 这里原来有一族 [data-theme="dark"] input[type=…] 的 background/border-color 覆盖，第 64 轮删掉：
   base 走 --border-color / --bg-input 之后它只是把同一个色号再画一遍，而它的特异度（0,2,1）比每个组件
   自己的 :focus 边框规则（0,2,0）高，害得暗色下所有单行输入框的焦点边框都不生效。 */

/* 发送按钮 - 主要按钮 */
.chat-input-area > button {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--primary-600), var(--primary-700));
  color: var(--text-white);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.15s ease;
  box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
  flex-shrink: 0;
}

.chat-input-area > button:hover:not(:disabled) {
  transform: scale(1.05);
  filter: brightness(1.05);
  box-shadow: 0 6px 16px rgba(139, 92, 246, 0.4);
}

.chat-input-area > button:active:not(:disabled) {
  transform: scale(0.98);
}

.chat-input-area > button:disabled {
  background: var(--neutral-300);
  cursor: not-allowed;
  box-shadow: none;
}

.chat-input-area > button svg {
  width: 18px;
  height: 18px;
}

/* 停止生成：只在流式回复期间由 send.js 切到可见 */
.chat-input-area .stop-gen-btn {
  background: linear-gradient(135deg, #ef4444, #dc2626);
  box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
}

.chat-input-area .stop-gen-btn:hover:not(:disabled) {
  box-shadow: 0 6px 16px rgba(239, 68, 68, 0.4);
}

/* 发送/停止按钮：必须用 > 只取直接子元素。
   少了这个 > 就会连带工具栏里那排 .toolbar-btn 一起变成
   44px 渐变圆，把 .toolbar-btn 自己的 32px 方形淡底完全顶掉（实测）。 */
/* 次要按钮 */
.btn-secondary {
  background: transparent;
  color: var(--accent-text);
  border: 1px solid var(--primary-500);
  border-radius: var(--radius-xl);
  padding: 8px 20px;
  font-weight: 500;
  transition: all 0.15s ease;
}

.btn-secondary:hover {
  background: rgba(139, 92, 246, 0.05);
}

.btn-secondary:active {
  transform: scale(0.98);
}

/* 图标按钮 */
.btn-icon {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  transition: all 0.15s ease;
  color: var(--text-secondary);
}

.btn-icon:hover {
  background: #f1f3f5;
  color: var(--text-primary);
}

.btn-icon:active {
  transform: scale(0.95);
}

[data-theme="dark"] .btn-icon:hover {
  background: var(--neutral-700);
}

/* @提及选择器 */
.mention-selector {
  display: flex;
  align-items: center;
  max-width: 100%;
}

.mention-selector select {
  padding: var(--spacing-xs) var(--spacing-md);
  border: 1.5px solid var(--border-color);
  border-radius: var(--radius-md);
  background: var(--bg-input);
  color: var(--text-primary);
  font-size: var(--font-size-xs);
  cursor: pointer;
  transition: all var(--transition-fast);
  /* <select> 的固有宽度是最宽那条 option（实测成员昵称一长就到 373px），
     nowrap 的按钮排会带着它顶穿输入区。按视口收上限，换行后也不超出这一列；
     写成 vw 而不是 100%，是因为 .mention-selector 自己被内容定宽，百分比算不出确定的基数。 */
  min-width: 0;
  max-width: calc(100vw - 44px);
}

.mention-selector select:focus {
  border-color: var(--border-focus);
  box-shadow: var(--shadow-input-focus);
}

/* @选人不可用时挂在输入区上方的常驻说明（第 52 轮）：toast 会自己消失，这一行不会。
   刻意不写 display——和第 45 轮 .message-edit-unsaved 同一个约定，靠 hidden 属性开关，
   规则里一旦写了 display 就把 [hidden] 的 display:none 顶掉了，那行字会常驻成背景噪音。
   底色沿用 --bg-card 而不是 --danger-soft：它压在 .chat-area 上，套红纱会和输入区脱开一截。
   文字仍用 --danger，对比度约 3.8:1 那笔账记在导览第 15 节第 4 条，没在这里自造新 Token。 */
.mention-status {
  flex-shrink: 0;
  margin: 0;
  padding: var(--spacing-xs) var(--spacing-lg);
  font-size: var(--font-size-sm);
  line-height: var(--line-height-normal);
  color: var(--danger);
  background: var(--bg-card);
  border-top: 1px solid var(--danger-outline);
}

/* -------------------------- */
/* 模态框                     */
/* -------------------------- */
.modal {
  display: none;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(4px);
  z-index: 1000;
  align-items: center;
  justify-content: center;
  padding: var(--spacing-lg);
}

.modal.show {
  display: flex;
  animation: modalIn 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}

.modal-content {
  background: rgba(255, 255, 255, 0.98);
  backdrop-filter: blur(20px);
  border-radius: 32px;
  width: min(90vw, 520px);
  max-height: 85vh;
  max-height: 85dvh;
  overflow: hidden;
  /* 纵向 flex：头部/底部钉住，只有中间滚。
     原来只写 overflow: hidden，横屏手机上 85vh 只剩三百像素，
     设置框的保存按钮被裁在卡片外面，怎么点都摸不到。 */
  display: flex;
  flex-direction: column;
  box-shadow: var(--shadow-lg);
  animation: scaleIn 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}

[data-theme="dark"] .modal-content {
  background: rgba(30, 41, 59, 0.98);
}

/* 暗色下这段正文的底色不跟着主题走，于是 --text-primary 的 #f8fafc 压在 --neutral-50 的
   #f8fafc 上——1.00:1，翻出来的原文根本看不见。参照 .code-block 的暗色内嵌层写法。 */
[data-theme="dark"] .translation-text {
  background: rgba(0, 0, 0, 0.28);
}

/* 翻译结果：卡片外壳复用上面的 .modal-content / .modal-body，这里只写两段正文 */
.translation-label {
  font-size: 12px;
  color: var(--text-muted);
  margin-bottom: 4px;
}

.translation-text {
  padding: var(--spacing-md);
  background: var(--neutral-50);
  border-radius: var(--radius-md);
  font-size: var(--font-size-base);
  color: var(--text-primary);
  margin-bottom: var(--spacing-md);
}

.translation-text.target {
  background: var(--primary-50);
  color: var(--primary-700);
  margin-bottom: 0;
}

.modal-header {
  padding: var(--spacing-lg);
  border-bottom: 1px solid var(--border-color);
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex: 0 0 auto;
}

.modal-header h2 {
  font-size: var(--font-size-xl);
  font-weight: 600;
  color: var(--text-primary);
}

/* 模态框与搜索面板右上角的 ×。原来这段写作 .modal-close，但 HTML 里全部用的是
   .close-btn，于是十个模态框的关闭键一直是未排版的原生按钮：实测只有 9×16px，
   手机上按十次中不了三次，而它往往是唯一的关闭入口。 */
.modal-header .close-btn,
.search-panel-header .close-btn {
  width: 32px;
  height: 32px;
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--font-size-xl);
  line-height: 1;
  color: var(--text-muted);
  transition: all 0.15s ease;
  background: transparent;
  border: none;
  cursor: pointer;
  flex-shrink: 0;
}

.modal-header .close-btn:hover,
.search-panel-header .close-btn:hover {
  background: var(--neutral-100);
  color: var(--text-primary);
}

/* 暗色主题没有重定义 --neutral-100，直接用会在 × 后面露出一块近白的底 */
[data-theme="dark"] .modal-header .close-btn:hover,
[data-theme="dark"] .search-panel-header .close-btn:hover {
  background: var(--neutral-700);
  color: var(--text-primary);
}

.modal-body {
  padding: var(--spacing-lg);
  overflow-y: auto;
  /* min-height: 0 是必须的：flex 子项默认 min-height 为 auto，
     不写它就永不收缩，超出的高度仍被卡片的 overflow: hidden 裁掉 */
  flex: 1 1 auto;
  min-height: 0;
  /* 名字可能是一整串没有空格的拉丁字符：实测 320px 手机上「确认删除」的正文排到
     420px，卡片只有 248px，不换行就得横向拖动才看得全是谁。 */
  overflow-wrap: break-word;
  /* 滚到尽头不许把这一滚交给外面（第 83 轮）：不加这句，长对话框滚到底再滚一格，
     交接的就是背后那一屏聊天记录。 */
  overscroll-behavior: contain;
}

.modal-footer {
  padding: var(--spacing-lg);
  border-top: 1px solid var(--border-color);
  display: flex;
  justify-content: flex-end;
  gap: 12px;
  flex: 0 0 auto;
}

/* 模态框入场动画 */
@keyframes modalIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* -------------------------- */
/* Toast 提示                 */
/* -------------------------- */
.toast {
  position: fixed;
  bottom: var(--spacing-xl);
  /* 原来靠 left: 50% + translateX(-50%) 居中。绝对定位的宽度是 shrink-to-fit，
     而可用宽度是从 left 量到包含块右缘，于是只剩视口的一半：实测 320px 手机上
     写好的 max-width: 90vw 根本轮不上，一句长提示被挤成 160×102 的五列竖条。
     改成左右都钉 0 + margin auto 居中，可用宽度回到整个视口，max-width 才真的管得住。 */
  left: 0;
  right: 0;
  margin-inline: auto;
  width: fit-content;
  padding: 12px 20px;
  border-radius: 40px;
  color: var(--text-white);
  font-size: var(--font-size-sm);
  font-weight: 500;
  z-index: 2000;
  animation: toastIn 0.3s ease;
  box-shadow: var(--shadow-lg);
  max-width: min(90vw, 600px);
  text-align: center;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s ease, visibility 0.3s ease;
}

.toast.show {
  opacity: 1;
  visibility: visible;
}

.toast.success {
  background: rgba(0, 0, 0, 0.8);
  border-left: 3px solid #10b981;
}

.toast.error {
  background: rgba(0, 0, 0, 0.8);
  border-left: 3px solid var(--danger);
}

.toast.info {
  background: rgba(0, 0, 0, 0.8);
  border-left: 3px solid #3b82f6;
}

/* Toast 入场动画 */
@keyframes toastIn {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* -------------------------- */
/* 消息高亮动画               */
/* -------------------------- */
.message.highlighted {
  animation: messageHighlight 2s ease-out;
}

@keyframes messageHighlight {
  0%, 100% {
    background: transparent;
  }
  10%, 60% {
    background: rgba(251, 191, 36, 0.3);
    box-shadow: 0 0 0 2px #fbbf24;
  }
}

/* -------------------------- */
/* 表情反应                   */
/* -------------------------- */
.reaction-btn {
  position: absolute;
  /* 贴在这一行的顶边、气泡对面那一侧：气泡最宽只到 80%，另一侧至少还剩两成宽度放这颗 28px。
     写在右下角的话，我发的那条会正好压在气泡的文字上。 */
  bottom: auto;
  right: 4px;
  top: 4px;
  width: 28px;
  height: 28px;
  border: none;
  background: var(--bg-card);
  border-radius: var(--radius-full);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  /* 看不见的时候也不许吃点击：这颗贴在行角的按钮有一整块 28×28 的隐形热区，
     不写这一句的话手指点在那片空白上会真的弹出选择器。
     长按菜单里的「表情反应」走的是 chip.click()，程序点击不受这里影响。 */
  pointer-events: none;
  transition: all var(--transition-fast);
  box-shadow: var(--shadow-sm);
  z-index: 5;
}

.message.user .reaction-btn {
  left: 4px;
  right: auto;
}

.message:hover .reaction-btn {
  opacity: 1;
  pointer-events: auto;
}

/* Tab 也就算了——一个 opacity:0 的按钮被键盘聚焦后还是看不见，
   回车却真的会弹出选择器，等于对着空气操作。 */
.reaction-btn:focus-visible {
  opacity: 1;
  pointer-events: auto;
}

.reaction-btn:hover {
  background: var(--neutral-100);
  transform: scale(1.1);
}

.reaction-btn span {
  font-size: 16px;
}

/* 表情选择器 */
.reaction-picker {
  position: fixed;
  background: var(--bg-card);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: 12px;
  display: none;
  z-index: 2000;
  border: 1px solid var(--border-color);
}

.reaction-picker.show {
  display: block;
}

.reaction-picker-title {
  font-size: var(--font-size-sm);
  color: var(--text-secondary);
  margin-bottom: 8px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border-color);
}

.reaction-list {
  /* 用 grid 钉住列数，不用 flex-wrap：这个浮层是 position:fixed，宽度靠 shrink-to-fit 算，
     贴着屏幕右边打开时可用宽度只剩几十像素，12 个表情会排成一条 557px 高的竖列
     （实测 left:626 的那次就是这样）。 */
  display: grid;
  grid-template-columns: repeat(6, 36px);
  gap: 4px;
}

.reaction-option {
  width: 36px;
  height: 36px;
  border: none;
  background: transparent;
  border-radius: var(--radius-md);
  cursor: pointer;
  font-size: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all var(--transition-fast);
}

.reaction-option:hover {
  background: var(--neutral-100);
  transform: scale(1.15);
}

/* 表情反应显示 */
.reactions {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: 6px;
  padding: 2px 0;
}

/* 容器被纵向那一层拉成气泡那么宽，自己排左的话，我发的消息上那排表情就会
   孤零零贴在气泡左上角，和右侧的消息体分家。 */
.message.user .reactions {
  justify-content: flex-end;
}

.reaction-item {
  display: flex;
  align-items: center;
  gap: 4px;
  background: var(--neutral-100);
  border-radius: var(--radius-full);
  padding: 2px 8px;
  font-size: 12px;
  cursor: pointer;
  transition: all var(--transition-fast);
  border: 1px solid transparent;
}

/* 它现在是真 button：不写这两行，Chrome 会用它自己那套系统字体和 buttontext 颜色，
   胶囊里那颗 emoji 就和旁边气泡的字不是同一副面孔。背景交给上面的 .reaction-item，
   这里再写一层 background 会把 :hover 顶掉。 */
button.reaction-item {
  font-family: inherit;
  color: inherit;
}

.reaction-item:focus-visible {
  outline: 2px solid var(--border-focus);
  outline-offset: -2px;
}

[data-theme="dark"] .reaction-item {
  background: var(--neutral-700);
}

.reaction-item:hover {
  background: var(--neutral-200);
}

[data-theme="dark"] .reaction-item:hover {
  background: var(--neutral-600);
}

.reaction-item.has-user {
  border-color: var(--primary-500);
  background: rgba(139, 92, 246, 0.1);
}

.reaction-emoji {
  font-size: 14px;
}

/* -------------------------- */
/* 文件上传                   */
/* -------------------------- */
/* 图片预览模态框 */
.image-preview-modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0,0,0,0.9);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 3000;
  flex-direction: column;
  gap: 16px;
}

.image-preview-modal img {
  max-width: 90vw;
  max-height: 70vh;
  max-height: 70dvh;
  border-radius: var(--radius-md);
}

.image-preview-modal .close-btn {
  position: absolute;
  top: 16px;
  right: 16px;
  width: 36px;
  height: 36px;
  background: rgba(255,255,255,0.1);
  border: none;
  border-radius: 50%;
  color: white;
  font-size: 24px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background var(--transition-fast);
}

.image-preview-modal .close-btn:hover {
  background: rgba(255,255,255,0.2);
}

.image-preview-modal .analyze-btn {
  position: absolute;
  top: 16px;
  left: 16px;
  padding: 10px 20px;
  /* 这颗按钮是浮在图片上的白字，压 --primary-500 只有 4.23:1；深一档 5.70:1（第 57 轮） */
  background: var(--primary-600);
  border: none;
  border-radius: var(--radius-lg);
  color: white;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 8px;
  transition: all var(--transition-fast);
}

.image-preview-modal .analyze-btn:hover {
  background: var(--primary-700);
  transform: translateY(-1px);
}

/* 图片分析面板 */
.analysis-panel {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  background: rgba(0,0,0,0.95);
  border-top-left-radius: var(--radius-xl);
  border-top-right-radius: var(--radius-xl);
  padding: 24px;
  max-height: 50vh;
  overflow-y: auto;
}

.analysis-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16px;
}

.analysis-header h3 {
  color: white;
  font-size: 18px;
  font-weight: 600;
  margin: 0;
}

.analysis-header .close-analysis-btn {
  width: 28px;
  height: 28px;
  background: rgba(255,255,255,0.1);
  border: none;
  border-radius: 50%;
  color: white;
  font-size: 18px;
  cursor: pointer;
}

.analysis-content {
  color: white;
}

.analysis-result {
  white-space: pre-wrap;
  line-height: 1.6;
  font-size: 14px;
  color: rgba(255,255,255,0.9);
}

.analysis-error {
  /* 这一行永远压在图片查看器那层深底上，所以取亮档红、不走主题 token：
     --danger 在浅色主题=#b91c1c，压在近黑底上只有 2.8:1。它从前写的是 var(--error-500)，全站没这个定义。 */
  color: #ffb3b3;
  font-size: 14px;
}

.loading-indicator {
  display: flex;
  justify-content: center;
  padding: 20px;
}

/* 这组点同时用在图片上传的那颗 pill（.loading-indicator）和 1:1 里"AI 正在组织语言"
   的那条消息行上。以前三条规则全挂在 .loading-indicator 下面，消息行够不着——
   addLoadingIndicator 画的三个 span 没有任何尺寸，看到的是三个 0×0 的空东西。 */
.loading-dots {
  display: flex;
  gap: 8px;
}

.loading-dots span {
  width: 8px;
  height: 8px;
  background: var(--primary-500);
  border-radius: 50%;
  animation: dotPulse 1.4s infinite ease-in-out;
}

.loading-dots span:nth-child(1) { animation-delay: 0s; }
.loading-dots span:nth-child(2) { animation-delay: 0.2s; }
.loading-dots span:nth-child(3) { animation-delay: 0.4s; }

@keyframes dotPulse {
  0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; }
  40% { transform: scale(1); opacity: 1; }
}


/* -------------------------- */
/* 骨架屏                     */
/* -------------------------- */
.skeleton {
  background: var(--neutral-200);
  border-radius: var(--radius-md);
  animation: skeletonPulse 1.5s infinite ease-in-out;
}

[data-theme="dark"] .skeleton {
  background: var(--neutral-700);
}

/* 骨架屏消息 */
.skeleton-message {
  display: flex;
  margin-bottom: var(--spacing-md);
  animation: fadeInUp 0.3s ease-out;
}

.skeleton-message.skeleton-user {
  justify-content: flex-end;
}

.skeleton-message.skeleton-assistant {
  justify-content: flex-start;
}

.skeleton-body {
  display: flex;
  gap: var(--spacing-sm);
  max-width: 70%;
}

.skeleton-user .skeleton-body {
  flex-direction: row-reverse;
}

.skeleton-avatar {
  width: 32px;
  height: 32px;
  border-radius: var(--radius-md);
  flex-shrink: 0;
  background: var(--neutral-200);
  animation: skeletonPulse 1.5s infinite ease-in-out;
}

[data-theme="dark"] .skeleton-avatar {
  background: var(--neutral-700);
}

.skeleton-content-wrapper {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-sm);
}

.skeleton-content {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: var(--spacing-md) var(--spacing-lg);
  border-radius: var(--radius-lg);
  background: var(--neutral-100);
}

[data-theme="dark"] .skeleton-content {
  background: var(--neutral-700);
}

.skeleton-line {
  height: 14px;
  background: var(--neutral-200);
  border-radius: var(--radius-sm);
  animation: skeletonPulse 1.5s infinite ease-in-out;
}

[data-theme="dark"] .skeleton-line {
  background: var(--neutral-600);
}

.skeleton-line.short {
  width: 60%;
}

.skeleton-line.medium {
  width: 80%;
}

.skeleton-line.long {
  width: 100%;
}

.skeleton-meta {
  display: flex;
  justify-content: flex-end;
  gap: var(--spacing-sm);
  padding: 0 var(--spacing-sm);
}

.skeleton-time {
  width: 40px;
  height: 10px;
  border-radius: var(--radius-sm);
  background: var(--neutral-200);
  animation: skeletonPulse 1.5s infinite ease-in-out;
}

[data-theme="dark"] .skeleton-time {
  background: var(--neutral-600);
}

/* -------------------------- */
/* 移动端适配                 */
/* -------------------------- */

/* 手机横屏 480px - 768px：抽屉模式 */
@media (max-width: 768px) {
  .sidebar {
    position: fixed;
    left: -280px;
    top: 0;
    height: 100vh;
    height: 100dvh;
    z-index: 900;
    /* visibility 那一条延后 0.25s，等的就是 left 这段滑动——写成 0s 的话抽屉在收起动画
       开始的那一帧就消失了，滑动只剩下一半。 */
    transition: left 0.25s ease-out, visibility 0s linear 0.25s;
    width: 280px;
    /* 收起来的抽屉只是被挪出屏幕，它自己一直是 visible。实测 375 宽下里面仍有 15 颗控件
       走得到 Tab（好友/群聊两张标签、每一个角色、反馈、退出），键盘用户从页首走一圈全是
       够不着的东西，读屏也把没打开的抽屉当正文念。visibility 一处就把「退出 Tab 序」和「退出
       无障碍树」两件事一起还掉；inert 那个属性要 Chrome 102／Firefox 112 才有，本项目的口径
       是把滞后的内核当存在的，所以不用它。 */
    visibility: hidden;
  }

  .sidebar.open {
    left: 0;
    visibility: visible;
    /* 拉开时不能等：那一档延时只属于收回去那一下 */
    transition: left 0.25s ease-out, visibility 0s;
  }
  
  .chat-area {
    width: 100%;
  }
  
  .message-body {
    max-width: 85%;
  }

  /* 1:1 在手机上把上限抬到 85%，群聊那条要跟着抬，否则窄屏上群聊的气泡明显比单聊窄一截 */
  .message.group .message-content {
    max-width: calc(85% - 32px - var(--spacing-sm));
  }
  
  .chat-header {
    padding: 0 var(--spacing-md);
    padding-left: max(var(--spacing-md), env(safe-area-inset-left));
    padding-right: max(var(--spacing-md), env(safe-area-inset-right));
  }
  
  .chat-actions {
    gap: var(--spacing-xs);
  }
  
  .action-btn {
    width: 32px;
    height: 32px;
  }
  
  .chat-input-area {
    padding: var(--spacing-sm) var(--spacing-md);
    padding-left: max(var(--spacing-sm), env(safe-area-inset-left));
    padding-right: max(var(--spacing-sm), env(safe-area-inset-right));
    padding-bottom: calc(var(--spacing-sm) + env(safe-area-inset-bottom));
    /* 工具栏独占一行。它和输入框、发送键本来是同一条 nowrap 的 flex 行，
       窄屏下 252px 的按钮排会把输入框挤到 34px 宽（实测 360px），
       输入框再按行高一撑就把整个输入区顶到 177px 高 */
    flex-wrap: wrap;
  }

  .input-toolbar {
    flex: 0 0 100%;
    /* flex-basis: 100% 只是起点，轨道的下限是内容的 min-content——上面那个 @选择器
       的固有宽度会把输入区整条链路顶宽。min-width: 0 才能让这一排真的收到行宽。 */
    min-width: 0;
  }

  .modal-content {
    max-width: calc(100% - 24px);
    border-radius: var(--radius-xl);
  }
}

/* 手机竖屏 < 480px */
@media (max-width: 480px) {
  html {
    font-size: 13px;
  }
  
  .sidebar {
    width: 100%;
    left: -100%;
    /* 抽屉在这个宽度下是全屏的，底部不留出横条安全区，
       「个人空间 / 设置」那一排会被圆角切掉一半 */
    padding-bottom: env(safe-area-inset-bottom);
  }
  
  .sidebar.open {
    left: 0;
  }
  
  .chat-messages {
    padding: var(--spacing-md);
  }
  
  .message-content {
    padding: var(--spacing-sm) var(--spacing-md);
    font-size: var(--font-size-base);
  }
  
  .avatar {
    width: 36px;
    height: 36px;
    font-size: 14px;
    border-radius: 10px;
  }
  
  .message .avatar {
    width: 28px;
    height: 28px;
    font-size: 12px;
    border-radius: 8px;
  }

  /* 手机上头像收到 28px，那一列扣掉的宽度也跟着改，别浪费 4px */
  .message.group .message-content {
    max-width: calc(85% - 28px - var(--spacing-sm));
  }
  
  .toast {
    bottom: calc(var(--spacing-xl) + env(safe-area-inset-bottom));
  }
}

/* 群聊"正在输入"：复用的是消息行的结构，所以这里只画气泡内的三个弹跳点，
   不再额外套一层白底圆角——那会在消息行外再叠一个胶囊，比正常气泡高出一截 */
.typing-dots {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin: 0;
}

.typing-indicator .typing-dot {
  width: 6px;
  height: 6px;
  background: var(--primary-500);
  border-radius: 50%;
  animation: typingBounce 1.4s infinite ease-in-out;
}

.typing-indicator .typing-dot:nth-child(1) { animation-delay: 0s; }
.typing-indicator .typing-dot:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator .typing-dot:nth-child(3) { animation-delay: 0.4s; }

@keyframes typingBounce {
  0%, 60%, 100% { transform: translateY(0); }
  30% { transform: translateY(-8px); }
}

/* 搜索面板样式 */
.search-panel {
  display: none;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
  z-index: 2000;
  justify-content: center;
  align-items: flex-start;
  /* 固定 100px 的顶部留白 + 70vh 的面板，在横屏手机（视口只剩 360px）上
     已经把面板顶到贴底，键盘一弹起就直接被裁在屏幕外。 */
  padding-top: min(100px, 14vh);
  padding-top: min(100px, 14dvh);
  animation: fadeIn 0.2s ease-out;
}

.search-panel.active {
  display: flex;
}

.search-panel-content {
  background: var(--bg-card);
  border-radius: 12px;
  box-shadow: var(--shadow-lg);
  width: 90%;
  max-width: 600px;
  max-height: 70vh;
  max-height: 70dvh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.search-panel-header {
  padding: 16px;
  border-bottom: 1px solid var(--border-color);
  display: flex;
  gap: 12px;
  align-items: center;
}

/* 搜索范围切换：本会话 / 全部会话。复用侧栏那两个 tab 的配色，只在面板里补一行容器。 */
.search-scope {
  display: flex;
  gap: var(--spacing-xs);
  padding: 8px 16px 0;
}

.search-scope .tab-btn {
  padding: 6px 12px;
}

.search-panel-input {
  flex: 1;
  /* input 的 min-width:auto 按 size 属性留了 20 个字符宽，不肯再缩；手机上 × 补到
     44px 之后整行顶出卡片右缘 9px。写成 0 才真的把剩余空间让给按钮。 */
  min-width: 0;
  padding: 10px 14px;
  border: 2px solid var(--border-color);
  border-radius: 8px;
  font-size: 14px;
  outline: none;
  transition: border-color var(--transition-fast);
}

.search-panel-input:focus {
  border-color: var(--border-focus);
}

.search-panel-results {
  flex: 1;
  overflow-y: auto;
  padding: 8px;
}

.search-result-item {
  padding: 12px;
  border-radius: 8px;
  cursor: pointer;
  transition: background var(--transition-fast);
  margin-bottom: 4px;
}

/* 搜索结果项是 button，键盘才走得通；先把浏览器那套按钮外观抹平，
   收藏面板里的结果行仍是 div（里面还套着「取消收藏」），所以只挑 button 来重置。 */
button.search-result-item {
  display: block;
  width: 100%;
  border: none;
  background: transparent;
  font: inherit;
  color: inherit;
}

.search-result-item:focus-visible {
  outline: 2px solid var(--border-focus);
  outline-offset: -2px;
}

/* 面板里那句「输入关键词开始搜索」不是可点的东西，别借用结果的 hover 外观 */
.search-result-hint {
  padding: 12px;
  font-size: var(--font-size-sm);
  color: var(--text-muted);
}

/* 跨会话那 50 条是按时间切的，这一行说清这批被谁占满；chip 点下去就把范围收到那个会话 */
.search-tally {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  padding: 2px 12px 8px;
  font-size: var(--font-size-xs);
  color: var(--text-muted);
}

/* 别名一起搜的时候，那几条命中的行里可能一个字都没对上用户打的字，这一行交代来历 */
.search-alias-hint {
  margin: 0 12px 6px;
  padding: 6px 10px;
  border-left: 2px solid var(--primary-500);
  border-radius: var(--radius-sm);
  background: var(--bg-chat);
  color: var(--text-secondary);
  font-size: var(--font-size-xs);
  line-height: 1.6;
}

.search-tally-chip {
  padding: 3px 10px;
  border: 1px solid var(--border-color);
  border-radius: var(--radius-full);
  background: var(--bg-card);
  color: var(--text-secondary);
  font-size: var(--font-size-xs);
  cursor: pointer;
}

.search-tally-chip:hover {
  border-color: var(--primary-500);
  color: var(--accent-text);
}

.search-tally-chip:focus-visible {
  outline: 2px solid var(--border-focus);
  outline-offset: -2px;
}

.search-result-item:hover {
  background: var(--primary-50);
}

.search-result-item .result-message {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  /* 搜索结果一行最多三行。收藏复用同一套样式，一条两千字的长回答以前能把面板撑得
     只能靠滚动找，命中位置根本对不齐。三行之后截断，跳转还是跳得过去。 */
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
  font-size: 14px;
  color: var(--text-primary);
  margin-bottom: 4px;
  /* 只要命中一条带长 token 的消息（URL、文件名、一串代码），这一行就能排到两千多像素：
     实测 320px 手机上结果区 scrollWidth 2151 对 288，得横向拖动才看得到后半段。 */
  overflow-wrap: break-word;
}

.search-result-item .result-meta {
  display: block;
  font-size: 12px;
  color: var(--text-muted);
}

.search-result-item .result-highlight {
  background: rgba(139, 92, 246, 0.2);
  padding: 2px 4px;
  border-radius: 4px;
  font-weight: 500;
}

.search-panel-footer {
  padding: 12px 16px;
  border-top: 1px solid var(--border-color);
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 12px;
  color: var(--text-muted);
}

.search-panel-footer .shortcut-hint {
  background: var(--neutral-200);
  padding: 4px 8px;
  border-radius: 4px;
  font-family: monospace;
}

/* 这块牌子的字色不在它自己身上，是 .search-panel-footer 的 --text-muted 继承下来的：
   浅色档浅灰底配深灰字实测 6.15:1，暗色档底子照旧浅灰、字却换成近白，实测 1.20:1（改刻度之前也是 3.86:1，
   本来就不够）。同一对颜色在两套主题里方向相反，一档改不成一档好，所以暗色把底压回 --neutral-700
   ——第 57 轮那三处悬停底色用的是同一个办法。 */
[data-theme="dark"] .search-panel-footer .shortcut-hint {
  background: var(--neutral-700);
}

/* 平板 768px - 1024px：侧边栏缩小至 240px */
@media (min-width: 769px) and (max-width: 1024px) {
  .sidebar {
    width: 240px;
    min-width: 240px;
    max-width: 240px;
  }
}

/* 桌面 > 1024px：侧边栏 33.333% */
@media (min-width: 1025px) {
  .sidebar {
    width: 33.333%;
    min-width: 280px;
    max-width: 400px;
  }
}

/* 宽屏正文限宽：侧栏最宽 400px，剩下的宽度若全给正文，
   2560 屏上一行能排到上百个汉字，根本读不了。这里用内边距夹出
   一条 900px 的列并居中，背景纹理仍铺满整块，不会切出异色区域。 */
@media (min-width: 1280px) {
  .chat-messages,
  .chat-input-area {
    padding-left: max(var(--spacing-lg), calc((100% - var(--chat-measure)) / 2));
    padding-right: max(var(--spacing-lg), calc((100% - var(--chat-measure)) / 2));
  }
}

/* 移动端触摸优化 */
@media (max-width: 768px) {
  * {
    -webkit-tap-highlight-color: transparent;
  }
  
  .character-item:active,
  .group-item:active,
  .action-btn:active,
  .toolbar-btn:active,
  .footer-btn:active {
    opacity: 0.7;
  }
  
  .chat-messages {
    -webkit-overflow-scrolling: touch;
  }
}

/* 减少动画（无障碍） */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  
  .message,
  .message.user,
  .message.assistant,
  .skeleton,
  .skeleton-message,
  .modal,
  .modal-content,
  .toast {
    animation: none !important;
    transition: none !important;
  }
}

/* 个人空间卡片 - 侧边栏底部 */
.sidebar-footer {
  padding: var(--spacing-md);
  border-top: 1px solid var(--border-color);
  display: flex;
  flex-direction: column;
  gap: var(--spacing-md);
}

.user-profile-card {
  display: flex;
  align-items: center;
  gap: var(--spacing-md);
  padding: var(--spacing-md);
  background: var(--bg-card);
  border-radius: var(--radius-lg);
  cursor: pointer;
  transition: all var(--transition-normal);
  margin-bottom: var(--spacing-md);
}

[data-theme="dark"] .user-profile-card {
  background: rgba(139, 92, 246, 0.08);
}

.user-profile-card:hover {
  background: var(--bg-hover);
  transform: translateY(-1px);
  box-shadow: var(--shadow-sm);
}

[data-theme="dark"] .user-profile-card:hover {
  background: rgba(139, 92, 246, 0.15);
}

/* 卡片现在是 button，键盘才走得进侧栏底部。这里不许写 background：
   底色归上面 .user-profile-card（权重 0,1,0）压过全局 button{background:none}（0,0,1），
   一旦被 button.user-profile-card 盖掉，卡片就没底色了。 */
button.user-profile-card {
  width: 100%;
  min-width: 0;
  text-align: left;
}

.user-profile-card:focus-visible {
  outline: 2px solid var(--border-focus);
  outline-offset: -2px;
}

.user-profile-card .user-avatar {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--primary-600), var(--primary-800));
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 24px;
  font-weight: 600;
  color: var(--text-white);
  overflow: hidden;
  flex-shrink: 0;
  box-shadow: 0 2px 8px rgba(139, 92, 246, 0.3);
}

.user-profile-card .user-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.user-profile-card .user-info-text {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.user-profile-card .user-nickname {
  display: block;
  font-size: 16px;
  font-weight: 600;
  color: var(--text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.user-profile-card .user-status {
  display: block;
  font-size: 12px;
  color: var(--text-secondary);
  margin-top: 2px;
}

.user-profile-card .profile-arrow {
  font-size: 20px;
  color: var(--text-secondary);
  transition: transform var(--transition-fast);
}

.user-profile-card:hover .profile-arrow {
  transform: translateX(2px);
}

.footer-actions {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
}

/* 个人空间模态框 */
.profile-modal {
  max-width: 480px !important;
  width: 90vw;
}

.profile-modal .modal-header h2 {
  text-align: center;
  font-size: 20px;
  font-weight: 600;
}

.profile-avatar-section {
  text-align: center;
  padding: var(--spacing-xl) 0;
  border-bottom: 1px solid var(--border-color);
  margin-bottom: var(--spacing-xl);
}

.profile-avatar-preview {
  width: 100px;
  height: 100px;
  border-radius: 50%;
  margin: 0 auto var(--spacing-lg);
  background: linear-gradient(135deg, var(--primary-400), var(--primary-600));
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  box-shadow: var(--shadow-md);
}

.profile-avatar-preview .avatar-placeholder {
  font-size: 48px;
}

.profile-avatar-preview img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.avatar-actions {
  display: flex;
  justify-content: center;
  gap: var(--spacing-md);
  margin-top: var(--spacing-lg);
}

.upload-avatar-btn,
.delete-avatar-btn {
  padding: var(--spacing-sm) var(--spacing-lg);
  background: var(--bg-secondary);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md);
  font-size: 14px;
  cursor: pointer;
  transition: all var(--transition-fast);
}

.upload-avatar-btn:hover,
.delete-avatar-btn:hover {
  background: var(--bg-hover);
  transform: translateY(-1px);
}

.delete-avatar-btn {
  color: var(--danger);
}

.avatar-hint {
  font-size: 12px;
  color: var(--text-secondary);
  margin-top: var(--spacing-md);
}

.profile-nickname-section {
  margin-bottom: var(--spacing-xl);
}

.profile-nickname-section label {
  display: block;
  font-size: 14px;
  font-weight: 500;
  color: var(--text-primary);
  margin-bottom: var(--spacing-sm);
}

.profile-nickname-section input {
  width: 100%;
  padding: var(--spacing-md) var(--spacing-lg);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md);
  font-size: 16px;
  transition: all var(--transition-fast);
}

.profile-nickname-section input:focus {
  outline: none;
  border-color: var(--border-focus);
  box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1);
}

.nickname-hint {
  font-size: 12px;
  color: var(--text-secondary);
  margin-top: var(--spacing-sm);
}

.profile-actions {
  text-align: center;
  padding-top: var(--spacing-lg);
}

.save-profile-btn {
  width: 100%;
  padding: var(--spacing-md) var(--spacing-xl);
  /* 同 .auth-btn：白字压 500 只有 4.23:1，整条降一档，悬停再深一档保住"按得下去"的反馈 */
  background: linear-gradient(135deg, var(--primary-600), var(--primary-700));
  color: white;
  border: none;
  border-radius: var(--radius-lg);
  font-size: 16px;
  font-weight: 500;
  cursor: pointer;
  transition: all var(--transition-fast);
}

.save-profile-btn:hover {
  background: linear-gradient(135deg, var(--primary-700), var(--primary-800));
  transform: translateY(-1px);
  box-shadow: var(--shadow-md);
}

.save-profile-btn:active {
  transform: translateY(0);
}

.save-profile-btn .btn-loading {
  display: inline-block;
  margin-left: var(--spacing-sm);
}

/* 数据管理区域 */
.data-management-section {
  margin-top: var(--spacing-xl);
  padding-top: var(--spacing-xl);
  border-top: 1px solid var(--border-color);
}

.data-management-section h3 {
  font-size: 14px;
  font-weight: 600;
  color: var(--text-primary);
  margin-bottom: var(--spacing-lg);
}

.data-management-actions {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-sm);
}

.data-action-btn {
  width: 100%;
  padding: var(--spacing-md) var(--spacing-lg);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md);
  font-size: 14px;
  cursor: pointer;
  transition: all var(--transition-fast);
  background: var(--bg-secondary);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--spacing-sm);
}

.data-action-btn:hover {
  background: var(--bg-hover);
  border-color: var(--primary-300);
  transform: translateY(-1px);
}

.export-btn {
  color: var(--primary-600);
}

/* 这颗按钮的底由同一段兄弟类 .data-action-btn 画（--bg-card），字色却只有本条写着——
   第 63 轮浏览器按真底量：暗色 #7c3aed 压 #334155 是 1.82:1，和 .sender-name 那一族同一个漏。
   换 300 是 5.61:1；亮色压白卡是 5.70:1，本来就该过，不动。 */
[data-theme="dark"] .export-btn {
  color: var(--primary-300);
}

.import-btn {
  color: var(--success);
}

.data-hint {
  font-size: 12px;
  color: var(--text-secondary);
  margin-top: var(--spacing-md);
  text-align: center;
}

/* 焦点指示器（无障碍）：这一条管全站没写自己焦点样式的控件。
   全站 12 处焦点环一律走 --border-focus，不许写死色号：焦点环要和自己落在那块面上差 3:1
   （WCAG 1.4.11 非文本对比），第 64 轮之前有 11 处写死 --primary-500，暗色卡面 #334155 上只有 2.45:1。
   组件级的紫环（搜索结果、侧栏行、chip）用 -2px 内描边，各自覆盖这条。 */
:focus-visible {
  outline: 2px solid var(--border-focus);
  outline-offset: 2px;
}

/* 隐藏默认焦点轮廓（如果有自定义焦点样式） */
:focus:not(:focus-visible) {
  outline: none;
}

/* 表情选择器：与快捷回复面板同样挂在 .chat-input-area 上方弹出 */
.emoji-picker {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 0;
  width: min(320px, calc(100vw - 24px));
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  z-index: 100;
}

.emoji-grid {
  display: grid;
  /* 按宽度自动回流：32px 屏宽下不会像固定 8 列那样把格子挤扁 */
  /* 行高、间隙、看得见几整行都收成变量，max-height 由它们倒推。从前这里手抄 216px，
     实测画满 5 整行后只给第 6 行留下 30px——那 2px 缺口点得着，但看着像被切了一刀。 */
  --emoji-row: 32px;
  --emoji-gap: 2px;
  --emoji-rows: 6;
  grid-template-columns: repeat(auto-fill, minmax(34px, 1fr));
  gap: var(--emoji-gap);
  padding: var(--spacing-sm);
  max-height: calc(var(--emoji-rows) * (var(--emoji-row) + var(--emoji-gap)) - var(--emoji-gap) + 2 * var(--spacing-sm));
  overflow-y: auto;
}

.emoji-grid button {
  display: flex;
  align-items: center;
  justify-content: center;
  height: var(--emoji-row);
  padding: 0;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  font: inherit;
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  transition: background 0.15s ease;
}

.emoji-grid button:hover {
  background: var(--neutral-100);
}

/* 格子之间只有 2px 间隙，焦点环必须画在格子里侧，往外扩就压到邻居 */
.emoji-grid button:focus-visible {
  outline: 2px solid var(--border-focus);
  outline-offset: -2px;
}

[data-theme="dark"] .emoji-picker {
  background: var(--neutral-800);
}

[data-theme="dark"] .emoji-grid button:hover {
  background: var(--neutral-700);
}

/* 快捷回复面板 */
.quick-reply-panel {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 0;
  /* 面板左边落在工具栏的 8px 内边距上，所以 100vw 要留出 24px 才不会被右缘切掉 */
  width: min(320px, calc(100vw - 24px));
  background: var(--bg-card);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-lg);
  border: 1px solid var(--border-color);
  overflow: hidden;
  z-index: 100;
  /* 表单展开后面板会长高，横屏矮屏上会顶出视口；限高并让列表先收缩。
     从前这两行手抄的是 calc(100vh - 160px)，那个 160 是输入区的高度却没人量过——第 87 轮触屏档
     现量 125（待机）到 293（320 宽群聊打满七行）：按 160 算，320×640 群聊那一档面板底在 348、
     上限 480，顶边算到 −132，连面板自己的 × 都在屏外。现在从 --composer-h（定义在
     .chat-input-area、由 chat-input.js 覆写）倒推，留下的 68 = 头部 60（.chat-header 的 height）
     + 面板弹起的那条缝 8，两处都是本表里读得出的数。实测内容撑满限高时顶边各档都落在 69：
     竖屏头部底 60（留 9px），横屏 667×375 头部底 48（多让 20px——这条 68 按竖屏算，没给横屏另开一档）。 */
  display: flex;
  flex-direction: column;
  max-height: calc(100vh - var(--composer-h) - 68px);
  max-height: calc(100dvh - var(--composer-h) - 68px);
}

.quick-reply-panel .panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--spacing-md);
  border-bottom: 1px solid var(--border-color);
}

.quick-reply-panel .panel-header h3 {
  font-size: var(--font-size-base);
  font-weight: 600;
  color: var(--text-primary);
  margin: 0;
}

.quick-reply-panel .panel-header .close-btn {
  width: 28px;
  height: 28px;
  border-radius: var(--radius-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  transition: all 0.15s ease;
}

.quick-reply-panel .panel-header .close-btn:hover {
  /* 和上面 .action-btn:hover 同一族：写死浅色档的 --neutral-100，暗色下闪一块近白、图标跟着消失 */
  background: var(--bg-hover);
  color: var(--text-primary);
}

.quick-reply-list {
  max-height: 240px;
  overflow-y: auto;
  padding: var(--spacing-sm);
  flex: 0 1 auto;
  min-height: 0;
}

/* 一行两层：外层只管定位（删除键要压在行的右端），里面那条才是能回车插入的按钮 */
.quick-reply-row {
  position: relative;
  margin-bottom: 4px;
}

button.quick-reply-item {
  display: block;
  width: 100%;
  padding: var(--spacing-sm) var(--spacing-md);
  padding-right: 34px;
  border: none;
  border-radius: var(--radius-md);
  background: transparent;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  transition: all 0.15s ease;
}

.quick-reply-item:hover {
  background: var(--neutral-100);
}

.quick-reply-item:active {
  background: var(--neutral-200);
}

.quick-reply-item:focus-visible {
  outline: 2px solid var(--border-focus);
  outline-offset: -2px;
}

.quick-reply-item .reply-text {
  display: block;
  font-size: var(--font-size-sm);
  color: var(--text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.quick-reply-item .reply-tags {
  display: flex;
  gap: 4px;
  margin-top: 4px;
}

.quick-reply-item .reply-tag {
  font-size: 10px;
  padding: 2px 6px;
  background: var(--primary-50);
  color: var(--primary-600);
  border-radius: var(--radius-sm);
}

/* 条目上的删除键：以前快捷回复只能加不能删，加错了只能去清 localStorage */
.quick-reply-row .reply-remove {
  position: absolute;
  top: 50%;
  right: 5px;
  transform: translateY(-50%);
  width: 26px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-muted);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
}

.quick-reply-row .reply-remove:hover {
  background: var(--danger-soft);
  color: var(--danger);
}

/* 面板内的内联添加表单，取代原来的两次 prompt() */
.quick-reply-form {
  display: grid;
  gap: 8px;
  padding: var(--spacing-sm) var(--spacing-md);
  border-top: 1px solid var(--border-color);
}

/* 表单展开时把列表收起：编辑一条固定回复不需要边看边选，腾出的高度归表单。
   横屏矮屏上限高仍然装不下时，让面板自己滚，保存键不能永远够不到。 */
.quick-reply-panel.form-open .quick-reply-list {
  display: none;
}

.quick-reply-panel.form-open {
  overflow-y: auto;
}

.qr-field {
  display: grid;
  gap: 4px;
  font-size: var(--font-size-xs);
  color: var(--text-muted);
}

.qr-field input {
  padding: 8px 10px;
  border: 1.5px solid var(--border-color);
  border-radius: var(--radius-md);
  background: var(--bg-input);
  color: var(--text-primary);
  font-size: var(--font-size-sm);
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}

.qr-field input:focus {
  outline: none;
  border-color: var(--border-focus);
  box-shadow: var(--shadow-input-focus);
}

.qr-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}

.qr-actions button {
  min-height: 32px;
  padding: 6px 14px;
  border-radius: var(--radius-md);
  font-size: var(--font-size-sm);
  cursor: pointer;
}

.qr-cancel {
  background: transparent;
  border: 1px solid var(--border-color);
  color: var(--text-secondary);
}

.qr-save {
  background: var(--primary-600);
  border: 1px solid var(--primary-600);
  color: var(--text-white);
}

.quick-reply-panel .panel-footer {
  padding: var(--spacing-sm);
  border-top: 1px solid var(--border-color);
}

.quick-reply-panel .panel-footer button {
  width: 100%;
  padding: var(--spacing-sm);
  border: 1px dashed var(--border-color);
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--text-muted);
  font-size: var(--font-size-sm);
  cursor: pointer;
  transition: all 0.15s ease;
}

.quick-reply-panel .panel-footer button:hover {
  border-color: var(--primary-500);
  color: var(--accent-text);
}

[data-theme="dark"] .quick-reply-panel {
  background: var(--neutral-800);
}

[data-theme="dark"] .quick-reply-item:hover {
  background: var(--neutral-700);
}

[data-theme="dark"] .quick-reply-item:active {
  background: var(--neutral-600);
}

[data-theme="dark"] .quick-reply-item .reply-tag {
  background: rgba(139, 92, 246, 0.2);
  /* 这块淡底有两个能走到的地板：静止时压在面板 #1e293b 上，鼠标走进这一行时压在 .quick-reply-item:hover
     的 #475569 上（父级一悬停，标签的地板就跟着抬亮）。第 63 轮实测 --primary-600 是 2.06 / 1.21，
     --primary-300 是 6.35 / 3.71——后面那一档还差一点。换 200 之后 8.44 / 4.93，两块地板都过。 */
  color: var(--primary-200);
}

/* 收藏消息样式 */
.message.favorited {
  position: relative;
}

.message.favorited::before {
  content: '★';
  position: absolute;
  top: -4px;
  right: -4px;
  font-size: 14px;
  /* 这颗星是"这句被收藏过"在气泡上唯一的记号，第 63 轮按真底量：#fbbf24 压在白气泡上只有 1.67:1，
     连图形那档 3:1 都不够，换成 #b45309 是 5.02:1。暗色反过来——同一档深金压 #334155 只有 2.06:1，
     所以暗色留亮的那档（6.20:1），两条各按各的地板取。 */
  color: #b45309;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
  animation: starPop 0.3s ease-out;
}

[data-theme="dark"] .message.favorited::before {
  color: #fbbf24;
}

@keyframes starPop {
  0% {
    transform: scale(0);
    opacity: 0;
  }
  50% {
    transform: scale(1.3);
  }
  100% {
    transform: scale(1);
    opacity: 1;
  }
}

/* 管理模态框中的人物卡片样式 */
.character-grid {
  display: grid;
  /* 轨道下限写成 min(280px, 100%)：容器的 auto 最小值是 min-content，固定 280px 的
     轨道在窄屏上顶穿网格本身——实测 320px 手机上模态框正文只有 242px，网格却排到
     304px，人物卡片得横向拖动才看得全。容器够宽时两种写法完全等价。 */
  grid-template-columns: repeat(auto-fill, minmax(min(280px, 100%), 1fr));
  gap: var(--spacing-md);
  margin-bottom: var(--spacing-lg);
}

.character-card {
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-lg);
  padding: var(--spacing-md);
  display: flex;
  align-items: center;
  gap: var(--spacing-md);
  transition: all var(--transition-normal);
}

.character-card:hover {
  border-color: var(--primary-300);
  box-shadow: var(--shadow-md);
  transform: translateY(-2px);
}

.character-card .avatar {
  width: 48px;
  height: 48px;
  font-size: 20px;
  margin-right: 0;
}

.character-card .info {
  flex: 1;
  min-width: 0;
}

.character-card .info h3 {
  font-size: var(--font-size-base);
  font-weight: 600;
  color: var(--text-primary);
  margin: 0 0 4px 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.character-card .info p {
  font-size: var(--font-size-xs);
  color: var(--text-secondary);
  margin: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* 别名以前只有重开编辑框才看得见，填完想核对要点两下。
   跟正文一样单行截断，卡片不许因为这行变高——列表是二十几个人物排下来的。 */
.character-card .info .character-alias-line {
  margin-top: 2px;
}

/* 预算没到位的那几段，在卡片上先只报个数量——一句话排不下，细节在编辑框里念。
   照样单行截断：卡片不许因为这行变高。 */
.character-card .info .character-budget-line {
  margin-top: 2px;
  color: var(--danger);
}

.character-card .actions {
  display: flex;
  gap: 8px;
  flex-shrink: 0;
}

.character-card .edit-btn {
  padding: 6px 12px;
  border: 1px solid var(--primary-300);
  background: var(--primary-50);
  color: var(--primary-600);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-xs);
  font-weight: 500;
  cursor: pointer;
  transition: all var(--transition-fast);
  font-family: 'Segoe UI', system-ui, sans-serif;
}

.character-card .edit-btn:hover {
  background: var(--primary-100);
  border-color: var(--primary-400);
  transform: translateY(-1px);
}

.character-card .delete-btn {
  padding: 6px 12px;
  border: 1px solid var(--neutral-300);
  background: var(--neutral-50);
  color: var(--text-secondary);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-xs);
  font-weight: 400;
  cursor: pointer;
  transition: all var(--transition-fast);
  font-family: 'Arial', sans-serif;
}

.character-card .delete-btn:hover {
  background: #fee2e2;
  border-color: #fca5a5;
  /* 写死的 #dc2626 压这块淡红底只有 3.95:1；--danger 在亮色是 #b91c1c，同一块底上 5.30:1（第 63 轮） */
  color: var(--danger);
  transform: translateY(-1px);
}

[data-theme="dark"] .character-card .edit-btn {
  background: rgba(139, 92, 246, 0.1);
  border-color: var(--primary-500);
  /* 同一层淡底压在暗色卡片底 #334155 上，--primary-400 只有 3.51:1，换 300 实测 5.17:1；
     悬停那层淡底更亮（卡片本身也悬停时 4.75:1），两档都还在 AA 之上（第 63 轮实测） */
  color: var(--primary-300);
}

[data-theme="dark"] .character-card .edit-btn:hover {
  background: rgba(139, 92, 246, 0.2);
  border-color: var(--primary-400);
}

[data-theme="dark"] .character-card .delete-btn {
  background: rgba(220, 38, 38, 0.1);
  border-color: var(--neutral-600);
  color: var(--text-secondary);
}

[data-theme="dark"] .character-card .delete-btn:hover {
  background: rgba(220, 38, 38, 0.2);
  border-color: #ef4444;
  /* 写死的 #f87171 压这层淡红底 3.57:1，换浅一档的 #fca5a5 是 5.21:1（第 63 轮实测） */
  color: #fca5a5;
}

/* 提醒高亮动画 */
.message.reminder-highlight {
  animation: reminderPulse 1s ease-in-out infinite;
}

@keyframes reminderPulse {
  0%, 100% {
    box-shadow: 0 0 0 0 rgba(251, 191, 36, 0.4);
  }
  50% {
    box-shadow: 0 0 0 8px rgba(251, 191, 36, 0);
  }
}

/* 聊天统计面板 */
.stats-modal {
  max-width: 600px;
}

.stats-grid {
  display: grid;
  /* 1fr 其实是 minmax(auto,1fr)：轨道下限等于内容的 min-content，一个七位数字
     就能把轨道顶到 149px——实测 320px 手机正文只有 266px，两列排到 337px，
     卡片直接戳出模态框右缘 65px。写成 minmax(0,1fr) 后轨道只跟容器走。 */
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--spacing-md);
  margin-bottom: var(--spacing-lg);
}

.stat-card {
  background: var(--neutral-50);
  border-radius: var(--radius-lg);
  padding: var(--spacing-md);
  text-align: center;
  transition: all 0.2s ease;
}

.stat-card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-sm);
}

.stat-icon {
  font-size: 24px;
  margin-bottom: var(--spacing-xs);
}

.stat-value {
  font-size: 24px;
  font-weight: 700;
  color: var(--accent-text);
  margin-bottom: 2px;
}

.stat-label {
  font-size: 12px;
  color: var(--text-muted);
}

.stats-charts {
  margin-top: var(--spacing-lg);
}

.stats-charts h3 {
  font-size: var(--font-size-base);
  font-weight: 600;
  margin-bottom: var(--spacing-md);
  color: var(--text-primary);
}

.chart-container {
  height: 200px;
  background: var(--neutral-50);
  border-radius: var(--radius-lg);
  display: flex;
  align-items: flex-end;
  justify-content: space-around;
  padding: var(--spacing-md);
  gap: 4px;
}

.chart-bar {
  flex: 1;
  background: linear-gradient(to top, var(--primary-500), var(--primary-400));
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  min-height: 4px;
  transition: height 0.3s ease;
  position: relative;
}

.chart-bar:hover {
  background: linear-gradient(to top, var(--primary-600), var(--primary-500));
}

.chart-bar::after {
  content: attr(data-label);
  position: absolute;
  bottom: -20px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 10px;
  color: var(--text-muted);
  white-space: nowrap;
}

[data-theme="dark"] .stat-card {
  background: var(--neutral-700);
}

[data-theme="dark"] .chart-container {
  background: var(--neutral-700);
}

/* 使用量统计面板。配额那一栏原先在 style.css 里一个选择器都没有：
   .progress-bar/.progress-fill 是没有高度也没有底色的空 div，
   JS 算出来的 width 一直在写，只是永远看不见。 */
.dashboard-modal {
  max-width: 560px;
}

.stat-item {
  background: var(--neutral-50);
  border-radius: var(--radius-lg);
  padding: var(--spacing-md) var(--spacing-sm);
  text-align: center;
}

[data-theme="dark"] .stat-item {
  background: var(--neutral-700);
}

.quota-section {
  margin-top: var(--spacing-lg);
}

.quota-section h3 {
  font-size: var(--font-size-base);
  font-weight: 600;
  color: var(--text-primary);
  margin: 0 0 var(--spacing-xs);
}

.quota-note,
.stats-scope {
  font-size: var(--font-size-sm);
  /* 这两行是解释"这个数是什么意思"的，用 --text-muted 的话暗色下只有 2.6:1，
     正好在最需要看清的地方糊成一片 */
  color: var(--text-secondary);
  line-height: 1.6;
  margin: 0 0 var(--spacing-md);
}

.quota-item + .quota-item {
  margin-top: var(--spacing-md);
}

.quota-header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--spacing-sm);
  font-size: var(--font-size-sm);
  color: var(--text-secondary);
  margin-bottom: var(--spacing-xs);
}

.quota-header span:last-child {
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

.progress-bar {
  height: 8px;
  background: var(--neutral-100);
  border-radius: var(--radius-full);
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  width: 0;
  border-radius: var(--radius-full);
  background: linear-gradient(90deg, var(--primary-400), var(--primary-500));
  transition: width 0.3s ease;
}

[data-theme="dark"] .progress-bar {
  background: var(--neutral-700);
}

/* 没设上限就没有"用了多少"这回事，进度条直接不画 */
.quota-item.is-unlimited .progress-bar {
  display: none;
}

@media (max-width: 480px) {
  .stats-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  /* 人物卡片一行塞不下「头像 + 名字 + 编辑/删除」：按钮组 flex-shrink:0 占掉 108px，
     实测 320px 上名字那列被挤到 0 宽，管理列表只剩一串头像。让操作键掉到第二行。 */
  .character-card {
    flex-wrap: wrap;
  }

  .character-card .actions {
    flex: 0 0 100%;
    justify-content: flex-end;
  }
}

/* ==================== AI 记忆包面板 ==================== */
.memory-modal {
  max-width: 520px;
}

/* 我的收藏：列表行复用 .search-result-item 的观感，这里只管容器和取消收藏键 */
.favorites-modal {
  max-width: 560px;
}

.favorites-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-height: 320px;
  overflow-y: auto;
  padding: 8px;
  background: var(--neutral-50);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-lg);
}

[data-theme="dark"] .favorites-list {
  background: var(--neutral-700);
}

.favorites-empty {
  padding: var(--spacing-lg);
  text-align: center;
  font-size: var(--font-size-sm);
  color: var(--text-muted);
}

/* 收藏的每一行是「跳转」和「取消收藏」两个平级按钮：整行可聚焦可回车，
   取消收藏缩进一格，表示它属于上面那行而不是并列的第三行 */
.favorite-row {
  margin-bottom: 8px;
}

.favorite-row .search-result-item {
  margin-bottom: 0;
}

.favorite-row .favorite-remove {
  margin-left: 12px;
}

.favorite-remove {
  margin-top: 6px;
  padding: 4px 12px;
  border: 1px solid var(--border-color);
  border-radius: var(--radius-full);
  background: var(--bg-card);
  color: var(--text-secondary);
  font-size: var(--font-size-xs);
  cursor: pointer;
}

.favorite-remove:hover {
  border-color: var(--danger);
  color: var(--danger);
}

/* 快捷键帮助 */
.shortcuts-modal {
  max-width: 460px;
}

.shortcuts-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-height: 320px;
  overflow-y: auto;
}

.shortcut-row {
  display: flex;
  align-items: center;
  gap: var(--spacing-md);
  font-size: var(--font-size-sm);
  color: var(--text-secondary);
}

.shortcut-row kbd {
  flex-shrink: 0;
  min-width: 150px;
  padding: 4px 8px;
  border: 1px solid var(--border-color);
  border-bottom-width: 2px;
  border-radius: var(--radius-sm);
  background: var(--neutral-50);
  color: var(--text-primary);
  font-family: inherit;
  font-size: var(--font-size-xs);
}

[data-theme="dark"] .shortcut-row kbd {
  background: var(--neutral-700);
}

/* 全量导入的合并方式选择 */
.merge-modal {
  max-width: 460px;
}

.merge-options {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-sm);
}

.merge-option {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  padding: var(--spacing-md);
  border: 2px solid var(--border-color);
  border-radius: var(--radius-lg);
  background: var(--bg-card);
  text-align: left;
  cursor: pointer;
  transition: border-color var(--transition-fast), background var(--transition-fast);
}

.merge-option:hover {
  border-color: var(--primary-300);
  background: var(--primary-50);
}

.merge-option-title {
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--text-primary);
}

.merge-option-desc {
  font-size: var(--font-size-xs);
  color: var(--text-muted);
}

.memory-scope {
  font-size: 13px;
  font-weight: 600;
  color: var(--accent-text);
  margin-bottom: var(--spacing-md);
}

.memory-summary {
  background: var(--neutral-50);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-lg);
  padding: var(--spacing-md);
  font-size: 14px;
  line-height: 1.7;
  color: var(--text-primary);
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 240px;
  overflow-y: auto;
}

.memory-summary.is-empty {
  color: var(--text-muted);
  font-style: italic;
}

.memory-meta {
  margin-top: var(--spacing-sm);
  font-size: 12px;
  color: var(--text-muted);
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.memory-chip {
  background: var(--neutral-100);
  border-radius: var(--radius-sm);
  padding: 3px 8px;
}

[data-theme="dark"] .memory-chip {
  background: var(--neutral-700);
}

[data-theme="dark"] .memory-summary {
  background: var(--neutral-700);
}

/* 长期记忆清单：这一轮真的发给模型的那几条，内容来自 AI，前端只按 textContent 画 */
.memory-facts-title {
  margin: var(--spacing-md) 0 var(--spacing-sm);
  font-size: 13px;
  font-weight: 600;
  color: var(--text-secondary);
}

.memory-facts {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--spacing-sm);
  max-height: 220px;
  overflow-y: auto;
}

/* 卡片是两列：左边那三条（类别 / 正文 / 想起次数）占满弹性列，右边一格给「忘掉」。
   按钮塞进任何一条都会把「想起 3 次 · 最近 …」挤到下一行，读起来像它成了记忆的一个属性，
   所以它只能站在三行外面、竖着居中（第 101 轮单条删除）。 */
.memory-fact {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: auto auto auto;
  grid-template-areas:
    "head del"
    "body del"
    "meta del";
  gap: 2px var(--spacing-md);
  align-items: start;
  border: 1px solid var(--border-color);
  border-radius: var(--radius-lg);
  padding: var(--spacing-sm) var(--spacing-md);
  font-size: 13px;
  line-height: 1.6;
}

.memory-fact-head {
  grid-area: head;
  font-weight: 600;
  color: var(--text-primary);
}

.memory-fact-body {
  grid-area: body;
  color: var(--text-secondary);
  word-break: break-word;
}

.memory-fact-meta {
  grid-area: meta;
  font-size: 12px;
  color: var(--text-muted);
}

/* 和收藏面板那颗「取消收藏」同一套样子：默认不刺眼，指针碰上去才变危险色 */
.memory-fact-del {
  grid-area: del;
  align-self: center;
  justify-self: end;
  padding: 4px 12px;
  border: 1px solid var(--border-color);
  border-radius: var(--radius-full);
  background: var(--bg-card);
  color: var(--text-secondary);
  font-size: var(--font-size-xs);
  cursor: pointer;
}

.memory-fact-del:hover {
  border-color: var(--danger);
  color: var(--danger);
}

/* 服务端没带 id 的卡片删不动，这颗得看着就按不下去，而不是点了才报错 */
.memory-fact-del:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* 空态那张卡片没有按钮那一格，收成一列一行，免得三条行槽白占两个 gap */
.memory-fact.is-empty {
  grid-template-columns: 1fr;
  grid-template-rows: auto;
  grid-template-areas: "text";
  border-style: dashed;
  color: var(--text-muted);
  font-style: italic;
}

/* 模态框底部按钮：默认按钮没有背景，这里补齐可点击样式 */
.modal-footer button {
  border-radius: var(--radius-xl);
  padding: 8px 20px;
  font-weight: 500;
  transition: all 0.15s ease;
}

.modal-footer .danger {
  background: transparent;
  color: var(--danger);
  border: 1px solid var(--danger-outline);
}

.modal-footer .danger:hover {
  background: var(--danger-soft);
}

.modal-footer .danger:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* ==================== 页面内确认框 ==================== */
/* 第 14 轮：14 处原生 confirm() 换成 confirm-dialog.js 的 askConfirm()。 */

/* 确认框只有一句正文和两个按钮，比一般模态框窄一档，别让空白把警告撑散 */
.confirm-modal {
  width: min(90vw, 420px);
}

/* 正文里的 \n 是真换行（重置数据分两段警告），textContent 写进去要靠 pre-line 才留得住；
   用户名、昵称可能是一整串没有空格的拉丁字符，还得允许断行。 */
.confirm-body {
  white-space: pre-line;
  overflow-wrap: break-word;
  line-height: 1.6;
  color: var(--text-primary);
}

/* 全局 button 是 background:none，非危险的确认（备份、转发）在模态框里会退化成
   一行没有边界的文字，看不出是个按钮。 */
.modal-footer .confirm-ok {
  /* 渐变里最浅那一档也会直接压在白字上，所以整条降到 600→700：最差那端 5.70:1 */
  background: linear-gradient(135deg, var(--primary-600), var(--primary-700));
  color: #fff;
}

.modal-footer .confirm-ok:hover {
  filter: brightness(1.06);
}

/* 危险确认也走实心，只是换成红：权重比上面两条都高，才压得住 .modal-footer .danger 的描边 */
.modal-footer .confirm-ok.danger {
  background: var(--danger-solid);
  border: none;
  color: #fff;
}

.modal-footer .confirm-ok.danger:hover {
  background: var(--danger-solid);
  filter: brightness(1.08);
}

/* ==================== 编辑人物模态框 ==================== */
.edit-modal {
  max-width: 520px !important;
  max-height: 90vh !important;
  max-height: 90dvh !important;
}

/* 卡片的纵向 flex、头部固定和中间滚动现在由 .modal-content / .modal-body 统一负责，
   这里只保留它特有的底部按钮条（用的不是 .modal-footer 而是 .form-actions）。 */
.edit-modal .form-actions {
  flex-shrink: 0;
  padding: var(--spacing-lg);
  border-top: 1px solid var(--border-color);
  display: flex;
  justify-content: flex-end;
  gap: var(--spacing-md);
}

.edit-modal .form-group {
  margin-bottom: var(--spacing-lg);
}

.edit-modal .form-group label {
  display: block;
  font-size: var(--font-size-sm);
  font-weight: 500;
  color: var(--text-primary);
  margin-bottom: var(--spacing-sm);
}

/* AI头像编辑区域 */
.avatar-group {
  text-align: center;
}

.avatar-preview {
  width: 120px;
  height: 120px;
  border-radius: 50%;
  margin: 0 auto var(--spacing-lg);
  background: linear-gradient(135deg, var(--primary-600), var(--primary-800));
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  box-shadow: var(--shadow-md);
  cursor: pointer;
  transition: all var(--transition-normal);
  position: relative;
}

.avatar-preview:hover {
  transform: scale(1.05);
  box-shadow: var(--shadow-lg);
}

.avatar-preview .preview-placeholder {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  color: var(--text-white);
}

.avatar-preview .preview-placeholder span {
  font-size: 32px;
  margin-bottom: 4px;
}

.avatar-preview .preview-placeholder p {
  font-size: 12px;
  /* 从前这里带 opacity: .8。量尺只看 color，看不见整元素那层透明度，量出来 2.72:1；
     浏览器真量是 2.13:1——"点击上传头像"这句提示从来没读清过。渐变起点已经按第 57 轮
     那条口径写到 --primary-600（白字压它 5.70:1），这层透明度再压掉一档就没必要了。 */
  margin: 0;
}

.avatar-preview img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  position: absolute;
  top: 0;
  left: 0;
}

.avatar-options {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--spacing-md);
  align-items: center;
}

.upload-btn {
  padding: var(--spacing-sm) var(--spacing-lg);
  background: linear-gradient(135deg, var(--primary-600), var(--primary-700));
  color: var(--text-white);
  border-radius: var(--radius-md);
  font-size: var(--font-size-sm);
  cursor: pointer;
  transition: all var(--transition-fast);
  box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
}

.upload-btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 6px 16px rgba(139, 92, 246, 0.4);
}

/* 这颗键是个 <label>，而 .edit-modal .form-group label 把里面的 label 字色定成深色
   （特异性 0,2,1 压过 .upload-btn 的 0,1,0）：浏览器里量到深色字压紫底只有 2.57:1，
   上面那句 color: var(--text-white) 从来没画上过。按同一族写法把特异性抬到 0,2,1 并排在它后面。 */
.edit-modal label.upload-btn {
  color: var(--text-white);
}

.emoji-input-wrapper {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
  /* 表情候选是一整排 32px 的键，加起来 356px：不换行时这一项作为 flex 子项
     按 min-content 也不肯缩（min-width:auto），实测 320px 手机上它排到 444px，
     直接顶出模态框右缘 83px。 */
  flex-wrap: wrap;
}

.emoji-input-wrapper input {
  width: 80px;
  padding: var(--spacing-sm) var(--spacing-md);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md);
  font-size: 18px;
  text-align: center;
}

.emoji-suggestions {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
}

.emoji-btn {
  width: 32px;
  height: 32px;
  border-radius: var(--radius-sm);
  font-size: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: all var(--transition-fast);
  background: transparent;
  border: none;
}

.emoji-btn:hover {
  background: var(--neutral-100);
  transform: scale(1.2);
}

[data-theme="dark"] .emoji-btn:hover {
  background: var(--neutral-700);
}

.clear-btn {
  padding: var(--spacing-sm) var(--spacing-md);
  background: var(--neutral-100);
  color: var(--text-secondary);
  border-radius: var(--radius-md);
  font-size: var(--font-size-sm);
  cursor: pointer;
  transition: all var(--transition-fast);
  border: none;
}

.clear-btn:hover {
  background: var(--danger-soft);
  color: var(--danger);
}

[data-theme="dark"] .clear-btn {
  background: var(--neutral-700);
}

/* 高级设置 */
.advanced-settings {
  border-top: 1px solid var(--border-color);
  padding-top: var(--spacing-lg);
}

.advanced-toggle {
  width: 100%;
  padding: var(--spacing-sm);
  background: transparent;
  color: var(--text-secondary);
  font-size: var(--font-size-sm);
  cursor: pointer;
  transition: all var(--transition-fast);
  display: flex;
  align-items: center;
  justify-content: space-between;
  border: none;
}

.advanced-toggle:hover {
  color: var(--text-primary);
}

.param-group {
  margin-bottom: var(--spacing-md);
}

.param-group label {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: var(--font-size-sm) !important;
}

.param-value {
  font-weight: 600;
  color: var(--accent-text);
  font-family: monospace;
}

.param-group input[type="range"] {
  width: 100%;
  margin: var(--spacing-sm) 0;
}

.param-hint {
  font-size: 11px;
  color: var(--text-muted);
  margin: 0;
}

/* 人设有段落没塞进提示词时才会出现（默认 hidden）。
   这行不是 toast：toast 一闪就没了，写完的人根本不知道自己哪一段白写。 */
.persona-budget-note {
  font-size: 11px;
  line-height: 1.5;
  color: var(--danger);
  background: var(--danger-soft);
  border: 1px solid var(--danger-outline);
  border-radius: var(--radius-md);
  padding: 6px 8px;
  margin: 6px 0 0;
}

/* 分字段人设 / 当前状态：面板里一格一行。
   .form-group input 那条只管 input，textarea 以前全靠浏览器默认样式，
   所以这几格连同上面的人设整段一起补齐。 */
.field-grid {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-md);
  padding-top: var(--spacing-sm);
}

.field-row {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-xs);
}

.field-row label {
  font-size: var(--font-size-sm);
  font-weight: 500;
  color: var(--text-secondary);
}

.edit-modal textarea,
.field-row input[type="text"],
.field-row textarea {
  width: 100%;
  padding: var(--spacing-sm) var(--spacing-md);
  border: 1.5px solid var(--border-color);
  border-radius: var(--radius-lg);
  background: var(--bg-input);
  color: var(--text-primary);
  font-size: var(--font-size-sm);
  line-height: 1.5;
  transition: all var(--transition-fast);
}

.edit-modal textarea,
.field-row textarea {
  min-height: 54px;
  resize: vertical;
}

.edit-modal textarea:focus,
.field-row input[type="text"]:focus,
.field-row textarea:focus {
  border-color: var(--border-focus);
  box-shadow: var(--shadow-input-focus);
}

.edit-modal textarea::placeholder,
.field-row input::placeholder,
.field-row textarea::placeholder {
  color: var(--text-muted);
}

/* 表单操作按钮 */
.form-actions {
  display: flex;
  gap: var(--spacing-md);
  justify-content: flex-end;
  margin-top: var(--spacing-xl);
  padding-top: var(--spacing-lg);
  border-top: 1px solid var(--border-color);
}

.form-actions button {
  padding: var(--spacing-sm) var(--spacing-xl);
  border-radius: var(--radius-md);
  font-size: var(--font-size-base);
  font-weight: 500;
  cursor: pointer;
  transition: all var(--transition-fast);
  border: none;
}

.form-actions button[type="submit"] {
  background: linear-gradient(135deg, var(--primary-600), var(--primary-700));
  color: var(--text-white);
  box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
}

.form-actions button[type="submit"]:hover:not(:disabled) {
  transform: translateY(-2px);
  box-shadow: 0 6px 16px rgba(139, 92, 246, 0.4);
}

.form-actions button[type="button"] {
  background: var(--neutral-100);
  color: var(--text-secondary);
}

.form-actions button[type="button"]:hover {
  background: var(--neutral-200);
  color: var(--text-primary);
}

[data-theme="dark"] .form-actions button[type="button"] {
  background: var(--neutral-700);
}

[data-theme="dark"] .form-actions button[type="button"]:hover {
  background: var(--neutral-600);
}

/* 暗色模式下表单输入框文字颜色 */
[data-theme="dark"] .edit-modal input[type="text"],
[data-theme="dark"] .edit-modal textarea {
  color: var(--text-white) !important;
}

[data-theme="dark"] .edit-modal input[type="text"]::placeholder,
[data-theme="dark"] .edit-modal textarea::placeholder {
  color: var(--text-muted) !important;
}

/* ==================== 全局设置模态框 ==================== */
.settings-modal {
  max-width: 480px;
}

.settings-section {
  margin-bottom: var(--spacing-xl);
}

.settings-section:last-child {
  margin-bottom: 0;
}

.settings-section h3 {
  font-size: var(--font-size-base);
  font-weight: 600;
  color: var(--text-primary);
  margin-bottom: var(--spacing-md);
}

.settings-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--spacing-sm) 0;
}

.settings-row > label {
  font-size: var(--font-size-sm);
  color: var(--text-primary);
}

.settings-row select {
  padding: var(--spacing-sm) var(--spacing-md);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md);
  background: var(--bg-input);
  color: var(--text-primary);
  font-size: var(--font-size-sm);
  cursor: pointer;
  transition: all var(--transition-fast);
  min-width: 180px;
}

.settings-row select:focus {
  outline: none;
  border-color: var(--border-focus);
  box-shadow: var(--shadow-input-focus);
}

.settings-hint {
  font-size: 12px;
  color: var(--text-muted);
  margin-top: var(--spacing-xs);
}

/* 开关 */
.switch {
  position: relative;
  display: inline-block;
  width: 44px;
  height: 24px;
  flex-shrink: 0;
}

.switch input {
  opacity: 0;
  width: 0;
  height: 0;
}

.switch-slider {
  position: absolute;
  inset: 0;
  background: var(--neutral-300);
  border-radius: 24px;
  cursor: pointer;
  transition: background 0.2s ease;
}

.switch-slider::before {
  content: '';
  position: absolute;
  width: 18px;
  height: 18px;
  left: 3px;
  top: 3px;
  background: #fff;
  border-radius: 50%;
  transition: transform 0.2s ease;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}

.switch input:checked + .switch-slider {
  background: var(--primary-500);
}

.switch input:checked + .switch-slider::before {
  transform: translateX(20px);
}

/* ==================== 群组与好友管理面板 ==================== */
/* 第 48 轮。scripts/dead-markup.js 反向对账（"这个类名有没有人给它写样式"）在 index.html 里
   查出几个名字：元素上贴着类，样式表却一条规则都没写过。浏览器实测它们只剩全局那条
   `button { border: none; background: none }`——于是「解散群组」和「+ 新增好友」画成两行黑字，
   勾中一个成员除了那个勾之外没有任何反馈，「立即刷新」是一团带内边距的透明。
   下面补的都是那个名字本来就写着的意思，全部用现成 Token，不引入新的视觉概念。 */

/* 成员选择区：以前是个既不滚也不画边的裸 div，人物一多就把模态框顶穿 */
.member-list {
  max-height: 240px;
  overflow-y: auto;
  padding: var(--spacing-xs);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-sm);
}

/* 44px 是热区底线，写在这条基本规则里而不是最后一个 @media (hover: none) 段：
   这一行本来就带着 40px 头像，桌面端也该有这个高度，不是"触屏才要"的补丁 */
.member-item {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
  min-height: 44px;
  padding: var(--spacing-xs) var(--spacing-sm);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background var(--transition-fast);
}

.member-item:hover {
  background: var(--bg-page);
}

/* groups.js 一直在 toggle('selected')，只是没人画它：勾中与不勾以前看不出区别 */
.member-item.selected {
  background: var(--primary-50);
}

[data-theme="dark"] .member-item.selected {
  background: rgba(139, 92, 246, 0.15);
}

/* 表单底下那行辅助说明：以前和正文同色同粗，读起来像上一行标签的尾巴。
   取 --text-secondary 而不是 .settings-hint 那套 --text-muted——后者压在白卡上约 2.8:1，小字够不到 AA */
.hint {
  color: var(--text-secondary);
  margin-top: var(--spacing-xs);
}

/* 解散群组：整个面板唯一不可逆的操作，以前没有边框、没有底色、没有内边距。
   配色照 .modal-footer .danger 那三条（描边 + 悬停淡红 + 停用半透），不另立危险色。
   同一笔欠账见第 15 节第 4 条：--danger 压在 --bg-card 上约 3.8:1，够不到 AA 正文的 4.5:1，
   要修得连着全站所有 color: var(--danger) 一起过，这里不自造新 Token。 */
.danger-btn {
  padding: var(--spacing-sm) var(--spacing-md);
  border: 1px solid var(--danger-outline);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--danger);
  font-size: var(--font-size-sm);
  cursor: pointer;
  transition: background var(--transition-fast);
}

.danger-btn:hover {
  background: var(--danger-soft);
}

.danger-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* 「+ 新增好友」：虚线空格子——它是个动作，不是一张人物卡，所以刻意不像卡片 */
.add-btn {
  display: block;
  width: 100%;
  margin-top: var(--spacing-md);
  padding: var(--spacing-sm) var(--spacing-md);
  border: 1px dashed var(--border-color);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-secondary);
  font-size: var(--font-size-base);
  cursor: pointer;
  transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
}

.add-btn:hover {
  border-color: var(--border-focus);
  background: var(--bg-page);
  color: var(--text-primary);
}

.add-btn:focus-visible {
  outline: 2px solid var(--border-focus);
  outline-offset: 2px;
}

/* 版本更新框底部那两个按钮：.form-actions 的底色是按 type= 属性配的，而这两个按钮在 form 之外、
   没写 type，于是只拿到内边距、拿不到颜色。两条都补上 type="button" 之后：
   #remindLater 直接归给既有的中性规则，主操作由下面这条接住。
   选择器里那个 [type="button"] 不是冗余：上面 [data-theme="dark"] .form-actions button[type="button"]
   的特异度是 (0,3,1)，写成 .form-actions button.primary-btn 只有 (0,2,1)，暗色下主按钮会被刷成灰底。
   同分看先后，这条在它后面才压得住——去掉属性或去掉按钮上的 type，暗色主题就回退。 */
.form-actions button[type="button"].primary-btn {
  background: linear-gradient(135deg, var(--primary-600), var(--primary-700));
  color: var(--text-white);
  box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
}

.form-actions button[type="button"].primary-btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 6px 16px rgba(139, 92, 246, 0.4);
}

/* 版本信息两行：以前标签和值只差一个空格，读起来像"当前版本1.17.0" */
.version-info {
  padding: var(--spacing-sm) var(--spacing-md);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-sm);
}

.version-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--spacing-md);
  padding: var(--spacing-xs) 0;
}

.version-row + .version-row {
  border-top: 1px solid var(--border-color);
}

.version-row span:last-child {
  color: var(--text-secondary);
}

/* 更新内容：UA 那个 700/15.2px 的 h3 和正文同一色，收成家规的 600/14px，正文转灰并放宽行高 */
.changelog {
  margin-top: var(--spacing-md);
}

.changelog h3 {
  margin-bottom: var(--spacing-xs);
  font-size: var(--font-size-base);
  font-weight: 600;
}

.changelog p {
  color: var(--text-secondary);
  line-height: var(--line-height-relaxed);
}

/* ==================== 字体大小档位 ==================== */
body.font-small .message-content {
  font-size: 13px;
}

body.font-medium .message-content {
  font-size: var(--font-size-lg);
}

body.font-large .message-content {
  font-size: 17px;
  line-height: 1.7;
}

/* ==================== 流式打字机光标 ==================== */
.message-content.streaming::after {
  content: '';
  display: inline-block;
  width: 2px;
  height: 1em;
  margin-left: 2px;
  vertical-align: text-bottom;
  background: var(--primary-500);
  border-radius: 1px;
  animation: cursorBlink 0.8s ease-in-out infinite;
}

@keyframes cursorBlink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0; }
}

/* ==================== 消息转发选择器 ==================== */
.forward-selector {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 320px;
  max-width: calc(100vw - 32px);
  /* 卡片限高、只有列表自己滚，和 .modal-content 同一套路。原来没有 max-height：
     实测 640×320 横屏上卡片 359px 高，居中后 top = -19，标题和 × 一起跑到屏幕上沿外面，
     转发面板根本关不掉；740×360 上最后一个联系人也被 overflow: hidden 裁掉半截。 */
  max-height: calc(100dvh - 16px);
  display: flex;
  flex-direction: column;
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  z-index: 1000;
}

.forward-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--spacing-md);
  border-bottom: 1px solid var(--border-color);
  flex-shrink: 0;
}

.forward-header h3 {
  font-size: var(--font-size-base);
  font-weight: 600;
  color: var(--text-primary);
  margin: 0;
}

.forward-header .close-btn {
  width: 28px;
  height: 28px;
  border-radius: var(--radius-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  color: var(--text-muted);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  transition: all 0.15s ease;
}

.forward-header .close-btn:hover {
  /* 全站这一族（.footer-btn / .modal-header .close-btn / .chat-input-area textarea）都各配了一条
     [data-theme="dark"] 覆盖，只有这一条漏了——暗色下悬停闪一块近白、图标 1.05:1 直接看不见。
     改用第 56 轮的 --bg-hover 就不用再靠"记得补一条暗色覆盖"。 */
  background: var(--bg-hover);
  color: var(--text-primary);
}

.forward-list {
  max-height: 280px;
  overflow-y: auto;
  padding: var(--spacing-sm);
  /* 卡片限高后这一栏是被压缩的那个：min-height: 0 才允许它缩到内容高度以下去滚，
     flex-basis 用 auto 而不是 0，免得自动高度的容器把列表算成 0 高。 */
  flex: 1 1 auto;
  min-height: 0;
}

.forward-item {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
  padding: var(--spacing-sm) var(--spacing-md);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: all 0.15s ease;
  background: transparent;
}

/* 行是 button 了：Chrome 会把按钮缩到内容宽，一排名字就参差成几块小饼干。
   整行可点还得整行看起来可点，所以宽度交给容器、文字左对齐。 */
button.forward-item {
  width: 100%;
  border: none;
  font-family: inherit;
  color: inherit;
}

/* 列表是 overflow-y:auto 的，环画在行外面会被裁掉一截，所以画在里面 */
.forward-item:focus-visible {
  outline: 2px solid var(--border-focus);
  outline-offset: -2px;
}

.forward-item:hover {
  background: var(--primary-50);
}

.forward-item .avatar {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}

.forward-item .name {
  font-size: var(--font-size-base);
  color: var(--text-primary);
}

.forward-empty {
  padding: var(--spacing-lg);
  text-align: center;
  font-size: var(--font-size-sm);
  color: var(--text-muted);
}

/* ==================== 消息内容渲染（最小 Markdown） ==================== */
.md-line {
  margin: 0 0 2px;
}

.md-list-item {
  padding-left: 4px;
}

.inline-code {
  padding: 1px 5px;
  border-radius: 4px;
  background: var(--neutral-100, rgba(0, 0, 0, 0.06));
  border: 1px solid var(--border-color);
  font-family: 'JetBrains Mono', Consolas, Monaco, monospace;
  font-size: 0.92em;
  word-break: break-all;
}

.code-block {
  margin: 8px 0;
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md, 8px);
  background: var(--neutral-50, rgba(0, 0, 0, 0.03));
  overflow: hidden;
  max-width: 100%;
}

.code-block-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 4px 8px;
  border-bottom: 1px solid var(--border-color);
  background: var(--neutral-100, rgba(0, 0, 0, 0.05));
  font-size: 11px;
  color: var(--text-muted);
}

.code-block-header span {
  font-family: 'JetBrains Mono', Consolas, Monaco, monospace;
  letter-spacing: 0.02em;
}

.code-copy-btn {
  padding: 2px 8px;
  border: 1px solid var(--border-color);
  border-radius: 6px;
  background: var(--bg-card);
  color: var(--text-secondary);
  font-size: 11px;
  cursor: pointer;
  transition: all var(--transition-fast);
}

.code-copy-btn:hover {
  color: var(--primary-600);
  border-color: var(--primary-200);
}

/* 悬停只改字色不改底，底还是本条那颗 --bg-card——暗色压上去 1.82:1，换 300 是 5.61:1（第 63 轮） */
[data-theme="dark"] .code-copy-btn:hover {
  color: var(--primary-300);
}

.code-block pre {
  margin: 0;
  padding: 10px 12px;
  overflow-x: auto;
  max-width: 100%;
}

.code-block code {
  font-family: 'JetBrains Mono', Consolas, Monaco, monospace;
  font-size: 12.5px;
  line-height: 1.55;
  white-space: pre;
  color: var(--text-primary);
}

.message-content a {
  color: var(--accent-text);
  text-decoration: underline;
  word-break: break-all;
}

/* 用户自己那句气泡是 --primary-600 起步的渐变底，链接再写 --primary-600 就是同一条颜色压在
   自己身上——级联感知量出来 1.00:1，等于发的消息里链接整条看不见。跟着气泡正文的白字走，
   靠上面那条 underline 认它。 */
.message.user .message-content a {
  color: inherit;
}

/* 向上翻页控件 */
.load-earlier {
  display: block;
  width: auto;
  margin: 4px auto 12px;
  padding: 6px 14px;
  border: 1px solid var(--border-color);
  border-radius: 999px;
  background: var(--bg-card);
  color: var(--text-secondary);
  font-size: 12px;
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: all var(--transition-fast);
}

.load-earlier:hover:not(:disabled) {
  color: var(--primary-600);
  border-color: var(--primary-200);
}

/* 同一族：悬停只改字色，底还是那颗 --bg-card——暗色 1.82:1，换 300 是 5.61:1（第 63 轮） */
[data-theme="dark"] .load-earlier:hover:not(:disabled) {
  color: var(--primary-300);
}

.load-earlier:disabled {
  opacity: 0.6;
  cursor: default;
}

/* 按天分组的时间分隔线 */
.day-divider {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
  /* 聊天区是纵向 flex，横向 auto 外边距会让盒子退回内容宽度、两侧横线画不出来，
     所以这里给死宽度再用 align-self 居中 */
  align-self: center;
  width: min(100%, 420px);
  margin: var(--spacing-lg) 0 var(--spacing-md);
  padding: 0 4px;
  color: var(--text-secondary);
  font-size: var(--font-size-xs);
  letter-spacing: 0.04em;
  user-select: none;
}

.day-divider::before,
.day-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border-color);
}

[data-theme="dark"] .day-divider::before,
[data-theme="dark"] .day-divider::after {
  background: var(--neutral-700);
}

/* 聊天区空状态与失败重试条：都靠 auto 外边距在消息区居中 */
.conversation-empty {
  align-self: center;
  margin: auto;
  padding: var(--spacing-lg);
  color: var(--text-secondary);
  font-size: var(--font-size-sm);
  text-align: center;
}

.conversation-error {
  display: flex;
  align-items: center;
  gap: var(--spacing-md);
  align-self: center;
  margin: auto;
  padding: var(--spacing-md) var(--spacing-lg);
  border: 1px solid var(--danger-outline);
  border-radius: var(--radius-lg);
  background: var(--danger-soft);
  color: var(--danger);
  font-size: var(--font-size-sm);
}

.conversation-error-retry {
  padding: 4px 12px;
  border: 1px solid var(--danger-outline);
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--danger);
  font-size: var(--font-size-sm);
  cursor: pointer;
  transition: background var(--transition-fast);
}

.conversation-error-retry:hover {
  background: var(--danger-soft);
}

.conversation-error-retry:focus-visible {
  outline: 2px solid var(--danger);
  outline-offset: 2px;
}

[data-theme="dark"] .code-block {
  background: rgba(0, 0, 0, 0.28);
}

[data-theme="dark"] .code-block-header {
  background: rgba(0, 0, 0, 0.36);
}

[data-theme="dark"] .inline-code {
  background: rgba(255, 255, 255, 0.08);
}

/* ==================== 窄屏抽屉侧栏与菜单分组 ==================== */
.sidebar-toggle {
  display: none;
}

/* 抽屉的关闭键：宽屏侧栏常驻，这一颗没有存在的理由，只在下面的抽屉模式里出现 */
.sidebar-close {
  display: none;
}

.sidebar-scrim {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(15, 23, 42, 0.45);
  z-index: 880;
}

.menu-group-label {
  margin: var(--spacing-xs) var(--spacing-md) 2px;
  font-size: var(--font-size-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

.menu-group-label.danger {
  color: var(--danger);
}

/* 只在窄屏出现的菜单分组。基础规则写在 .menu-item（靠前的组件样式）之后，
   展开规则放进下面同一个媒体查询里，两条同权重靠位置分胜负，不会互相盖掉。 */
.narrow-only {
  display: none;
}

@media (max-width: 768px) {
  .sidebar-toggle {
    display: flex;
  }

  /* 抽屉模式下这颗 × 是唯一说得清「把这一片收回去」的东西。上面那条 @media (max-width:480px)
     把抽屉改成 width:100%，整片屏幕被它自己盖住、遮罩再也按不到，而手机键盘上没有 Escape
     （sidebar.js 那条 Escape 只对真键盘有效）——拉开之后就剩这一条收回去的路。
     481–768 那一段抽屉是 280px、遮罩还在，× 也一并留着：同一件事不该有两种收法。 */
  .sidebar-close {
    display: flex;
    width: 40px;
    height: 40px;
    flex-shrink: 0;
    align-items: center;
    justify-content: center;
    border: none;
    border-radius: var(--radius-md);
    background: transparent;
    color: var(--text-muted);
    font-size: var(--font-size-xl);
    line-height: 1;
    cursor: pointer;
  }

  .sidebar-close:hover {
    /* 和快捷回复面板那颗 × 同一族：--neutral-100 在暗色下会闪一块近白 */
    background: var(--bg-hover);
    color: var(--text-primary);
  }

  .sidebar-scrim.is-visible {
    display: block;
  }

  .sidebar.open {
    box-shadow: var(--shadow-lg);
  }

  .narrow-only {
    display: block;
  }

  /* 手机头部要同时塞下头像、标题、服务状态和一排图标：实测 320px 下 6 个图标
     就占掉 212px，标题被挤成 0 宽，用户根本看不出在和谁聊。通知/统计/记忆收进
     「更多」菜单，头部只留列表、搜索、更多三个键。 */
  .chat-actions > #notificationBtn,
  .chat-actions > #statsBtn,
  .chat-actions > #memoryBtn,
  .chat-actions > #favoritesBtn {
    display: none;
  }
}

/* 320~375px：头部这一排要挤下头像、标题、服务状态和图标，实测标题只剩 28~42px，
   触屏上（图标撑到 40px）连一个完整的角色名都放不下。状态点留着，离线时还看得出，
   只收起「百炼 / DeepSeek」这段文字，把宽度还给标题。 */
@media (max-width: 480px) {
  .api-label {
    display: none;
  }

  /* 带代码的那条消息在手机上让气泡吃满整行：实测 320 屏代码可视宽只有 154px、375 屏 177px，
     一屏装得下十来个字符，等于没法读。吃满整行不等于不管别的——头像那一列照旧占位，
     超长行仍由 .code-block pre 自己横向滚（min-width:0 那句守着，不许撑破屏幕）。
     群聊那一版宽度不是卡在同一层：外层 .message-content 带着 calc(85% - 28px - sm)，
     光放气泡它照旧只有 172px，所以两条一起放。 */
  .message.has-code .message-body,
  .message.group.has-code .message-content {
    max-width: 100%;
  }
}

/* 上一条要给带鼠标的设备留回 😊 那颗的走道（28 宽 + 那侧 4px 偏移 = 32px）：触屏上它本来
   就是 opacity:0 + pointer-events:none（第 29 轮那条），留走道是白留；有鼠标时不留的话
   单聊那条的 😊（z-index:5）正好压在代码块右上角那颗「复制」上，鼠标按下去弹的是表情选择器。
   窄窗口拖到 480 以下也算这一类。写成平行的 and 而不是嵌套 @media——嵌套要 Chrome 112 起，
   微信内置浏览器这类滞后内核会整块读不到。 */
@media (max-width: 480px) and (hover: hover) {
  .message.has-code .message-body,
  .message.group.has-code .message-content {
    max-width: calc(100% - 32px);
  }
}

/* -------------------------- */
/* 三端适配收尾               */
/* -------------------------- */

/* 下面两组必须留在文件末尾：表情格、快捷回复列表、上下文菜单这些组件样式都定义在
   靠后的位置，写在移动端区块里会被同权重、更晚出现的组件规则覆盖掉
   （实测 740×360 时 max-height 完全不生效，浮层直接弹到屏幕外）。 */

/* 触屏热区。按 (hover: none) 判定而不是按宽度：横版平板同样用手指点，
   但它不该被拖进手机窄屏的字号与布局里。
   .toolbar-btn 不在这儿——它带 .btn-icon 已经是 40px，写进来反而会把尺寸压回去。 */
@media (hover: none) {
  .action-btn {
    width: 40px;
    height: 40px;
  }

  .context-menu button {
    min-height: 44px;
    padding: 12px;
  }

  .emoji-grid {
    /* 触屏上格子加高到 40px，面板高度跟着行数走，不会露出半行 */
    --emoji-row: 40px;
  }

  .quick-reply-item {
    min-height: 44px;
  }

  /* 删除键只有 26px，手指按不准 */
  .quick-reply-row .reply-remove {
    width: 40px;
    height: 40px;
  }

  /* 引用栏的 × 实测只有 9×16px，手指按下去多半点在正文上，取消引用就成了点不到的功能 */
  .quote-remove {
    width: 40px;
    height: 40px;
  }

  /* 编辑框的保存/取消是触屏上唯一的出路（没有 Esc），按不准就等于改完出不去 */
  .message-edit-btn {
    min-height: 44px;
    padding: 8px 18px;
  }

  .qr-actions button {
    min-height: 40px;
  }

  /* 收藏面板的「取消收藏」原本只有 26px 高，手指按不准就会点整行跳到消息上 */
  .favorite-remove {
    min-height: 40px;
  }

  /* 记忆卡片那颗「忘掉」是这条记忆唯一的删除入口，按不准就会点到卡片正文上 */
  .memory-fact-del {
    min-height: 40px;
    padding: 8px 14px;
  }

  /* 跨会话结果那行会话名 chip 只有 22px，手指点不准就会误跳到第一条消息 */
  .search-tally-chip {
    min-height: 40px;
  }

  /* 代码块右上角那颗「复制」实测 39×21，是这块代码唯一的复制入口
     （长按菜单那颗「复制消息」抄的是整条消息，含上面的散文）。按不准就落在代码区，
     而代码区自己横向滚动，误按还会带走一段选区。 */
  .code-copy-btn {
    min-height: 40px;
    padding: 8px 14px;
  }

  /* 登录/注册切换只有 8px 上下留白，手指点不准；输入框和提交键本身已超 44px */
  .auth-tab {
    min-height: 44px;
  }

  .image-preview-modal .close-btn {
    width: 44px;
    height: 44px;
  }

  /* 表态胶囊原本 26px 高，手指按不准就会滑到旁边的气泡上 */
  .reaction-item {
    min-height: 36px;
    padding: 4px 10px;
  }

  /* 表情选择器是手机上唯一能加表情的路（长按菜单 → 表情反应），浮层里 36px 的格子
     手指按偏就点到下一个表情上。格子跟着放大，列宽要用同一个数重写一遍：
     grid 的列轨道不吃子元素的 width，只改 .reaction-option 会让 36px 的轨道塞不下 40px 的按钮。
     放大后整块 284px（6×40 + 5×4 间距 + 24 内边距），320px 的屏还放得下。 */
  .reaction-list {
    grid-template-columns: repeat(6, 40px);
  }

  .reaction-option {
    width: 40px;
    height: 40px;
  }

  /* 缩略图那颗 :hover 的放大是给鼠标的：手指点完预览关掉，图还赖在 1.02 倍不走
     （弹层自己每次销毁，所以里面的 hover 不留后患，只有留在页面上的这张图要管）。
     按下时缩一点，手指才知道这一下落在图上。 */
  .message-image:hover {
    transform: none;
  }

  button.image-open-btn:active .message-image {
    transform: scale(0.97);
  }

  /* 抽屉式侧栏是手机上唯一的导航入口，可里面的控件全都低于 44px：好友/群聊 tab
     实测 35px、底部一排 34px、管理按钮 36px、群条目的编辑笔只有 26px。
     tab 和底部按钮只补高度（宽度是 flex: 1，不受影响），编辑笔在 72px 的行里放得下。 */
  .tab-btn,
  .footer-btn,
  .logout-btn {
    min-height: 44px;
  }

  .manage-btn {
    width: 44px;
    height: 44px;
  }

  /* 抽屉唯一的关闭入口，和并排那颗管理键同高，手指别按到隔壁去 */
  .sidebar-close {
    width: 44px;
    height: 44px;
  }

  .group-edit-btn {
    width: 40px;
    height: 40px;
  }

  /* 窄屏把通知/统计/记忆折叠进「更多」菜单之后，菜单成了这些功能的唯一入口，
     35px 的行高手指点不准。菜单自己带 overflow-y: auto，加高也不会顶出屏幕。 */
  .menu-item {
    min-height: 44px;
  }

  /* 十个模态框的关闭键（桌面 32px）、快捷回复面板的 ×（28px）、转发面板的 ×（也是 28px）
     和「+ 添加快捷回复」（实测 37px 高）在手机上都按不准。 */
  .modal-header .close-btn,
  .search-panel-header .close-btn,
  .forward-header .close-btn {
    width: 44px;
    height: 44px;
  }

  /* 确认框靠两个按钮分辨「取消」还是「永久删除」，36px 的按钮手指一滑就会点错。 */
  .modal-footer button {
    min-height: 44px;
  }

  .quick-reply-panel .panel-header .close-btn {
    width: 40px;
    height: 40px;
  }

  .quick-reply-panel .panel-footer button {
    min-height: 44px;
  }

  /* @选择器实测只有 24px 高（padding 上下各 4px + 12px 字号），是群聊里唯一的
     指派入口，手指按不准 */
  .mention-selector select {
    min-height: 44px;
  }

  /* 触屏上 :hover 会粘在最后点过的那一下不放，反馈交给 :active。
     静息态是透明底（.btn-icon 的定义比 .toolbar-btn 的淡底晚，是它赢），所以复位成透明 */
  .toolbar-btn:hover,
  .action-btn:hover {
    background: transparent;
    color: var(--text-secondary);
  }

  /* 暗色主题的原规则带属性选择器，这里得同样带上才压得住 */
  [data-theme="dark"] .toolbar-btn:hover,
  [data-theme="dark"] .action-btn:hover {
    background: var(--neutral-700);
    color: var(--text-secondary);
  }
}

/* 横屏手机：整块视口只剩三百来像素高，头部、输入区、向上弹的浮层都要让位，
   否则输入区上方连一个面板都放不下。 */
@media (max-height: 560px) {
  .chat-header {
    height: 48px;
  }

  .chat-messages {
    padding-top: var(--spacing-sm);
    padding-bottom: var(--spacing-sm);
  }

  .chat-input-area {
    padding-top: var(--spacing-xs);
    padding-bottom: calc(var(--spacing-xs) + env(safe-area-inset-bottom));
  }

  .chat-input-area textarea {
    max-height: 64px;
  }

  .emoji-picker,
  .quick-reply-panel {
    bottom: calc(100% + 4px);
  }

  .emoji-grid {
    /* 矮屏（横屏手机）只留两整行。从前写死 96px，量出来配 32px 的行是两整行加第三行的 12px，
       配触屏那 40px 的行是一整行加第二行的 38px——两档都不是整行数，所以永远有一行被切。 */
    --emoji-rows: 2;
  }

  .quick-reply-panel .panel-header,
  .quick-reply-panel .panel-footer {
    padding: var(--spacing-xs) var(--spacing-sm);
  }

  .quick-reply-list {
    max-height: 120px;
  }
}
