agentcss 0.24.2 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/01-tokens/README.md +1 -1
  2. package/01-tokens/tokens.css +67 -1
  3. package/01-tokens/typography.css +1 -1
  4. package/02-primitives/button/demo.html +8 -0
  5. package/02-primitives/card/README.md +15 -0
  6. package/02-primitives/choice/README.md +30 -0
  7. package/02-primitives/choice/choice.css +1 -1
  8. package/02-primitives/input/README.md +1 -0
  9. package/02-primitives/select/README.md +42 -2
  10. package/02-primitives/select/demo.html +11 -6
  11. package/02-primitives/select/select.js +184 -94
  12. package/02-primitives/timeline/demo.html +2 -2
  13. package/02-primitives/timeline/timeline.css +1 -1
  14. package/03-patterns/accordion/README.md +15 -0
  15. package/03-patterns/content/README.md +11 -0
  16. package/03-patterns/drawer/README.md +1 -1
  17. package/03-patterns/dropdown/demo.html +1 -1
  18. package/03-patterns/form-validation/README.md +18 -0
  19. package/03-patterns/form-validation/form-validation.css +12 -1
  20. package/03-patterns/list/README.md +11 -0
  21. package/03-patterns/overlay/README.md +26 -6
  22. package/03-patterns/overlay/overlay.css +22 -3
  23. package/03-patterns/overlay/overlay.js +63 -17
  24. package/03-patterns/states/README.md +15 -0
  25. package/03-patterns/states/states.css +18 -1
  26. package/03-patterns/tooltip/README.md +12 -0
  27. package/04-recipes/longform/longform.html +3 -3
  28. package/04-recipes/report/README.md +9 -0
  29. package/04-recipes/report/report.css +5 -0
  30. package/04-recipes/table/README.md +16 -0
  31. package/04-recipes/tierA-tokens.css +3 -3
  32. package/09-assets/README.md +18 -0
  33. package/09-assets/bar/bar.css +25 -10
  34. package/09-assets/echarts-adapter/demo.html +3 -1
  35. package/09-assets/echarts-adapter/echarts-adapter.js +107 -34
  36. package/09-assets/scientific-plot/README.md +2 -1
  37. package/09-assets/scientific-plot/scientific-plot.js +0 -7
  38. package/AGENTS.md +2 -1
  39. package/CHANGELOG.md +379 -18
  40. package/CONTRIBUTING.md +6 -5
  41. package/README.md +6 -2
  42. package/README.zh-CN.md +19 -7
  43. package/SECURITY.md +5 -2
  44. package/START-HERE.md +7 -7
  45. package/SUPPORT.md +86 -24
  46. package/ai/START-HERE.md +3 -2
  47. package/ai/cli.js +126 -6
  48. package/ai/components.json +258 -2
  49. package/ai/components.meta.json +257 -40
  50. package/ai/components.schema.json +96 -0
  51. package/ai/contract.schema.json +11 -0
  52. package/ai/pdf-pages.js +107 -0
  53. package/ai/pdf.js +354 -0
  54. package/ai/tokens.json +2 -2
  55. package/ai/validate.js +48 -1
  56. package/dist/01-tokens/tokens.css +1 -1
  57. package/dist/02-primitives/select/select.js +158 -71
  58. package/dist/03-patterns/form-validation/form-validation.css +1 -1
  59. package/dist/03-patterns/overlay/overlay.css +1 -1
  60. package/dist/03-patterns/overlay/overlay.js +40 -11
  61. package/dist/03-patterns/states/states.css +1 -1
  62. package/dist/04-recipes/report/report.css +1 -1
  63. package/dist/04-recipes/tierA-tokens.css +1 -1
  64. package/dist/09-assets/bar/bar.css +1 -1
  65. package/dist/09-assets/echarts-adapter/echarts-adapter.js +75 -33
  66. package/dist/09-assets/scientific-plot/scientific-plot.js +0 -7
  67. package/dist/COSTS.md +37 -37
  68. package/docs/ACCESSIBILITY.md +3 -3
  69. package/docs/FACTS.md +1 -1
  70. package/docs/LAYER-BASELINE.md +28 -3
  71. package/docs/PRINT.md +450 -0
  72. package/docs/RELEASE-PROCESS.md +78 -3
  73. package/index.html +1 -1
  74. package/llms-full.txt +7 -7
  75. package/llms.txt +5 -0
  76. package/package.json +6 -1
