# DESIGN-site —— skillpress 站点设计规范 v0.1

> **这份文档的性质**：站点**界面与信息架构**的**唯一设计依据**。
> 从它往下才是实现（`shell/*.mbt` + 引擎 + moobile）。
> 与既有文档的关系：`SPEC.md` 管**内容源**（skills 怎么写），`PLAN.md` / `PLAN-ui.md` 管**做什么**与**已发现的缺陷**，
> 本文管**长什么样、为什么这样、怎么算做对了**。
>
> **"全盘重来"的边界**（用户 2026-10-07 定）：
> - **保留**：内容管线（引擎解析 / 构建期高亮 / 门 / 逐字节对账）、内容源的唯一性、moobile 的 TEA 与 mount、
>   `Span` / `Block` 这套数据形状（可扩字段，不推倒）。
> - **重写**：`shell/` 那一整套界面（现在是把内容字段直接当界面字段用，见 `PLAN-ui.md` §11.2）。
> - **本文不写代码**。它先定"要什么"，实现再按它补洞。
>
> ⚠️ 每条设计都标了 **【实现依赖】**：今天 moobile 表达不了的那些，会在这里被点名（不是悄悄绕过）。
> 不确定的一律写「未验证」，不许当已成立。

---

## 1. 目标与验收

**一句话**：读者打开它，**30 秒内知道这是什么、该从哪读**；并且愿意把它发给别人。

**五条验收（都可自动化或可并排看截图）**

| # | 验收 | 判据 |
|---|---|---|
| A1 | **能分享** | 每一页有独立 URL；刷新/后退/直达都对 |
| A2 | **读得下去** | 正文 ≥15px、行高 ≥1.7、行长 **38–47 个中文字**（720px ÷ 15.5px = 46.5 字 —— 上界原来写 46，与 720 自相矛盾，已改）；标题层级 3 档以上 |
| A3 | **找得到** | 任一页到任一页 ≤2 次操作（侧栏 / 目录 / 搜索 / 上下页） |
| A4 | **两套主题都成立** | 明暗 × N 套，**任何底色面上**的正文 ≥4.5、次要文字也 ≥4.5（原来对"次要"只要求 ≥3 —— 实测 `--fg-3` 在白底 3.18 勉强算过，一落到侧栏底就掉到 2.97，所以这条改成"任何底、且次要也是 4.5"） |
| A5 | **不像半成品** | 与 VitePress 同页并排截图，"读者感知"逐项不落下风；无空白首屏、无死链接、无层级断裂 |

**反面验收（这几条出现即红）**：一个页面在 HTML 里没有内容；点了没反应的元素；读者的视线落在"机器名"上；两栏挤在一起；夜里刺眼；
**样式写了但选择器一个都没命中**（原型真犯过，见 §11 —— 判据要查"生效的读数"，不是"有没有写"）。

---

## 2. 信息架构（先定这个，其余从它长出来）

### 2.1 四类页面

| 页面 | 是什么 | 例子 |
|---|---|---|
| **首页** | 一屏说清"这是什么 + 从哪开始" | `/` |
| **书架** | 全部 skill 的总览（卡片/列表） | `/shelf/` |
| **主题页** | 一份 skill 一页（= 一个主题） | `/s/<slug>/` |
| **子页** | 那份 skill 的 `references/`（深水区） | `/s/<slug>/<kid>/` |
| 系统页 | 404 / 空态 / 加载 | `/404` 等 |

⇒ 四类页面**各有各的模板**，不再"首页与文档区共用一套渲染"（现状两模式就是这么坏的）。

### 2.2 URL 方案（**【实现依赖】今天没有 URL**）

```
/                              首页
/shelf/                        书架总览
/s/<skill-slug>/               主题页
/s/<skill-slug>/<kid-name>/    子页
/404                           未找到
```

- 一条规矩：**URL 只用机器名**（`slug` / `name`），界面上的名字一律用中文 `title`。
- 【实现依赖】站点现在切页只发 `Msg::Go`（`shell/*.mbt` 里 `history/location/hash` **零命中**）⇒
  要么在宿主层补 history/hash 路由，要么把 `nav` 包在 web 上接起来（它在原生端不适用，站点是 web-only）。

### 2.3 命名规矩（**这条治"三种写法各行其是"**）

| 界面位置 | 只能用什么 | 规矩 |
|---|---|---|
| 顶栏导航 | 站点配置 `nav[].label` | ≤6 字、无序号、无标点（"文档"而不是"一、站点的形状"） |
| 侧栏一级 | skill 的 `title`（中文） | ≤12 字 |
| 侧栏二级 | 子页的 `title`（中文） | ≤16 字 |
| 页面大标题 | 同上（与侧栏一致） | —— |
| 面包屑 | 同上（各级 title） | —— |
| 辅助行 / kicker | `slug` / `name`（mono、小字、次要色） | **机器名只准出现在这里** |
| Agent 出口 | 源文件路径（mono） | 见 5.x |

⇒ 顶栏导航**不再吃正文的 `##`**（现状 D3 就是那么干的：内容是章节标题 → 顶栏变成目录）。
导航来源改为**站点配置**（`.skillpress/` 下），内容标题与导航彻底解耦。

**"四处"是硬约束，不是修辞**：机器名出现在第五处（原型第一版就是：侧栏每行下面挂一行 slug，一列 10 个）
⇒ 侧栏立刻从"树"变成"两行制的机器名清单"，中文标题被挤成配角。**侧栏只准显示 `title`**，
slug 想给 Agent 看，走面包屑末位与 Agent 面板（那两处本来就是给机器准备的）。

### 2.4 层级与广度

- 侧栏：一级（skill）默认**只展开当前那一份**；分组标题用"Skill 书架 · N 份"这类**统计信息**，不编号；
- 一级 ≥8 份时给出**书架页**作为总览出口（侧栏底部一条"全部 N 份 →"）；
- 二级不做三级：`references/` 里的再分层用**页内目录**解决，不加深侧栏。

---

## 3. 设计 token

### 3.1 结构：两层，不是一层

```
原语（primitive）  --n-0 … --n-900（中性灰 9 档）、--brand-1…4、每个语义色 4 档
        ↓ 只在这里写死色值
语义（semantic）   --canvas / --surface / --surface-2 / --inset / --overlay /
                   --fg-1 / --fg-2 / --fg-3 / --line-1 / --line-2 /
                   --brand / --brand-hover / --brand-soft / --brand-ring /
                   --state-hover / --state-active /
                   --code-bg / --code-line / --tok-* / --note* / --tip* / --warn* / --danger*
        ↓ 组件**只准**引用语义名
尺度（scale，明暗不变量）  --sp-* / --fs-* / --lh-* / --radius-* / --shadow-* / --dur-*
```

**铁律**：渲染代码里**不许出现字面色号**（可判：grep + 白名单）。
现状破了这条：颜色分散在 `theme.mbt`（13 个）与 `blocks.mbt` 的 `tok_color`（9 个）两处 —— 换皮必漏，深色做不出。

### 3.2 颜色（真值；浅色 / 深色成对）

中性灰（偏冷、低饱和）与品牌色（靛紫）：

