/* =============================================================================
 * components.css — freechat 组件层样式
 * owner：W4-components（本文件由该席独占，其它席不得改）
 *
 * 真源（两份冻结文档；两者冲突时以 ui-contract.md 为准并在回报里点名）：
 *   ① apps/freechat/docs/ui-contract.md      §3 DOM 契约 / §0 U2「只用 token」
 *   ② docs/design/ui-tokens.md               §6 组件清单 / §2–§5 取值阶梯
 *
 * 本文件硬约束（复跑命令见文件末尾注释）：
 *   R1 零 hex、零 px 字面量：颜色与尺寸一律走 var(--fc-*)；连 1 物理像素的描边也写成
 *      var(--fc-hairline)（ui-tokens §8.4 明确把 --fc-hairline 与 --fc-radius-full 列为
 *      允许值，即"非字面量"路径）。因此本文件里不存在任何 `NNpx`。
 *   R2 组件只引用语义层 token（ui-tokens §8.1）。唯一例外是开关"关"态的
 *      var(--fc-gray-5) —— 那是 §6.8 Switch 行逐字指定的取值。
 *   R3 深色主题不写死浅色值：本文件不含任何 [data-theme="dark"] 覆盖块，
 *      深浅差异全部由 tokens.css 对语义层的覆盖承担 ⇒ 组件层两主题共用同一套规则。
 *   R4 焦点可见：全局 :focus-visible 一定带 outline（见 §2），本文件里不存在把 outline 关掉的写法。
 *   R5 零外部资源：无 at-import 样式导入语句、无外链、无字体文件、无外链图片（ui-contract U1）。
 *
 * 组件 class 词表（W6/W7/W8 生成 DOM 时按此对齐）：
 *   契约硬钩子（ui-contract §3，不可改名）：#app[data-view] / #rail / .rail__item[data-nav] /
 *     #list-pane / #list-header / #conversation-list / #content-pane / #content-header /
 *     #message-flow / #composer / #search-overlay[hidden] /
 *     data-testid="chat-item|bubble|send|empty-state" / data-side="in|out" / data-kind="image|file"
 *   本文件新增（建议对齐；凡带 data-testid 的都有"类名 or 属性"两条路径，W6 任选其一）：
 *     [data-testid="avatar"]（.avatar[data-size="sm|md|lg"][data-shape="single|group"]）/
 *     [data-testid="badge"]（.badge[--muted]）/
 *     .chat-item__main|__title|__preview|__meta|__time|__flag /
 *     .msg[data-side][data-grouped][data-status] > .msg__bubbles > [data-testid="bubble"] /
 *     .bubble__text|__meta|__quote|__status|__edited /
 *     [data-testid="time-separator"]（.time-sep）/ [data-testid="system-message"] /
 *     .attachment-grid / [data-kind="file"] / .attachment__name|__size /
 *     .composer__input / .composer__toolbar / [data-testid="local-only-badge"]（.composer__local-badge）/
 *     .search-panel / .search-field / .search-results / [data-testid="search-result"] /
 *     .empty-state__art|__title|__desc|__actions /
 *     [data-testid="card"]（.card）/ .card__title|__row|__label|__value|__hint /
 *     .btn[--primary|--text|--icon|--danger] / .switch /
 *     [role="menu"] > [role="menuitem"] / .progress > .progress__fill
 *
 * 取值推导表（本文件所有 calc / 百分比的来源，全部由既有 token 算出，不含字面量）：
 *   2    = var(--fc-hairline) * 2   … §5.6「2 像素焦点环」、§6.1/§6.2/§6.3「2 像素指示条」
 *   96   = var(--fc-space-48) * 2                   … §6.7 空态自绘插图 96×96
 *   168  = var(--fc-space-24) * 7                   … §6.4 文本区 1–7 行（24–168）后滚动
 *   320  = var(--fc-space-64) * 5                   … §6.3 附件图片最大高 320
 *   360  = var(--fc-space-64) * 5 + var(--fc-space-40)  … §6.7 空态说明最大宽 360
 *   12   = var(--fc-hit-min) - var(--fc-space-16) - var(--fc-hairline) * 4  … §6.8 开关滑块行程
 *   44   = var(--fc-space-40) + var(--fc-space-4)  … 触控命中下限（WCAG 2.5.5 Target Size）。
 *          §3 按钮 / §6 rail 项 / §13 搜索框 / §15 设置行 / §17 菜单项 / §19 闸门输入与勾选行共用；
 *          开关（§16）与文本区（§12）另按各自形状表达（伪元素外扩 / 等量内边距居中）。
 *          （32 轨道 − 16 滑块 − 2×1 描边 − 2×1 内衬；描边见 §16 注释：关态填充色对 surface 不足 3:1）
 *   72%  = §2.4 气泡宽 min(560, 72%)；12% = §6.7 空态上移；88% = 系统消息窄容器兜底
 *   字号/行高/字重：一律取 tokens.css 的 --fc-font-<档>-size / -line-height / -weight 三元组
 *     （即 §4.2 阶梯的三个列位），不自己算行高、不写 400/500/600 字面量；
 *     唯一例外是 §6.2 的"未读会话标题 600"——§4.2 阶梯里没有 600 的 label 档，故显式写 600 并注明。
 *   z-index：内容 0 / 粘性头 10 / 下拉 100 / 遮罩 200 / 模态 300 / 提示条 400（§5.6，纯整数）
 *
 * 已核对的接口事实（2026-09-29 03:3x 读 tokens.css / themes.css 当次内容）：
 *   · tokens.css 给数值型 token 补了 px 单位（--fc-hairline 为 1 个物理像素、--fc-space-* 为栅格值），
 *     所以本文件的 border / calc(var(--fc-hairline) * 2) 都在两种主题下成立。
 *   · --fc-elevation-1/2/3 已在 **tokens.css（默认浅色）** 与 themes.css（dark 覆盖）两侧都有定义，
 *     浅色/深色下 box-shadow 均生效（2026-09-29 修复：此前浅色侧漏生成，见
 *     docs/design/verify/V3-shell-recheck.md 问题 1 与 tools/gen-tokens-css.py 的分作用域闭包腿）。
 * ========================================================================== */

/* ---------------------------------------------------------------------------
 * 0. `[hidden]` 的**通用**兜底（2026-09-30 实撞事故的修复）
 *
 * 为什么必须有这一条：`hidden` 属性靠 UA 样式表里的 `[hidden] { display: none }` 生效，
 * 它与作者样式里的 `.notice { display: flex }` / `.btn { display: inline-flex }` **特异性相同**，
 * 而"作者样式胜出"⇒ 只要给元素加了会设 `display` 的类，`hidden` 就**失效**（元素照样显示）。
 * 实测事故：`#login-continue[hidden]`（"以未接入方式继续"）在**所有**平台的登录页上常驻可见，
 * 点一下就绕过解锁闸门 —— 那不是显示瑕疵，是闸门失效；`#login-error[hidden]` / `#login-status[hidden]`
 * 同样变成两个常驻空框。
 *
 * 为什么通用兜底 > 逐个列 id（本仓既有写法见 layout.css §6）：
 *   1. 逐个列 id 是**枚举**——新增一个带 `hidden` 的元素就重新破一个洞，且破了**没有任何征兆**；
 *      通用规则对所有现在与将来的 `[hidden]` 元素一次成立（本次事故就是这么来的：`#app`/`#login-gate`
 *      列了，闸门里那三个漏了）。
 *   2. `!important` 在这里是**语义正确**的：`hidden` 是"这个元素此刻不该存在"的**状态声明**，
 *      不是一条普通样式偏好；让它无条件胜过组件外观是它该有的优先级。
 *   3. 代价可控：本文件（以及全仓 UI）没有任何"明知 hidden 却要显示"的用法 ⇒ 不会误伤。
 *   4. 天花板（如实写）：本规则只压 `display`。若哪天有人用 `visibility`/`opacity`/`clip` 自己实现隐藏，
 *      本规则不管 —— 那种情况要用**计算样式断言**去抓（见 tools 侧登录闸门腿），别拿"我写了 !important"当保证。
 * ------------------------------------------------------------------------- */
[hidden] {
  display: none !important;
}

/* ---------------------------------------------------------------------------
 * 1. 基线：#app 上的排版默认值（字体栈来自 ui-tokens §4.1 的白名单，非外链字体）
 * ------------------------------------------------------------------------- */
#app {
  font-family: "Inter", "Source Han Sans SC", "Noto Sans CJK SC", system-ui, sans-serif;
  color: var(--fc-text-primary);
}

/* 等宽（代码块 / 消息里的路径）：§4.1 等宽栈，不引外部字体 */
.mono,
code,
kbd,
samp {
  font-family: "JetBrains Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;
}

