# report.html 的设计约定

报告由 `scripts/novel-characters.mjs render --html` 生成，样式全部内联在 `renderHtml()` 里。**改样式只改那一处**，不要另起模板。

这份文档记录为什么长这样，免得后来的改动把它改回一个通用的卡片墙。

## 它是什么

不是营销页，是**制作用的角色设定集**。读者是要拿这些提示词去真出图、真配音的人。页面唯一的工作：**让人快速抓到对的提示词，同时相信画像有原文依据。**

所有设计决定都从这一句推出来。

## 多语言

界面文案在 `novel-characters.mjs` 的 `STRINGS` 表里，内置 `zh` / `en` / `ja` 三套。加语言就加一个键，**别把中文硬编码进模板**——`renderCharacter` / `renderGraph` / `renderHtml` 拿到的 `t` 就是那张表。

内置表没有的语言，由 skill 现场翻一份塞进 `cast.json` 的 `ui`，`strings()` 合并进来。所以支持的语言不受内置表限制。函数模板（`docTitle` / `counts` / `graphCounts` 这些）**不接受 `ui` 覆盖**，未知语言码会退回 `en` 的骨架，但 `<html lang>` 仍写实际语言码。

## 骨架

```
┌─ 顶栏 60px ─────────────────────────────────────────────┐
│ 书名        [搜索]                     角色设定集 · 计数   │
├──────────────┬──────────────────────────────────────────┤
│ 左栏 400px    │ 主区 max-width 1500px                     │
│ 故事摘要（三行）│                                          │
│ ▸ 关系图谱     │  ← 图谱和角色详情互斥，一次只显示一个       │
│ 角色 01 …     │                                          │
│ 角色 02 …     │  01 角色名  主角        一句话              │
│ 角色 03 …     │  ┌──────────────┬──────────┐             │
│              │  │ 设定图 + 画像   │ 信息卡     │  1fr / 500px │
│              │  ├──────────────┴──────────┤             │
│              │  │ 原文依据                  │             │
│              │  ├──────────────┬──────────┤             │
│              │  │ 出图提示词     │ 音色提示词 │  1fr / 1fr   │
└──────────────┴──────────────────────────────────────────┘
```

左栏 `position:sticky` 满高滚动；主区一次只 `display:block` 一个 `.char`。

**断点**：`.shell` 1080 塌单栏、`.upper` 1240 塌单栏、`.prompts` 900 塌单栏、`.graph-body` 1100 塌单栏。

## 四条不能破的规矩

### 1. 双字域

中文印刷体本身就分工，直接用：

| 字体 | 语域 | 用在 |
| --- | --- | --- |
| **宋体** `--serif` | 叙事——关于故事的 | 书名、角色名、一句话、**故事摘要**、**原文引文**、图谱的节点名 |
| **黑体** `--sans` | 分析——关于角色的 | 画像正文、字段标签、界面、图谱的关系文字 |
| **等宽** `--mono` | 机器输入 | 英文提示词、序号 |

真正逐字来自原文的只有 `.source blockquote`，它额外带铁锈红边栏——**那条边栏是「这是原文」的唯一标记**，别拿去装饰摘要或一句话。

这是这份报告跟任何一个角色卡生成器的区别所在，别把它统一成一种字体。

全部用系统字体，**不许引外部字体**——报告要能离线双击打开。

### 2. 屏幕上收，纸上全展开

屏幕上一次只看一个角色，提示词折叠在 `<details>` 里，摘要只留三行。**但 `@media print` 里全部强制展开**：所有角色、所有提示词、关系图谱都要打得出来。

这是有意的取舍：早期版本把四个角色平铺成卡片墙，看着满，实际一屏什么都读不完。收起来是为了让当前这个角色能被读完，不是为了藏东西——所以纸上必须是完整的一份。

改折叠相关的样式时，**print 那一段要一起改**，自测里有断言盯着。

### 3. 推断标记要高亮

`（推断）` 和 `(inferred)` 由 `marked()` 自动包成 `<span class="inf">`，带浅铁锈红底。半角/全角 × 中英四种写法都要认。

这是本报告的签名细节。这个工具的核心承诺就是「哪些有原文依据、哪些是为了能用而补的」，把它做成视觉，比写在文档里有用。页脚也有一句说明。

### 4. 自包含