package/ai/pdf.js ADDED
@@ -0,0 +1,354 @@
1
+ /*
2
+ * pdf.js — HTML → PDF 导出。零运行时依赖(不需要 puppeteer,也不需要联网)。
3
+ *
4
+ * ============================================================================
5
+ * 🔴 为什么导出器在**这里**,而不是在组件库里
6
+ * ============================================================================
7
+ * `@page` 是**文档级决定**:纸多大、边距多少,取决于"这份东西要交给谁",
8
+ * 不取决于某个按钮长什么样。库一旦在 `tokens.css` 里声明 `@page`,
9
+ * 使用者的边距控制会**静默失效**(实测同一份页面:943 runs vs 978 runs,
10
+ * 只是多了一条 `@page` 规则)。所以:
11
+ *
12
+ * 组件库 → `@media print` 里的**行为**(隐藏什么、展开什么、不跨页切断)
13
+ * 导出器 → `@page` 里的**版式**(纸幅、边距)
14
+ *
15
+ * 这条分工不是偏好:谁声明 `@page`,谁就接管了使用者的边距控制。
16
+ * 所以它只能由"产出这份文件的人"给 —— 这正是本文件存在的理由。
17
+ *
18
+ * ============================================================================
19
+ * 🔴 为什么用 Chrome 的命令行,而不是 puppeteer
20
+ * ============================================================================
21
+ * 本库的承诺是**零运行时依赖**(package.json 里 dependencies 是空的)。
22
+ * 为了一个"5% 的人才会用"的导出功能塞一个 ~300 MB 的浏览器包,
23
+ * 等于让 95% 的人为 5% 的人付安装代价 —— 那是这条承诺唯一的实质内容。
24
+ *
25
+ * 所以:`--print-to-pdf` 走使用者**已经装好的** Chrome/Chromium/Edge。
26
+ * 找不到就**明确报错**(`no-chrome`),绝不静默降级成一个空文件。
27
+ *
28
+ * ============================================================================
29
+ * ⚠️ 一条必须写下来的观测差异
30
+ * ============================================================================
31
+ * 本工具产出的 PDF **包含底色**(headless 打印的默认行为,实测:
32
+ * `.bg{background:#1b4d8f}` 在内容流里是 `.1059 .302 .5608 rg … re f`),
33
+ * 而浏览器打印对话框默认**不勾**"背景图形"。
34
+ *
35
+ * ⇒ 同一份页面,「导出」与「Ctrl+P」在**彩色块**上观感不同:
36
+ * 导出里白字压深蓝可读;对话框里底色被丢弃、白字会被压成 #ABABAB。
37
+ * 两者不是谁错了,是两个产品。`docs/PRINT.md` §八 写了这件事。
38
+ *
39
+ * 用法:
40
+ * const { exportPdf } = require('./pdf');
41
+ * const r = exportPdf({ src: 'page.html', out: 'page.pdf', paper: 'a4',
42
+ * margin: '18mm 15mm', budget: 8 });
43
+ * if (!r.ok) … // r.code ∈ usage|no-chrome|render|timeout|over-budget|unreadable
44
+ */
45
+ 'use strict';
46
+
47
+ const fs = require('fs');
48
+ const os = require('os');
49
+ const path = require('path');
50
+ const { spawnSync } = require('child_process');
51
+ const { pathToFileURL } = require('url');
52
+ const pages = require('./pdf-pages');
53
+
54
+ /* 纸幅。CSS 的 `size` 关键字就够,不需要自己换算 pt ——
55
+ 换算过的地方迟早和引擎的取整方式差 0.28pt,然后有人拿它当"回归"。 */
56
+ const PAPERS = {
57
+ a3: { css: 'A3', w: 841.89, h: 1190.55 },
58
+ a4: { css: 'A4', w: 595.28, h: 841.89 },
59
+ a5: { css: 'A5', w: 419.53, h: 595.28 },
60
+ letter: { css: 'Letter', w: 612, h: 792 },
61
+ legal: { css: 'Legal', w: 612, h: 1008 },
62
+ };
63
+
64
+ const DEFAULT_PAPER = 'a4';
65
+ /* 18mm/15mm 是"文档"而不是"网页"的常见留白;12mm/10mm 是浏览器默认量级。
66
+ 两者都不神圣 —— 关键是**由导出方显式给出**,不是由库偷偷定。 */
67
+ const DEFAULT_MARGIN = '18mm 15mm';
68
+
69
+ /* ============================================================================
70
+ * Chrome 定位
71
+ * ============================================================================
72
+ * ⚠️ 这张表与 `05-audit/browser.js` 的候选表**有意重复**:那一份要给
73
+ * puppeteer 用,还要认 Playwright 的缓存目录与引擎参数;这一份要能
74
+ * 跟着包发到使用者机器上、不许 require 任何东西。两边的共同判据只有一条:
75
+ * **找不到就明确报错,不许静默降级**。
76
+ * 漂移由 `05-audit/pdf-export-gate.js` 的 E8 交叉核对(两边都找到时必须是同一个文件)。
77
+ */
78
+ function chromeCandidates() {
79
+ const out = [];
80
+ if (process.env.AGENTCSS_CHROME) out.push(process.env.AGENTCSS_CHROME);
81
+ if (process.env.CHROME_PATH) out.push(process.env.CHROME_PATH);
82
+ if (process.env.CHROME_BIN) out.push(process.env.CHROME_BIN);
83
+ if (process.platform === 'win32') {
84
+ const local = process.env.LOCALAPPDATA;
85
+ out.push(
86
+ 'C:/Program Files/Google/Chrome/Application/chrome.exe',
87
+ 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe',
88
+ local && path.join(local, 'Google/Chrome/Application/chrome.exe'),
89
+ 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
90
+ 'C:/Program Files/Microsoft/Edge/Application/msedge.exe');
91
+ } else if (process.platform === 'darwin') {
92
+ out.push(
93
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
94
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
95
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge');
96
+ } else {
97
+ out.push('/usr/bin/google-chrome', '/usr/bin/google-chrome-stable',
98
+ '/usr/bin/chromium', '/usr/bin/chromium-browser', '/snap/bin/chromium');
99
+ }
100
+ /* CI 上没有系统 Chrome:Playwright 装的那份也得认,否则"能导出"只在开发机上成立 */
101
+ for (const root of [path.join(os.homedir(), '.cache/ms-playwright'),
102
+ path.join(os.homedir(), 'Library/Caches/ms-playwright')]) {
103
+ let names;
104
+ try { names = fs.readdirSync(root); } catch (e) { continue; }
105
+ for (const n of names) {
106
+ if (!/^chromium/.test(n)) continue;
107
+ for (const rel of ['chrome-linux/chrome',
108
+ 'chrome-mac/Chromium.app/Contents/MacOS/Chromium',
109
+ 'chrome-win/chrome.exe', 'chrome-linux/headless_shell']) {
110
+ out.push(path.join(root, n, ...rel.split('/')));
111
+ }
112
+ }
113
+ }
114
+ return out.filter(Boolean);
115
+ }
116
+
117
+ /**
118
+ * @param {string} [explicit] 显式路径(`--chrome=`)
119
+ * @param {object} [opts] `{candidates}` —— **只为可测性**留的口子:
120
+ * 没有它,"一个浏览器都没有"这条报错就只能靠"这台机器恰好没装 Chrome"才验证得了,
121
+ * 而有 Chrome 的开发机上它**永远测不到**(与 validate.js 的 `opt.components` 同一个理由)。
122
+ * @returns {{path:string, source:string}|{error:string}}
123
+ */
124
+ function resolveChrome(explicit, opts) {
125
+ if (explicit) {
126
+ /* 显式给了却不存在 ⇒ 立刻报错。悄悄回落到自动探测会让"我明明指定了它"
127
+ 变成一句谎话,而且失败现场看起来像是"这个 Chrome 不能打印"。 */
128
+ if (!fs.existsSync(explicit)) {
129
+ return { error: '--chrome 指定的文件不存在:' + explicit
130
+ + '\n 这里要的是 Chrome / Chromium / Edge 的**可执行文件**路径。' };
131
+ }
132
+ return { path: explicit, source: 'explicit' };
133
+ }
134
+ const list = (opts && opts.candidates) || chromeCandidates();
135
+ for (const p of list) {
136
+ try { if (fs.existsSync(p)) return { path: p, source: 'auto' }; } catch (e) { /* 下一个 */ }
137
+ }
138
+ return {
139
+ error: '找不到 Chrome / Chromium / Edge。\n'
140
+ + ' 用法:--chrome="C:/Program Files/Google/Chrome/Application/chrome.exe"\n'
141
+ + ' 或设环境变量 AGENTCSS_CHROME / CHROME_PATH。\n'
142
+ + ' 本工具不打包浏览器:为了 5% 的导出需求让 95% 的人多装 300 MB,不值。',
143
+ };
144
+ }
145
+
146
+ /* ============================================================================
147
+ * 版式
148
+ * ============================================================================ */
149
+
150
+ /** `18mm` / `18mm,15mm` / `18mm 15mm` / 四段 → CSS margin 简写。非法则返回 null。 */
151
+ function normalizeMargin(v) {
152
+ if (v === undefined || v === null || v === '') return DEFAULT_MARGIN;
153
+ const parts = String(v).trim().split(/[\s,]+/).filter(Boolean);
154
+ if (parts.length < 1 || parts.length > 4) return null;
155
+ for (const p of parts) {
156
+ if (!/^\d+(\.\d+)?(mm|cm|in|pt|px)$/.test(p)) return null;
157
+ }
158
+ return parts.join(' ');
159
+ }
160
+
161
+ function paperSpec(name) {
162
+ const k = String(name || DEFAULT_PAPER).toLowerCase();
163
+ return PAPERS[k] ? { key: k, ...PAPERS[k] } : null;
164
+ }
165
+
166
+ /** 注入到 `<head>` 末尾(**必须在使用者样式之后**,否则同特异性被覆盖 —— E254)。 */
167
+ function injectSheet(html, css) {
168
+ const style = '<style id="agentcss-paper" data-agentcss="paper">' + css + '</style>';
169
+ if (/<\/head\s*>/i.test(html)) return html.replace(/<\/head\s*>/i, style + '</head>');
170
+ if (/<html[^>]*>/i.test(html)) return html.replace(/<html[^>]*>/i, '$&' + style);
171
+ return style + html;
172
+ }
173
+
174
+ function cssFor(spec, margin) {
175
+ return '@page{size:' + spec.css + ';margin:' + margin + '}';
176
+ }
177
+
178
+ /* ============================================================================
179
+ * 导出
180
+ * ============================================================================ */
181
+
182
+ function isRemote(src) {
183
+ return /^https?:\/\//i.test(src);
184
+ }
185
+
186
+ /**
187
+ * @param {object} o
188
+ * @param {string} o.src 本地 HTML 路径,或 http(s) URL
189
+ * @param {string} o.out 输出 PDF 路径(父目录不存在会创建)
190
+ * @param {string} [o.paper] a3|a4|a5|letter|legal(默认 a4)
191
+ * @param {string} [o.margin] CSS margin 简写(默认 18mm 15mm)
192
+ * @param {number} [o.budget] 页数上限;超了返回 over-budget
193
+ * @param {string} [o.chrome] Chrome 可执行文件
194
+ * @param {number} [o.timeoutMs]
195
+ * @returns {object} `{ok:true, ...}` 或 `{ok:false, code, message, ...}`
196
+ */
197
+ function exportPdf(o) {
198
+ o = o || {};
199
+ const t0 = Date.now();
200
+ if (!o.src) return { ok: false, code: 'usage', message: '缺少 src' };
201
+ if (!o.out) return { ok: false, code: 'usage', message: '缺少 out' };
202
+
203
+ const spec = paperSpec(o.paper);
204
+ if (!spec) {
205
+ return { ok: false, code: 'usage',
206
+ message: '未知纸幅 ' + JSON.stringify(o.paper) + '(可用:'
207
+ + Object.keys(PAPERS).join(' / ') + ')' };
208
+ }
209
+ const margin = normalizeMargin(o.margin);
210
+ if (!margin) {
211
+ return { ok: false, code: 'usage',
212
+ message: '边距写法不认识:' + JSON.stringify(o.margin)
213
+ + '(要 CSS 简写,例:18mm 或 "18mm 15mm")' };
214
+ }
215
+ if (o.budget !== undefined && o.budget !== null) {
216
+ const b = Number(o.budget);
217
+ if (!Number.isInteger(b) || b < 1) {
218
+ /* ⚠️ 用 String() 而不是 JSON.stringify:`JSON.stringify(NaN)` 是 `"null"`,
219
+ 于是 `--max-pages=abc` 会报「收到 null」—— 使用者据这句话会去查一个
220
+ 根本不存在的 null(实测踩过,见 E264)。 */
221
+ return { ok: false, code: 'usage',
222
+ message: '--max-pages 必须是 ≥1 的整数,收到 ' + String(o.budget) };
223
+ }
224
+ }
225
+
226
+ const chrome = resolveChrome(o.chrome);
227
+ if (chrome.error) return { ok: false, code: 'no-chrome', message: chrome.error };
228
+
229
+ const outAbs = path.resolve(o.out);
230
+ fs.mkdirSync(path.dirname(outAbs), { recursive: true });
231
+
232
+ /* 本地文件才注入 @page:远程页面我们拿不到它的字节,硬注入等于假装改了它。 */
233
+ let target = o.src;
234
+ let tmpHtml = null;
235
+ if (!isRemote(o.src)) {
236
+ const srcAbs = path.resolve(o.src);
237
+ if (!fs.existsSync(srcAbs)) {
238
+ return { ok: false, code: 'usage', message: '找不到输入文件:' + srcAbs };
239
+ }
240
+ const html = fs.readFileSync(srcAbs, 'utf8');
241
+ /* ⚠️ 临时文件必须落在**同一个目录**:换成系统临时目录,页面里的
242
+ `./tokens.css` / `/02-primitives/...` 会全部 404,而产物看起来"只是没样式"。 */
243
+ tmpHtml = path.join(path.dirname(srcAbs),
244
+ '.agentcss-print-' + process.pid + '-' + Date.now().toString(36) + '.html');
245
+ fs.writeFileSync(tmpHtml, injectSheet(html, cssFor(spec, margin)), 'utf8');
246
+ target = pathToFileURL(tmpHtml).href;
247
+ }
248
+
249
+ const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'agentcss-chrome-'));
250
+ const args = [
251
+ '--headless=new',
252
+ '--disable-gpu',
253
+ '--disable-extensions',
254
+ '--no-first-run',
255
+ '--no-default-browser-check',
256
+ /* 每次一个新 profile:使用者可能正开着 Chrome,共用 profile 会抢锁 */
257
+ '--user-data-dir=' + profile,
258
+ /* 页眉页脚(日期 / 标题 / 1/2)默认是开的,必须显式关掉 */
259
+ '--no-pdf-header-footer',
260
+ '--print-to-pdf-no-header',
261
+ '--print-to-pdf=' + outAbs,
262
+ target,
263
+ ];
264
+ /* 容器里以 root 跑才需要关沙箱 —— 默认不开,也不替使用者决定 */
265
+ if (process.env.AGENTCSS_NO_SANDBOX === '1') args.unshift('--no-sandbox');
266
+
267
+ let res;
268
+ try {
269
+ res = spawnSync(chrome.path, args, {
270
+ timeout: o.timeoutMs || 60000,
271
+ encoding: 'utf8',
272
+ windowsHide: true,
273
+ stdio: ['ignore', 'pipe', 'pipe'],
274
+ });
275
+ } finally {
276
+ /* 清理必须**同步**做完:CLI 随后就 process.exit,挂一个 setTimeout
277
+ 等于每次导出都在系统临时目录里留一个 Chrome profile(实测漏了 6 个才发现)。
278
+ 删不掉不算错(Windows 上偶发 EBUSY),但不能因为删不掉就不删。 */
279
+ if (tmpHtml) { try { fs.unlinkSync(tmpHtml); } catch (e) { /* 已删 */ } }
280
+ try { fs.rmSync(profile, { recursive: true, force: true, maxRetries: 3 }); }
281
+ catch (e) { /* 交给系统清理 */ }
282
+ }
283
+
284
+ const ms = Date.now() - t0;
285
+ const stderr = String((res && res.stderr) || '').slice(-800);
286
+ if (res && res.error && res.error.code === 'ETIMEDOUT') {
287
+ return { ok: false, code: 'timeout', message: 'Chrome 在 '
288
+ + Math.round((o.timeoutMs || 60000) / 1000) + 's 内没有结束(页面可能有死循环或等待网络)', ms };
289
+ }
290
+ if (!fs.existsSync(outAbs)) {
291
+ return { ok: false, code: 'render', ms,
292
+ message: 'Chrome 没有产出文件(退出码 ' + (res ? res.status : '?') + ')'
293
+ + (stderr ? '\n ' + stderr.trim().split('\n').slice(-4).join('\n ') : '') };
294
+ }
295
+
296
+ const buf = fs.readFileSync(outAbs);
297
+ if (buf.length < 5 || buf.slice(0, 5).toString('latin1') !== '%PDF-') {
298
+ return { ok: false, code: 'render',
299
+ message: '产出的文件不是 PDF(' + buf.length + ' 字节)—— 通常是页面加载失败', ms };
300
+ }
301
+
302
+ const info = pages.inspect(buf);
303
+ const injected = !isRemote(o.src);
304
+ const base = {
305
+ out: outAbs, bytes: buf.length, pdf: buf, ms,
306
+ /* 远程页面没注入 @page ⇒ `paper` 只能是 null:报 "a4" 是在陈述一个
307
+ 没有发生的事实(实测远程导出是 612×792)。真实纸幅看 `mediaBox`。 */
308
+ paper: injected ? spec.key : null,
309
+ margin: injected ? margin : null, chrome: chrome.path,
310
+ pages: info.pages, mediaBox: info.mediaBox,
311
+ injected: injected,
312
+ };
313
+
314
+ /* 页数两套说法不一致 ⇒ 不判预算,也不假装通过 */
315
+ if (!info.agree && o.budget !== undefined && o.budget !== null) {
316
+ return { ...base, ok: false, code: 'unreadable',
317
+ message: 'PDF 里 `/Type /Page` 数出 ' + info.pages + ' 页、页树 `/Count` 说 '
318
+ + info.countFromTree + ' 页 —— 两套说法不一致,无法判定预算' };
319
+ }
320
+
321
+ if (o.budget !== undefined && o.budget !== null && info.pages > o.budget) {
322
+ return { ...base, ok: false, code: 'over-budget', budget: Number(o.budget),
323
+ message: overBudgetMessage(info.pages, Number(o.budget), spec, margin, outAbs,
324
+ info.mediaBox, !isRemote(o.src)) };
325
+ }
326
+ return { ...base, ok: true,
327
+ budget: (o.budget === undefined || o.budget === null) ? null : Number(o.budget) };
328
+ }
329
+
330
+ /* 报错要**可执行**:三条出路 + 一条"我不代做"的说明。
331
+ 为什么不实现 truncate:截断 = 静默丢内容,正是这套门禁在防的事。
332
+ 一个会替你悄悄删掉后半份报告的工具,比一个报错停下的工具危险得多。 */
333
+ function overBudgetMessage(got, budget, spec, margin, out, box, injected) {
334
+ const size = box ? (Math.round(box.width) + '×' + Math.round(box.height) + 'pt') : '?';
335
+ /* 远程页面没注入 @page ⇒ 纸幅与边距都不是我们给的。
336
+ 这时报"A4 / 18mm 15mm"是在描述一个不存在的事实(实测:远程导出是 612×792,
337
+ 而参数写着 a4)—— 报错里写错事实,比不写更误导。 */
338
+ const layout = injected
339
+ ? '(' + spec.css + ',' + size + ',边距 ' + margin + ')'
340
+ : '(页面自定义纸幅,实测 ' + size + ';未注入 @page,--paper/--margin 不生效)';
341
+ return '超出页数预算:' + got + ' 页 > ' + budget + ' 页' + layout + '\n'
342
+ + ' 产出文件保留在:' + out + '\n'
343
+ + ' 三条出路(按代价从低到高):\n'
344
+ + ' ① 拆文档 把一章拆成单独文件分别导出:agentcss pdf chapter-2.html --out ch2.pdf\n'
345
+ + ' ② 进附录 明细移到附录,正文只留汇总 —— 需要作者改内容,不是换个参数\n'
346
+ + ' ③ 截断 **本工具不代做**。静默丢内容正是这套判据在防的事;\n'
347
+ + ' 真要截断,请删掉要丢的段落再导出(这样它会出现在 diff 里)\n'
348
+ + ' 确认预算定低了,就调高它:--max-pages=' + Math.max(got, budget);
349
+ }
350
+
351
+ module.exports = {
352
+ exportPdf, resolveChrome, normalizeMargin, paperSpec, PAPERS,
353
+ DEFAULT_PAPER, DEFAULT_MARGIN,
354
+ };
package/ai/tokens.json CHANGED
@@ -593,11 +593,11 @@
593
593
  "$description": "实算 原来的 #959ba3 对 paper 只有 4.14:1 不达 AA,提到这个值"