/* ---------------------------------------------------------------------------
 * 2. 焦点可见（§5.6）：2 px 主色环 + 1 px 偏移；键盘用户必须看得见
 *    取值：calc(var(--fc-hairline) * 2) = 2，outline-offset = var(--fc-hairline) = 1
 *    另加一层 surface 色"垫圈"（3 px = 环宽 2 + 偏移 1）：焦点环可能落在 canvas /
 *    sunken / 选中底上，而 §3.8 只给了"焦点环对 bg-surface ≥3:1"的判据；垫圈把环的
 *    紧邻背景统一成 surface（浅色 3.09:1），同时让环在任何底色上都看得清。
 * ------------------------------------------------------------------------- */
:focus-visible {
  outline: calc(var(--fc-hairline) * 2) solid var(--fc-focus-ring);
  outline-offset: var(--fc-hairline);
  box-shadow: 0 0 0 calc(var(--fc-hairline) * 3) var(--fc-bg-surface);
}

/* 模态/浮层的容器获得焦点时不画环（自身不是可操作目标），但仍不把 outline 关掉 */
#search-overlay:focus-visible,
#message-flow:focus-visible {
  outline: calc(var(--fc-hairline) * 2) solid var(--fc-focus-ring);
  outline-offset: calc(var(--fc-hairline) * -2);
}

/* ---------------------------------------------------------------------------
 * 3. 按钮 .btn（§6.7 主操作 / §6.5 工具条按钮 / §6.8 菜单外的常规按钮）
 *    命中高 = 44（触控口径；--fc-hit-min 32 是桌面指针口径，见下面的取值注释）；圆角 md（§5.2）
 * ------------------------------------------------------------------------- */
.btn,
[data-testid="send"] {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--fc-space-4);
  box-sizing: border-box;
  /* 触控命中下限 44（WCAG 2.5.5 Target Size）：--fc-hit-min 是 ui-tokens §6.1 的桌面指针口径 32，
     手机上不够。44 = var(--fc-space-40) + var(--fc-space-4) —— 全用既有 token 算，不写 px 字面量
     （U2；ui-check 的 u2_px 腿可复量）。所有 .btn 变体（含主按钮 / 文字按钮 / 危险按钮）一并生效。 */
  min-block-size: calc(var(--fc-space-40) + var(--fc-space-4));
  padding-inline: var(--fc-space-12);
  padding-block: 0;
  border: var(--fc-hairline) solid transparent;
  border-radius: var(--fc-radius-md);
  background: transparent;
  color: var(--fc-text-primary);
  font-family: inherit;
  font-size: var(--fc-font-label-size);
  line-height: var(--fc-font-label-line-height);
  font-weight: var(--fc-font-body-strong-weight);
  cursor: pointer;
  transition: background-color var(--fc-motion-fast) var(--fc-ease-standard),
    color var(--fc-motion-fast) var(--fc-ease-standard),
    border-color var(--fc-motion-fast) var(--fc-ease-standard);
}

.btn:hover {
  background: var(--fc-bg-hover);
}

.btn:active {
  background: var(--fc-bg-selected);
}

.btn--primary,
[data-testid="send"] {
  background: var(--fc-brand-solid);
  color: var(--fc-text-on-brand);
}

.btn--primary:hover,
[data-testid="send"]:hover {
  background: var(--fc-brand-hover);
  color: var(--fc-text-on-brand);
}

/* 文字按钮永远落在 §3.8 列出的"品牌色 / surface"配对上（浅色 4.71:1）；
   若直接铺在 canvas 上只有约 4.47:1，故显式给 surface 底。
   悬停用描边而不是 bg-hover 底色：后者会把品牌色文字压到约 4.16:1。 */
.btn--text {
  padding-inline: var(--fc-space-8);
  background: var(--fc-bg-surface);
  color: var(--fc-text-brand);
}

.btn--text:hover {
  background: var(--fc-bg-surface);
  border-color: var(--fc-text-brand);
}

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

/* 危险按钮悬停不铺 hover 底：浅色主题下 danger 对 bg-hover 只有约 4.2:1；
   改铺 bg-surface（§3.8 已列出的 ≥4.5:1 配对）并用描边表达悬停 */
.btn--danger:hover {
  background: var(--fc-bg-surface);
  border-color: var(--fc-danger);
}

/* 图标按钮：44×44 命中（与 .btn 同一条 44 口径），内部图标仍 20×20（§6.5） */
.btn--icon,
.btn[data-variant="icon"] {
  inline-size: calc(var(--fc-space-40) + var(--fc-space-4));
  padding-inline: 0;
}

.btn--icon > svg,
.btn[data-variant="icon"] > svg {
  inline-size: var(--fc-space-20);
  block-size: var(--fc-space-20);
}

.btn > svg,
.rail__item > svg,
.chat-item__flag > svg {
  flex: 0 0 auto;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* 禁用态：颜色 + 光标 + aria 三通道（§3.5 text-disabled 有意低于 4.5:1，
   WCAG 1.4.3 对禁用控件豁免；但不得只靠颜色表达 ⇒ 必须同时带 aria-disabled/disabled） */
.btn:disabled,
.btn[disabled],
.btn[aria-disabled="true"],
[data-testid="send"][disabled],
[data-testid="send"][aria-disabled="true"],
.rail__item[aria-disabled="true"],
.menu__item[aria-disabled="true"] {
  background: var(--fc-bg-sunken);
  color: var(--fc-text-disabled);
  border-color: transparent;
  cursor: not-allowed;
}

/* ---------------------------------------------------------------------------
 * 4. 头像 .avatar（§6.8）：24 / 32 / 40；单聊全圆、群聊 md 圆角
 *    底色由 JS 按 hash(contact_id) mod 8 取 --fc-avatar-<k> 后内联设置（§3.7）；
 *    本文件只给"无色相"兜底与几何。两套前景：
 *      · 无色相兜底（未设色相）= text-secondary on bg-sunken（浅深两主题都 ≥4.5:1）
 *      · 设了色相 = text-on-brand 白（§3.7：白字/白图案在 8 档上均 ≥4.5:1）
 *    判据：只要 DOM 带 data-hue 或内联样式里出现 --fc-avatar，就按白前景处理
 * ------------------------------------------------------------------------- */
.avatar,
[data-testid="avatar"] {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  flex: 0 0 auto;
  inline-size: var(--fc-avatar-lg);
  block-size: var(--fc-avatar-lg);
  border-radius: var(--fc-radius-full);
  background-color: var(--fc-bg-sunken);
  color: var(--fc-text-secondary);
  font-size: var(--fc-font-caption-size);
  line-height: var(--fc-font-caption-line-height);
  font-weight: var(--fc-font-caption-weight);
  overflow: hidden;
  user-select: none;
}

.avatar[data-hue],
.avatar[style*="--fc-avatar"] {
  color: var(--fc-text-on-brand);
}

.avatar[data-size="sm"] {
  inline-size: var(--fc-avatar-sm);
  block-size: var(--fc-avatar-sm);
  font-size: var(--fc-font-micro-size);
}

.avatar[data-size="md"] {
  inline-size: var(--fc-avatar-md);
  block-size: var(--fc-avatar-md);
}

.avatar[data-shape="group"] {
  border-radius: var(--fc-radius-md);
}

/* 组内已省略头像时的占位（保持气泡列不跳动，§6.3） */
.avatar--spacer {
  visibility: hidden;
}

.avatar > svg {
  inline-size: 100%;
  block-size: 100%;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
}

/* ---------------------------------------------------------------------------
 * 5. 未读角标 .badge（§6.2 / §6.8）：高 20、全圆、主色底（R7-4：不是红点）
 *    静音会话改用 sunken 底 + 次要文字（不是红色）
 * ------------------------------------------------------------------------- */
.badge,
[data-testid="badge"] {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  min-inline-size: var(--fc-space-20);
  block-size: var(--fc-space-20);
  padding-inline: var(--fc-space-4);
  border-radius: var(--fc-radius-full);
  background: var(--fc-badge-bg);
  color: var(--fc-badge-text);
  font-size: var(--fc-font-micro-size);
  line-height: var(--fc-font-micro-line-height);
  font-weight: var(--fc-font-micro-weight);
  font-variant-numeric: tabular-nums;
}

.badge--muted,
.badge[data-muted="true"],
[data-testid="chat-item"][data-muted="true"] .badge,
[data-testid="chat-item"][data-muted="true"] [data-testid="badge"] {
  background: var(--fc-bg-sunken);
  color: var(--fc-text-secondary);
}

/* ---------------------------------------------------------------------------
 * 6. 左导航项 .rail__item（§6.1）：44×44 命中（触控口径）+ 24×24 图标
 *    默认 tertiary / 悬停 bg-hover / 选中 text-brand + 左侧 2 px 指示条
 *    rail 容器自身的三栏布局归 layout.css（W3），这里只写项的状态
 * ------------------------------------------------------------------------- */
.rail__item,
[data-nav] {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--fc-space-8);
  box-sizing: border-box;
  /* 40 → 44：rail 是手机上唯一的导航面，按触控目标给（rail 轨道 64 的内边距同步收到 10，
     见 layout.css §3 —— 44 + 2×10 = 64，刚好不溢出、不被 overflow-x 裁） */
  inline-size: calc(var(--fc-space-40) + var(--fc-space-4));
  block-size: calc(var(--fc-space-40) + var(--fc-space-4));
  padding: 0;
  border: 0;
  border-radius: var(--fc-radius-full);
  background: transparent;
  /* 对比度（2026-10-06 S5 实测）：tertiary #6D6D6D 落在 rail 的 sunken #EEEEEE 上只有 4.46:1，
     低于正文 4.5:1（13 px 的展开态文字不够；24 px 图标本来只要 3:1）。首屏常驻面按文字口径
     提到 secondary #555555 ⇒ 6.43:1。深色侧由 themes.css 的 secondary 承担（现读 8.75:1）。 */
  color: var(--fc-text-secondary);
  font-family: inherit;
  font-size: var(--fc-font-label-size);
  line-height: var(--fc-font-label-line-height);
  cursor: pointer;
  transition: background-color var(--fc-motion-fast) var(--fc-ease-standard),
    color var(--fc-motion-fast) var(--fc-ease-standard);
}