| 语义 | 浅色 | 深色 | 用途 |
|---|---|---|---|
| `--canvas` | `#ffffff` | `#18181b` | 页面底 |
| `--surface` | `#ffffff` | `#18181b` | 正文卡片 |
| `--surface-2` | `#f7f7f8` | `#131316` | 侧栏/次级面 |
| `--inset` | `rgba(120,124,150,.14)` | `rgba(113,113,122,.18)` | 行内代码、kbd |
| `--overlay` | `#ffffff` | `#1f1f23` | 浮层（下拉/搜索/sheet） |
| `--fg-1` | `#35363b` | `#e4e4e7` | 正文 |
| `--fg-2` | `#64656c` | `#a1a1aa` | 次要文字（**可读的一切次要文字都用它**：分组标题、侧栏行、目录行、meta） |
| `--fg-3` | `#8f9096` | `#71717a` | **只给"机器串与装饰"**（内容根指纹、`kbd` 边框、分隔点）。它落在 `--canvas` 上 3.18、落在 `--surface-2` 上只有 **2.97**（暗色 3.84）⇒ **不许承载可读文字** |
| `--line-1` | `#e5e5e8` | `#27272a` | 分隔线 |
| `--line-2` | `#c8c9ce` | `#3f3f46` | 需要被看见的边界 |
| `--brand` | `#4f46e5` | `#a5b4fc` | 链接、当前项、强调 |
| `--brand-hover` | `#4338ca` | `#c7d2fe` | 悬停 |
| `--brand-soft` | `rgba(79,70,229,.12)` | `rgba(79,70,229,.20)` | 选中底、行内码底 |
| `--brand-ring` | `rgba(79,70,229,.35)` | `rgba(165,180,252,.45)` | 焦点环 |
| `--state-hover` | `rgba(24,24,27,.045)` | `rgba(255,255,255,.055)` | 悬停底（**落在 `--canvas` 上**） |
| `--state-hover-side` | `rgba(24,24,27,.07)` | `rgba(255,255,255,.085)` | 悬停底（**落在 `--surface-2` 上**）：同一叠色在侧栏灰底上会被吃掉，所以单开一档 |
| `--code-bg` | `#f7f7f8` | `#131316` | 代码块底 |
| `--note/tip/warn/danger` | `#2b5db8 / #15803d / #9a6a10 / #b3261e` | `#8ab4f8 / #6ddba0 / #e8c06a / #f2938c` | 语义色（**本轮只用于链接态与错误**，标注块不做） |

**四条规则（前三条抄 VitePress 学到的，值是我们自己的；第四条是原型改出来的）**：
1. **淡底一律半透明**（`rgba`）—— 跟着底走，所以同一套 token 进深色天然成立；
2. **深色海拔用更亮**：`--overlay` 深色下比 `--canvas` **亮**（`#1f1f23 > #18181b`），而次级面 `--surface-2` 比它**暗**（`#131316 < #18181b`）；
3. **一级强调只有色相不同、明度要够**：深色品牌色必须比浅色**更亮**（`#a5b4fc` vs `#4f46e5`），否则在深底上读不出；
4. **"次要"不等于"可以看不清"**：可以读的文字一律 ≥4.5（含分组标题、侧栏行、目录行）。
   `--fg-3` 是为**机器串与装饰**留的（指纹、kbd、分隔点），它的使命是"退到背景里"，不是"承载信息"。
   ⚠️ 一条实测量的教训：同一个 token 换个底就掉档 —— `--fg-3` 在白底 3.18、在侧栏底 2.97（暗色 3.84），
   所以**对比度判据必须带上"底"这个参数**，`对比度(x, 白底)` 过不代表成立。

### 3.3 尺度

| 族 | 值 |
|---|---|
| 间距 `--sp-*` | 4 / 8 / 12 / 16 / 24 / 32 / 48（**只用这 7 档**；现状混用 6/7/10/12/14/18/24/32） |
| 字号 `--fs-*` | 11.5（meta）/ 12.5（辅助）/ 13.5（小正文）/ **15.5（正文）** / 17（h3）/ 22（h2）/ 30（h1） |
| 行高 `--lh-*` | 1.45（标题）/ 1.6（辅助）/ **1.75（正文，中文要宽）** |
| 字距 | 正文 `0`；大标题 `-0.01em`；全大写小标签 `0.06em` |
| 圆角 `--radius-*` | 4 / 6 / 10 / 14 / full |
| 阴影 `--shadow-*` | 5 档，全部 4%–16% 透明（现状 **0 档**，全靠 1px 边框撑层次） |
| 宽度 | 正文 `720px`（≈46 个中文字）/ 侧栏 `272px` / 右栏 `220px` / 顶栏 `56px` / 站点最大宽 `1440px` |
| 动效 `--dur-*` | 120ms（悬停）/ 200ms（浮层）；**【实现依赖】`transition` 今天编译期就写不出来** |

### 3.4 中文排版（我们跟 VitePress 不同的地方，必须自己定）

- 中文正文行高 **1.75**（比拉丁文需要更宽），字号 **15.5px**（14px 中文在桌面偏小）；
- 中西混排：数字/英文用 `font-variant-numeric: tabular-nums`，代码与正文之间**手动留半角空格**（写作规矩，不是 CSS）；
- 正文与代码/机器名并存时的**字号阶梯**要明显：正文 15.5 / 行内码 13.5 / 辅助 12.5 / meta 11.5；
- 字体栈：`system-ui, "PingFang SC", "Microsoft YaHei", "Noto Sans SC"`（**不加载 web 字体**）；
  等宽：`ui-monospace, "SF Mono", Consolas, Menlo` —— **等宽只给代码、机器名、kicker**（现状把它用在站名与页标题上，是气质损失的来源）。
- **中文小标签不要抄拉丁那套**：`text-transform:uppercase` 对中文是空操作，`letter-spacing:.06em` 只会让 11.5px 的中文"散架"
  （原型第一版的侧栏分组标题就是这样：11.5px + 正字距 + 灰 `--fg-3`，看上去发虚）。
  小标签 = **12.5px / 字距 0 / `--fg-2` / 600 字重**；正字距只留给纯拉丁的全大写标签（如 `KBD`）。

---

## 4. 布局骨架

### 4.1 桌面（≥1280px）

```
┌──────────────────────────────────────────────────────────────────────┐
│ 顶栏 56px：白底 + 1px 底边（不是深色条）                              │
│  [skillpress]  文档  书架  规矩        [⌘K 搜索] [◐ 主题] [GitHub]  │
├────────────┬───────────────────────────────────────┬─────────────────┤
│ 侧栏 272   │ 正文 720（居中，两侧留白）             │ 右栏 220        │
│ 分组标题   │ 面包屑 / kicker                        │ 本页目录        │
│ 一级·二级  │ H1 + 一句话定位                        │ 当前节高亮      │
│ 当前项左条 │ 正文块（标题带锚点 / 代码块带头 / 表格）│ 回到顶部        │
│ 独立滚动   │ 上一页 / 下一页                        │ 独立滚动        │
│            │ 页脚（来源 · 许可 · 生成时间 · 版本）   │                 │
└────────────┴───────────────────────────────────────┴─────────────────┘
```

**三栏各自独立滚动**（`侧栏/右栏 = sticky + 视口高 - 顶栏`，正文滚自己的容器）。
**【实现依赖】** `@style` 没有 `sticky`、也没有媒体查询 ⇒ 只能靠"根容器固定高 + 三栏各自 overflow"这套**结构**表达；
这需要先把 P2 那条"实际滚的是整个 document"的未决项解决。