**不许引任何外部资源**——没有 CDN 脚本、没有外部字体、没有远程图片。关系图谱是手写的内联 SVG 而不是 d3，就是这条规矩逼出来的。自测里有断言正面反面都盯着（既检查产物里没有 `<script src>`，也检查那条正则本身抓得到东西，免得是个永远为真的假测试）。

## 配色

冷灰印张 + 铁锈红印记。**红色只用在跟原文有关的地方和当前选中态**：引文边栏、主角标签、图谱里被点亮的弦。别拿它当通用强调色。

| token | 值 | 用途 |
| --- | --- | --- |
| `--paper` | `#eceded` | 冷灰印张，不是暖奶油 |
| `--panel` | `#f5f6f5` | 提示词块、信息卡、图谱画布 |
| `--side` | `#e4e6e3` | 左栏 |
| `--ink` | `#191d21` | 蓝黑，不是纯黑 |
| `--seal` | `#8a3324` | 铁锈红 |
| `--seal-soft` | `#8a332412` | 推断标记的底、图谱选中行的底 |

**没有深色模式。** 设定图是白底印张，报告是冷灰印张，两者一起反色会让设定图看着像贴上去的。

## 关系图谱

一个跟角色详情互斥的全景视图，`.main.gmode` 时显示。

**布局在 Node 里算好写进 SVG**，浏览器端只管加 `hot` / `dim` 两个类。理由同上——不引库。

- 圆环布局，半径跟人数走：`max(130, min(260, 40 + n*14))`，两侧留 110 给名字
- 弦是二次贝塞尔，控制点往圆心拉 0.35。直线在人多时会糊成一团网
- 节点大小按 `importance` 分档，主角实心铁锈红
- 每个节点带一个 **r=24 的透明命中区**（`.ghit`）。可见圆点只有十来个像素，不加这层压不准

⚠️ **命中区和可见圆点必须各有各的类**（`.ghit` / `.gdot`）。都写成 `circle` 的话，`.gnode.lead circle` 会把命中区一起染成一个大红块。

⚠️ **关系文字沿弦按序号错位**（`t = 0.5 + ((i%3)-1)*0.14`），不要都放 t=0.5。正对面两个角色之间，弦的中点正好是圆心，全放中点会摞成一坨。错位挪的是文字，弦本身不动。

关系文字截到 **6 字**，全文进 `<title>`。画布只有 480 单位宽，一条 12 字的标签占掉 27%，相邻两条直接叠上。边 ≤ 14 条默认标出来，再多默认收起，顶部有开关；**收起状态下悬停的那条仍然要显示**。

## 导出 JSON

顶栏那个按钮下载的**就是 `cast.json` 本身的形状**，不是另一套导出格式。这条是契约：外部工具改完要能直接喂回 `render`，也要能过 `validate`，自测里有断言盯着。

数据以 `<script type="application/json" id="cast-data">` 内嵌一份，点导出只是包成 Blob 下载，**不发网络请求**。

⚠️ **JSON 里的 `<` 必须转成 `<`**。正文里出现一个 `</script` 就能把数据块提前截断。`<` 在 JSON 里只可能出现在字符串值中，所以整体替换是安全的。

⚠️ **`URL.revokeObjectURL` 不能跟在 `a.click()` 后面立刻调**。Safari 会抢在下载读完之前撤掉 blob，用户存下来是个空文件。延后回收。

## 每段提示词一个复制按钮

不能少。目前每个角色 7 个：出图本地/EN、设定图 EN、反向、音色本地/EN、整份 JSON。有设定图的还多一个「复制图片」——复制的是**图片本身**不是路径，非 PNG 先过一遍 canvas 转码（Safari 只认 `image/png`）。自测里有断言盯着这个数。

## 质量底线

- 响应式到手机（所有多栏都有断点塌成一栏）
- 键盘焦点可见（`.copy` / `.rost` / `.gnode` / `.grow` 都有 `:focus-visible`）
- SVG 的 `<g>` 不是原生按钮，回车/空格要自己接
- `prefers-reduced-motion` 关掉全部动效和平滑滚动
- `@media print` 隐藏顶栏和左栏、展开全部内容、条目不跨页断
- 角色数据是模型生成的，**一律 `esc()` 转义**后再拼进 HTML；`marked()` 内部已经转过，别再套一层