.rail__item > svg,
[data-nav] > svg {
  inline-size: var(--fc-space-24);
  block-size: var(--fc-space-24);
}

.rail__item:hover,
[data-nav]:hover {
  background: var(--fc-bg-hover);
  color: var(--fc-text-primary);
}

.rail__item[aria-current="page"],
.rail__item[data-active="true"],
[data-nav][aria-current="page"],
[data-nav][data-active="true"] {
  background: var(--fc-bg-selected);
  color: var(--fc-text-brand);
}

/* 选中指示条：宽 calc(var(--fc-hairline) * 2) = 2，画在项外侧（rail 内边距内，不越界） */
.rail__item[aria-current="page"]::before,
.rail__item[data-active="true"]::before,
[data-nav][aria-current="page"]::before,
[data-nav][data-active="true"]::before {
  content: "";
  position: absolute;
  inset-block: var(--fc-space-8);
  inset-inline-start: calc(var(--fc-space-4) * -1);
  inline-size: calc(var(--fc-hairline) * 2);
  border-radius: var(--fc-radius-full);
  background: var(--fc-text-brand);
}

/* 展开态（rail 容器带 data-expanded，容器宽度 --fc-rail-w-expanded 归 layout.css） */
.rail__item__label {
  display: none;
}

#rail[data-expanded="true"] .rail__item {
  inline-size: auto;
  padding-inline: var(--fc-space-12);
  border-radius: var(--fc-radius-md);
}

#rail[data-expanded="true"] .rail__item__label {
  display: inline;
}

/* 展开态选中项的文字：§6.1 说选中用主色，但主色落在 bg-selected 上浅色主题只有约 3.75:1
   （对 24 px 图标够 3:1，对 13 px 文字不够 4.5:1）⇒ 文字提到 text-primary（约 10.2:1），
   图标仍走 currentColor 保持主色 + 指示条，归属/选中依旧是三重编码 */
#rail[data-expanded="true"] .rail__item[aria-current="page"] .rail__item__label,
#rail[data-expanded="true"] .rail__item[data-active="true"] .rail__item__label,
#rail[data-expanded="true"] [data-nav][aria-current="page"] .rail__item__label,
#rail[data-expanded="true"] [data-nav][data-active="true"] .rail__item__label {
  color: var(--fc-text-primary);
}

/* ---------------------------------------------------------------------------
 * 7. 会话列表项（§6.2）高 64 = 12 + 40 + 12；紧凑密度 48 = 8 + 32 + 8
 *    default / hover(bg-hover) / selected(bg-selected + 2 px 指示条 + aria-selected)
 *    pinned / muted / unread(标题 600)
 * ------------------------------------------------------------------------- */
.chat-item,
[data-testid="chat-item"] {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--fc-space-8);
  box-sizing: border-box;
  inline-size: 100%;
  min-block-size: var(--fc-item-h);
  padding: var(--fc-space-12) var(--fc-space-16);
  border: 0;
  border-radius: var(--fc-radius-none);
  background: transparent;
  color: var(--fc-text-primary);
  font-family: inherit;
  text-align: start;
  cursor: pointer;
  transition: background-color var(--fc-motion-fast) var(--fc-ease-standard);
}

/* 项间分隔线画在项边界上，用装饰色（§5.3：不承担控件边界） */
#conversation-list > * + * {
  border-block-start: var(--fc-hairline) solid var(--fc-line-subtle);
}

.chat-item:hover,
[data-testid="chat-item"]:hover {
  background: var(--fc-bg-hover);
}

/* 选中：浅底 + 指示条 + aria-selected 三重（R7-3；颜色不是唯一编码） */
.chat-item[aria-selected="true"],
.chat-item[data-state="selected"],
[data-testid="chat-item"][aria-selected="true"],
[data-testid="chat-item"][data-state="selected"] {
  background: var(--fc-bg-selected);
}

.chat-item[aria-selected="true"]::before,
.chat-item[data-state="selected"]::before,
[data-testid="chat-item"][aria-selected="true"]::before,
[data-testid="chat-item"][data-state="selected"]::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: calc(var(--fc-hairline) * 2);
  background: var(--fc-text-brand);
}

.chat-item__main {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-inline-size: 0;
}