#### 4.1.1 侧栏的行语言（原型改出来的四条，写死）

| # | 规矩 | 为什么 |
|---|---|---|
| 1 | **一行一个链接**：中文 `title`，单行省略（`nowrap + ellipsis`，完整文本进 `title` 属性） | 换行 ⇒ 行高参差 ⇒ 一列"毛边"；单行 ⇒ 每行等高、左边缘一条干净的竖线 |
| 2 | **机器名不进侧栏**（见 §2.3） | 两行制（标题 + slug）把树读成"机器名清单"，中文标题降级成配角 |
| 3 | **层级靠缩进 + 引导线，不靠符号**：雪佛龙列 12px，1px 引导线落在雪佛龙**轴心**；**二级标题与一级标题同一列** | "再缩进一点"会让每一级各有一个左边缘；对齐的竖边比"看起来有层次"更整齐 |
| 4 | **状态只有一条通道**：行左侧 2px 圆头条 —— hover 灰（`--line-2`）、当前靛紫（`--brand`） | 底色那点变化在侧栏灰底上只有 **1.15:1**（实测），肉眼几乎不可见；左条不依赖亮度差 |
| 5 | **本页目录是树的第三级，挂在"正在读的那个文件"下面**（缩进再进一格 = +16px，字号小一档 12.5） | 读者眼里"目录"属于**这一页**，不属于侧栏；挂到侧栏底部会让它变成一个和书架平级的栏目（那是 IA 错误，不只是不好看） |

配套：分组标题 = "Skill 书架 · N 份"（统计信息，不编号，见 §2.4）；雪佛龙**只展开/收起、不跳页**
（点一个看起来像控件的箭头却跳走，就是假控件）；当前那份默认展开，其余收起，**手动展开的选择本次会话内记住**（§9 第 3 条）。

### 4.2 断点阶梯（**Model 驱动，不是 CSS 媒体查询**）

**阶梯只有三档，判据是"目录往哪让"** —— 放不下的时候，第一个让位的不是目录的**内容**，而是目录的**轨道**：

| 档 | 视口 | 版面 | 本页目录在哪 | 为什么这么切 |
|---|---|---|---|---|
| **rail 3** | ≥1280 | 三栏：侧栏 272 / 正文 / 目录轨 220 | 右栏那条**轨**（细线 + 当前段） | 272+64+720+220 = 1276 ⇒ 1280 是"三样都放得下"的最小宽 |
| **rail 2** | 1024–1279 | 两栏：侧栏 / 正文 | **塞进侧栏，且挂在"正在读的那个文件"节点下面**（树的第三级，见 4.1.1 第 5 条），跟侧栏一起滚、仍跟读高亮 | 宁可让目录"换个地方待着"，也不让它挤压正文：这档正文仍有 688–720px = 44–46 字（A2 成立） |
| **rail 1** | <1024 | 一栏 + 三个贴边按钮（见 4.3） | 同上（在侧栏里，而侧栏整个进"书架"sheet）；另给"目录"按钮一个直达 sheet | 一栏放不下任何侧向内容 |

**"塞进侧栏"塞到哪一格，有三个候选，选了第三个**：
① 侧栏底部单独一段 —— 那等于把"本页目录"提升成**和书架平级的栏目**（IA 错误，不只是不好看）；
② 正文顶部一个"本页目录"块 —— 把机器的骨架塞进人的阅读流，还要滚回去找；
③ **挂在当前文件节点下面**（选定）—— 读者读到哪一条、它的分节就在那一条底下，层级关系一眼可读，
   而且它的高亮和树的"当前项"在同一个视觉通道里（左条）。
**兜底**：树里没有对应节点的页面（首页 / 书架 / llms / 404）挂在树末，带一条分隔线（`.stocwrap`）。

**怎么切档**：视口宽度写进根节点的 `data-rail`（原型：JS 读 `innerWidth` + 三条 `matchMedia` 监听；真站：`@sub.current_viewport` + `on_resize` 改 Model）。
**不用 CSS 媒体查询**——moobile 里没有媒体查询，"设计必须与实现同形"这条在这儿才不算空话。
顶栏的收窄（`data-top=narrow`）**单独一条**：那是"这一排排不下"，不是"版面换了"（<700px 导航进 sheet、搜索收成图标）。

**换档要处理两件事**（原型都做了，真站照着做）：
① **目录要搬家** —— 只渲染一份内容（同一份锚点标记），按档写进"当前落点"；rail 3 是右栏轨、rail 2/1 是树里当前节点的第三级；
   换档时侧栏结构要重建一次（目录要进树 / 出树），重建之后再把目录写进新的挂载点；
② **别丢"当前节"** —— 重挂之前先记下当前高亮的那个 id，重挂后补回去（否则读者一改窗口，目录的高亮就没了）。

⚠️ **原型改之前是错的**：三栏一直挂到 1024 —— 1100px 下正文只剩 586px = **37.8 字**（A2 破线），
而目录还占着 220px。体检当时只查了"没有横向溢出"，所以这条**从体检报告里漏了出去**（见 §11 的账）。

**【实现依赖】** 视口宽度进 Model（`@sub.current_viewport` + `on_resize` 都已接宿主能力 ✅）——这条是今天就能做的。

### 4.3 移动端（用户口述 + 细化）

三个**贴边浮动按钮**，各自弹一个**从下往上**的 sheet：

| 按钮 | 位置 | sheet 内容 |
|---|---|---|
| 导航 | 屏幕**左上** | 站名 + 一级导航；**栏目（原顶栏那排）= 它的下一级** |
| 书架 | 屏幕**左下** | 全部 skill 树（分组、当前项高亮）**+ 本页目录**（rail 2 起目录就住在侧栏里，sheet 是它的镜像） |
| 目录 | 屏幕**右下** | 本页目录（当前节高亮）—— 与"书架"sheet 里那一段是同一份内容，两个入口（一个"随手看这一页"，一个"连书架一起看"） |

**两级交互**：平时**贴边停靠**（半藏，不挡内容）→ **第一次点：按钮浮出**（离开边缘、进入可见态）→ **第二次点：sheet 弹出**。
sheet：高度上限 70vh、圆角上沿、下滑关闭、点遮罩关闭、**三个互斥**。

**【实现依赖】—— 2026-10-07 更新：三样里少了半样，另外两样还在**：

| # | 依赖 | 今天的状态 |
|---|---|---|
| ① | **portal**（浮层不被父容器 `overflow` 裁） | ❌ 仍然没有。绕法还是老那条：**根不滚 + 绝对定位挂在根上**（等价 fixed），所以浮层必须渲染在 `layout.shell` 的最后一个孩子里 |
| ② | **遮罩 + 点击穿透**（`@style` 里 `pointer` 命中 0） | ❌ 仍然没有 `pointerEvents`。所以遮罩**只在需要时画**（关着的时候页面上一个多余的可点区域都没有），不是靠 `pointer-events: none` 让它透明 |
| ③ | **过渡**（`transition`） | ❌ 仍然没有（乙级，D-UI-4 明确不做）⇒ "浮出"与"弹出"**还是硬切** |
| ~~④~~ | ~~**位移**（"浮出"要挪 22px）~~ | ✅ **已还清（`transform`）**：`Style::transform` 落地之后，"半藏/浮出"用 `Translate` 表达，**不再用负偏移** ⇒ 也不再把横向滚动宽撑出去（实现在 `shell/mobile.mbt`，读数 `tools/ui-probe.mjs`：**布局 `offsetLeft` 不变、`transform` 从 `matrix(…,22,0)` 变到 `matrix(…,-12,0)`**）。⚠️ 别把它读成"有动画了"—— 那要 ③ |

