/* =============================================================================
 * layout.css — freechat 三栏骨架 / 左导航栏 / 面板分隔（W3）
 *
 * owner：W3-shell（本文件由该席独占，其它席不得改）
 *
 * 真源（两份冻结文档；两者冲突时以 ui-contract.md 为准并在回报里点名）：
 *   ① apps/freechat/docs/ui-contract.md   §0 U1/U2、§1 文件清单、§3 DOM 契约
 *   ② docs/design/ui-tokens.md            §2 布局骨架（2.1 4 px 栅格 / 2.2 尺寸 token /
 *                                         §2.3 三栏门槛与降级 / §2.5 骨架示意）
 *
 * 分工边界（**不越界**）：
 *   · 本文件只管"容器几何"：页面基线、#app 的三栏轨道、#rail / #list-pane / #content-pane
 *     的排列与分隔线、两个滚动容器、首屏骨架的尺寸。
 *   · 组件外观（列表项、气泡、按钮、输入区、搜索面板、空态…）一律归 components.css（W4）；
 *     #message-flow / #composer / #search-overlay 的视觉已在那份里，本文件**不重复定义**，
 *     只补"它们作为弹性/网格子项时不被内容撑破"的最小几何（min-inline-size / min-block-size 归零）。
 *   · #list-header / #content-header / #composer 的**内容**归视图（chat.js 等）；
 *     本文件只给容器高度与内边距。
 *
 * 硬约束（可复跑，判定行以当次输出为准）：
 *   R1 零 hex、零 px 字面量：颜色/尺寸一律 var(--fc-*)；连 1 物理像素描边也走 --fc-hairline。
 *      判据（先剥注释行再看，本文件的换算表与"复跑自检"注释里必然出现 px 字样）：
 *        grep -nE '#[0-9a-fA-F]{3,8}|[0-9]+px' apps/freechat/web/css/layout.css \
 *          | grep -vE '^[0-9]+: *[*]'      期望无输出（rc=1）
 *      等价机检：python3 tools/ui-check.py ⇒ l2_u2_px（该腿自己剥注释后统计）
 *   R2 只引用已定义 token：本文件出现的每个 var(--fc-*) 名都必须能在 css/tokens.css 或
 *      css/themes.css 里找到定义（ui-check.py L2 会逐名判红）。
 *   R3 零外部资源（U1）：无导入语句、无外链、无字体文件、无外链图片。
 *   R4 不关闭焦点可见：本文件不出现 outline: none / outline: 0。
 *
 * 单位换算（媒体查询里不能写 var()，所以断点用 em 表达，根字号 16 时 1em = 16px）：
 *   64em = 1024 = --fc-window-min-w（三栏下限，ui-tokens §2.3）
 *   73.75em = 1180 = "会话列表开始收窄"的档位（ui-tokens §2.3 表格第 2 行）
 *   35em = 560 = "输入区折叠"的窗高门槛（ui-tokens §2.3 末段）
 *   本文件**不出现 px 字面量**，上述 px 值只写在注释里供对照。
 *
 * 已知限制（AGENTS.md E1：不许把纪律写成保证）
 *   1. **< 1024 的两栏降级未实现**。ui-tokens §2.3 规定窗口 < 1024 时改成"左导航 + 会话列表、
 *      内容区整屏切换"，但切换需要一个"进入会话"的 JS 钩子（在 #app 上落一个状态属性），
 *      ui-contract §3 的 DOM 契约里没有这个钩子（#app 只有 data-view，五值里没有"会话内/会话外"）。
 *      本文件因此采取**显式兜底**：窗口窄于下限时保持三栏 + 允许页面横向滚动，
 *      而不是做一个没有真源依据的隐式切换。要做真的两栏，先改 ui-contract §3。
 *   2. **窗高 < 640 时不强制塌陷**：§2.3 的 `408 ≤ 640` 是纵向预算，不是几何约束。
 *      本文件只保证消息流吃掉剩余高度（超出即滚动）；"输入区折成 40 单行"由 chat.js 的
 *      折叠开关（#composer[data-collapsed]）承担，本文件不复制它的几何。
 *   3. 真实渲染（层叠结果、滚动条行为、缩放下是否发虚）**本机无无头浏览器 ⇒ 未验收**；
 *      本文件只做过语法/令牌名/零字面量的机检。
 * ========================================================================== */