.chat-item__title {
  color: var(--fc-text-primary);
  font-size: var(--fc-font-body-strong-size);
  line-height: var(--fc-font-body-strong-line-height);
  font-weight: var(--fc-font-body-strong-weight);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-item__preview {
  color: var(--fc-text-secondary);
  font-size: var(--fc-font-label-size);
  line-height: var(--fc-font-label-line-height);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-item__meta {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--fc-space-4);
  flex: 0 0 auto;
}

.chat-item__time,
[data-testid="chat-item"] time {
  color: var(--fc-text-tertiary);
  font-size: var(--fc-font-caption-size);
  line-height: var(--fc-font-caption-line-height);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* 行状态底色一变，tertiary 就掉到 4.5:1 以下（浅色主题实测：对 bg-hover 约 4.46、
   对 bg-selected 约 4.02），所以悬停/选中时把时间提到 secondary（对两者约 6.43 / 5.80）。
   凡是我给"行"加了状态底色的地方，都跟一条同型修正（文本类与图形类阈值不同，见文件头）。 */
.chat-item:hover .chat-item__time,
.chat-item:hover time,
.chat-item[aria-selected="true"] .chat-item__time,
.chat-item[aria-selected="true"] time,
[data-testid="chat-item"]:hover .chat-item__time,
[data-testid="chat-item"]:hover time,
[data-testid="chat-item"][aria-selected="true"] .chat-item__time,
[data-testid="chat-item"][aria-selected="true"] time,
[data-testid="chat-item"][data-state="selected"] time {
  color: var(--fc-text-secondary);
}

/* 未读：标题加重到 600（§6.2；未读数本体由角标与 aria-label 双通道表达） */
.chat-item[data-unread="true"] .chat-item__title,
[data-testid="chat-item"][data-unread="true"] .chat-item__title {
  font-weight: 600;
}

/* 置顶 / 静音的小图标（针形 / 斜杠铃铛，SVG 由 W6 内联） */
.chat-item__flag {
  display: inline-flex;
  align-items: center;
  gap: var(--fc-space-4);
  color: var(--fc-text-tertiary);
}

.chat-item__flag > svg {
  inline-size: var(--fc-space-16);
  block-size: var(--fc-space-16);
}

/* 紧凑密度（--fc-item-h-compact 48 = 8 + 32 + 8，头像换 md） */
[data-testid="chat-item"][data-density="compact"],
.chat-item--compact {
  min-block-size: var(--fc-item-h-compact);
  padding: var(--fc-space-8) var(--fc-space-16);
}

/* ---------------------------------------------------------------------------
 * 8. 消息流（§6.3）：行 = [头像 40] [气泡列]；我方镜像；同发送者成组
 *    组内间距 4、组间 8；仅组尾显示头像（其余用 .avatar--spacer 占位）
 * ------------------------------------------------------------------------- */
#message-flow {
  display: flex;
  flex-direction: column;
  padding: var(--fc-space-16) var(--fc-space-24);
  background: var(--fc-bg-canvas);
  color: var(--fc-text-primary);
  font-size: var(--fc-font-body-size);
  line-height: var(--fc-font-body-line-height);
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
}

.msg {
  display: flex;
  align-items: flex-end;
  gap: var(--fc-space-8);
  margin-block-start: var(--fc-space-8);
  min-inline-size: 0;
}

.msg[data-side="out"] {
  flex-direction: row-reverse;
}

/* 组内：间距收紧到 4（§6.3）；写在 .msg:first-child 之前，故首行仍归零 */
.msg[data-grouped="true"],
.msg--grouped {
  margin-block-start: var(--fc-space-4);
}

.msg:first-child {
  margin-block-start: 0;
}

/* 气泡列：封顶 min(560, 72%)（§2.4）；72% 相对 .msg 行宽解析（行宽确定，
   不会退化成"百分比未定义"） */
.msg__bubbles {
  display: flex;
  flex-direction: column;
  gap: var(--fc-space-4);
  min-inline-size: 0;
  max-inline-size: min(var(--fc-bubble-max-w), 72%);
}

/* 气泡本体：内边距 12/16；单行高 12 + 24 + 12 = 48（§6.3） */
[data-testid="bubble"] {
  box-sizing: border-box;
  inline-size: fit-content;
  max-inline-size: 100%;
  padding: var(--fc-space-12) var(--fc-space-16);
  border-radius: var(--fc-radius-lg);
  font-size: var(--fc-font-body-size);
  line-height: var(--fc-font-body-line-height);
  overflow-wrap: anywhere;
  white-space: pre-wrap;
}

/* 若 W6 不生成 .msg__bubbles 列（气泡直接挂在行上），气泡自己封顶，避免出现
   "百分比相对未定尺寸的 flex 项解析"这种退化；两条规则不会叠加（选择器互斥） */
.msg > [data-testid="bubble"] {
  max-inline-size: min(var(--fc-bubble-max-w), 72%);
}

/* 非对称圆角表达归属（§5.2 / R7-2）：近头像角 4，其余三角 12；无尖角尾巴 */
[data-testid="bubble"][data-side="in"],
[data-side="in"] [data-testid="bubble"] {
  background: var(--fc-bubble-in-bg);
  color: var(--fc-bubble-in-text);
  border-radius: var(--fc-radius-sm) var(--fc-radius-lg) var(--fc-radius-lg)
    var(--fc-radius-lg);
}

[data-testid="bubble"][data-side="out"],
[data-side="out"] [data-testid="bubble"] {
  background: var(--fc-bubble-out-bg);
  color: var(--fc-bubble-out-text);
  border-radius: var(--fc-radius-lg) var(--fc-radius-sm) var(--fc-radius-lg)
    var(--fc-radius-lg);
}

.bubble__text {
  margin: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* 气泡内元信息：12/16、tabular-nums、对比度按 §3.8 判据（bubble-meta 对两种气泡底） */
.bubble__meta,
[data-testid="bubble"] time {
  display: block;
  margin-block-start: var(--fc-space-4);
  color: var(--fc-bubble-meta);
  font-size: var(--fc-font-caption-size);
  line-height: var(--fc-font-caption-line-height);
  font-variant-numeric: tabular-nums;
}

.bubble__edited {
  margin-inline-start: var(--fc-space-4);
  font-size: var(--fc-font-caption-size);
  line-height: var(--fc-font-caption-line-height);
}

/* 引用回复：左侧 2 px 主色条 + sunken 块（§6.3） */
.bubble__quote {
  display: block;
  margin-block-end: var(--fc-space-4);
  padding: var(--fc-space-4) var(--fc-space-8);
  border-inline-start: calc(var(--fc-hairline) * 2) solid var(--fc-text-brand);
  border-radius: var(--fc-radius-sm);
  background: var(--fc-bg-sunken);
  color: var(--fc-text-secondary);
  font-size: var(--fc-font-label-size);
  line-height: var(--fc-font-label-line-height);
  white-space: pre-wrap;
}

/* 发送中 / 失败 / 已删除（§6.3）
   发送中不给气泡整体降透明度：0.72 透明会把 15 px 正文压到 4.5:1 以下
   （与 ui-tokens §3.8 判据冲突）；改为"400 ms 后才出现的指示器"由 W6 控时序，
   指示器与正文同为满对比度。 */
.msg[data-status="sending"] [data-testid="bubble"] {
  border: var(--fc-hairline) dashed var(--fc-line-subtle);
}

.msg[data-status="failed"] [data-testid="bubble"] {
  border: var(--fc-hairline) solid var(--fc-danger);
}

.bubble__status {
  display: inline-flex;
  align-items: center;
  gap: var(--fc-space-4);
  margin-block-start: var(--fc-space-4);
  color: var(--fc-danger);
  font-size: var(--fc-font-caption-size);
  line-height: var(--fc-font-caption-line-height);
}

.bubble__status > svg {
  inline-size: var(--fc-space-16);
  block-size: var(--fc-space-16);
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
}

/* 已删除：不显示原文，只用斜体占位（R11：删除即删除）。
   底色取 canvas 而非 sunken：§6.3 指定 --fc-text-tertiary 作前景，而该前景对
   background 为 sunken 时只有约 4.46:1（浅色主题，低于 4.5），对 canvas 才是
   §3.8 已列出的 ≥4.5:1 配对。 */
[data-testid="bubble"][data-deleted="true"],
.bubble--deleted {
  background: var(--fc-bg-canvas);
  color: var(--fc-text-tertiary);
  font-style: italic;
}

/* 纯图标/表情消息不带底色时仍需可读 */
[data-testid="bubble"][data-plain="true"] {
  background: transparent;
  padding-inline: 0;
}

/* ---------------------------------------------------------------------------
 * 9. 时间分隔（§6.6）：13/20 + tertiary，居中文字 + 两侧 hairline 细线
 *    （不是灰底药丸）上下外边距 12；是否插入由 W6 按 5 分钟/跨日规则判定
 * ------------------------------------------------------------------------- */
.time-sep,
[data-testid="time-separator"] {
  display: flex;
  align-items: center;
  gap: var(--fc-space-8);
  margin-block: var(--fc-space-12);
  color: var(--fc-text-tertiary);
  font-size: var(--fc-font-label-size);
  line-height: var(--fc-font-label-line-height);
  font-variant-numeric: tabular-nums;
}

.time-sep::before,
.time-sep::after,
[data-testid="time-separator"]::before,
[data-testid="time-separator"]::after {
  content: "";
  flex: 1 1 auto;
  border-block-start: var(--fc-hairline) solid var(--fc-line-subtle);
}

.time-sep > time,
[data-testid="time-separator"] > time {
  white-space: nowrap;
}

/* ---------------------------------------------------------------------------
 * 10. 系统消息（"XX 撤回了一条消息"这类，非时间分隔）
 *     文字用 secondary 而不是 tertiary：底色是 bg-sunken，浅色主题下 tertiary 只有
 *     约 4.46:1（12 px 文字要求 ≥4.5:1），secondary 对 sunken 约 6.43:1
 * ------------------------------------------------------------------------- */
.msg--system,
[data-testid="system-message"] {
  align-self: center;
  max-inline-size: min(var(--fc-bubble-max-w), 88%);
  margin-block: var(--fc-space-8);
  padding: var(--fc-space-4) var(--fc-space-12);
  border-radius: var(--fc-radius-full);
  background: var(--fc-bg-sunken);
  color: var(--fc-text-secondary);
  font-size: var(--fc-font-caption-size);
  line-height: var(--fc-font-caption-line-height);
  text-align: center;
}

/* ---------------------------------------------------------------------------
 * 11. 附件（§6.3）：图片最大高 320（calc(--fc-space-64) * 5）、多图 2×2 网格、
 *     文件卡片 = 图标 + 文件名 + 大小（tabular-nums）
 * ------------------------------------------------------------------------- */
.attachment-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--fc-space-4);
  margin: 0;
  padding: 0;
  list-style: none;
}

[data-kind="image"] {
  margin: 0;
  line-height: 0;
}

[data-kind="image"] > img,
[data-kind="image"] > svg,
.attachment--image > img {
  display: block;
  inline-size: 100%;
  block-size: auto;
  max-block-size: calc(var(--fc-space-64) * 5);
  border-radius: var(--fc-radius-md);
  object-fit: cover;
}

[data-kind="file"],
.attachment--file {
  display: flex;
  align-items: center;
  gap: var(--fc-space-8);
  box-sizing: border-box;
  min-block-size: var(--fc-hit-min);
  padding: var(--fc-space-8) var(--fc-space-12);
  border: var(--fc-hairline) solid var(--fc-line-subtle);
  border-radius: var(--fc-radius-md);
  background: var(--fc-bg-sunken);
  color: var(--fc-text-primary);
  text-decoration: none;
}

[data-kind="file"] > svg {
  inline-size: var(--fc-space-24);
  block-size: var(--fc-space-24);
  flex: 0 0 auto;
  fill: none;
  stroke: var(--fc-text-secondary);
  stroke-width: 1.5;
}