---

## 5. 组件清单（站点要造的"件"）

| 件 | 数据来源 | 状态 | 依赖 |
|---|---|---|---|
| 顶栏 | **站点配置**（nav）+ 主题开关 + 搜索入口 | 悬停 / 当前项 / 移动端折叠 | 配置（见 §7） |
| 侧栏树 | 生成物 `skills()`（分组 + 一级 + 二级 + 当前项） | 展开/收起（雪佛龙，**只展开不跳页**）/ 当前项左条 / 独立滚动 | 独立滚动（4.1）+ 行语言（4.1.1） |
| 面包屑 | 当前页的层级 | —— | 契约加"父级" |
| 页内目录 | 生成期算的标题层级 + 锚点 | 当前节高亮；**落点随档变**：rail 3 在右栏那条轨；rail 2/1 作为**树的第三级挂在当前文件节点下面**（见 4.1.1 / 4.2） | 锚点（引擎）+ 视口进 Model |
| 正文块 | `Block` | —— | 链接/图片（契约加 `Span::Link` / `Block::Image`） |
| 代码块 | `Code(lang, text, runs)` | 悬停显示复制 | 语言标签 + 复制（**渲染层，不需新语法**） |
| 表格 | `Table` | —— | 改真表格：**列宽按内容、横向可滚**（现状 div 等分列宽） |
| 上下页 | 生成期算页序 | 悬停 | —— |
| 搜索 | **构建期索引** | 打开/输入/命中/键盘 | 面板需浮层（portal） |
| 主题开关 | 站点配置 + 用户选择 | 三态（跟随/亮/暗） | 偏好存储（**今天没有 KV 存储** ⇒ 先只做"跟随系统"） |
| 页脚 | 生成物（来源/许可/版本/生成时间/内容根） | —— | 生成期多带几个字段 |
| Agent 出口 | 该页源文件路径 + `/llms.txt` | 复制 | 生成期算（**我们的差异点**） |
| 404 / 空态 / 加载 | —— | —— | 需要"骨架屏"（首屏 JS 未就绪时） |

---

## 6. 页面模板

- **首页**：H1（一句定位）+ 3 张入口卡（从哪开始 / 书架 / 规矩）+ "这里有 N 份 skill、M 个子页"的统计行 + 页脚。
  **不吃正文 `##` 当导航**（现状顶栏就是那么坏的）。
- **书架**：按分组列出全部 skill（标题 + 一句话 + 子页数），每张卡可点。
- **主题页 / 子页**：面包屑 → kicker（机器名）→ H1 → 定位句 → 正文块 → 上下页 → 页脚 + Agent 出口。
- **404**：一句人话 + 回首页/书架两个出口。
- **空态**：内容根为空时给"怎么开始"的指引（引擎今天退 2；界面要有对应画面）。

---

## 7. 站点配置（`.skillpress/` 下的那一份）

**【实现依赖】** 需要新增。建议**扁平单行标量**（复用 G1"都是单行标量"的解析器，不写 TOML 解析器）：

```
title = skillpress
nav.1.label = 文档
nav.1.href  = /shelf/
nav.2.label = 规矩
nav.2.href  = /rules/
theme.palette = indigo
theme.brand   = #4f46e5
footer.license = Apache-2.0
```

两条规矩：① 它是**站点输入** ⇒ 进 G8 指纹（与首页源同例）；② `attach` 已存在就**不覆盖**（现状就是这么守手写文件的）。

**⚠️ 2026-10-07 用户当场问的那一下（值得留档）**："站点配置这玩意不就是 WEBSITE.md？"
——**今天确实就是**（顶栏那排 = 首页源里那几个 `##`），而那正是 §2.3 点名的 D3。三条实证：

| 证据（生成物/源文件原文） | 撞了哪条规矩 |
|---|---|
| 首页源的 `##` 是"一、两份 skill""二、这个站点怎么起来的""四、两份都在下面""五、改完必须跑的" | §2.3：顶栏 nav **≤6 字、无序号、无标点** —— 它们三个条件都不满足 |
| 其中一节的下拉菜单 label 是 `moobile-app-development` / `moobile-pitfalls` …（**slug**） | §2.3：机器名**只准**出现在 kicker / 面包屑末位 / Agent 面板 / URL —— 进 nav 就是第五处 |
| 改一次首页正文（增删一个小节），顶栏就跟着变 | IA 跟着**内容**漂：同一份内容印成两个站点（D16）时，nav 却改不动 |

⇒ 三个选项，**已拍 ①**：
① **独立输入**（本节写的 `.skillpress/` 那份）：nav 是**站点参数**，不属于任何一页。**代价**：多一个输入 + 进指纹 + 要 `attach` 守手写。
② 留在 WEBSITE.md：接受"顶栏 = 首页目录"，那要么放宽 nav 规矩（允许序号/长标签），要么把首页正文**写成导航的样子**（内容去伺候外壳）。
③ 折中：WEBSITE.md 里已经有两种导航形状（**纯链接节** `## [名字](目标)`、**链接列表 = 下拉**）⇒ 把它们**升格**成显式的"导航声明"并由门管。代价同 ②：nav 仍归内容。

**选 ① 的真正理由不是"更干净"，是"同一份内容要能印成两个站点"**：内容根是内容，nav / 主题 / 许可 是本站在它之上做的决定。
⚠️ 过渡期（M4 进行中）：顶栏仍从 `home.sections` 取，但那被收进 `shell/topbar.mbt` 的 `nav_items(ctx)` **一个接缝函数**里 —— 配置做出来时只改它，且判据里的"nav 不许出现 `一、` 序号条目"那条反证会立刻变红，逼着这次迁移发生。

---

## 8. `shell/` 的重排方案（实现侧，本文只定形状）

| 新文件 | 装什么 | 从哪儿来 |
|---|---|---|
| `tokens.mbt` | 原语 + 语义色 + 尺度（明暗两套） | `theme.mbt` 13 常量 + `blocks.mbt` 的 `tok_color` 9 个 ✅ 收口 |
| `theme.mbt` | `theme_of(palette, scheme)` + 色号映射 | 新 |
| `layout.mbt` | 三栏骨架、**阶梯（`data-rail` 从 Model 的视口算）**、独立滚动容器 | 新（现状没有布局层） |
| `topbar.mbt` / `sidebar.mbt` / `toc.mbt` / `footer.mbt` | 各件；**目录的落点由 `layout` 传下去**（轨 / 侧栏），`toc.mbt` 自己不知道自己在哪 | 从 `topbar.mbt`/`docs.mbt` 拆 |
| `blocks/*.mbt` | 块渲染（按 token 引用） | `blocks.mbt` / `inline.mbt` |
| `pages/*.mbt` | 首页 / 书架 / 文档页 / 404 | `home.mbt` / `docs.mbt` |

---

## 9. 要你拍的板（只有五个）