/* ---------------------------------------------------------------------------
 * 1. 页面基线：整窗高度、无外边距、画布底色
 *    body 的字体栈与 #app 一致（components.css 在 #app 上写了字体栈；
 *    这里给 body 同一条，避免 noscript 空态落到浏览器默认字体上）。
 * ------------------------------------------------------------------------- */
html {
  block-size: 100%;
  background: var(--fc-bg-canvas);
}

body {
  margin: 0;
  block-size: 100%;
  background: var(--fc-bg-canvas);
  color: var(--fc-text-primary);
  font-family: "Inter", "Source Han Sans SC", "Noto Sans CJK SC", system-ui, sans-serif;
  font-size: var(--fc-font-body-size);
  line-height: var(--fc-font-body-line-height);
  /* 窄窗口兜底（见文件头"已知限制 1"）：三栏不压缩到 680 以下，宁可横向滚动 */
  overflow-x: auto;
}

/* ---------------------------------------------------------------------------
 * 2. #app：三栏轨道 [rail | list | content]
 *    轨道宽全部取 token：64 / 300 / 剩余（内容区下限 680，ui-tokens §2.2/§2.3）
 *    #search-overlay 是 position: fixed（components.css），不参与网格，无需占位。
 * ------------------------------------------------------------------------- */
#app {
  display: grid;
  grid-template-columns: var(--fc-rail-w) var(--fc-list-w) minmax(var(--fc-content-min-w), 1fr);
  grid-template-rows: 100%;
  box-sizing: border-box;
  block-size: 100vh;
  min-inline-size: var(--fc-window-min-w);
  min-block-size: var(--fc-window-min-h);
  overflow: hidden;
}

/* 动态视口单位更准（移动端地址栏）；不支持时沿用上面的 100vh */
@supports (block-size: 100dvh) {
  #app {
    block-size: 100dvh;
  }
}

/* ---------------------------------------------------------------------------
 * 3. #rail：固定 64 轨道，纵向图标列，右侧一条 hairline 分隔
 *    §6.1 原式：内边距 12（两侧 12 + 项宽 40 = 64），项间距 --fc-space-4
 *    2026-10-06（S5）改：横向内边距 12 → 10（= --fc-space-8 + --fc-hairline * 2），
 *    因为项宽 40 → 44（触控口径，components.css §6）；44 + 2×10 = 64 仍是 --fc-rail-w，
 *    恰好整除、不触发本容器的 overflow-x: hidden 裁切。纵向内边距仍是 12。
 *    展开态由 components.css 按 #rail[data-expanded="true"] 处理宽度相关的项；
 *    这里补容器在展开态下的轨道宽（--fc-rail-w-expanded = 208）
 * ------------------------------------------------------------------------- */
#rail {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--fc-space-4);
  box-sizing: border-box;
  inline-size: var(--fc-rail-w);
  padding: var(--fc-space-12) calc(var(--fc-space-8) + var(--fc-hairline) * 2);
  border-inline-end: var(--fc-hairline) solid var(--fc-line-subtle);
  background: var(--fc-bg-sunken);
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: none;
}

#rail[data-expanded="true"] {
  inline-size: var(--fc-rail-w-expanded);
  align-items: stretch;
}

/* ---------------------------------------------------------------------------
 * 4. #list-pane：会话列表栏（固定轨道，列头 56 + 可滚动列表）
 * ------------------------------------------------------------------------- */
#list-pane {
  display: flex;
  flex-direction: column;
  min-inline-size: 0;
  min-block-size: 0;
  box-sizing: border-box;
  border-inline-end: var(--fc-hairline) solid var(--fc-line-subtle);
  background: var(--fc-bg-surface);
}