.attachment__name {
  min-inline-size: 0;
  font-size: var(--fc-font-label-size);
  line-height: var(--fc-font-label-line-height);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 文件大小同样不能用 tertiary：文件卡底色是 bg-sunken（浅色约 4.46:1 < 4.5:1） */
.attachment__size {
  flex: 0 0 auto;
  color: var(--fc-text-secondary);
  font-size: var(--fc-font-caption-size);
  line-height: var(--fc-font-caption-line-height);
  font-variant-numeric: tabular-nums;
}

/* ---------------------------------------------------------------------------
 * 12. 输入区 #composer（§6.4）：96 = 12 + 24 + 8 + 工具条 36 + 16
 *     2026-10-06（S5）：文本区命中高 24 → 44（触控口径），并用等量上下内边距把首行**居中**在
 *     44 里（否则单行文字贴在 44 高的盒子顶端、看着像没对齐）。折叠态（40 高）显式回到 24，
 *     见本节末尾与 layout.css §9 的矮窗规则。
 *     表面 bg-surface + 顶部 hairline；上限 --fc-composer-h-max 240
 * ------------------------------------------------------------------------- */
#composer {
  display: flex;
  flex-direction: column;
  gap: var(--fc-space-8);
  box-sizing: border-box;
  min-block-size: var(--fc-composer-h);
  max-block-size: var(--fc-composer-h-max);
  padding: var(--fc-space-12) var(--fc-space-16) var(--fc-space-16);
  border-block-start: var(--fc-hairline) solid var(--fc-line-subtle);
  background: var(--fc-bg-surface);
  color: var(--fc-text-primary);
}

.composer__input,
#composer textarea,
#composer [contenteditable="true"] {
  box-sizing: border-box;
  inline-size: 100%;
  /* 触控命中下限 44（同 §3 的口径）；折叠态另有 24 的显式回落（见下） */
  min-block-size: calc(var(--fc-space-40) + var(--fc-space-4));
  max-block-size: calc(var(--fc-space-24) * 7);
  margin: 0;
  /* (44 − 24) / 2 = 10 上下内边距：单行时首行居中，多行时照旧往下长（JS 的 autoGrow 读 scrollHeight，
     含内边距 ⇒ 量出来的高度与这里一致） */
  padding: 0;
  padding-block: calc((var(--fc-space-40) + var(--fc-space-4) - var(--fc-font-body-line-height)) / 2);
  border: 0;
  background: transparent;
  color: var(--fc-text-primary);
  font-family: inherit;
  font-size: var(--fc-font-body-size);
  line-height: var(--fc-font-body-line-height);
  resize: none;
  overflow-y: auto;
  scrollbar-width: thin;
}

.composer__input::placeholder,
#composer textarea::placeholder {
  color: var(--fc-text-tertiary);
  opacity: 1;
}

.composer__input:disabled,
.composer__input[aria-disabled="true"],
#composer textarea:disabled {
  color: var(--fc-text-disabled);
  cursor: not-allowed;
}

/* 工具条（§6.5）：高 36，内部 32 按钮垂直居中（上下各 2 是居中余量，不属布局间距） */
.composer__toolbar,
#composer [role="toolbar"] {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--fc-space-4);
  min-block-size: var(--fc-composer-toolbar-h);
}

/* 组间 16、组内 4（§6.5：分组之间不加分隔线，用间距表达） */
.composer__toolbar > [role="group"] + [role="group"] {
  margin-inline-start: var(--fc-space-16);
}

.composer__toolbar > .composer__spacer,
#composer [role="toolbar"] > .composer__spacer {
  flex: 1 1 auto;
}

/* "仅本地"徽标（§6.4 composer-local 模式） */
.composer__local-badge,
[data-testid="local-only-badge"] {
  display: inline-flex;
  align-items: center;
  gap: var(--fc-space-4);
  box-sizing: border-box;
  block-size: var(--fc-space-20);
  padding-inline: var(--fc-space-8);
  border-radius: var(--fc-radius-full);
  background: var(--fc-bg-sunken);
  color: var(--fc-text-secondary);
  font-size: var(--fc-font-micro-size);
  line-height: var(--fc-font-micro-line-height);
  font-weight: var(--fc-font-micro-weight);
}

/* 折叠态（P1 / R7-12：--fc-composer-h 归 40 = 8 + 24 + 8）：
   只剩一行文本区，工具条与"仅本地"徽标折起 —— 否则 24+8+36 会撑破 40 的固定高度。
   折叠开关与"展开"入口由 W6 负责（本文件只定义折叠后的几何） */
#composer[data-collapsed="true"],
.composer--collapsed {
  min-block-size: var(--fc-space-40);
  max-block-size: var(--fc-space-40);
  padding-block: var(--fc-space-8);
}

/* 折叠态（几何 40 = 8 + 24 + 8，ui-tokens §6.4）：文本区显式回到 24 并去掉居中内边距 ——
   否则上面那条 44 的 min-block-size 会顶破 40 的固定高度（同一处回落也写在 layout.css §9，
   因为矮窗折叠是**窗高**触发的，没有 data-collapsed 属性可挂）。 */
#composer[data-collapsed="true"] .composer__input,
#composer[data-collapsed="true"] textarea,
.composer--collapsed .composer__input {
  min-block-size: var(--fc-space-24);
  padding-block: 0;
}

#composer[data-collapsed="true"] .composer__toolbar,
#composer[data-collapsed="true"] [role="toolbar"],
#composer[data-collapsed="true"] .composer__local-badge,
#composer[data-collapsed="true"] [data-testid="local-only-badge"] {
  display: none;
}

/* ---------------------------------------------------------------------------
 * 13. 搜索浮层 #search-overlay（§6.8 SearchField + ui-contract §4.3）
 *     2026-10-06（S5）：搜索框命中高 32 → 44（触控口径），并把内部 input 拉满整高
 *     （align-self: stretch）—— 否则点框的上下内边距不会聚焦，只有点到那条 20 px 文字才行。
 *     hidden 属性必须压得住 display: flex（否则 Esc 关不掉）
 * ------------------------------------------------------------------------- */
#search-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  box-sizing: border-box;
  padding: var(--fc-space-64) var(--fc-space-24);
  background: var(--fc-bg-overlay);
}

#search-overlay[hidden] {
  display: none;
}

.search-panel,
#search-overlay [role="dialog"] {
  display: flex;
  flex-direction: column;
  gap: var(--fc-space-8);
  box-sizing: border-box;
  inline-size: min(var(--fc-bubble-max-w), 100%);
  max-block-size: 100%;
  padding: var(--fc-space-20);
  border-radius: var(--fc-radius-xl);
  background: var(--fc-bg-surface);
  box-shadow: var(--fc-elevation-3);
  overflow: hidden;
}

.search-field {
  display: flex;
  align-items: center;
  gap: var(--fc-space-8);
  box-sizing: border-box;
  block-size: calc(var(--fc-space-40) + var(--fc-space-4));
  padding-inline: var(--fc-space-12);
  border: var(--fc-hairline) solid var(--fc-line-strong);
  border-radius: var(--fc-radius-md);
  background: var(--fc-bg-sunken);
  color: var(--fc-text-primary);
}

.search-field > svg {
  inline-size: var(--fc-space-16);
  block-size: var(--fc-space-16);
  flex: 0 0 auto;
  fill: none;
  /* 对比度：tertiary 对搜索框自己的 sunken 底只有 4.46:1（16 px 图形阈值 3:1 本可过，
     但它是控件的一部分，跟占位符一起提到 secondary 更稳） */
  stroke: var(--fc-text-secondary);
  stroke-width: 1.5;
}

.search-field > input {
  flex: 1 1 auto;
  /* 拉满搜索框整高：点击框内任意位置都聚焦（32→44 的命中面否则只属于那个 div） */
  align-self: stretch;
  min-inline-size: 0;
  border: 0;
  background: transparent;
  color: var(--fc-text-primary);
  font-family: inherit;
  font-size: var(--fc-font-label-size);
  line-height: var(--fc-font-label-line-height);
}

.search-field > input::placeholder {
  /* 对比度（2026-10-06 S5 实测）：tertiary #6D6D6D 对搜索框自己的 sunken #EEEEEE = 4.46:1 < 4.5；
     secondary #555555 ⇒ 6.43:1 */
  color: var(--fc-text-secondary);
  opacity: 1;
}

/* 结果列表：一行一条命中；命中词用主色（不是黄底 mark） */
.search-results {
  display: flex;
  flex-direction: column;
  gap: 0;
  margin: 0;
  padding: 0;
  list-style: none;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
}