| # | 岔口 | 选项 | 我倾向 |
|---|---|---|---|
| 1 | **品牌色** | 靛紫 `#4f46e5` ｜ 靛蓝（VitePress 同款 `#3451b2`） ｜ 别的 | 靛紫（有辨识度且够现代） |
| 2 | **顶栏形态** | 白底 + 1px 线（现代文档站默认） ｜ 保留深色条 | 白底（深色条与暖纸/白底都是"两套气质"的根源） |
| 3 | **侧栏默认展开** | 只展开当前一份 ｜ 全展开 ｜ 记住用户选择（要 KV 存储） | 只展开当前 |
| 4 | **移动端"先浮出来"的语义** | 按钮自己从边缘挪出来 ｜ 面板先露一条预览 | 按钮自己挪出来（更省一次误解） |
| 5 | **首页定位** | 门户（卡片 + 三入口） ｜ 一页长文（现状） | 门户 |

---

## 10. 接下来（设计层的三步）

1. **本文定稿**（你拍完 §9 那五条）；
2. **视觉原型**：按本文做一份单文件 HTML 原型（可切明暗、可看 ≥1280 / 1024–1280 / <1024 三档），
   **先看，再改设计**，而不是先改代码；
3. **蓝图验收**：拿原型逐条过 §1 的五条验收，全绿之后才动 `shell/` —— 那时 `shell/` 是"照图施工"，不是"边写边想"。

---

## 11. 视觉原型（本文的实现物）与它的实测读数

**原型**：`_scratch/ui-ref/demo.html` —— 单文件、零依赖、不基于 moobile。
它是本文的**唯一权威形态**（本文描述、原型体现；两者冲突时以原型 + 本节读数为准）。

**它在"人机共读"上多做的一件事**（这是本文最想立住的主张，所以做成了能点的）：
同一页有三种读法 —— **渲染**（人看）/ **原文 .md**（Agent 看，就是那份 markdown 本体）/ **`llms.txt`**（机器的整站索引）；
并且每个标题都有**可分享的锚点 URL**、每页有**源文件路径**与"复制原文"、页脚交代**出处与指纹**。

**体检**：`node _scratch/ui-ref/demo-probe.mjs`（真 Chrome × **5 个真视口** × 约 40 项，全绿）。
另有两件仪器：`sidebar-audit.mjs`（只量侧栏与阶梯：文字在什么底上、对比多少、父子标题差几像素、目录挂在哪）、
`demo-shot.mjs`（出图到 `.audit/`：三档版面 + 侧栏放大 ×明暗）。

| 读数（5 个视口实测：1440 / 1100 / 1024 / 768 / 375） | 结果 |
|---|---|
| 横向溢出 / JS 报错 | 0px / 0 条（每个视口都是） |
| **阶梯** | `data-rail` = 3 / 2 / 2 / 1 / 1；目录在 rail 3 挂右栏轨（2 条）、在 rail 2/1 **塞进侧栏并挂在当前文件节点下面**（2 条、`data-in=side`） |
| **目录挂在哪（正面判据）** | 父级 = "样式"**（就是当前项）**、它是那个节点的后代；缩进比同级条目 **+16px**（x=56 vs 40）；主题页同样挂在"应用开发"那个节点下；树里没有的页面（首页）走 `stocwrap` 兜底 |
| **动态换档** | 同一页里把 iframe 从 1440 改到 1100：目录**自己搬进树里**、右栏清空；375 → 1440：搬回右栏轨（两侧的条数都跟着对） |
| **行长（A2）** | 720 / 720 / 688 / 720 / 343px = 46.5 / 46.5 / **44.4** / 46.5 / 22.1 字 —— rail ≥2 全在 38–47 内 |
| **侧栏**（rail 2/3） | 行高一律 36px；父子标题同一列（x=40 / 40）；当前项左条 = `rgb(79,70,229)`、其文字对比 4.93 |
| **侧栏文字对比** | 分组 5.42 / 行 5.42（底 `#f7f7f8`）—— 改之前是 **2.97**；`--fg-3` 基准仍只有 2.97，所以它不承载文字 |
| **侧栏里的机器名** | 0 个（只剩页脚那一处"内容根指纹"，`sha256:…`）—— 改之前是 10 个 |
| 路由 | h1、`<title>`、`meta description`、侧栏当前项、面包屑全部跟着换 |
| 深链到某一节 | `?s=<id>` 真的滚到那一节（329 / 329 / 329 / 486 / 564） |
| 三种读法 | 切到"原文"读到的是 `# 样式…` 本体；能切回；`llms.txt` 一直在顶栏 |
| 搜索 | ⌘K 打开 → 输入"样式" → 命中 3 条 → Esc 关 |
| 主题 | 三态齐全；`canvas` 取值 2 种（`#ffffff` / `#18181b`） |
| **对比度** | 亮 12.05 / 5.8 / 6.29，暗 13.96 / 6.91 / 8.89（正文 / 次要 / 链接）；暗色侧栏 `--fg-2` 7.24 |
| **暗色海拔规则** | `overlay #1f1f23`(亮) > `canvas #18181b` > `surface-2 #131316`(暗) ✅ |
| 窄屏（rail 1） | 侧栏收起、三按钮出现；第一次点**浮出**（`translateX(22px)` → `none`）、第二次点才弹 sheet、Esc 关；"书架"sheet 19 条链接、**零重复 id**（带目录 2 条） |

⚠️ **原型里发现并已修的三个 bug**（都是量出来的，不是看出来的）：

| # | bug | 证据（改之前） | 修法 |
|---|---|---|---|
| 1 | **侧栏那一整块 CSS 一条都没命中** —— `renderSidebar()` 吐的是**裸 `<li>`**，没有 `<ul class="tree">` 外壳 | 首行 `a` 实测 `color rgb(79,70,229)`（链接蓝）/ `font-size 15.5px`（跟正文一样大）/ `display inline` / `padding 0`；当前项 `background transparent`、**没有任何高亮** | 补外壳 + 按 §4.1.1 重写行语言 |
| 2 | **`.main` 这个类不存在** —— HTML 里是 `<main class="col" id="main">`，于是正文左右内边距 0px | `#main` 的 `padding-left` 实测 **0**；375px 下正文顶到屏幕边 | 加上类名（`class="main col"`） |
| 3 | 窄顶栏溢出（早前那轮）：**实测 55px**（导航该进 sheet、搜索该收成图标） | —— | 顶栏收窄（现在是 `data-top=narrow`） |

⚠️ **原型没有证明的**（别读大）：内容是内联数据（真站来自 `press` 的生成物）；搜索索引是客户端算的（真站在构建期算）；
**它自己也是 JS 渲染的**（HTML 里没有正文）—— 所以 A5 里"无 JS 可读/SEO"那一条**原型不满足**，
那要靠预渲染那条线（`PLAN.md` T10），与设计形状无关。

⚠️ **仪器自己的五笔账都记在这儿**（本仓规矩：判据自己也要能被证伪）：
① `--file` 缺省时取到了 `argv[0]`（node.exe 的路径）⇒ 体检的对象成了 node 自己；
② `$` 与 `$$` 两个助手被写成同一个名字 ⇒ `$('h1')` 返回数组、断言全假红；
③ `transform` 的读数取到了过渡中间值（与参考稿体检那次同一个坑）⇒ 改成量之前先 `transition:none`；
④ **"420px 视口"从来不是 420px** —— Chrome headless 的窗口有最小宽度（请求 420 → 实测 `innerWidth` **512**），
   所以上一版读数表里的"420px"是 **512px**，那条"420px 顶栏溢出 55px"的**宽度标签是错的**（现象本身在 512px 下成立）。
   现改成"把 demo 装进指定宽度的 iframe"（iframe 的视口宽度就是它自己的宽度），并加了 `--allow-file-access-from-files` 直读子页；