594
594
  },
595
595
  "--text-on-accent": {
596
- "$value": "#0D1114",
596
+ "$value": "#15181c",
597
597
  "$type": "color"
598
598
  },
599
599
  "--text-on-solid": {
600
- "$value": "#0D1114",
600
+ "$value": "#15181c",
601
601
  "$type": "color"
602
602
  },
603
603
  "--skeleton-bg": {
package/ai/validate.js CHANGED
@@ -368,8 +368,55 @@ function validateDoc(doc, opt) {
368
368
  ')不允许携带扩展字段' });
369
369
  }
370
370
 
371
+ // ---- 介质:paper 才追加的打印义务
372
+ //
373
+ // 🔴 为什么这一节不能省("打印不是已经在 CSS 里做了吗")
374
+ // CSS 里做的是**行为**(隐藏什么、展开什么),它回答不了三个问题:
375
+ // ① 这份文档准备印几页?没有 pageBudget,"几百张纸"是惊喜而不是错误;
376
+ // ② 文档里 `pagination:"server"` 的表格,纸上会不会只剩一页?
377
+ // ③ 用了 `printPolicy:"omit"` 的组件,作者知道它不会印出来吗?
378
+ // 前两个是错,第三个是提示。三条都只在 paper 介质下成立 ——
379
+ // 所以默认 screen 时一条都不跑(95% 的人不该为打印付任何注意力)。
380
+ var medium = opt.medium || 'screen';
381
+ if (['screen', 'paper'].indexOf(medium) < 0) {
382
+ errors.push({ path: '/medium',
383
+ msg: '未知介质 ' + medium + '(可用:screen / paper)' });
384
+ medium = 'screen';
385
+ }
386
+ if (medium === 'paper') {
387
+ if (doc.pageBudget === undefined || doc.pageBudget === null) {
388
+ errors.push({ path: '/pageBudget',
389
+ msg: 'paper 介质下必须声明 pageBudget(页数上限,正整数)。'
390
+ + '页数取决于内容,库不给默认值 —— 猜一个数字比不给更坏' });
391
+ }
392
+ var omitSeen = {};
393
+ ids.forEach(function (id) {
394
+ var n = nodes[id];
395
+ if (n.kind === 'text' || n.kind === 'slot') return;
396
+ var c = comps[n.kind];
397
+ if (!c) return; // 未知组件上面已经报过,不重复计错
398
+ if (!Array.isArray(c.printPolicy)) {
399
+ errors.push({ path: '/nodes/' + id, msg: '组件 ' + n.kind
400
+ + ' 在契约里没有 printPolicy 字段 —— paper 介质下无法判断它在纸上会发生什么' });
401
+ }
402
+ if (c.pagination === 'server') {
403
+ errors.push({ path: '/nodes/' + id, msg: '组件 ' + n.kind
404
+ + ' 是服务端分页(pagination:"server"):纸上只有当前一页,'
405
+ + '而导出的文件看起来是完整的 —— 要全量数据必须先换 client 分页' });
406
+ }
407
+ if (omitSeen[n.kind]) return; // 同一个组件只提示一次
408
+ (Array.isArray(c.printPolicy) ? c.printPolicy : []).forEach(function (r) {
409
+ if (r.policy !== 'omit' || omitSeen[n.kind]) return;
410
+ omitSeen[n.kind] = true;
411
+ warnings.push({ path: '/nodes/' + id, msg: '组件 ' + n.kind + ' 的 '
412
+ + r.selector + ' 声明 omit:这部分**不会出现在纸上**'
413
+ + (r.why ? '(' + r.why + ')' : '') });
414
+ });
415
+ });
416
+ }
417
+
371
418
  return { ok: errors.length === 0, errors: errors, warnings: warnings,
372
- profile: profileName };
419
+ profile: profileName, medium: medium };
373
420
  }