.search-result,
[data-testid="search-result"] {
  display: flex;
  flex-direction: column;
  gap: var(--fc-space-4);
  box-sizing: border-box;
  /* 命中下限 44（内容两行本来约 60 高，这条是"就算将来内容变短也不许低于 44"的兜底） */
  min-block-size: calc(var(--fc-space-40) + var(--fc-space-4));
  padding: var(--fc-space-8) var(--fc-space-12);
  border-radius: var(--fc-radius-md);
  color: var(--fc-text-primary);
  cursor: pointer;
}

.search-result:hover {
  background: var(--fc-bg-hover);
}

.search-result[aria-selected="true"],
[data-testid="search-result"][aria-selected="true"] {
  background: var(--fc-bg-selected);
}

.search-result mark {
  background: transparent;
  color: var(--fc-text-brand);
  font-weight: var(--fc-font-body-strong-weight);
}

/* 状态底色下主色文字不够 4.5:1（浅色主题对 bg-hover 约 4.16、对 bg-selected 约 3.75）⇒
   悬停/选中行里的命中词改用 text-primary，靠字重继续表达"命中"（颜色不是唯一编码） */
.search-result:hover mark,
.search-result[aria-selected="true"] mark,
[data-testid="search-result"]:hover mark,
[data-testid="search-result"][aria-selected="true"] mark {
  color: var(--fc-text-primary);
}

.search-result__text {
  font-size: var(--fc-font-body-size);
  line-height: var(--fc-font-body-line-height);
  overflow-wrap: anywhere;
}

.search-result__meta {
  color: var(--fc-text-tertiary);
  font-size: var(--fc-font-caption-size);
  line-height: var(--fc-font-caption-line-height);
  font-variant-numeric: tabular-nums;
}

/* 同 .chat-item__time：结果行一旦有状态底色，tertiary 元信息就要提到 secondary */
.search-result:hover .search-result__meta,
.search-result[aria-selected="true"] .search-result__meta,
[data-testid="search-result"]:hover .search-result__meta,
[data-testid="search-result"][aria-selected="true"] .search-result__meta {
  color: var(--fc-text-secondary);
}

/* ---------------------------------------------------------------------------
 * 14. 空态（§6.7）：插图 96×96 + 标题 20/28 + 说明 13/20（最大宽 360）+ 主操作
 *     容器 role="status"；垂直居中后上移 12%（top 百分比对容器高解析）
 * ------------------------------------------------------------------------- */
[data-testid="empty-state"],
.empty-state {
  position: relative;
  /* 上移 12% 只在"真用 auto 外边距居中"的那一处保留（见下面 #message-flow 那条）。
     理由（2026-10-06 S5 修的一个真裁切）：position: relative 的百分比 top 解析的是**容器**高度，
     不是自身高度；在 #conversation-list / 卡片这类容器里无条件 -12%，容器高 600 时整块被顶上去
     72 px —— 96 px 的插图有 3/4 在上边界外，而滚动条回不去（scrollTop 不能为负）⇒
     用户看到的是"空态没有插图、标题贴顶"。改成 0 后不再裁切；要居中请靠 auto 外边距。 */
  top: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--fc-space-16);
  box-sizing: border-box;
  min-block-size: 0;
  padding: var(--fc-space-40) var(--fc-space-24);
  text-align: center;
  /* 继承色（说明文字 / 下一步提示里的 <kbd>）也提到 secondary：tertiary 对 canvas 只有 4.79:1 的边缘值 */
  color: var(--fc-text-secondary);
}

.empty-state__art,
[data-testid="empty-state"] > svg {
  inline-size: calc(var(--fc-space-48) * 2);
  block-size: calc(var(--fc-space-48) * 2);
  flex: 0 0 auto;
  fill: none;
  stroke: var(--fc-line-strong);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.empty-state__title {
  margin: 0;
  color: var(--fc-text-primary);
  font-size: var(--fc-font-title-size);
  line-height: var(--fc-font-title-line-height);
  font-weight: var(--fc-font-title-weight);
}

.empty-state__desc {
  margin: 0;
  max-inline-size: calc(var(--fc-space-64) * 5 + var(--fc-space-40));
  color: var(--fc-text-secondary);
  font-size: var(--fc-font-label-size);
  line-height: var(--fc-font-label-line-height);
}

.empty-state__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--fc-space-8);
  /* 320 px 窄屏上"插图 + 两个按钮"不许把容器撑出横向滚动：
     换行 + 居中 + 内容最大宽吃满容器（说明文字有 360 上限，操作行不该有） */
  max-inline-size: 100%;
}

/* 空态要塞进消息流时：靠 auto 外边距吃掉剩余空间完成垂直居中
   （§6.7 的"上移 12%"**只在这里**继续承担 —— 这一处是真居中，上移不会跑出容器） */
#message-flow > [data-testid="empty-state"],
#message-flow > .empty-state {
  margin: auto;
  top: -12%;
}

/* ---------------------------------------------------------------------------
 * 15. 卡片与设置行（§6 同批；设置页 / 资料库分组）
 * ------------------------------------------------------------------------- */
.card,
[data-testid="card"] {
  box-sizing: border-box;
  padding: var(--fc-space-20);
  border: var(--fc-hairline) solid var(--fc-line-subtle);
  border-radius: var(--fc-radius-lg);
  background: var(--fc-bg-surface);
  color: var(--fc-text-primary);
}

.card + .card,
[data-testid="card"] + [data-testid="card"] {
  margin-block-start: var(--fc-space-16);
}

.card__title {
  margin: 0 0 var(--fc-space-12);
  font-size: var(--fc-font-heading-size);
  line-height: var(--fc-font-heading-line-height);
  font-weight: var(--fc-font-heading-weight);
}

.card__row,
.setting-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--fc-space-16);
  box-sizing: border-box;
  /* 设置行是整行可点的开关/动作宿主（.switch 的命中框靠 ::before 外扩到 44，见 §16），
     行本身也给到 44，免得外扩部分压到相邻行 */
  min-block-size: calc(var(--fc-space-40) + var(--fc-space-4));
  padding-block: var(--fc-space-8);
}

.card__row + .card__row,
.setting-row + .setting-row {
  border-block-start: var(--fc-hairline) solid var(--fc-line-subtle);
}

.card__label {
  font-size: var(--fc-font-label-size);
  line-height: var(--fc-font-label-line-height);
  color: var(--fc-text-secondary);
}