#list-header {
  display: flex;
  align-items: center;
  box-sizing: border-box;
  block-size: var(--fc-list-header-h);
  min-block-size: var(--fc-list-header-h);
  padding-inline: var(--fc-space-12);
  border-block-end: var(--fc-hairline) solid var(--fc-line-subtle);
  background: var(--fc-bg-surface);
}

/* 列头行：标题 + 说明 + 弹性占位 + 动作按钮（chat.js 也会画同一结构并自带等价内联样式） */
.list-header__row {
  display: flex;
  align-items: center;
  gap: var(--fc-space-8);
  inline-size: 100%;
  min-inline-size: 0;
}

.list-header__spacer {
  flex: 1 1 auto;
}

#conversation-list {
  flex: 1 1 auto;
  min-block-size: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
}

/* ---------------------------------------------------------------------------
 * 5. #content-pane：会话头 56 + 消息流（弹性）+ 输入区 96
 *    minmax(0, 1fr) 保证消息流是那个"吃掉剩余高度并自己滚动"的行，
 *    内容再长也不会把 #composer 顶出窗口（§2.3 的纵向预算）。
 * ------------------------------------------------------------------------- */
#content-pane {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  min-inline-size: 0;
  min-block-size: 0;
  box-sizing: border-box;
  background: var(--fc-bg-canvas);
}

#content-header {
  display: flex;
  align-items: center;
  gap: var(--fc-space-8);
  box-sizing: border-box;
  block-size: var(--fc-content-header-h);
  min-block-size: var(--fc-content-header-h);
  padding-inline: var(--fc-space-24);
  border-block-end: var(--fc-hairline) solid var(--fc-line-subtle);
  background: var(--fc-bg-surface);
}

/* 消息流的视觉（内边距/滚动/底色）在 components.css；这里只保证它是可收缩的网格项 */
#message-flow {
  min-block-size: 0;
}

/* ---------------------------------------------------------------------------
 * 6. [hidden] 兜底：本文件给这些容器写了 display（flex/grid），
 *    而 [hidden] 的 UA 规则是 display: none 的**同权重**声明 —— 后加载的作者样式会赢，
 *    于是 hidden 属性失效。app.js 会写 `#composer.hidden = view !== 'chat'`
 *    （preparePanes：输入区只属于聊天视图），所以这条不是装饰，是修 bug。
 * ------------------------------------------------------------------------- */
#composer[hidden],
#list-pane[hidden],
#content-pane[hidden],
#message-flow[hidden],
#rail[hidden] {
  display: none;
}

/* ---------------------------------------------------------------------------
 * 7. 首屏骨架（index.html 里的静态占位；视图渲染前可见，渲染时被整段替换）
 *    骨架条本体用 components.css 的 .skeleton（颜色/呼吸动效归 W4），
 *    这里只给"骨架条在骨架项里的尺寸与排布"。
 * ------------------------------------------------------------------------- */
.chat-item--skeleton {
  cursor: default;
  pointer-events: none;
}

.chat-item__skeleton-avatar {
  flex: 0 0 auto;
  inline-size: var(--fc-avatar-lg);
  block-size: var(--fc-avatar-lg);
  border-radius: var(--fc-radius-full);
}

.chat-item__skeleton-line {
  display: block;
  block-size: var(--fc-font-body-strong-size);
  margin-block-end: var(--fc-space-8);
}

.chat-item__skeleton-line:last-child {
  margin-block-end: 0;
}

.chat-item__skeleton-line--short {
  inline-size: 60%;
}

.msg--skeleton {
  pointer-events: none;
}

.bubble__skeleton-line {
  display: block;
  block-size: var(--fc-font-body-size);
  min-inline-size: var(--fc-bubble-max-w);
  margin-block-end: var(--fc-space-8);
}

.bubble__skeleton-line:last-child {
  margin-block-end: 0;
}

.bubble__skeleton-line--short {
  min-inline-size: 0;
  inline-size: 60%;
}