374
421
 
375
422
  module.exports = {
@@ -1 +1 @@
1
- :root {--raw-paper:#f6f7f8;--raw-surface:#ffffff;--raw-surface-sunken:#f1f3f5;--raw-surface-raised:#ffffff;--raw-scrollbar-thumb:#7A808A;--raw-scrollbar-thumb-hov:#6E747E;--raw-text-primary:#15181c;--raw-text-secondary:#4f555e;--raw-text-tertiary:#606770;--raw-text-on-accent:#ffffff;--raw-text-on-solid:#ffffff;--raw-skeleton-bg:#e9ecef;--raw-skeleton-shimmer:rgba(255, 255, 255, 0.72);--raw-border-control:#868b94;--raw-border-decor:#e2e5e9;--raw-border-decor-str:#c8cdd4;--raw-danger:#c0392b;--raw-danger-hover:#8f1e17;--raw-scrim:rgba(21, 24, 28, 0.4);--raw-success:#2f6b3a;--raw-warning:#8a5a00;--raw-info:#1d5f8a;--raw-danger-bg:#fdeeec;--raw-success-bg:#eaf2e6;--raw-warning-bg:#fbf1dd;--raw-info-bg:#e8f1f8;--raw-dir-up:#c0392b;--raw-dir-down:#2f6b3a;--raw-accent:#1b4d8f;--raw-switch-on:#1b4d8f;--raw-accent-hover:#163a6d;--raw-accent-soft:#e6eef7;--raw-tooltip-bg:#15181c;--raw-tooltip-fg:#f6f7f8;--raw-tooltip-border:#15181c;--paper:var(--raw-paper);--surface:var(--raw-surface);--surface-sunken:var(--raw-surface-sunken);--surface-raised:var(--raw-surface-raised);--scrollbar-size:10px;--scrollbar-size-thin:6px;--scrollbar-thumb:var(--raw-scrollbar-thumb);--scrollbar-thumb-hov:var(--raw-scrollbar-thumb-hov);--scrollbar-radius:5px;--text-primary:var(--raw-text-primary);--text-secondary:var(--raw-text-secondary);--text-tertiary:var(--raw-text-tertiary);--text-on-accent:var(--raw-text-on-accent);--text-on-solid:var(--raw-text-on-solid);--skeleton-bg:var(--raw-skeleton-bg);--skeleton-shimmer:var(--raw-skeleton-shimmer);--skeleton-h:1em;--skeleton-size:40px;--skeleton-duration:1.4s;--ease-out:cubic-bezier(0.23, 1, 0.32, 1);--progress-h:6px;--progress-dur:240ms;--r-full:999px;--ring-size:64px;--ring-thickness:8px;--progress:0%;--progress-num:0;--z-popover:60;--popover-min-w:200px;--popover-max-w:320px;--popover-p:var(--sp-3);--popover-gap:8px;--popover-dur:140ms;--border-control:var(--raw-border-control);--border-decor:var(--raw-border-decor);--border-decor-str:var(--raw-border-decor-str);--danger:var(--raw-danger);--danger-hover:var(--raw-danger-hover);--scrim:var(--raw-scrim);--success:var(--raw-success);--warning:var(--raw-warning);--info:var(--raw-info);--danger-bg:var(--raw-danger-bg);--success-bg:var(--raw-success-bg);--warning-bg:var(--raw-warning-bg);--info-bg:var(--raw-info-bg);--dir-up:var(--raw-dir-up);--dir-down:var(--raw-dir-down);--accent:var(--raw-accent);--switch-on:var(--raw-switch-on);--accent-hover:var(--raw-accent-hover);--accent-soft:var(--raw-accent-soft);--tooltip-bg:var(--raw-tooltip-bg);--tooltip-fg:var(--raw-tooltip-fg);--tooltip-border:var(--raw-tooltip-border);--sp-1:4px;--sp-2:8px;--sp-3:12px;--sp-4:clamp(1rem, 0.95rem + 0.3vw, 1.25rem);--sp-5:clamp(1.25rem, 1.15rem + 0.7vw, 1.75rem);--sp-6:clamp(1.5rem, 1.3rem + 1.2vw, 2.25rem);--sp-7:clamp(2rem, 1.7rem + 1.8vw, 3.5rem);--sp-8:clamp(2.5rem, 2.1rem + 2.4vw, 4.5rem);--bp-sm:30rem;--bp-md:48rem;--bp-lg:64rem;--bp-xl:80rem;--fs-xs:0.75rem;--fs-sm:0.875rem;--fs-base:clamp(1rem, 0.95rem + 0.3vw, 1.125rem);--fs-md:clamp(1.125rem, 1.05rem + 0.5vw, 1.3125rem);--fs-lg:clamp(1.3125rem, 1.2rem + 0.7vw, 1.5rem);--fs-xl:clamp(1.5rem, 1.3rem + 1.2vw, 1.875rem);--fs-2xl:clamp(1.75rem, 1.45rem + 1.8vw, 2.5rem);--lh-tight:1.25;--lh-base:1.7;--lh-loose:1.85;--measure-page:52rem;--tracking-title:-0.02em;--r-sm:6px;--r-xs:3px;--r-md:10px;--r-lg:14px;--r-pill:999px;--shadow-1:0 1px 2px rgba(31, 29, 25, 0.05), 0 1px 3px rgba(31, 29, 25, 0.06);--shadow-2:0 2px 4px rgba(31, 29, 25, 0.05), 0 6px 16px rgba(31, 29, 25, 0.08);--dur-fast:120ms;--dur-base:180ms;--dur-slow:280ms;--ease-out:cubic-bezier(0.23, 1, 0.32, 1);--ease-in-out:cubic-bezier(0.77, 0, 0.175, 1);--ease-drawer:cubic-bezier(0.32, 0.72, 0, 1);--lift-sm:1px;--lift-md:2px;--lift-lg:8px;--z-base:0;--z-sticky:10;--z-header:20;--z-raised:5;--z-overlay:30;--z-modal:40;--z-toast:50;--sz-touch:44px;--sz-touch-sm:32px;}[data-cq] {container-type:inline-size;}@media (prefers-reduced-motion:reduce) {*, *::before, *::after {animation-duration:0.01ms !important;animation-iteration-count:1 !important;transition-duration:0.01ms !important;scroll-behavior:auto !important;}}:focus-visible {outline:2px solid var(--accent);outline-offset:2px;border-radius:var(--r-sm);}:root {--measure-prose:34em;--measure-title:20em;--measure-lede:32em;--measure-pull:30em;--measure-aside:32em;}:root:lang(en), :root:lang(en-US), :root:lang(en-GB), :root:lang(fr), :root:lang(de), :root:lang(es), :root:lang(pt), :root:lang(ru), :root:lang(el), :root:lang(tr), :root:lang(vi) {--measure-prose:68ch;--measure-title:34ch;--measure-lede:62ch;--measure-pull:58ch;--measure-aside:56ch;}:root:lang(ar) {--measure-prose:60ch;--measure-title:30ch;--measure-lede:56ch;--measure-pull:52ch;--measure-aside:50ch;}:root:lang(hi), :root:lang(th), :root:lang(km), :root:lang(my) {--measure-prose:56ch;--measure-title:28ch;--measure-lede:52ch;--measure-pull:48ch;--measure-aside:46ch;}:root:lang(zh), :root:lang(zh-CN), :root:lang(zh-TW), :root:lang(ja), :root:lang(ko) {--measure-prose:34em;--measure-title:20em;--measure-lede:32em;--measure-pull:30em;--measure-aside:32em;}@media (prefers-color-scheme:dark) {:root:not([data-theme="light"]) {--raw-paper:#14171A;--raw-surface:#1C2024;--raw-surface-raised:#232830;--raw-surface-sunken:#1A1D21;--raw-scrollbar-thumb:#6E747E;--raw-scrollbar-thumb-hov:#7D828A;--raw-border-decor:#2A3038;--raw-border-decor-str:#3A424C;--raw-border-control:#8A9099;--raw-text-primary:#E4E7EA;--raw-text-secondary:#AEB6C0;--raw-text-tertiary:#9BA3AD;--raw-text-on-accent:#0D1114;--raw-text-on-solid:#0D1114;--raw-skeleton-bg:#2E343C;--raw-skeleton-shimmer:rgba(255, 255, 255, 0.06);--raw-danger:#E07A6E;--raw-danger-hover:#F09A90;--raw-danger-bg:#341D1A;--raw-success:#6FAA7A;--raw-success-bg:#16241A;--raw-dir-up:#E07A6E;--raw-dir-down:#6FAA7A;--raw-warning:#C99A3F;--raw-warning-bg:#242A31;--raw-info:#5B9BC4;--raw-info-bg:#13242F;--raw-accent:#7AA9DE;--raw-switch-on:#3E6EA8;--raw-accent-hover:#8FB8E6;--raw-accent-soft:#1C2C3D;--raw-tooltip-bg:#232830;--raw-tooltip-fg:#E4E7EA;--raw-tooltip-border:#2A3038;--raw-scrim:rgba(0, 0, 0, 0.62);}}[data-theme="light"] {--raw-accent:#1b4d8f;--raw-accent-hover:#163a6d;--raw-accent-soft:#e6eef7;--raw-border-control:#868b94;--raw-border-decor:#e2e5e9;--raw-border-decor-str:#c8cdd4;--raw-danger:#c0392b;--raw-danger-bg:#fdeeec;--raw-danger-hover:#8f1e17;--raw-dir-down:#2f6b3a;--raw-dir-up:#c0392b;--raw-info:#1d5f8a;--raw-info-bg:#e8f1f8;--raw-paper:#f6f7f8;--raw-scrim:rgba(21, 24, 28, 0.4);--raw-scrollbar-thumb:#7A808A;--raw-scrollbar-thumb-hov:#6E747E;--raw-skeleton-bg:#e9ecef;--raw-skeleton-shimmer:rgba(255, 255, 255, 0.72);--raw-success:#2f6b3a;--raw-success-bg:#eaf2e6;--raw-surface:#ffffff;--raw-surface-raised:#ffffff;--raw-surface-sunken:#f1f3f5;--raw-switch-on:#1b4d8f;--raw-text-on-accent:#ffffff;--raw-text-on-solid:#ffffff;--raw-text-primary:#15181c;--raw-text-secondary:#4f555e;--raw-text-tertiary:#606770;--raw-tooltip-bg:#15181c;--raw-tooltip-border:#15181c;--raw-tooltip-fg:#f6f7f8;--raw-warning:#8a5a00;--raw-warning-bg:#fbf1dd;--accent:var(--raw-accent);--accent-hover:var(--raw-accent-hover);--accent-soft:var(--raw-accent-soft);--border-control:var(--raw-border-control);--border-decor:var(--raw-border-decor);--border-decor-str:var(--raw-border-decor-str);--danger:var(--raw-danger);--danger-bg:var(--raw-danger-bg);--danger-hover:var(--raw-danger-hover);--dir-down:var(--raw-dir-down);--dir-up:var(--raw-dir-up);--info:var(--raw-info);--info-bg:var(--raw-info-bg);--paper:var(--raw-paper);--scrim:var(--raw-scrim);--scrollbar-thumb:var(--raw-scrollbar-thumb);--scrollbar-thumb-hov:var(--raw-scrollbar-thumb-hov);--skeleton-bg:var(--raw-skeleton-bg);--skeleton-shimmer:var(--raw-skeleton-shimmer);--success:var(--raw-success);--success-bg:var(--raw-success-bg);--surface:var(--raw-surface);--surface-raised:var(--raw-surface-raised);--surface-sunken:var(--raw-surface-sunken);--switch-on:var(--raw-switch-on);--text-on-accent:var(--raw-text-on-accent);--text-on-solid:var(--raw-text-on-solid);--text-primary:var(--raw-text-primary);--text-secondary:var(--raw-text-secondary);--text-tertiary:var(--raw-text-tertiary);--tooltip-bg:var(--raw-tooltip-bg);--tooltip-border:var(--raw-tooltip-border);--tooltip-fg:var(--raw-tooltip-fg);--warning:var(--raw-warning);--warning-bg:var(--raw-warning-bg);}:root[data-theme="dark"], [data-theme="dark"] {--raw-accent:#7AA9DE;--raw-accent-hover:#8FB8E6;--raw-accent-soft:#1C2C3D;--raw-border-control:#8A9099;--raw-border-decor:#2A3038;--raw-border-decor-str:#3A424C;--raw-danger:#E07A6E;--raw-danger-bg:#341D1A;--raw-danger-hover:#F09A90;--raw-dir-down:#6FAA7A;--raw-dir-up:#E07A6E;--raw-info:#5B9BC4;--raw-info-bg:#13242F;--raw-paper:#14171A;--raw-scrim:rgba(0, 0, 0, 0.62);--raw-scrollbar-thumb:#6E747E;--raw-scrollbar-thumb-hov:#7D828A;--raw-skeleton-bg:#2E343C;--raw-skeleton-shimmer:rgba(255, 255, 255, 0.06);--raw-success:#6FAA7A;--raw-success-bg:#16241A;--raw-surface:#1C2024;--raw-surface-raised:#232830;--raw-surface-sunken:#1A1D21;--raw-switch-on:#3E6EA8;--raw-text-on-accent:#0D1114;--raw-text-on-solid:#0D1114;--raw-text-primary:#E4E7EA;--raw-text-secondary:#AEB6C0;--raw-text-tertiary:#9BA3AD;--raw-tooltip-bg:#232830;--raw-tooltip-border:#2A3038;--raw-tooltip-fg:#E4E7EA;--raw-warning:#C99A3F;--raw-warning-bg:#242A31;--accent:var(--raw-accent);--accent-hover:var(--raw-accent-hover);--accent-soft:var(--raw-accent-soft);--border-control:var(--raw-border-control);--border-decor:var(--raw-border-decor);--border-decor-str:var(--raw-border-decor-str);--danger:var(--raw-danger);--danger-bg:var(--raw-danger-bg);--danger-hover:var(--raw-danger-hover);--dir-down:var(--raw-dir-down);--dir-up:var(--raw-dir-up);--info:var(--raw-info);--info-bg:var(--raw-info-bg);--paper:var(--raw-paper);--scrim:var(--raw-scrim);--scrollbar-thumb:var(--raw-scrollbar-thumb);--scrollbar-thumb-hov:var(--raw-scrollbar-thumb-hov);--skeleton-bg:var(--raw-skeleton-bg);--skeleton-shimmer:var(--raw-skeleton-shimmer);--success:var(--raw-success);--success-bg:var(--raw-success-bg);--surface:var(--raw-surface);--surface-raised:var(--raw-surface-raised);--surface-sunken:var(--raw-surface-sunken);--switch-on:var(--raw-switch-on);--text-on-accent:var(--raw-text-on-accent);--text-on-solid:var(--raw-text-on-solid);--text-primary:var(--raw-text-primary);--text-secondary:var(--raw-text-secondary);--text-tertiary:var(--raw-text-tertiary);--tooltip-bg:var(--raw-tooltip-bg);--tooltip-border:var(--raw-tooltip-border);--tooltip-fg:var(--raw-tooltip-fg);--warning:var(--raw-warning);--warning-bg:var(--raw-warning-bg);}:root {--sans:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Source Han Sans SC', 'Noto Sans CJK SC', 'Geeza Pro', 'Segoe UI Historic', sans-serif;--mono:ui-monospace, 'SF Mono', 'Cascadia Mono', 'Segoe UI Mono', 'Roboto Mono', Menlo, Consolas, 'Sarasa Mono SC', 'Microsoft YaHei Mono', monospace;--serif:'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, 'Songti SC', 'SimSun', 'Noto Serif CJK SC', 'Source Han Serif SC', serif;--tracking-normal:0;}:root:lang(ar), :root:lang(fa), :root:lang(he), :root:lang(ur) {--tracking-normal:0.01em;}a, button, label, summary, input[type="checkbox"], input[type="radio"], input[type="button"], input[type="submit"], [role="button"], [role="switch"], [role="tab"], [role="option"], .choice, .switch, .btn, .accordion__trigger, .tabs__tab, .card, .list__row {-webkit-tap-highlight-color:transparent;}.choice:active, .switch:active, .btn:active, .list__row:active, .card:active {opacity:0.82;}@media (hover:hover) and (pointer:fine) {.choice:active, .switch:active, .btn:active, .list__row:active, .card:active {opacity:1;}}
1
+ :root {--raw-paper:#f6f7f8;--raw-surface:#ffffff;--raw-surface-sunken:#f1f3f5;--raw-surface-raised:#ffffff;--raw-scrollbar-thumb:#7A808A;--raw-scrollbar-thumb-hov:#6E747E;--raw-text-primary:#15181c;--raw-text-secondary:#4f555e;--raw-text-tertiary:#606770;--raw-text-on-accent:#ffffff;--raw-text-on-solid:#ffffff;--raw-skeleton-bg:#e9ecef;--raw-skeleton-shimmer:rgba(255, 255, 255, 0.72);--raw-border-control:#868b94;--raw-border-decor:#e2e5e9;--raw-border-decor-str:#c8cdd4;--raw-danger:#c0392b;--raw-danger-hover:#8f1e17;--raw-scrim:rgba(21, 24, 28, 0.4);--raw-success:#2f6b3a;--raw-warning:#8a5a00;--raw-info:#1d5f8a;--raw-danger-bg:#fdeeec;--raw-success-bg:#eaf2e6;--raw-warning-bg:#fbf1dd;--raw-info-bg:#e8f1f8;--raw-dir-up:#c0392b;--raw-dir-down:#2f6b3a;--raw-accent:#1b4d8f;--raw-switch-on:#1b4d8f;--raw-accent-hover:#163a6d;--raw-accent-soft:#e6eef7;--raw-tooltip-bg:#15181c;--raw-tooltip-fg:#f6f7f8;--raw-tooltip-border:#15181c;--paper:var(--raw-paper);--surface:var(--raw-surface);--surface-sunken:var(--raw-surface-sunken);--surface-raised:var(--raw-surface-raised);--scrollbar-size:10px;--scrollbar-size-thin:6px;--scrollbar-thumb:var(--raw-scrollbar-thumb);--scrollbar-thumb-hov:var(--raw-scrollbar-thumb-hov);--scrollbar-radius:5px;--text-primary:var(--raw-text-primary);--text-secondary:var(--raw-text-secondary);--text-tertiary:var(--raw-text-tertiary);--text-on-accent:var(--raw-text-on-accent);--text-on-solid:var(--raw-text-on-solid);--skeleton-bg:var(--raw-skeleton-bg);--skeleton-shimmer:var(--raw-skeleton-shimmer);--skeleton-h:1em;--skeleton-size:40px;--skeleton-duration:1.4s;--ease-out:cubic-bezier(0.23, 1, 0.32, 1);--progress-h:6px;--progress-dur:240ms;--r-full:999px;--ring-size:64px;--ring-thickness:8px;--progress:0%;--progress-num:0;--z-popover:60;--popover-min-w:200px;--popover-max-w:320px;--popover-p:var(--sp-3);--popover-gap:8px;--popover-dur:140ms;--border-control:var(--raw-border-control);--border-decor:var(--raw-border-decor);--border-decor-str:var(--raw-border-decor-str);--danger:var(--raw-danger);--danger-hover:var(--raw-danger-hover);--scrim:var(--raw-scrim);--success:var(--raw-success);--warning:var(--raw-warning);--info:var(--raw-info);--danger-bg:var(--raw-danger-bg);--success-bg:var(--raw-success-bg);--warning-bg:var(--raw-warning-bg);--info-bg:var(--raw-info-bg);--dir-up:var(--raw-dir-up);--dir-down:var(--raw-dir-down);--accent:var(--raw-accent);--switch-on:var(--raw-switch-on);--accent-hover:var(--raw-accent-hover);--accent-soft:var(--raw-accent-soft);--tooltip-bg:var(--raw-tooltip-bg);--tooltip-fg:var(--raw-tooltip-fg);--tooltip-border:var(--raw-tooltip-border);--sp-1:4px;--sp-2:8px;--sp-3:12px;--sp-4:clamp(1rem, 0.95rem + 0.3vw, 1.25rem);--sp-5:clamp(1.25rem, 1.15rem + 0.7vw, 1.75rem);--sp-6:clamp(1.5rem, 1.3rem + 1.2vw, 2.25rem);--sp-7:clamp(2rem, 1.7rem + 1.8vw, 3.5rem);--sp-8:clamp(2.5rem, 2.1rem + 2.4vw, 4.5rem);--bp-sm:30rem;--bp-md:48rem;--bp-lg:64rem;--bp-xl:80rem;--fs-xs:0.75rem;--fs-sm:0.875rem;--fs-base:clamp(1rem, 0.95rem + 0.3vw, 1.125rem);--fs-md:clamp(1.125rem, 1.05rem + 0.5vw, 1.3125rem);--fs-lg:clamp(1.3125rem, 1.2rem + 0.7vw, 1.5rem);--fs-xl:clamp(1.5rem, 1.3rem + 1.2vw, 1.875rem);--fs-2xl:clamp(1.75rem, 1.45rem + 1.8vw, 2.5rem);--lh-tight:1.25;--lh-base:1.7;--lh-loose:1.85;--measure-page:52rem;--tracking-title:-0.02em;--r-sm:6px;--r-xs:3px;--r-md:10px;--r-lg:14px;--r-pill:999px;--shadow-1:0 1px 2px rgba(31, 29, 25, 0.05), 0 1px 3px rgba(31, 29, 25, 0.06);--shadow-2:0 2px 4px rgba(31, 29, 25, 0.05), 0 6px 16px rgba(31, 29, 25, 0.08);--dur-fast:120ms;--dur-base:180ms;--dur-slow:280ms;--ease-out:cubic-bezier(0.23, 1, 0.32, 1);--ease-in-out:cubic-bezier(0.77, 0, 0.175, 1);--ease-drawer:cubic-bezier(0.32, 0.72, 0, 1);--lift-sm:1px;--lift-md:2px;--lift-lg:8px;--z-base:0;--z-sticky:10;--z-header:20;--z-raised:5;--z-overlay:30;--z-modal:40;--z-toast:50;--sz-touch:44px;--sz-touch-sm:32px;}[data-cq] {container-type:inline-size;}@media (prefers-reduced-motion:reduce) {*, *::before, *::after {animation-duration:0.01ms !important;animation-iteration-count:1 !important;transition-duration:0.01ms !important;scroll-behavior:auto !important;}}:focus-visible {outline:2px solid var(--accent);outline-offset:2px;border-radius:var(--r-sm);}:root {--measure-prose:34em;--measure-title:20em;--measure-lede:32em;--measure-pull:30em;--measure-aside:32em;}:root:lang(en), :root:lang(en-US), :root:lang(en-GB), :root:lang(fr), :root:lang(de), :root:lang(es), :root:lang(pt), :root:lang(ru), :root:lang(el), :root:lang(tr), :root:lang(vi) {--measure-prose:68ch;--measure-title:34ch;--measure-lede:62ch;--measure-pull:58ch;--measure-aside:56ch;}:root:lang(ar) {--measure-prose:60ch;--measure-title:30ch;--measure-lede:56ch;--measure-pull:52ch;--measure-aside:50ch;}:root:lang(hi), :root:lang(th), :root:lang(km), :root:lang(my) {--measure-prose:56ch;--measure-title:28ch;--measure-lede:52ch;--measure-pull:48ch;--measure-aside:46ch;}:root:lang(zh), :root:lang(zh-CN), :root:lang(zh-TW), :root:lang(ja), :root:lang(ko) {--measure-prose:34em;--measure-title:20em;--measure-lede:32em;--measure-pull:30em;--measure-aside:32em;}@media screen and (prefers-color-scheme:dark) {:root:not([data-theme="light"]) {--raw-paper:#14171A;--raw-surface:#1C2024;--raw-surface-raised:#232830;--raw-surface-sunken:#1A1D21;--raw-scrollbar-thumb:#6E747E;--raw-scrollbar-thumb-hov:#7D828A;--raw-border-decor:#2A3038;--raw-border-decor-str:#3A424C;--raw-border-control:#8A9099;--raw-text-primary:#E4E7EA;--raw-text-secondary:#AEB6C0;--raw-text-tertiary:#9BA3AD;--raw-text-on-accent:#0D1114;--raw-text-on-solid:#0D1114;--raw-skeleton-bg:#2E343C;--raw-skeleton-shimmer:rgba(255, 255, 255, 0.06);--raw-danger:#E07A6E;--raw-danger-hover:#F09A90;--raw-danger-bg:#341D1A;--raw-success:#6FAA7A;--raw-success-bg:#16241A;--raw-dir-up:#E07A6E;--raw-dir-down:#6FAA7A;--raw-warning:#C99A3F;--raw-warning-bg:#242A31;--raw-info:#5B9BC4;--raw-info-bg:#13242F;--raw-accent:#7AA9DE;--raw-switch-on:#3E6EA8;--raw-accent-hover:#8FB8E6;--raw-accent-soft:#1C2C3D;--raw-tooltip-bg:#232830;--raw-tooltip-fg:#E4E7EA;--raw-tooltip-border:#2A3038;--raw-scrim:rgba(0, 0, 0, 0.62);}}[data-theme="light"] {--raw-accent:#1b4d8f;--raw-accent-hover:#163a6d;--raw-accent-soft:#e6eef7;--raw-border-control:#868b94;--raw-border-decor:#e2e5e9;--raw-border-decor-str:#c8cdd4;--raw-danger:#c0392b;--raw-danger-bg:#fdeeec;--raw-danger-hover:#8f1e17;--raw-dir-down:#2f6b3a;--raw-dir-up:#c0392b;--raw-info:#1d5f8a;--raw-info-bg:#e8f1f8;--raw-paper:#f6f7f8;--raw-scrim:rgba(21, 24, 28, 0.4);--raw-scrollbar-thumb:#7A808A;--raw-scrollbar-thumb-hov:#6E747E;--raw-skeleton-bg:#e9ecef;--raw-skeleton-shimmer:rgba(255, 255, 255, 0.72);--raw-success:#2f6b3a;--raw-success-bg:#eaf2e6;--raw-surface:#ffffff;--raw-surface-raised:#ffffff;--raw-surface-sunken:#f1f3f5;--raw-switch-on:#1b4d8f;--raw-text-on-accent:#ffffff;--raw-text-on-solid:#ffffff;--raw-text-primary:#15181c;--raw-text-secondary:#4f555e;--raw-text-tertiary:#606770;--raw-tooltip-bg:#15181c;--raw-tooltip-border:#15181c;--raw-tooltip-fg:#f6f7f8;--raw-warning:#8a5a00;--raw-warning-bg:#fbf1dd;--accent:var(--raw-accent);--accent-hover:var(--raw-accent-hover);--accent-soft:var(--raw-accent-soft);--border-control:var(--raw-border-control);--border-decor:var(--raw-border-decor);--border-decor-str:var(--raw-border-decor-str);--danger:var(--raw-danger);--danger-bg:var(--raw-danger-bg);--danger-hover:var(--raw-danger-hover);--dir-down:var(--raw-dir-down);--dir-up:var(--raw-dir-up);--info:var(--raw-info);--info-bg:var(--raw-info-bg);--paper:var(--raw-paper);--scrim:var(--raw-scrim);--scrollbar-thumb:var(--raw-scrollbar-thumb);--scrollbar-thumb-hov:var(--raw-scrollbar-thumb-hov);--skeleton-bg:var(--raw-skeleton-bg);--skeleton-shimmer:var(--raw-skeleton-shimmer);--success:var(--raw-success);--success-bg:var(--raw-success-bg);--surface:var(--raw-surface);--surface-raised:var(--raw-surface-raised);--surface-sunken:var(--raw-surface-sunken);--switch-on:var(--raw-switch-on);--text-on-accent:var(--raw-text-on-accent);--text-on-solid:var(--raw-text-on-solid);--text-primary:var(--raw-text-primary);--text-secondary:var(--raw-text-secondary);--text-tertiary:var(--raw-text-tertiary);--tooltip-bg:var(--raw-tooltip-bg);--tooltip-border:var(--raw-tooltip-border);--tooltip-fg:var(--raw-tooltip-fg);--warning:var(--raw-warning);--warning-bg:var(--raw-warning-bg);}@media screen {:root[data-theme="dark"], [data-theme="dark"] {--raw-accent:#7AA9DE;--raw-accent-hover:#8FB8E6;--raw-accent-soft:#1C2C3D;--raw-border-control:#8A9099;--raw-border-decor:#2A3038;--raw-border-decor-str:#3A424C;--raw-danger:#E07A6E;--raw-danger-bg:#341D1A;--raw-danger-hover:#F09A90;--raw-dir-down:#6FAA7A;--raw-dir-up:#E07A6E;--raw-info:#5B9BC4;--raw-info-bg:#13242F;--raw-paper:#14171A;--raw-scrim:rgba(0, 0, 0, 0.62);--raw-scrollbar-thumb:#6E747E;--raw-scrollbar-thumb-hov:#7D828A;--raw-skeleton-bg:#2E343C;--raw-skeleton-shimmer:rgba(255, 255, 255, 0.06);--raw-success:#6FAA7A;--raw-success-bg:#16241A;--raw-surface:#1C2024;--raw-surface-raised:#232830;--raw-surface-sunken:#1A1D21;--raw-switch-on:#3E6EA8;--raw-text-on-accent:#0D1114;--raw-text-on-solid:#0D1114;--raw-text-primary:#E4E7EA;--raw-text-secondary:#AEB6C0;--raw-text-tertiary:#9BA3AD;--raw-tooltip-bg:#232830;--raw-tooltip-border:#2A3038;--raw-tooltip-fg:#E4E7EA;--raw-warning:#C99A3F;--raw-warning-bg:#242A31;--accent:var(--raw-accent);--accent-hover:var(--raw-accent-hover);--accent-soft:var(--raw-accent-soft);--border-control:var(--raw-border-control);--border-decor:var(--raw-border-decor);--border-decor-str:var(--raw-border-decor-str);--danger:var(--raw-danger);--danger-bg:var(--raw-danger-bg);--danger-hover:var(--raw-danger-hover);--dir-down:var(--raw-dir-down);--dir-up:var(--raw-dir-up);--info:var(--raw-info);--info-bg:var(--raw-info-bg);--paper:var(--raw-paper);--scrim:var(--raw-scrim);--scrollbar-thumb:var(--raw-scrollbar-thumb);--scrollbar-thumb-hov:var(--raw-scrollbar-thumb-hov);--skeleton-bg:var(--raw-skeleton-bg);--skeleton-shimmer:var(--raw-skeleton-shimmer);--success:var(--raw-success);--success-bg:var(--raw-success-bg);--surface:var(--raw-surface);--surface-raised:var(--raw-surface-raised);--surface-sunken:var(--raw-surface-sunken);--switch-on:var(--raw-switch-on);--text-on-accent:var(--raw-text-on-accent);--text-on-solid:var(--raw-text-on-solid);--text-primary:var(--raw-text-primary);--text-secondary:var(--raw-text-secondary);--text-tertiary:var(--raw-text-tertiary);--tooltip-bg:var(--raw-tooltip-bg);--tooltip-border:var(--raw-tooltip-border);--tooltip-fg:var(--raw-tooltip-fg);--warning:var(--raw-warning);--warning-bg:var(--raw-warning-bg);}}:root {--sans:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Source Han Sans SC', 'Noto Sans CJK SC', 'Geeza Pro', 'Segoe UI Historic', sans-serif;--mono:ui-monospace, 'SF Mono', 'Cascadia Mono', 'Segoe UI Mono', 'Roboto Mono', Menlo, Consolas, 'Sarasa Mono SC', 'Microsoft YaHei Mono', monospace;--serif:'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, 'Songti SC', 'SimSun', 'Noto Serif CJK SC', 'Source Han Serif SC', serif;--tracking-normal:0;}:root:lang(ar), :root:lang(fa), :root:lang(he), :root:lang(ur) {--tracking-normal:0.01em;}a, button, label, summary, input[type="checkbox"], input[type="radio"], input[type="button"], input[type="submit"], [role="button"], [role="switch"], [role="tab"], [role="option"], .choice, .switch, .btn, .accordion__trigger, .tabs__tab, .card, .list__row {-webkit-tap-highlight-color:transparent;}.choice:active, .switch:active, .btn:active, .list__row:active, .card:active {opacity:0.82;}@media (hover:hover) and (pointer:fine) {.choice:active, .switch:active, .btn:active, .list__row:active, .card:active {opacity:1;}}.print-only {display:none;}@media print {:root {color-scheme:light;--text-on-accent:#15181c;--text-on-solid:#15181c;}.no-print {display:none !important;}.print-only {display:block;}.tabs .tabs__panel[hidden] {display:block;}.accordion .accordion__panel[hidden] {display:grid !important;grid-template-rows:1fr;visibility:visible;}.toast, .tooltip, .popover, .skeleton, .tabs__list, .nav__list, .nav__toggle, .pagination, .accordion__marker {display:none !important;}tr, img, figure, svg, .card, .metric, .sparkline, .splot {break-inside:avoid;}.print-allow-break {break-inside:auto;}}