⑤ **判据要看"生效"，不能看"写了"** —— 旧体检断言的"侧栏当前项 = 样式"读的是 `aria-current` **属性**，
   而属性一直在、样式一直没生效（bug 1 就藏在这条绿灯后面）；同理"没有横向溢出"过不了"行长"这一关
   （rail 2 缺失时正文被压到 37.8 字，表里却全是绿的）。**绿灯不等于对**。


---

## 12. 真站落地（M0–M4）与读数

**§11 记的是原型，这一节记的是"设计落到真站"**（`shell/` 这个包）。
目标不是"照抄原型"——`@style` 的子集是**故意封闭**的（§4 的那些【实现依赖】），
原型里有一半要等 moobile 那边补能力。落到哪一步、缺什么，全写在这儿。

### 12.1 已完成的四档

| 档 | 交了什么 | 读数（`node tools/ui-probe.mjs`，真 Chrome，1440 / 1100 / 768） |
|---|---|---|
| **M1** | `tokens.mbt`（色只在这里写死：靛紫亮/暗 + 暖纸、7 档间距、字号/行高/圆角/阶梯阈值）、`theme.mbt`（**主题是值**：`Scheme`/`Pref` + `Palette` + 色号→颜色函数）、`layout.mbt`（三档阶梯 + 三栏骨架） | 0 JS 报错 / 0 横向溢出 / **页面本身不滚（顶栏不动）** / 每栏自己滚 / 正文 647–651px = **46.2–46.5 字**；rail 3 与 rail 2 都是「侧栏 272 + 正文」，rail 1 侧栏**整栏不画** |
| **M2** | `sidebar.mbt`（§4.1.1 那五条行语言）、`toc.mbt`（目录**挂在当前文件节点下面**）、**契约补 `Doc.title`** | rail 3 三栏（272 / 正文 / **220**，目录在右栏轨 2 行、树里 **0** 行）；rail 2 目录在**树里**；窄屏侧栏整栏不画；侧栏 6 行**中文标题**、**0 个机器名**、全列**只有 1 条**品牌色左条（`rgb(79,70,229)`） |

**契约那一刀值得单记**：`Doc` 原本**没有中文名字段**（`PLAN-ui §10.4` 的病根就是这个），
现在补了 `title`（取法与子页 `Kid.title` **同一套**：frontmatter `title` → 正文 H1 → 回退 slug），
生成物重跑、`press --check` 绿 ⇒ **界面上的名字从此有唯一来源**，侧栏不再拿 slug 当标签。

| **M3** | 正文块换主题（`blocks.mbt`：9 个写死色号 → `theme.tok(色号)`）、**文档页骨架**（`article.mbt`：面包屑 → kicker → H1 → 定位句 → 正文块 → 上下页 → 页脚）、`docs.mbt` 退休（旧 `pane` 删掉，`site_pane` 挪进 `home.mbt`） | 点进一页文档后：H1 = **中文 title**（"moobile 应用开发：…"）、kicker = **机器名**（源文件路径、mono）、面包屑 4 格、代码块底色 = `code_bg`（`rgb(247,247,248)` 实测）、**点「下一页」真的换页**（H1 从"应用开发…"变成"事件与订阅…"）；三栏各自滚 + **页面本身不滚**；0 横向溢出 / 0 JS 报错；`moon check` **0 error**（19 warnings）、`theme-check` 全过、R9 ✓ 128 文件全部 ≤400 行、实例 `app.mbt` 仍 **17 行** |

| **M4** | **新顶栏**（白底 + 1px 线、**高 56**、站名回首页、nav、**主题三态开关**；导航来源收进 `nav_items(ctx)` 一个接缝）、**三张页面脸**（`pages.mbt`：首页门户 / 书架 / 404）、`home.mbt` 的旧长文 `site_pane` 退休 | ✓ **顶栏高 56px**（旧 55）、顶栏底色 `rgb(255,255,255)`（深色条拆掉）、nav = `站名 / 首页 / 书架 / 从哪一份开始 / ◐`、（曾有一条 nav 序号探针，**已作废**：`nav_items()` 会把正文 `##` 的序号剥掉再吐 ⇒ 构造上恒绿、证明不了解耦；见 `PLAN-ui.md §13.1c`）、**点站名真的回首页**（H1 回到首页源那句）；正文 709px = **45.7 字**；三档版面与文档页那组全过；0 报错 / 0 横向溢出；`moon check` **0 error / 16 warnings**（比 M3 的 19 更低）、`theme-check` 四过、R9 ✓ **130 文件全部 ≤400 行**、实例 `app.mbt` **17 行** |

**M4 里判据逮到的两个真问题**（都记在这儿）：

1. **深色下白字打在品牌色上**：`topbar.mbt` 写死 `#ffffff`（照原型抄的方块标记字色）。深色品牌色是**亮靛紫** `#a5b4fc` ⇒ 白字对比度只有 **~1.9:1**。修法是**补 token**（`Palette.on_brand`：浅色近白、深色必须深）并把判据扩一条（`on_brand` 落在 `brand` 上也是 ≥4.5）——**让这类错以后自己会被抓住**。
2. **A2 行长那条判据自己写得不严**：探针"取最宽的叶子文字元素"在新首页上挑中了 **11.5px 的 meta 行**（720px ÷ 62.6 字 = 11.5px），报出"62.6 字"。改成**按正文字号（15.5px）筛**，且**筛不到就明确报"没量到正文"**（不许回落到最宽元素）——回落正是它刚才绿得莫名其妙、红得也莫名其妙的根源。

⚠️ 顺带一条仪器的账：探针加检查之后涨到 **448 行**，撞了 R9（每文件 ≤400 行）⇒ 按"一件事"切成 `ui-probe.mjs`（外层：起服务/起 Chrome/iframe 视口/打印判定）+ `ui-probe-page.mjs`（注入页面的那段探针），**判据口径一个字没变**。

| **M5（功能对齐期）** | **窄屏入口**（`shell/mobile.mbt` 243 行：三个贴边按钮 + 两级交互 + 遮罩互斥 + Esc/点遮罩关；状态进 Model 的 `sheet`/`peek`）＋ **探针换 CDP 真输入**（`tools/cdp.mjs` 188 行，Node 自带 WebSocket ⇒ **零依赖**；真鼠标/真按键 + `Emulation.setDeviceMetricsOverride` **真视口**，不再用 iframe） | rail 1：✓ 三按钮 · ✓ 第一次点**浮出 34px** · ✓ 第二次才弹 sheet · ✓ **再点同一按钮收起** · ✓ **从 sheet 进文档页**（试了 4 条进到了）· ✓ Esc 关；三档全绿 ×3 连跑 |

**M5 里两条"假绿"被真输入逮住**（这是本轮最值钱的读数 —— 换真输入前后**同一批判据、同一份代码**）：