.card__value {
  font-size: var(--fc-font-body-size);
  line-height: var(--fc-font-body-line-height);
  color: var(--fc-text-primary);
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

.card__hint {
  font-size: var(--fc-font-caption-size);
  line-height: var(--fc-font-caption-line-height);
  /* 对比度（2026-10-06 S5 实测）：tertiary 对 surface #FCFCFC = 5.04（过）、对 canvas = 4.79（过），
     但它会跟状态底色同框（卡片/提示条/选中行里都有），且是 12 px 小字 ⇒ 统一提到 secondary
     （对 surface 7.27 / 对 canvas 6.9 / 对 sunken 6.43 / 对 selected 5.79，全部 ≥4.5）。 */
  color: var(--fc-text-secondary);
}

.card__actions {
  display: flex;
  align-items: center;
  gap: var(--fc-space-8);
  margin-block-start: var(--fc-space-12);
}

/* 危险操作区（删除、清除）用语义色，不用主色 */
.card--danger,
.card[data-variant="danger"] {
  border-color: var(--fc-danger);
}

.card--danger .card__title {
  color: var(--fc-danger);
}

/* 提示 / 告警 / 成功条（导入完成、接近上限）
   底色取 surface 而非 sunken：§3.8 列出的配对是状态色对 bg-surface ≥4.5:1；
   若铺在 sunken 上，浅色主题的 danger / warning / success 都只有约 4.2:1。 */
.notice {
  display: flex;
  align-items: flex-start;
  gap: var(--fc-space-8);
  padding: var(--fc-space-8) var(--fc-space-12);
  border: var(--fc-hairline) solid var(--fc-line-subtle);
  border-radius: var(--fc-radius-md);
  background: var(--fc-bg-surface);
  font-size: var(--fc-font-label-size);
  line-height: var(--fc-font-label-line-height);
}

.notice--warning {
  color: var(--fc-warning);
}

.notice--success {
  color: var(--fc-success);
}

.notice--danger {
  color: var(--fc-danger);
}

/* ---------------------------------------------------------------------------
 * 16. 开关 .switch（§6.8）：32×20 轨道；开 = 主色底，关 = gray-5；焦点环必现
 *     轨道内衬 2 = calc(var(--fc-hairline) * 2)；滑块 16；行程 12 =
 *     32 - 16 - 2 * 2（用 calc 表达，不写 px）
 * ------------------------------------------------------------------------- */
.switch {
  position: relative;
  display: inline-flex;
  align-items: center;
  box-sizing: border-box;
  flex: 0 0 auto;
  inline-size: var(--fc-hit-min);
  block-size: var(--fc-space-20);
  /* 内衬 + 描边各 1：内容盒 = 32 − 2×1(描边) − 2×1(内衬) = 28 宽、20 − 2 − 2 = 16 高，
     正好容下 16 的滑块，行程 = 28 − 16 = 12（与文件头推导表一致） */
  padding: var(--fc-hairline);
  /* 关态底是 §6.8 指定的 --fc-gray-5；它对浅色 surface 只有约 1.9:1，
     所以给轨道补一圈 --fc-line-strong 描边（对两种主题的 surface 都 ≥3:1，
     与 §3.8 的 line-strong 判据一致），控件边界不靠填充色单独承担 */
  border: var(--fc-hairline) solid var(--fc-line-strong);
  border-radius: var(--fc-radius-full);
  background: var(--fc-gray-5);
  cursor: pointer;
  transition: background-color var(--fc-motion-fast) var(--fc-ease-standard);
}

.switch[aria-checked="true"],
.switch[data-on="true"] {
  background: var(--fc-brand-solid);
}

.switch[disabled],
.switch[aria-disabled="true"] {
  background: var(--fc-bg-sunken);
  cursor: not-allowed;
}

/* 命中框外扩到 44（触控口径）：轨道视觉仍是 32×20（§6.8 钉死的形状，不动），
   但整块命中区不许小于 44 —— 外扩量上下 (44−20)/2 = 12、左右 (44−32)/2 = 6，全部由 token 算。
   这个伪元素是**真命中区**（不是只画个样），.switch 本来就有 position: relative。
   代价（如实写）：外扩会压到宿主行的内边距上，宿主行（.card__row / .setting-row）已给到 44，
   所以只吃行内空白、不压邻居；若将来把这开关塞进小于 44 的行里，外扩会盖住相邻控件。 */
.switch::before {
  content: "";
  position: absolute;
  inset-block: calc((var(--fc-space-40) + var(--fc-space-4) - var(--fc-space-20)) / -2);
  inset-inline: calc((var(--fc-space-40) + var(--fc-space-4) - var(--fc-hit-min)) / -2);
}

.switch::after {
  content: "";
  display: block;
  inline-size: var(--fc-space-16);
  block-size: var(--fc-space-16);
  border-radius: var(--fc-radius-full);
  background: var(--fc-text-on-brand);
  transition: transform var(--fc-motion-fast) var(--fc-ease-standard);
}

.switch[aria-checked="true"]::after,
.switch[data-on="true"]::after {
  transform: translateX(
    calc(var(--fc-hit-min) - var(--fc-space-16) - var(--fc-hairline) * 4)
  );
}

/* 开关的可见文字标签（关态也靠文字，不只用颜色） */
.switch__label {
  margin-inline-start: var(--fc-space-8);
  font-size: var(--fc-font-label-size);
  line-height: var(--fc-font-label-line-height);
  color: var(--fc-text-secondary);
}

/* ---------------------------------------------------------------------------
 * 17. 上下文菜单（§6.8）：项高 32、圆角 md、elevation-2、项内边距 8/12
 *     危险项用 danger 色（同时带图标/文案，不只用颜色）
 * ------------------------------------------------------------------------- */
[role="menu"],
.menu {
  position: relative;
  z-index: 100;
  display: flex;
  flex-direction: column;
  gap: 0;
  box-sizing: border-box;
  min-inline-size: var(--fc-space-64);
  margin: 0;
  padding: var(--fc-space-4);
  border: var(--fc-hairline) solid var(--fc-line-subtle);
  border-radius: var(--fc-radius-md);
  background: var(--fc-bg-surface);
  box-shadow: var(--fc-elevation-2);
  list-style: none;
}

[role="menuitem"],
.menu__item {
  display: flex;
  align-items: center;
  gap: var(--fc-space-8);
  box-sizing: border-box;
  min-block-size: calc(var(--fc-space-40) + var(--fc-space-4));
  padding: var(--fc-space-8) var(--fc-space-12);
  border: 0;
  border-radius: var(--fc-radius-md);
  background: transparent;
  color: var(--fc-text-primary);
  font-family: inherit;
  font-size: var(--fc-font-label-size);
  line-height: var(--fc-font-label-line-height);
  text-align: start;
  cursor: pointer;
}

[role="menuitem"]:hover,
.menu__item:hover {
  background: var(--fc-bg-hover);
}

[role="menuitem"][data-variant="danger"],
.menu__item--danger {
  color: var(--fc-danger);
}

[role="menuitem"] > svg,
.menu__item > svg {
  inline-size: var(--fc-space-16);
  block-size: var(--fc-space-16);
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
}

/* 气泡工具条（悬停/选中气泡时出现，§5.4 elevation-2 的用途之一） */
.bubble-toolbar {
  position: absolute;
  z-index: 100;
  display: flex;
  align-items: center;
  gap: var(--fc-space-4);
  padding: var(--fc-space-4);
  border: var(--fc-hairline) solid var(--fc-line-subtle);
  border-radius: var(--fc-radius-md);
  background: var(--fc-bg-surface);
  box-shadow: var(--fc-elevation-2);
}

/* ---------------------------------------------------------------------------
 * 18. 进度 / 导入条（§6.8）：高 4、全圆；轨道 sunken、填充主色
 *     只动 transform（§5.5），进度由 JS 设 style="transform: scaleX(<0..1>)"
 * ------------------------------------------------------------------------- */
.progress {
  display: block;
  inline-size: 100%;
  block-size: var(--fc-space-4);
  border-radius: var(--fc-radius-full);
  background: var(--fc-bg-sunken);
  overflow: hidden;
}

.progress__fill {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  border-radius: var(--fc-radius-full);
  background: var(--fc-brand-solid);
  transform-origin: left center;
  transform: scaleX(0);
  transition: transform var(--fc-motion-base) var(--fc-ease-standard);
}

/* ---------------------------------------------------------------------------
 * 19. 骨架屏（首屏加载占位；只动 opacity，§5.5）与动效降级
 * ------------------------------------------------------------------------- */
.skeleton {
  border-radius: var(--fc-radius-sm);
  background: var(--fc-bg-sunken);
  animation: fc-breathe var(--fc-motion-emphasis) var(--fc-ease-standard) infinite alternate;
}

@keyframes fc-breathe {
  from {
    opacity: 0.6;
  }
  to {
    opacity: 1;
  }
}

/* §5.5 / R7-9：reduce 模式下所有时长归 0（不是缩短） */
@media (prefers-reduced-motion: reduce) {
  #app *,
  #app *::before,
  #app *::after {
    transition-duration: 0s;
    animation-duration: 0s;
    animation-iteration-count: 1;
  }
}

/* 高对比度偏好下把装饰分隔线提到控件边界色（WCAG 1.4.11 的保守做法） */
@media (prefers-contrast: more) {
  #conversation-list > * + *,
  .card__row + .card__row,
  .time-sep::before,
  .time-sep::after {
    border-block-start-color: var(--fc-line-strong);
  }
}

/* ---------------------------------------------------------------------------
 * 复跑自检（W4 实测命令，判定行以当次输出为准）：
 *   ① 零 hex / 零 px：grep -nE "#[0-9a-fA-F]{3,8}|[0-9]+px" \
 *        apps/freechat/web/css/components.css      ⇒ 期望：无输出（rc=1）
 *   ② 引用名闭包：grep -oE -- "--fc-[a-z0-9]+(-[a-z0-9]+)*" <本文件> 的结果
 *        必须 ⊆ docs/design/ui-tokens.md 里出现的 token 名集合
 *   ③ 无「关掉 outline」写法 / 无样式表导入语句 / 无外链：三条 grep 均期望无输出（rc=1）
 * ------------------------------------------------------------------------- */

/* ---------------------------------------------------------------------------
 * 19. 解锁闸门（需求 2026-09-30；契约 docs/design/user-auth-and-cloud.md §1.1）
 *     · `hidden` 的兜底**由 §0 的通用规则统一承担**（`[hidden]{display:none !important}`）——
 *       这里下面两条 `#app[hidden]` / `#login-gate[hidden]` 保留，是为了让"闸门相关"的规则
 *       在本节读得完整；**真正的保证在 §0**（逐个列 id 正是当初漏掉闸门内三个元素的原因）。
 *     · 放行路径（`#login-continue`）**不存在**：放行只由 `passphrase_set=false` 或鉴权未接入
 *       这两种情形在 app.js 里直接表达（结构上没有任何可点的绕过按钮）。
 *     · 只用 token（U2）；闸门是首屏唯一可见面，深色模式由 tokens/themes 承担。
 * ------------------------------------------------------------------------- */
#app[hidden] {
  display: none;
}

#login-gate {
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  min-block-size: 100vh;
  padding: var(--fc-space-24);
  background: var(--fc-bg-canvas);
  color: var(--fc-text-primary);
}

#login-gate[hidden] {
  display: none;
}