/* ---------------------------------------------------------------------------
 * 8. 窄窗口：会话列表按 §2.3 收窄（clamp(240, W − 64 − 2 − 680, 300)）
 *    断点 73.75em = 1180px；100vw 减掉 rail、两条 hairline 与内容区下限。
 *    注意：这里**不**改内容区下限，所以窗口真的放不下时由 body 的横向滚动兜底。
 *    ⚠️ 写法必须是 (max-width: …)，**不能**用 MQ4 的区间语法 (width < …)：后者要 Chrome 104+，
 *    而 Android 版 WebView 实测是 Chrome/83 ⇒ 区间语法整条被忽略、手机上永远走桌面三栏
 *    （2026-10-02 设备实测，/hf3fs-jd/data/tmp/freechat-auth/device-account.md §5.2）。
 * ------------------------------------------------------------------------- */
@media (max-width: 73.75em) {
  #app {
    grid-template-columns:
      var(--fc-rail-w)
      clamp(
        var(--fc-list-w-min),
        calc(100vw - var(--fc-rail-w) - var(--fc-hairline) * 2 - var(--fc-content-min-w)),
        var(--fc-list-w)
      )
      minmax(var(--fc-content-min-w), 1fr);
  }
}

/* ---------------------------------------------------------------------------
 * 9. 窄窗高：输入区自动折成一行（§2.3 末段"窗口高 < 560 时折叠为 40 单行"）。
 *    几何与 components.css 的 #composer[data-collapsed="true"] 保持一致；
 *    区别只是这里由**窗口高度**触发，不依赖 chat.js 的折叠开关有没有被点过。
 *    ⚠️ 同 §8：(max-height: …) 是 WebView 83 认的写法，MQ4 的 (height < …) 不认。
 * ------------------------------------------------------------------------- */
@media (max-height: 35em) {
  #composer {
    min-block-size: var(--fc-space-40);
    max-block-size: var(--fc-space-40);
    padding-block: var(--fc-space-8);
  }

  /* 折成 40（8 + 24 + 8）时文本区必须回到 24：components.css §12 给文本区定的 44 触控下限
     会顶破这 40 的固定高度。这里是**窗高**触发，没有 data-collapsed 属性可挂 ⇒ 同一条回落
     必须在 components.css §12 与本节各写一次（只改一处就漏）。 */
  #composer .composer__input,
  #composer textarea {
    min-block-size: var(--fc-space-24);
    padding-block: 0;
  }

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

/* ---------------------------------------------------------------------------
 * 10. 无障碍辅助：只给读屏的文本（视觉隐藏但仍在可访问树里）
 * ------------------------------------------------------------------------- */
.visually-hidden {
  position: absolute;
  inline-size: var(--fc-hairline);
  block-size: var(--fc-hairline);
  margin: calc(var(--fc-hairline) * -1);
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---------------------------------------------------------------------------
 * 复跑自检（本席实测命令，判定行以当次输出为准）：
 *   ① 零 hex / 零 px（剥掉注释行）：
 *        grep -nE '#[0-9a-fA-F]{3,8}|[0-9]+px' apps/freechat/web/css/layout.css \
 *          | grep -vE '^[0-9]+: *[*]'   ⇒ 期望：无输出（rc=1）
 *   ② 引用名闭包：grep -oE -- '--fc-[a-z0-9]+(-[a-z0-9]+)*' apps/freechat/web/css/layout.css
 *        的结果必须 ⊆ css/tokens.css ∪ css/themes.css 里定义过的名字
 *        （等价机检：python3 tools/ui-check.py --tokens ⇒ l2_tokens_undefined=0）
 *   ③ 零外部资源（同样先剥注释行；下面这行自己的模式串会自匹配）：
 *        grep -nE 'http://|https://|//cdn|@import' apps/freechat/web/css/layout.css \
 *          | grep -vE '^[0-9]+: *[*]'   ⇒ 期望：无输出（rc=1）
 *   ④ 不关焦点：grep -nE 'outline: *(none|0)' apps/freechat/web/css/layout.css \
 *        | grep -vE '^[0-9]+: *[*]'   ⇒ 预期：无输出（rc=1）
 * ------------------------------------------------------------------------- */