1. **「点下一页真的换页」原先的绿是作弊**：合成事件不需要元素看得见，而量到的坐标是 **`y=3800`（视口 900）**、`elementFromPoint` 返回 `null` ⇒ **那一下点在页面外**，合成派发照样"成功"。修法：要点的坐标**先 `scrollIntoView` 滚进可视区再量**（`reveal`）。
2. **「rail 2 目录挂在当前文件节点下面」前提就错**：原来在**首页**上量（首页在树里没有对应节点，走的是树末兜底）。已挪到进文档页之后量，并把判据从 `aria-current`（壳里标的是 `data-current`，**RNW 不一定透传 `data-*`**）换成**生效读数**：当前项那条 2px **品牌色左条**。

⇒ 结论写死：**页面内合成事件在这套宿主上不可信**（它触达不到 RNW 的 responder 记账），**交互判据一律走 CDP 真输入**。此前那条"关掉浮层之后再点连点 13 次一次都不进应用"的红，**判为仪器**，与 `shell/state.mbt` 的状态机无关（已排除：状态机附行号 / 游离节点 / Esc 特有 / 应用抛错两路各 0 条）。

| **M4b** | **页脚三样真值**（§5 页脚那一行）：许可 · 构建时间 · 内容根指纹 | ✓ 文档页页脚实测：`源文件 skills/…/SKILL.md · 许可 Apache-2.0 · 构建于 2026-10-07 15:29 · 内容根指纹 sha256:dc19ed96…9026`（mono / `--fg-3`；64 位只截头 8 尾 4 **展示**）。真源逐条：许可 = `press` 读**实例 `moon.mod`** 的声明（过渡口径）；构建时间 = `build-web.mjs` 往 `dist/index.html` 注入的构建戳；指纹 = **复用 G8 那套结构指纹**（`@gates.fingerprint`）+ sha256。四道门：`press --check` ✓ 940 行、`moon check` 0 error/14 warnings、R9 ✓ 133 文件、`theme-check` ✓；**判据自证**（同内容重跑逐字节一致 + 改一行正文则指纹从 `36352615…` 变 `a5f72ca7…`）＋ **native/js 逐字节一致**（指纹跨 target 不漂） |

| **M5-②** | **URL 路由**（`shell/route.mbt` 206 行 + 探针 +8 条判据/视口，三档 **84 条全绿**：hash ⇄ 状态双向；`state.mbt` 接线 + `@sub.on_url_changed`） | ✓ 四条**行为**读数（真 Chrome）：① **深链直达** `#/s/moobile-app-development/styles/` 一进去就是那一页、**不是先首页再跳**；② 点侧栏一条 ⇒ hash 变（`#/` → `#/s/moobile-app-development/`）且**正文跟着换**；③ **按后退真的回上一页**（hash 与 H1 双双还原）；④ `#/nope/nope/` ⇒ 渲染「没找到这一页」（**404 从此有到达路径**）；⑤ `#/` ⇒ 首页。实现里照做了那两个实测坑：`initial` **同步读一次**地址（注释原文："`on_url_changed` 首屏不推 ⇒ 读不到就空串 ⇒ 首页 —— 回落，不猜"）。
⚠️ **但要更正一处事实**：**`@sub.current_url()` 在钉住的 `moobile@0.5.0` 里不存在**（它是本地 moobile 仓今天新加的）⇒ 实现走的是 **0.5.0 里已有的那一条 DOM 路**（`@dom.window().current_url()` / `push_url()`），也就是 `on_url_changed` 内部自己用的那条（web 上它自己挂 `popstate`），**没有另造通道**。
⚠️ 另一条：**`@nav.push_url` 在 0.5.0 里是死代码** —— `rabbita/nav` 的 op **没有任何宿主实现**，调它是**静默 no-op**（正是本仓最忌讳的假通道），所以写地址用的是 `@dom.window().push_url()`（`pushState`，不触发 popstate ⇒ 无重入）。
👉 **抬版之后只改两个函数**（`url_now()` → `@sub.current_url()`、`push_url_cmd` → `@nav.push_url`），解析/生成与接线一行不用动。**今天不抬也能跑**（web 站点）；RN 上今天没有路由。仪器：`_scratch/route-check.mjs`（带地址进来）+ `_scratch/route-flow.mjs`（在页面里操作：点 → hash → `history.back()`） |

| **M5-⑧** | **moobile 甲级样式**（跨仓）：`cursor` + `box_shadow` 落地；~~`transform` 给取舍未动~~ ⇒ ✅ **2026-10-07 也落地了**（`PLAN-ui §6.7` 的第三处更正：字符串**两端都认** ⇒ 不必按后端分叉、`StyleValue` 一个变体没动；站点侧"半藏/浮出"随后改用 `Translate`，见 §4.3 那张表）；~~"滚到节点 / 元素测量"给形状未动~~ ⇒ ✅ 同日两刀（节点寻址 + 容器级滚动订阅） | ✓ `style_platform_check` **14 通过 / 0 失败**、`bash tools/verify_all.sh` **exit 0 全绿**、moobile `moon check` 0 errors。**并且它当场纠正了我两处错判断**（见 `PLAN-ui §6.7`）：① 我说 `cursor` 是"web 独有、原生不认" —— **错**，那条门读仓里**真 RN 登记表**（0.83.10 / 0.86.3）当场红："已经支持了：cursor"；② 我说 `box_shadow` 要按后端分叉 —— **不必**，现代 RN 与 RNW 都认一条字符串。⇒ 教训：**"这个能力各端有没有"不该靠推断，该读各端自己的登记表**（本仓已有那种机器校验的门）。**⑤ 的真卡点也探明了**：不是缺滚动 API，是缺**节点寻址**（moobile 没有节点句柄/ref 通道）⇒ 先补那一层，再谈滚到节点与测量 |

| **M5-③** | **主题跟随系统**：`shell/hosttheme.mbt`（58 行，读宿主全局，与引擎 `ts_shim.mbt` 同形）+ 两个实例 `index.html` 的 `<head>` 同步脚本（bundle 之前读 `localStorage` + `matchMedia`，把解析结果写进那格）+ `state.mbt` 接线（`initial` 同步读；`SetTheme` 走 `@cmd.custom_cmd` 写回宿主；`SysTick` 轮询，**值没变就返回同一个 model**） | ✓ **首屏不闪**（真 Chrome，`_scratch/theme-firstpaint.mjs`）：种 dark ⇒ **首帧壳底 rgb(24,24,27)**（不是先亮后暗）；种 light ⇒ 首帧白；不种 ⇒ 宿主那格的 dark 与 `matchMedia` **逐位一致**（应用真读了那格，不是自己猜）；点开关 ⇒ `localStorage` 写回 ✓。取首帧的办法：探针注在 `</body>` 之后 ⇒ bundle 已同步挂载完、**任何定时器都还没跑过**。门：`moon check` 0 error（warnings 我方量到 **15**，实现方报 14）/ R9 ✓ 135 文件 / `theme-check` ✓ / `ui-probe` 三档（实现方报绿）＋ **③ 的专项探针我方已独立复跑：`node _scratch/theme-firstpaint.mjs` ⇒ 7 条全过**（种 dark 首帧壳底 `rgb(24,24,27)` / 种 light 首帧白 / 不种则宿主那格与 `matchMedia` 一致且首帧底色跟上 / 点开关写回 `dark`→`auto`）——`ui-probe` 三档本身仍只记**实现方报绿**，别混读 |
| | ⚠️ **「跟随系统变化」只有「通道存在」，没有行为读数** —— 无头 Chrome 改不了系统级 `prefers-color-scheme`，触发不了。本仓用 `@sub.every(2000)` 轮询顶上，代价（2 秒粒度 + 常驻定时器）写在代码注释；**真正的事件通道要 moobile 补**（`@sub` 公开面没有媒体查询订阅）。**不许读成已解决。** |
| **M5-⑦** | **Agent 出口（引擎侧）**：同一次解析产出两件 —— `llms.txt`（`engine/content/file.mbt` 的 `llms_lines` + `gen_content_full()`）与 `md/**`（每页原文逐字节拷贝，地址 = 去掉 `skills/` 前缀的相对路径）；`press.mjs` 三件产物一条纪律（tmp→rename、`--check` 全覆盖、多出的产物也算不一致、**陈旧检查**）；两个 `build-web.mjs` 拷进 `dist/`（缺了不静默） | ✓ `press --check` **三处一致**（940 行 / 58 行 / 34 份逐字节）+ **同源自证**（31 个中文 title 一条不漏）；**可达** `/llms.txt` 200、两个 `/md/…` 200；新判据 `tools/agent-exports.sh` 四组全过**并做了证伪**；`moon check` 0 error；`ui-probe` 三档绿；R9 ✓ 137 文件。⚠️ 待补：**shell 侧的「看原文」按钮没做**（地址已可 fetch：`/md/<slug>/SKILL.md`）；`template/instance/build-web.mjs` 加了同段拷贝但**未重建验证**；它**重采了 `tools/fixtures/expected/gen-file.mbt` 的 golden**（起因是早先那次 `Doc.title` 契约新增，属有意升级，**该在提交信息里写清**） |