/* 「账号面板」形态（2026-10-02 设备级缺陷 5.3）：设置页点「登录 / 注册账号」时，app.js 给
   #login-gate 打上 data-panel="account" —— 同一容器里的**口令卡**藏起来，只留账号卡 + 返回按钮，
   于是"本机库未设口令 ⇒ 放行"的库里也有一条**产品入口**能走到验证码 / 注册 / 登录。
   这不是"解锁闸门的第二种形态"，也不新增放行路径：#app 照旧可见（app.js 不藏它），
   口令契约（passphrase_set=false ⇒ 放行 + #auth-notice 横幅）一个字不改。 */
#login-gate[data-panel="account"] #login-form {
  display: none;
}

/* 「返回」按钮只在账号面板形态下出现；平时 display:none（不用 hidden 属性：§0 的
   [hidden]{display:none !important} 会压掉下面的显示规则）。 */
#login-account-close {
  display: none;
  align-self: flex-start;
}

#login-gate[data-panel="account"] #login-account-close {
  display: inline-flex;
}

.login-card {
  display: flex;
  flex-direction: column;
  gap: var(--fc-space-8);
  box-sizing: border-box;
  inline-size: min(100%, calc(var(--fc-space-64) * 6));
  padding: var(--fc-space-24);
  border: var(--fc-hairline) solid var(--fc-line-subtle);
  border-radius: var(--fc-radius-lg);
  background: var(--fc-bg-surface);
  box-shadow: var(--fc-elevation-2);
}

.login-card__title {
  margin: 0;
  font-size: var(--fc-font-title-size);
  font-weight: var(--fc-font-title-weight);
  line-height: var(--fc-font-title-line-height);
}

.login-card__lead {
  margin: 0;
  color: var(--fc-text-secondary);
  font-size: var(--fc-font-body-size);
  line-height: var(--fc-font-body-line-height);
}

.login-card__label {
  margin-block-start: var(--fc-space-8);
  color: var(--fc-text-secondary);
  font-size: var(--fc-font-label-size);
  font-weight: var(--fc-font-label-weight);
  line-height: var(--fc-font-label-line-height);
}

.login-card__input {
  box-sizing: border-box;
  inline-size: 100%;
  /* 32 → 44：口令 / 手机号 / 验证码三个输入是闸门上的主操作面，按触控目标给（§3 同口径） */
  min-block-size: calc(var(--fc-space-40) + var(--fc-space-4));
  padding: var(--fc-space-8) var(--fc-space-12);
  border: var(--fc-hairline) solid var(--fc-line-strong);
  border-radius: var(--fc-radius-md);
  background: var(--fc-bg-canvas);
  color: var(--fc-text-primary);
  font: inherit;
}

.login-card__input:focus-visible {
  outline: var(--fc-hairline) solid var(--fc-focus-ring);
  outline-offset: var(--fc-hairline);
}

/* 闸门容器（原先是 index.html 上的内联 style，2026-10-06 S5 搬进样式表并**去掉内联**）：
   竖排 = 两张卡各 384 宽、间距 16、总高超过视口时自己滚动。
   为什么不留在内联：内联声明赢过任何样式表规则 ⇒ 只要 flex-direction 还写在那行上，
   下面 §20 的 min-width 断点就永远改不成并排（写了等于没写）。取值与原来逐条相同。 */
.login-stack {
  display: flex;
  flex-direction: column;
  gap: var(--fc-space-16);
  inline-size: min(100%, calc(var(--fc-space-64) * 6));
  max-block-size: calc(100vh - var(--fc-space-32));
  overflow-y: auto;
}

/* 移动端地址栏：dvh 比 vh 准；不支持时沿用上面的 vh（与 layout.css #app 同款兜底） */
@supports (block-size: 100dvh) {
  .login-stack {
    max-block-size: calc(100dvh - var(--fc-space-32));
  }
}

/* 「记住口令」这一行：勾选框本体 24×24，整行 label 给到 44（点文字也算点框）。
   不给 .login-card__label 统一加大：那会把「口令」「手机号」这些字段标签也撑成 44，白占首屏高度。 */
.login-card__check {
  display: flex;
  align-items: center;
  gap: var(--fc-space-8);
  min-block-size: calc(var(--fc-space-40) + var(--fc-space-4));
  cursor: pointer;
}

.login-card__check > input[type="checkbox"] {
  flex: 0 0 auto;
  inline-size: var(--fc-space-24);
  block-size: var(--fc-space-24);
  margin: 0;
}

/* 长解释的折叠块（<details>，首屏信息密度）：**原文一字不改**地留在页里，默认只占一行。
   summary 也是可点控件 ⇒ 跟着 44 的触控口径走。 */
.login-card__more {
  margin-block-start: var(--fc-space-4);
  color: var(--fc-text-secondary);
  font-size: var(--fc-font-caption-size);
  line-height: var(--fc-font-caption-line-height);
}

.login-card__more > summary {
  min-block-size: calc(var(--fc-space-40) + var(--fc-space-4));
  /* 上下等量内边距 = (44 − 16) / 2，把单行 summary 居中在那 44 里（不用 display:flex —— 
     那会把 Chrome 的展开三角一起吃掉，收起/展开就没有可见的可点提示了） */
  padding-block: calc((var(--fc-space-40) + var(--fc-space-4) - var(--fc-font-caption-line-height)) / 2);
  color: var(--fc-text-brand);
  cursor: pointer;
}

.login-card__more > p {
  margin: 0 0 var(--fc-space-8);
}

.login-card__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--fc-space-8);
  margin-block-start: var(--fc-space-8);
}

#login-gate .notice {
  margin: 0;
}

/* 未设口令横幅（#auth-notice 常驻在 #content-pane 顶部）：只做主界面内的间距 */
#auth-notice {
  margin: 0;
}

/* 云端开关的隐私文案：宽位用 113 字主版、窄位换成 69 字短版（同一事实，两种长度）。
   媒体查询用 em（不写 px，U2 的字面量腿保持 0）。 */
.cloud-honest__short {
  display: none;
}

@media (max-width: 40em) {
  .cloud-honest__long {
    display: none;
  }

  .cloud-honest__short {
    display: inline;
  }
}

/* ---------------------------------------------------------------------------
 * 20. 触控与响应式断点（2026-10-06 S5；**经典 media 语法**）
 *
 * 语法纪律（可复跑）：断点只写 (min|max)-(width|height): <长度>。
 * 为什么不能用 MQ4 的区间语法 (width < 64em)：它要 Chrome 104+，而本项目的 Android WebView
 * 实测是 Chrome/83 ⇒ 整条被忽略、手机上永远走桌面三栏（layout.css §8/§9 已记同一坑）。
 * 负向判据（在 web/css/ 目录下对四个 .css 逐个跑）：
 *   grep -nE '@media[^{]*(width|height)[^{]*[<>]' web/css/components.css web/css/layout.css
 *   ⇒ 期望无输出（rc=1）。⚠️ 本行刻意不写 shell 通配（星号斜杠在 CSS 注释里会留下一个多余的
 *   "注释开启符"，把只会数配对的尺子数成 97/96 —— 2026-10-06 实测踩过，去掉后 96/96）。
 *
 * 本节两条断点都是新增的经典语法：
 *   min-width: 64em  —— 闸门两张卡并排（竖排叠起来 > 1000 px，登录/注册那排按钮落在折线以下）
 *   max-height: 40em —— 空态折小（96 插图 + 标题 + 说明 + 操作在 640 高的窗里会顶掉标题）
 * ------------------------------------------------------------------------- */

/* 宽屏：两张卡并排，两张卡的主操作同屏可见 */
@media (min-width: 64em) {
  .login-stack {
    flex-direction: row;
    align-items: flex-start;
    inline-size: min(100%, calc(var(--fc-space-64) * 12));
  }

  .login-card {
    flex: 1 1 0;
    inline-size: auto;
    min-inline-size: 0;
    max-inline-size: calc(var(--fc-space-64) * 6);
  }

  /* 「账号面板」形态只有一张卡：回到竖排，且不许被拉成半屏宽 */
  #login-gate[data-panel="account"] .login-stack {
    flex-direction: column;
    inline-size: min(100%, calc(var(--fc-space-64) * 6));
  }

  #login-gate[data-panel="account"] .login-card {
    flex: 0 0 auto;
  }
}

/* 矮窗：空态折小（插图 96 → 72、间距 16 → 8、内边距 40 → 16），
   否则 640 高的窗里"插图 + 标题 + 说明 + 操作"会被顶掉一截 */
@media (max-height: 40em) {
  .empty-state {
    gap: var(--fc-space-8);
    padding: var(--fc-space-16);
  }

  .empty-state__art,
  [data-testid="empty-state"] > svg {
    inline-size: calc(var(--fc-space-48) * 1.5);
    block-size: calc(var(--fc-space-48) * 1.5);
  }
}