**① 一条结构性发现（已写进实现）：时间戳不能进内容包。**
`press --check` 是**逐行比对**"磁盘那份 vs 现跑那份"⇒ 时间戳一进内容包，这条门**每跑必红**，接着人就会去关掉它。
所以"构建时间"的真源只能在**构建侧**（宿主注入），契约里那一格留着但**今天恒空**、渲染时用构建戳顶上。
⇒ 一般的形状：**不可复现的值不许进"要被逐字节比对"的产物**，否则那条门会先被绕过、再被关掉。

**② 两条内容侧决定（今天不画，比画一个 404 强）**：
- **首页第 3 张入口卡（"规矩"）**：内容根里唯一像它的正是被 `skillpress.ignore.md` **有意忽略**的那份 `skillpress`（"怎么写一份 skill、怎么跑那几道门"，理由是"读者是改内容的人"）⇒ 要它得走"取消忽略"或"内容侧新写一页"两条路，**都是内容/产品决定**。
- **首页源的纯链接节指向一个不在货架上的页**：`## [先读哪一份](skillpress-user/SKILL.md)` 里的 `skillpress-user` 只在 skillpress 仓的 `skills/` 里；生成器已回落到 `moobile-app-development` 并打印提示 ⇒ 记成**内容侧的账**。

### 12.2 硬依赖（**进一格、退一格**，别读大）

| 缺什么 | 后果 | 归属 |
|---|---|---|
| ~~**滚到某个节点**~~ | ✅ **已还清（2026-10-07）**：moobile 补了**节点寻址**（`Attrs::id("…")` 写名字 + `@sub.scroll_to_node`），站点把目录行做成 `@html.button` ⇒ **目录可点了**。读数（真 Chrome + CDP 真鼠标，`_scratch/toc-click.mjs`）：内容区 `scrollTop` **0 → 3226**、点最后一行 ⇒ **sec-7 顶到内容区上沿（差 0px）**、再点第一行 ⇒ **sec-0（差 0px）**、0 条 JS 错误 | moobile 那半边已完成 |
| ~~**容器级滚动订阅**~~ | ✅ **已还清（2026-10-07，同日第二刀）**：moobile 补了 `@sub.on_node_scroll(node, cb)`（`document` **捕获阶段**的 `scroll` 监听 + 按 `id` 过滤；**装载后先补一次当前值** ⇒ 首屏就有值）。站点接上之后**跟读高亮成立**：读数（`_scratch/toc-click.mjs` ⑤⑥，真 Chrome，看**计算样式**）—— 装载后**恰好一条**高亮在第 0 条（600 加重 / 其余 400），把内容区滚到第 4 节附近后高亮**换到第 4 条**；0 条 JS 错误 | moobile ✅ + 站点 ✅ |
| ~~**rail 1 的入口**~~ | ✅ **已还清**（M5：三个贴边按钮 + sheet；探针里那条"跳过"已删，rail 1 的文档页检查真跑起来了） | —— |
| **推动路由（push / 深链）** | 路由要能"推"：实测 `on_url_request` 在 Web 上**零调用点**、`pushState` **不产生 popstate**；而现成的 `@nav` 内部 `@dom.push_url` **RN 上会抛** ⇒ 「推」必须走**宿主能力**（协议要加第三类"动作"，且动作要具名 + 声明哪端实现 + 进机检表） | moobile 侧（R 轨道，设计中） |
| **路由（整体）** | 设计落 `docs/design/DESIGN-ROUTER.md`；地基读数：`on_url_changed` **只通 popstate 一档**、**首屏不推**（已由 `@sub.current_url()` 补上）、`on_visibility_change` 通但**真切后台要真机** | moobile 侧（R 轨道） |
| **语义标签** | 整页**零个 `p` / 零个 `h1`**（RNW 把 Text 渲成 `div`/`span`，实测）⇒ A5 的"SEO / 无 JS 可读"**除了预渲染还要补一层标签映射** | 宿主/渲染层，`PLAN.md` T10 那条线上 |

### 12.3 与原型的三处**有意不一致**（不是没做，是做不出或不该做）

1. **悬停反馈**：原型有 hover，真站今天只有"当前项左条"。真 hover 是**乙级**能力（`press:` 的消费侧也还没接）⇒ 状态只走一条通道。
2. **层次**：原型有阴影（`--shadow-*` 五档），真站靠 1px 线 + 底色差。`box_shadow` 已拍定要补（`PLAN-ui §6.7`），补完这一条才对上。
3. ~~**目录**：形状同形（第三级、缩进再进一格），但**原型可点、真站不可点**~~ ⇒ ✅ **2026-10-07 起可点 + 可跟读**：
   节点寻址那一刀给了"滚到节点 / 量节点"，同日第二刀给了"容器级滚动订阅"⇒ 目录行点一下跳过去、
   滚动时当前那一节高亮（都是真 Chrome 读数，见 12.2 两行）。**这一格至此与原型同形**。

### 12.4 内环与官方判据的分工

- **内环**：`node tools/ui-probe.mjs` —— `moon build`（js，秒级）+ 一次无头 Chrome，量**新判据**。
  它把"真视口宽度"用 **iframe** 给（Chrome 的窗口有最小宽，`--window-size=420` 实测是 512 —— §11 那笔账）。
- **官方**：`engine/site/`（native `skillpress verify`）那批断言**已是新 IA**（23 条全绿，2026-10-07 换血，
  见 `PLAN.md` D45）—— 它量的是 `#topnav` 那一排 / `#sidebar` / `#prose` / `#copy-*` 这些**具名节点**，
  不再按整页文字猜（旧版那句"判的是旧 IA"是换血之前的坐标）
  ⇒ **M4 要整体换血**，在那之前两边不可混读。
